TypeScript SDKGithubEdit on GitHub

React hooks

@kortix/sdk/react — one hook to run a session (useSession), plus reactive hooks for models, config, PTY, and the session list.

@kortix/sdk/react is the reactive half of the SDK. Where createKortix(...) is imperative — you call a method, you get a promise — these hooks subscribe to live data and re-render as it changes. They're built on React Query and zustand, and kept current by OpenCode's SSE event stream.

You almost never touch the runtime plumbing directly. One hook runs a session:

import { useSession } from '@kortix/sdk/react';

function Chat({ projectId, sessionId }: { projectId: string; sessionId: string }) {
  const s = useSession(projectId, sessionId);

  if (s.phase !== 'ready') return <Booting stage={s.stage} onRetry={s.retry} />;

  return (
    <>
      {s.messages.map(({ info, parts }) => <Message key={info.id} info={info} parts={parts} />)}
      <Composer
        busy={s.isBusy}
        disabled={s.runtimePhase !== 'ready'}
        onSend={(text) => s.send(text)}
        onStop={s.cancel}
      />
    </>
  );
}

That's the whole contract. No event provider to mount, no health poller, no "server store" to switch, no sandbox id, no canonical-session resolution — useSession owns all of it internally.

useSession(projectId, sessionId, options?)

The single hook for an open session. Internally it drives POST /start (the server long-polls), resolves the session's runtime, opens the SSE stream, resolves the canonical OpenCode id, and syncs messages — exposing one phase plus the thread, the actions, the interactive prompts, and the pre-runtime capabilities.

Readiness is server-truth: the runtime is ready when /start returns stage: 'ready' (the backend only returns that once the daemon answered). There is no client-side health poll — so a fresh session's first turn streams immediately, and the old "stuck until you hard-refresh" failure mode cannot occur.

Call it once per session view (like a provider) — it owns the SSE subscription and the /start poll for that (projectId, sessionId).

Returns

fieldtypewhat
phase'starting' | 'ready' | 'error'top-level gate — render the boot screen until ready
stageSessionStartStage | nullraw /start stage (provisioning / starting / ready / …) for boot UI
runtimePhase'connecting' | 'booting' | 'ready' | 'unreachable'granular live connection phase (for a mid-session reconnect pill)
messages{ info, parts }[]the thread; parts stream in live
status · isBusySessionStatus · booleansession status · agent is generating
isLoading · isErrorbooleanfirst hydrate in flight · terminal /start state
questions · permissions[]pending agent questions / approvals (from the live prompt store)
hasPendingbooleanthere are open questions/permissions
diffs · todos[]live file diffs · todo items
pendingstring | nullthe optimistic in-flight user message text
isSending · sendErrorboolean · KortixSendError | nullcurrent send call in flight · its last failure (billing / runtime-not-ready / runtime-error), reset on every new send
opencodeSessionIdstring | nullthe resolved canonical OpenCode root id
models · agents[]selectable models · agents (server-side, available pre-runtime)
defaultAgent · commandsstring | null · []the project default agent · slash commands
picksSessionPicksper-session model/agent selection (persisted)
sandbox · switched · retriablerow · boolean · booleanthe /start sandbox row · runtime switched-in · can re-poll
startError · reasonterminal /start failure · string | nullrender instead of spinning forever · latest /start reason (e.g. 'runtime_waking') for boot UI

Actions

actionwhat
send(text, override?)send a prompt (optimistic). override = { model?, agent? } for this message
cancel()abort the run and clear any pending prompt + open questions/permissions
runCommand(command, args)run a project slash-command (/command)
answerQuestion(id, answers)reply to a pending agent question through the server
rejectQuestion(id)reject a pending agent question through the server
answerPermission(id, reply, message?)reply to a pending permission request — reply is 'once' | 'always' | 'reject'
retry()force a re-poll of /start (e.g. a boot-screen Retry button)

removeQuestion(id) / removePermission(id) are also on the returned object but deprecated: they drop the prompt from local state without replying to the server, so the agent run stays blocked waiting on it. Use answerQuestion / rejectQuestion / answerPermission above instead.

Options

optiondefaultwhat
waitMs15000long-poll budget requested on /start (the server clamps it)
replayStartStashtruereplay a stashed first message (see start-stash) once ready + empty
enabledtruegate the whole hook — set false until a precondition resolves (e.g. a billing check)
chatEnginetruemount the chat-consumption engine (message sync + the question self-heal poll) on this call. Set false when the host mounts its own chat surface for the same session (e.g. apps/web's SessionChat) — avoids double-mounting the same pollers; messages/diffs/todos stay empty and replayStartStash is force-disabled in that mode

Sending is optimistic and fire-and-forget: send shows your message instantly, then SSE events stream the agent's reply into the thread. useSession clears the optimistic bubble when your message lands (or after a 30s backstop).

KortixProjectProvider

useSession itself needs no provider — but a few route-scoped hooks (like useOpenCodeProviders() under More hooks) resolve "the project the user is looking at" from React context instead of a parameter, since the SDK is router-agnostic and can't call useParams() itself. A Next (or other router-based) host derives the project id once and mounts the provider near its root:

import { KortixProjectProvider } from '@kortix/sdk/react';

function ProjectLayout({ projectId, children }: { projectId: string; children: React.ReactNode }) {
  return <KortixProjectProvider projectId={projectId}>{children}</KortixProjectProvider>;
}

useKortixRouteProjectId() is the read side — it's what those hooks call internally, and it's exported for a host that wants the same value. Outside a project scope it returns null.

Capability hooks (available before the runtime)

Model, agent, and command catalogs come from the server, so you can build a picker on a "new session" screen before any sandbox exists. These are plain React Query reads — no runtime required.

import { useProjectModels, useVisibleAgents, useProjectConfig } from '@kortix/sdk/react';

const models = useProjectModels(projectId);        // selectable model rows
const agents = useVisibleAgents({ projectId });    // visible agents
const config = useProjectConfig(projectId);        // { open_code_default_agent, commands, … }

useSession already exposes these as s.models / s.agents / s.defaultAgent / s.commands for a session view; use the standalone hooks on screens that don't hold a session yet.

Primitives

Small building blocks the white-label reference uses — all owned by the SDK so every host shares one implementation.

primitivewhat
useSessionPicks(sessionId){ model, agent, setModel, setAgent } — per-session selection, persisted locally
useRuntimePhase()the granular 'connecting' | 'booting' | 'ready' | 'unreachable' phase (also s.runtimePhase)
generateSessionId()an RFC-4122 v4 id, with a fallback for non-secure contexts (http on a LAN) — imported from the root @kortix/sdk entry, not /react

Start-stash — the new-session → workbench hand-off. Your "what would you like to build?" screen collects a prompt + model + agent before the runtime exists; stash them under the new id, and useSession replays them as the first message once ready:

import { writeStartStash } from '@kortix/sdk/react';
import { generateSessionId } from '@kortix/sdk'; // root entry — not re-exported from /react

// new-session screen:
const sessionId = generateSessionId();
await kortix.project(projectId).sessions.create({ session_id: sessionId, name });
writeStartStash(sessionId, { prompt, model, agent });
router.push(`/projects/${projectId}/sessions/${sessionId}`);
// the session page just calls useSession(...) — the stash replays automatically.

Also readStartStash(sessionId) / clearStartStash(sessionId) / startStashKey.

A sibling fork-draft stash covers the same hand-off for forking a session at a message: stash the draft under the new forked session's id before navigating to it, then useForkSession() (below, under "Session list & CRUD") picks it up the same way useSession picks up a start-stash. forkDraftKey, writeForkDraft(sessionId, draft), readForkDraft(sessionId), clearForkDraft(sessionId).

Session list & CRUD

The session list and per-session lifecycle, as React Query queries and mutations.

import {
  useOpenCodeSessions,
  useCreateOpenCodeSession,
  useUpdateOpenCodeSession,
  useDeleteOpenCodeSession,
} from '@kortix/sdk/react';

function Sidebar() {
  const { data: sessions = [] } = useOpenCodeSessions();
  const create = useCreateOpenCodeSession();
  const rename = useUpdateOpenCodeSession();
  const remove = useDeleteOpenCodeSession();
  // …
}
hookpurpose
useOpenCodeSessions() · useOpenCodeSession(id)the live session list · one record
useCreateOpenCodeSession()create { directory?, title? }
useUpdateOpenCodeSession()rename / archive { sessionId, title?, archived? }
useDeleteOpenCodeSession() · useForkSession()delete · branch at a message
useSummarizeOpenCodeSession() · useInitSession()compact the thread · run /init
useOpenCodeSessionDiff(id) · useOpenCodeSessionTodo(id)working diff · todo list

Project data hooks

Reactive counterparts of the imperative p.secrets / p.triggers / p.changeRequests surfaces on The client — each a thin React Query binding with its own query-key factory, and mutations that invalidate the list so a write reflects without a manual refetch.

import { useProjectSecrets, useProjectTriggers, useChangeRequests } from '@kortix/sdk/react';

const secrets = useProjectSecrets(projectId);      // { data, upsert, remove, setPersonal, removePersonal }
const triggers = useProjectTriggers(projectId);     // { data, create, update, remove, fire }
const crs = useChangeRequests(projectId, 'open');   // { data, open, merge, close, requestChanges }
hookmirrorsquery key
useProjectSecrets(projectId)p.secrets — list + upsert/remove, personal-override set/removeprojectSecretsKey(projectId)
useProjectTriggers(projectId)p.triggers — list + create/update/remove/fireprojectTriggersKey(projectId)
useChangeRequests(projectId, status?)p.changeRequests — list (filterable) + open/merge/close/requestChangeschangeRequestsKey(projectId)

Runtime config

Read and write OpenCode's runtime config (opencode.jsonc as the server sees it); updates are optimistic and roll back on error.

import { useOpenCodeConfig, useUpdateOpenCodeConfig } from '@kortix/sdk/react';

const { data: config } = useOpenCodeConfig();
const update = useUpdateOpenCodeConfig();
update.mutate({ permission: { edit: 'allow' } });

MCP servers

Being retired. Tools/connectors are moving to the Executor connectors model — manage them via kortix.project(id).connectors, not these runtime MCP hooks. Documented for the current runtime, but prefer connectors for new code.

useOpenCodeMcpStatus(), useAddMcpServer(), useConnectMcpServer(), useDisconnectMcpServer(), and the OAuth trio useMcpAuthStart() / useMcpAuthCallback() / useMcpAuthRemove().

PTY terminal

A live terminal into the session's sandbox — list/create/remove/resize PTYs, then open a WebSocket for the byte stream.

import { useOpenCodePtyList, useCreatePty, useRemovePty, getPtyWebSocketUrl } from '@kortix/sdk/react';

const { data: ptys = [] } = useOpenCodePtyList();
const create = useCreatePty();
const url = await getPtyWebSocketUrl(id);   // wss://… with the auth token appended

Use useUpdatePty() to push a title or { rows, cols } resize.

Model store

useModelStore(allModels, opts?) is a zustand-backed store of the user's model preferences — visibility, recents, per-agent and per-session selections, the global default, variants. Persisted client-side and pure UI state (it does not fetch — pass the flattened catalog from kortix.project(id).llmCatalog() or useProjectModels()).

import { useModelStore, type ModelKey } from '@kortix/sdk/react';

const store = useModelStore(models, { connectedProviderIds });
store.setSessionModel(sessionId, model);

Helpers include isVisible / setVisibility, recent / pushRecent, getVariant / setVariant, getSessionModel / setSessionModel, globalDefault / setGlobalDefault, and userPrefs; seedGlobalDefaultFromServer(model) — a plain function, not a hook — seeds the global default from the server the first time it resolves.

Lower-level hooks (what useSession composes)

Internal plumbing. These are the pieces useSession wraps — the SSE provider, message sync, the prompt mutations, and the OpenCode-id resolver. A chat-focused host uses useSession and never touches them. They stay exported for hosts that build beyond chat (a full IDE — file tree, terminal, git) and for apps/web's own session-chat; if you're just rendering a conversation, reach for useSession.

hookcomposed role
useSessionSync(sessionId)fetch + live-merge the thread off the sync store
useSendOpenCodeMessage() · useAbortOpenCodeSession()prompt · abort — both take no arguments; the runtime is resolved through the single global client, not a per-session one
useExecuteOpenCodeCommand()run a slash-command
OpenCodeEventStreamProvider / useOpenCodeEventStream()open the SSE stream → the stores
useCanonicalOpenCodeSession({ projectId, sessionId, pinFromStart })resolve the OpenCode root id
useOpenCodePendingStore()the live questions/permissions store
useRuntimeReconnect()the independent liveness probe that detects a mid-session runtime death the SSE heartbeat can't see — polls getSessionHealth/isRuntimeReady and writes into the connection store
useQuestionSelfHeal() / hasRunningQuestionToolpolls for a missed question.asked SSE event and repairs pending-question state
usePermissionSelfHeal() / findPermissionBlockedCandidate / hasActiveNonQuestionToolthe same self-heal watchdog for missed permission requests
useGatewayCatalogSync(projectId)keeps the gateway model catalog cache warm

More hooks

hookwhat
useProjectModels · useVisibleAgents · useProjectConfigserver-side models · agents · config (pre-runtime)
useOpenCodeProviders()configured providers + their models
useOpenCodeCommands() · useExecuteOpenCodeCommand()slash commands · run one
useOpenCodeToolIds() · useOpenCodeSkills()available tool ids · skills
useShareSession() · useUnshareSession()public share links
useBackgroundSessionPrefetch(sessions)warm message caches on hover

The opencodeKeys query-key factory is exported too, for targeted invalidateQueries / setQueryData against the same cache.

Kortix Master (tasks, tickets, credentials, projects)

@kortix/sdk/react also re-exports the full Kortix Master React Query layer — the hooks behind a project's task board, ticket/kanban system, credentials vault, and project/team management. This is the same hook set apps/web's Kortix UI runs on end to end; it's exported from the SDK so any host can build the same surface.

These hooks talk to the /kortix/* daemon routes and are otherwise plain React Query — no relation to useSession's runtime plumbing.

Most of these hooks take a KortixMasterIdentity as their first argument instead of reading an auth hook internally:

interface KortixMasterIdentity {
  userId: string | null;
  handle: string;      // stamped as actor_id / created_by_id on mutations
  isLoading: boolean;
}

This is the one seam between the SDK and a host's auth stack — derive it from Supabase, a mobile auth SDK, a service-account token, whatever the host uses, and pass it in. Hooks that don't need authorship (plain reads/writes scoped only by project or id) skip the argument.

Credentials

A project's secrets vault (/kortix/projects/:id/credentials) — values are never returned by the list call; useRevealCredential is a separate, audit-logged read.

import { useCredentials, useUpsertCredential, useRevealCredential } from '@kortix/sdk/react';

const { data: credentials = [] } = useCredentials(projectId);
const upsert = useUpsertCredential();
upsert.mutate({ projectId, name: 'STRIPE_KEY', value: 'sk_...', description: 'Stripe secret key' });
hookpurpose
useCredentials(projectId) · useCredentialEvents(projectId, name)list a project's credentials · one credential's audit trail
useUpsertCredential()create or replace a credential's value
useRevealCredential()decrypt + return one value (audit-logged as a read)
useDeleteCredential()remove a credential

Kortix projects

The project registry itself (/kortix/projects) — distinct from an SDK Project (a sandbox/session container); a Kortix project is the task/ticket/credentials workspace layered on top.

import { useKortixProjects, useKortixProject, usePatchProject } from '@kortix/sdk/react';

const { data: projects = [] } = useKortixProjects(identity);
const { data: project } = useKortixProject(identity, projectId);
hookpurpose
useKortixProjects(identity) · useKortixProject(identity, id)list · one project
useKortixProjectForSession(identity, sessionId)resolve the Kortix project a session belongs to (null if none)
useKortixProjectSessions(identity, projectId)OpenCode sessions linked to a project
usePatchProject()update name / description / user_handle
useDeleteProject()delete a project

Tasks

The async task queue (/kortix/tasks) — a task is a unit of work an agent picks up, with a verification_condition an approval step checks against.

import { useKortixTasks, useCreateKortixTask, useStartKortixTask, useApproveKortixTask } from '@kortix/sdk/react';

const { data: tasks = [] } = useKortixTasks(projectId, 'in_progress');
const create = useCreateKortixTask();
create.mutate({ project_id: projectId, title: 'Fix the flaky test', verification_condition: 'CI green' });
hookpurpose
useKortixTasks(projectId?, status?) · useKortixTask(id)list (optionally filtered) · one task, both poll every 3s
useKortixTaskEvents(id) · useKortixTaskStatus(id)a task's event log · live execution status
useCreateKortixTask() · useUpdateKortixTask()create · patch
useStartKortixTask()hand the task to an agent (status → in_progress)
useApproveKortixTask()approve a task sitting in awaiting_review
useDeleteKortixTask()delete
normalizeTask(raw)pure helper — coerces an unvalidated wire row to KortixTask, defaulting an unrecognized status to 'todo'

KortixTaskStatus is one of todo · in_progress · input_needed · awaiting_review · completed · cancelled.

Tickets & kanban

The full ticket system (/kortix/tickets) plus a project's board configuration — columns, custom fields, templates, and the team of project agents tickets get assigned to.

import { useTickets, useCreateTicket, useAssignTicket, useCommentTicket } from '@kortix/sdk/react';

const { data: tickets = [] } = useTickets(projectId);
const createTicket = useCreateTicket(identity);
createTicket.mutate({ project_id: projectId, title: 'Ship the react.mdx page' });
hookpurpose
useTickets(projectId?) · useTicket(id) · useTicketEvents(id)list (polls 3s) · one ticket · its comment/audit trail
useCreateTicket(identity) · useUpdateTicket(identity)create · patch (title, body, custom fields, template, milestone)
useUpdateTicketStatus(identity)move a ticket to another column
useAssignTicket(identity) · useUnassignTicket(identity)assign/unassign a user or agent
useCommentTicket(identity)post a comment (drives @mention notifications)
useDeleteTicket()delete
useColumns(projectId) · useReplaceColumns()board columns · replace the whole set (label, terminal/off-flow flags, default assignee)
useFields(projectId) · useReplaceFields()custom fields · replace the set (text / number / date / select)
useTemplates(projectId) · useReplaceTemplates()ticket templates · replace the set
useProjectAgents(projectId)the project's team of agents tickets can be assigned to
useCreateProjectAgent() · useUpdateProjectAgent() · useDeleteProjectAgent()manage a project agent (persona, execution mode, tool groups, model)
useAgentPersona(projectId, slug)one agent's full persona body
useEnsurePmSession()create-if-missing the project's Project-Manager chat session (idempotent)

Activity & notifications — derived client-side from the ticket event stream, no separate notifications endpoint:

import { useProjectActivity, computeNotifications, readLastSeen, writeLastSeen } from '@kortix/sdk/react';

const { data: events = [] } = useProjectActivity(projectId); // polls 10s
const notifications = computeNotifications(events, identity.handle, readLastSeen(projectId, identity.handle));
hook / helperpurpose
useProjectActivity(projectId)the project's raw ticket-event stream (200 most recent)
computeUnread(events, handle, sinceIso)pure — unread count assigned-to-me / @mentioning me, total + per-ticket
computeNotifications(events, handle, sinceIso)pure — the same rule set as a renderable ProjectNotification[]
readLastSeen(projectId, handle) · writeLastSeen(projectId, handle, iso)localStorage last-seen cursor (write emits kortix:last-seen-changed for same-tab listeners)

Milestones

Grouping/tracking layer above tickets (/kortix/projects/:id/milestones) — a milestone has an acceptance note and closes with a summary.

hookpurpose
useMilestones(projectId, statusFilter?)list, filtered 'open' | 'closed' | 'all' (polls 5s)
useMilestone(projectId, ref) · useMilestoneEvents(projectId, ref)one milestone (with progress) · its event log
useCreateMilestone() · useUpdateMilestone()create · patch
useCloseMilestone() · useReopenMilestone()close (with summary_md, optionally cancelled) · reopen
useDeleteMilestone()delete
useSetTicketMilestone()link/unlink a ticket's milestone (goes through PATCH /kortix/tickets/:id)

Sandbox services

Long-running background processes inside the sandbox (/kortix/services) — dev servers, databases, workers — registered and supervised by the daemon.

hookpurpose
useSandboxServices(identity, opts?)list registered services (polls 5s); includeAll also returns hidden/system ones
useSandboxServiceTemplates(identity)available service templates
useSandboxServiceLogs(identity, serviceId)a service's log tail (polls 3s while a serviceId is set)
useSandboxServiceAction(){ serviceId, action } — start/stop/restart a service
useSandboxServiceReconcile()re-sync the daemon's service state against what's actually running, optionally reload
useRegisterSandboxService()register a new service
useSandboxRuntimeReload(){ mode } — reload the OpenCode runtime itself (systemReload, shared with ../opencode/client)

Next

  • The client — the imperative createKortix facade.
  • Sessions — the imperative session handle + the runtime.
  • Modules — lower-level subpath imports.
React hooks – Kortix Docs