zidane 6.5.0 → 6.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-B-AOovHz.js → acp-565X8nVP.js} +6 -7
- package/dist/{acp-B-AOovHz.js.map → acp-565X8nVP.js.map} +1 -1
- package/dist/acp-cli.js +6 -6
- package/dist/acp.d.ts +1 -1
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +1 -1
- package/dist/{agent-SQ6rxfpT.js → agent-CvO7UOxv.js} +1427 -610
- package/dist/agent-CvO7UOxv.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -2
- package/dist/{anthropic-D3lkw5mv.js → anthropic-Dov2mC5e.js} +3 -3
- package/dist/{anthropic-D3lkw5mv.js.map → anthropic-Dov2mC5e.js.map} +1 -1
- package/dist/{auth-BzYbbdN_.js → auth-B8ZuBcKC.js} +5 -5
- package/dist/{auth-BzYbbdN_.js.map → auth-B8ZuBcKC.js.map} +1 -1
- package/dist/chat/pure.d.ts +4 -4
- package/dist/chat/pure.js +3 -3
- package/dist/chat.d.ts +114 -5
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +6 -6
- package/dist/context-segments-1mcqSCM3.js +494 -0
- package/dist/context-segments-1mcqSCM3.js.map +1 -0
- package/dist/{context-transport-CDBatvQc.js → context-transport-ClvadrlH.js} +3 -3
- package/dist/{context-transport-CDBatvQc.js.map → context-transport-ClvadrlH.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +3 -3
- package/dist/{index-CWBSrqI6.d.ts → index-DfERTrql.d.ts} +178 -6
- package/dist/index-DfERTrql.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -10
- package/dist/index.js.map +1 -1
- package/dist/{login-XZDS4K2o.js → login-Cl6ML2XN.js} +2 -2
- package/dist/{login-XZDS4K2o.js.map → login-Cl6ML2XN.js.map} +1 -1
- package/dist/{mcp-D27rP47I.js → mcp-DdntU47g.js} +3 -3
- package/dist/{mcp-D27rP47I.js.map → mcp-DdntU47g.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-C01kKGj0.js → messages-C_H9soi5.js} +2 -3
- package/dist/{messages-C01kKGj0.js.map → messages-C_H9soi5.js.map} +1 -1
- package/dist/{openai-compat-B3c_hzwf.js → openai-compat-CnHeo4Lz.js} +216 -3
- package/dist/openai-compat-CnHeo4Lz.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/plugins.d.ts +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +2 -2
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-z1vU0b2Q.js → providers-B6mFe7OD.js} +3 -3
- package/dist/{providers-z1vU0b2Q.js.map → providers-B6mFe7OD.js.map} +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +4 -4
- package/dist/restate.d.ts +178 -2
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +458 -4
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-cr9VdoEO.js → session-VpLrNaFQ.js} +2 -2
- package/dist/{session-cr9VdoEO.js.map → session-VpLrNaFQ.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +1 -1
- package/dist/{stdio-O-m9WVkE.js → stdio-BQeCMmuK.js} +2 -2
- package/dist/{stdio-O-m9WVkE.js.map → stdio-BQeCMmuK.js.map} +1 -1
- package/dist/{stdio-loader-BSGQmG4L.js → stdio-loader-BN1l1ZAG.js} +2 -2
- package/dist/{stdio-loader-BSGQmG4L.js.map → stdio-loader-BN1l1ZAG.js.map} +1 -1
- package/dist/{tool-formatters-gFqQbzPY.d.ts → tool-formatters-B2gBbKaS.d.ts} +2 -2
- package/dist/{tool-formatters-gFqQbzPY.d.ts.map → tool-formatters-B2gBbKaS.d.ts.map} +1 -1
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -2
- package/dist/{transcript-anchors-qCs3wZpu.d.ts → transcript-anchors-CeIxhKP7.d.ts} +2 -2
- package/dist/{transcript-anchors-qCs3wZpu.d.ts.map → transcript-anchors-CeIxhKP7.d.ts.map} +1 -1
- package/dist/{transcript-anchors-D4IxJifV.js → transcript-anchors-CmFKUw8M.js} +139 -19
- package/dist/transcript-anchors-CmFKUw8M.js.map +1 -0
- package/dist/tui.d.ts +74 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +819 -83
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-M9-jUyDd.d.ts → turn-operations-Dyv3Z-Ff.d.ts} +3 -3
- package/dist/{turn-operations-M9-jUyDd.d.ts.map → turn-operations-Dyv3Z-Ff.d.ts.map} +1 -1
- package/dist/{turn-operations-CAvi8NYt.js → turn-operations-iGu5J08j.js} +2 -2
- package/dist/{turn-operations-CAvi8NYt.js.map → turn-operations-iGu5J08j.js.map} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/{xai-BPwMQSuF.js → xai-Du6Wfnlb.js} +2 -2
- package/dist/{xai-BPwMQSuF.js.map → xai-Du6Wfnlb.js.map} +1 -1
- package/docs/CHAT.md +42 -1
- package/docs/INTERACTIONS.md +39 -0
- package/docs/RESTATE.md +102 -3
- package/docs/TUI.md +28 -2
- package/package.json +1 -1
- package/dist/agent-SQ6rxfpT.js.map +0 -1
- package/dist/aliasing-lVFbm81n.js +0 -217
- package/dist/aliasing-lVFbm81n.js.map +0 -1
- package/dist/context-breakdown-Dzo25N45.js +0 -155
- package/dist/context-breakdown-Dzo25N45.js.map +0 -1
- package/dist/edit-utils-EGosADZq.js +0 -234
- package/dist/edit-utils-EGosADZq.js.map +0 -1
- package/dist/index-CWBSrqI6.d.ts.map +0 -1
- package/dist/openai-compat-B3c_hzwf.js.map +0 -1
- package/dist/tool-state-DTs-3ZHH.js +0 -653
- package/dist/tool-state-DTs-3ZHH.js.map +0 -1
- package/dist/transcript-anchors-D4IxJifV.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { d as ClassifiedErrorKind, i as AgentContextExceededError, l as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, n as AgentAbortedError, o as AgentProviderError, p as matchesContextExceeded, r as AgentBudgetExceededError, s as AgentToolNotAllowedError, u as ClassifiedError } from "./timeout-DEaqslLO.js";
|
|
2
|
-
import { $
|
|
2
|
+
import { $m as RepeatGuardConfig, Ah as toolOutputBudgetByteLength, Bf as StreamCallbacks, Bm as McpToolHookContext, C as SuccessfulMutationOutcome, Ch as ToolResultImageContent, Cm as AgentClock, Dh as ToolResultVideoContent, Dp as OpenAIParams, Eh as ToolResultTextContent, Fh as ToolMap, Gf as ToolSpec, Gi as AgentHookMap, Gm as ProgressNudgeKind, Jh as ToolStateOp, Jm as PromptDocumentPart, Jp as AnthropicParams, Kf as TurnResult, Kh as StagedToolState, Ki as AgentHooks, Km as ProgressNudgeMessage, Lf as Provider, Lh as ReadStateEntry, Mh as toolResultToText, Nh as ToolContext, Ns as SpawnToolOptions, Oh as TurnFinishReason, Os as ChildAgent, Ph as ToolDef, Pm as DEFAULT_AGENT_CLOCK, Ps as SpawnToolState, Qm as PromptVideoPart, Rf as ProviderCapabilities, Rh as ReadStateMap, Rr as InteractionToolOptions, S as MutationValidationResult, Sg as RemoteStoreOptions, Sh as ToolResultDocumentRefContent, Sm as AgentBehavior, Th as ToolResultMetadata, Tm as AgentStats, Uf as ToolCall, Vf as StreamOptions, Wf as ToolResult, Wi as Agent, Wm as OAuthRefreshHookContext, Xh as ToolStateView, Xm as PromptPart, Yh as ToolStateStore, Ym as PromptImagePart, Zm as PromptTextPart, _ as statsByModel, _h as ToolHookContext, ag as SkillsConfig, ah as SessionContentBlock, b as MutationValidationHooksOptions, bh as ToolResultContent, cg as Session, ch as SessionMessage, dg as SessionRun, eh as RepeatGuardToolMatcher, fg as SessionStore, h as flattenTurns, hh as ThinkingLevel, ih as RunHookMap, jh as toolOutputByteLength, kh as TurnUsage, km as ChildRunStats, lg as SessionData, lh as SessionTurn, nh as ResolveContentRefBlock, og as CreateSessionOptions, oh as SessionEndStatus, p as ModelUsage, ph as StreamHookContext, qh as ToolStateController, qi as AgentOptions, qm as PromptAudioPart, rg as SkillResource, rh as RetryConfig, rp as OpenRouterParams, sh as SessionHookContext, t as Preset, t_ as McpConnection, tg as SkillConfig, th as ResolveContentRef, uh as SpawnHookContext, v as MutationValidationBatch, vh as ToolOutcome, vr as ValidationResult, wh as ToolResultImageRefContent, wm as AgentRunOptions, x as MutationValidationMutation, xh as ToolResultDocumentContent, y as MutationValidationFailure, yh as ToolResultAudioContent, zm as McpServerConfig, zp as CerebrasParams } from "./index-DfERTrql.js";
|
|
3
3
|
import { _ as TaskHandle, a as ContextType, c as ExecutionContext, g as TaskExitInfo, h as TaskEntry, l as ExecutionHandle, n as ContextCapabilities, o as DetachedTasksCapability, p as SpawnConfig, r as ContextHardening, s as ExecResult, t as BackgroundTaskStatus, v as TaskStallInfo } from "./types-M2DauK2D.js";
|
|
4
4
|
import { t as SandboxProvider } from "./sandbox-DLY6uah7.js";
|
|
5
5
|
export { type Agent, AgentAbortedError, type AgentBehavior, AgentBudgetExceededError, type AgentClock, AgentContextExceededError, type AgentHookMap, type AgentHooks, type AgentOptions, AgentProviderError, type AgentRunOptions, type AgentStats, AgentToolNotAllowedError, type AnthropicParams, type BackgroundTaskStatus, CONTEXT_EXCEEDED_MESSAGE_PATTERNS, type CerebrasParams, type ChildAgent, type ChildRunStats, type ClassifiedError, type ClassifiedErrorKind, type ContextCapabilities, type ContextHardening, type ContextType, type CreateSessionOptions, DEFAULT_AGENT_CLOCK, type DetachedTasksCapability, type ExecResult, type ExecutionContext, type ExecutionHandle, type InteractionToolOptions, type McpConnection, type McpServerConfig, type McpToolHookContext, type ModelUsage, type MutationValidationBatch, type MutationValidationFailure, type MutationValidationHooksOptions, type MutationValidationMutation, type MutationValidationResult, type OAuthRefreshHookContext, type OpenAIParams, type OpenRouterParams, type Preset, type ProgressNudgeKind, type ProgressNudgeMessage, type PromptAudioPart, type PromptDocumentPart, type PromptImagePart, type PromptPart, type PromptTextPart, type PromptVideoPart, type Provider, type ProviderCapabilities, type ReadStateEntry, type ReadStateMap, type RemoteStoreOptions, type RepeatGuardConfig, type RepeatGuardToolMatcher, type ResolveContentRef, type ResolveContentRefBlock, type RetryConfig, type RunHookMap, type SandboxProvider, type Session, type SessionContentBlock, type SessionData, type SessionEndStatus, type SessionHookContext, type SessionMessage, type SessionRun, type SessionStore, type SessionTurn, type SkillConfig, type SkillResource, type SkillsConfig, type SpawnConfig, type SpawnHookContext, type SpawnToolOptions, type SpawnToolState, type StagedToolState, type StreamCallbacks, type StreamHookContext, type StreamOptions, type SuccessfulMutationOutcome, type TaskEntry, type TaskExitInfo, type TaskHandle, type TaskStallInfo, type ThinkingLevel, type ToolCall, type ToolContext, type ToolDef, type ToolHookContext, type ToolMap, type ToolOutcome, type ToolResult, type ToolResultAudioContent, type ToolResultContent, type ToolResultDocumentContent, type ToolResultDocumentRefContent, type ToolResultImageContent, type ToolResultImageRefContent, type ToolResultMetadata, type ToolResultTextContent, type ToolResultVideoContent, type ToolSpec, type ToolStateController, type ToolStateOp, type ToolStateStore, type ToolStateView, type TurnFinishReason, type TurnResult, type TurnUsage, type ValidationResult, flattenTurns, matchesContextExceeded, statsByModel, toolOutputBudgetByteLength, toolOutputByteLength, toolResultToText };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { m as openaiCompat } from "./openai-compat-
|
|
1
|
+
import { m as openaiCompat } from "./openai-compat-CnHeo4Lz.js";
|
|
2
2
|
import { i as isOAuthAccessToken, r as extractRuntimeCredentials, s as resolveOAuthApiKey } from "./oauth-bkEOybqm.js";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
//#region src/chat/oauth-page/pkce.ts
|
|
@@ -376,4 +376,4 @@ function xai(params) {
|
|
|
376
376
|
//#endregion
|
|
377
377
|
export { createXaiOAuthProvider as n, generatePkce as r, xai as t };
|
|
378
378
|
|
|
379
|
-
//# sourceMappingURL=xai-
|
|
379
|
+
//# sourceMappingURL=xai-Du6Wfnlb.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xai-BPwMQSuF.js","names":[],"sources":["../src/chat/oauth-page/pkce.ts","../src/chat/oauth-page/xai.ts","../src/providers/xai.ts"],"sourcesContent":["/**\n * PKCE helper. Inlined here (rather than imported from pi-ai) because\n * `@earendil-works/pi-ai/oauth` does not re-export it, and we don't want\n * to reach into `node_modules/.../utils/oauth/pkce.js` — that's an\n * implementation-detail path that breaks on minor upstream rearrangements.\n *\n * Web Crypto API only. Works under Bun + Node 22+ without any node:crypto\n * import (matches pi-ai's own behavior). Output is base64url per RFC 7636.\n */\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes)\n binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '')\n}\n\nexport interface PkcePair {\n /** Random verifier used in the token-exchange step. */\n verifier: string\n /** SHA-256(verifier) sent on the authorize URL. */\n challenge: string\n}\n\nexport async function generatePkce(): Promise<PkcePair> {\n const verifierBytes = new Uint8Array(32)\n crypto.getRandomValues(verifierBytes)\n const verifier = base64UrlEncode(verifierBytes)\n\n const data = new TextEncoder().encode(verifier)\n const hashBuffer = await crypto.subtle.digest('SHA-256', data)\n const challenge = base64UrlEncode(new Uint8Array(hashBuffer))\n\n return { verifier, challenge }\n}\n","/**\n * xAI Grok OAuth flow (SuperGrok / X Premium+).\n *\n * xAI is **not** built into pi-ai, so — like Cursor — we implement the flow\n * here and register it with pi-ai's OAuth registry (`registerXaiOAuthProvider`)\n * so `getOAuthApiKey('xai-oauth', …)` resolves to it during lazy token refresh.\n *\n * Auth shape: OAuth 2.0 + PKCE with a loopback callback server, against xAI's\n * OIDC issuer (`https://auth.x.ai`). Differs from the Anthropic / Codex flows\n * (which use the shared `startCallbackServer`) on three points, so it gets its\n * own login impl rather than reusing that primitive:\n *\n * 1. The `redirect_uri` must be `http://127.0.0.1:56121/callback` verbatim\n * (xAI matches the registered loopback IP, not `localhost`).\n * 2. Endpoints come from OIDC discovery, not hard-coded constants.\n * 3. Token exchange + refresh are `application/x-www-form-urlencoded`\n * (the Anthropic flow posts JSON).\n *\n * The callback page still routes through {@link OAuthCallbackPageRenderer} so\n * the post-redirect HTML matches the rest of zidane's themed flows.\n *\n * Flow + constants adapted from the public reference implementations\n * (`stnly/pi-grok`, `BlockedPath/pi-xai-oauth`) and the Hermes Agent docs.\n * xAI's OAuth surface is unofficial/undocumented — re-verify the client id,\n * scopes, and callback port if login starts failing.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n} from '@earendil-works/pi-ai/oauth'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { OAuthProviderInterface } from '../../providers/oauth-registry'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { createServer } from 'node:http'\nimport { generatePkce } from './pkce'\n\n/** pi-ai / zidane OAuth provider id. Also the credentials-file key. */\nexport const XAI_OAUTH_PROVIDER_ID = 'xai-oauth'\n\nconst DEFAULT_BASE_URL = 'https://api.x.ai/v1'\nconst ISSUER = 'https://auth.x.ai'\nconst DISCOVERY_URL = `${ISSUER}/.well-known/openid-configuration`\n/** Public client id used by the Grok CLI OAuth surface. */\nconst DEFAULT_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828'\nconst DEFAULT_SCOPE = 'openid profile email offline_access grok-cli:access api:access'\n/** xAI matches the registered loopback IP exactly — `localhost` is rejected. */\nconst DEFAULT_CALLBACK_HOST = '127.0.0.1'\nconst DEFAULT_CALLBACK_PORT = 56121\nconst CALLBACK_PATH = '/callback'\nconst PROVIDER_NAME = 'xAI Grok'\n/** Refresh slightly early so requests don't race expiry. */\nconst REFRESH_SKEW_MS = 120_000\nconst CALLBACK_TIMEOUT_MS = 180_000\n\nfunction xaiOAuthClientId(): string {\n return process.env.PI_XAI_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID\n}\n\nfunction xaiOAuthScope(): string {\n return process.env.PI_XAI_OAUTH_SCOPE || DEFAULT_SCOPE\n}\n\nfunction xaiOAuthCallbackHost(): string {\n return process.env.PI_XAI_OAUTH_CALLBACK_HOST || DEFAULT_CALLBACK_HOST\n}\n\nfunction xaiOAuthCallbackPort(): number {\n const parsed = Number.parseInt(process.env.PI_XAI_OAUTH_CALLBACK_PORT || String(DEFAULT_CALLBACK_PORT), 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CALLBACK_PORT\n}\n\n/** Resolve the xAI API base URL (env override → default). */\nexport function xaiBaseUrl(): string {\n return (process.env.PI_XAI_BASE_URL || process.env.XAI_BASE_URL || DEFAULT_BASE_URL)\n .replace(/\\/+$/, '')\n}\n\ninterface XaiDiscovery {\n authorization_endpoint: string\n token_endpoint: string\n}\n\n/**\n * xAI OAuth credentials. Extends the base `{ access, refresh, expires }` with\n * the resolved `token_endpoint` so refresh doesn't need a second discovery\n * round-trip, plus the cached discovery doc for validation.\n */\nexport interface XaiOAuthCredentials extends OAuthCredentials {\n tokenEndpoint?: string\n discovery?: XaiDiscovery\n}\n\nfunction base64Url(bytes: Uint8Array): string {\n let binary = ''\n for (const b of bytes)\n binary += String.fromCharCode(b)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction randomToken(byteLength = 16): string {\n return base64Url(crypto.getRandomValues(new Uint8Array(byteLength)))\n}\n\n/**\n * Refuse any OIDC endpoint that isn't HTTPS on an xAI origin.\n *\n * The discovery doc is cached long-term in `credentials.json`. A single MITM\n * during initial login could otherwise pin a malicious `token_endpoint` that\n * receives every subsequent refresh token. Validating scheme + host keeps the\n * endpoint on `x.ai` / `*.x.ai`.\n */\nfunction validateEndpoint(value: string, field: string): string {\n let url: URL\n try {\n url = new URL(value)\n }\n catch {\n throw new Error(`xAI OAuth discovery returned an invalid ${field}: ${value}`)\n }\n if (url.protocol !== 'https:')\n throw new Error(`xAI OAuth ${field} must use HTTPS: ${value}`)\n const host = url.hostname.toLowerCase()\n if (host !== 'x.ai' && !host.endsWith('.x.ai'))\n throw new Error(`Refusing non-xAI OAuth ${field}: ${value}`)\n return url.toString()\n}\n\nasync function discover(): Promise<XaiDiscovery> {\n let response: Response\n try {\n response = await fetch(DISCOVERY_URL, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n })\n }\n catch (cause) {\n throw new Error(`xAI OIDC discovery failed: ${cause instanceof Error ? cause.message : String(cause)}`)\n }\n if (!response.ok)\n throw new Error(`xAI OIDC discovery returned ${response.status}`)\n\n const payload = await response.json() as Record<string, unknown>\n return {\n authorization_endpoint: validateEndpoint(String(payload.authorization_endpoint ?? ''), 'authorization_endpoint'),\n token_endpoint: validateEndpoint(String(payload.token_endpoint ?? ''), 'token_endpoint'),\n }\n}\n\ninterface CallbackResult {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\ninterface XaiCallbackServer {\n redirectUri: string\n waitForCallback: (timeoutMs: number) => Promise<CallbackResult>\n close: () => void\n}\n\n/**\n * Start the loopback callback server on `127.0.0.1:56121`. Falls back to an\n * OS-assigned port if 56121 is taken — xAI accepts any loopback port as long\n * as the `redirect_uri` we send on the authorize URL matches the listener.\n */\nasync function startXaiCallbackServer(renderPage: OAuthCallbackPageRenderer): Promise<XaiCallbackServer> {\n const callbackHost = xaiOAuthCallbackHost()\n const callbackPort = xaiOAuthCallbackPort()\n let settle: ((value: CallbackResult) => void) | undefined\n let settled = false\n const callbackPromise = new Promise<CallbackResult>((resolve) => {\n settle = (value) => {\n if (settled)\n return\n settled = true\n resolve(value)\n }\n })\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n try {\n // accounts.x.ai may issue a CORS preflight against the loopback listener.\n const origin = req.headers.origin\n if (origin === 'https://accounts.x.ai' || origin === 'https://auth.x.ai') {\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type')\n res.setHeader('Access-Control-Allow-Private-Network', 'true')\n res.setHeader('Vary', 'Origin')\n }\n if (req.method === 'OPTIONS') {\n res.statusCode = 204\n res.end()\n return\n }\n\n const url = new URL(req.url ?? '/', `http://${callbackHost}`)\n if (url.pathname !== CALLBACK_PATH) {\n res.statusCode = 404\n res.end('Not found')\n return\n }\n\n const result: CallbackResult = {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n error: url.searchParams.get('error') ?? undefined,\n errorDescription: url.searchParams.get('error_description') ?? undefined,\n }\n\n res.statusCode = result.error ? 400 : 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(renderPage(result.error\n ? {\n kind: 'error',\n provider: PROVIDER_NAME,\n message: `${PROVIDER_NAME} authentication did not complete.`,\n details: result.errorDescription ?? result.error,\n }\n : {\n kind: 'success',\n provider: PROVIDER_NAME,\n message: 'xAI authentication completed. You can close this window.',\n }))\n settle?.(result)\n }\n catch {\n res.statusCode = 500\n res.end('Internal error')\n }\n })\n\n const listen = (port: number) => new Promise<number>((resolve, reject) => {\n server.once('error', reject)\n server.listen(port, callbackHost, () => {\n server.removeListener('error', reject)\n const addr = server.address()\n resolve(typeof addr === 'object' && addr ? addr.port : port)\n })\n })\n\n let actualPort: number\n try {\n actualPort = await listen(callbackPort)\n }\n catch {\n actualPort = await listen(0)\n }\n\n return {\n redirectUri: `http://${callbackHost}:${actualPort}${CALLBACK_PATH}`,\n waitForCallback: timeoutMs => Promise.race([\n callbackPromise,\n new Promise<CallbackResult>(resolve =>\n setTimeout(\n resolve,\n timeoutMs,\n { error: 'timeout', errorDescription: 'Timed out waiting for the xAI OAuth callback.' },\n )),\n ]),\n close: () => {\n try { server.close() }\n catch { /* already closed */ }\n },\n }\n}\n\nfunction expiryFromPayload(payload: Record<string, unknown>): number {\n const expiresIn = typeof payload.expires_in === 'number'\n ? payload.expires_in\n : Number(payload.expires_in ?? 3600)\n return Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS\n}\n\nasync function exchangeCode(\n tokenEndpoint: string,\n code: string,\n redirectUri: string,\n verifier: string,\n discovery: XaiDiscovery,\n): Promise<XaiOAuthCredentials> {\n const clientId = xaiOAuthClientId()\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: clientId,\n code,\n redirect_uri: redirectUri,\n code_verifier: verifier,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token exchange failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n const refresh = String(payload.refresh_token ?? '')\n if (!access)\n throw new Error('xAI token exchange did not return an access_token.')\n if (!refresh)\n throw new Error('xAI token exchange did not return a refresh_token.')\n\n return { access, refresh, expires: expiryFromPayload(payload), tokenEndpoint, discovery }\n}\n\nexport async function loginXai(\n renderPage: OAuthCallbackPageRenderer,\n callbacks: OAuthLoginCallbacks,\n): Promise<XaiOAuthCredentials> {\n const discovery = await discover()\n const { verifier, challenge } = await generatePkce()\n const state = randomToken()\n const nonce = randomToken()\n const server = await startXaiCallbackServer(renderPage)\n\n try {\n const authUrl = new URL(discovery.authorization_endpoint)\n authUrl.searchParams.set('response_type', 'code')\n authUrl.searchParams.set('client_id', xaiOAuthClientId())\n authUrl.searchParams.set('redirect_uri', server.redirectUri)\n authUrl.searchParams.set('scope', xaiOAuthScope())\n authUrl.searchParams.set('code_challenge', challenge)\n authUrl.searchParams.set('code_challenge_method', 'S256')\n authUrl.searchParams.set('state', state)\n authUrl.searchParams.set('nonce', nonce)\n // Opt into xAI's generic OAuth plan tier (SuperGrok / X Premium+).\n authUrl.searchParams.set('plan', 'generic')\n authUrl.searchParams.set('referrer', 'zidane')\n\n callbacks.onAuth({\n url: authUrl.toString(),\n instructions: `Sign in to xAI and approve access. If the browser is on another machine, the callback listener is ${server.redirectUri}.`,\n })\n\n const result = await server.waitForCallback(CALLBACK_TIMEOUT_MS)\n if (result.error)\n throw new Error(result.errorDescription ?? result.error)\n if (result.state !== state)\n throw new Error('xAI OAuth state mismatch — possible CSRF. Please retry.')\n if (!result.code)\n throw new Error('xAI OAuth callback did not include an authorization code.')\n\n callbacks.onProgress?.('Exchanging authorization code for tokens...')\n return await exchangeCode(discovery.token_endpoint, result.code, server.redirectUri, verifier, discovery)\n }\n finally {\n server.close()\n }\n}\n\nexport async function refreshXaiToken(credentials: OAuthCredentials): Promise<XaiOAuthCredentials> {\n const xai = credentials as XaiOAuthCredentials\n const clientId = xaiOAuthClientId()\n if (!credentials.refresh)\n throw new Error('xAI credentials are missing a refresh token — re-login required.')\n\n const tokenEndpoint = xai.tokenEndpoint\n ?? xai.discovery?.token_endpoint\n ?? (await discover()).token_endpoint\n validateEndpoint(tokenEndpoint, 'token_endpoint')\n\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: clientId,\n refresh_token: credentials.refresh,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token refresh failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n if (!access)\n throw new Error('xAI token refresh did not return an access_token.')\n\n return {\n ...xai,\n access,\n // xAI may omit a rotated refresh token — keep the existing one.\n refresh: String(payload.refresh_token ?? credentials.refresh),\n expires: expiryFromPayload(payload),\n tokenEndpoint,\n }\n}\n\n/**\n * Build an xAI `OAuthProviderInterface`. Shape matches Anthropic / Codex /\n * Cursor so it drops into `BUILTIN_PROVIDERS` and `src/auth.ts` unchanged.\n *\n * `usesCallbackServer: true` — the loopback flow has a real callback server,\n * so the TUI doesn't need to prompt for a manual code paste.\n */\nexport function createXaiOAuthProvider(renderPage: OAuthCallbackPageRenderer): OAuthProviderInterface {\n return {\n id: XAI_OAUTH_PROVIDER_ID,\n name: 'xAI Grok (SuperGrok / X Premium+)',\n usesCallbackServer: true,\n login: callbacks => loginXai(renderPage, callbacks),\n refreshToken: refreshXaiToken,\n getApiKey: credentials => credentials.access,\n }\n}\n","import type { Provider, ProviderCapabilities, StreamCallbacks, StreamOptions, TurnResult } from '.'\nimport type { OAuthParams } from './oauth'\nimport { XAI_OAUTH_PROVIDER_ID, xaiBaseUrl } from '../chat/oauth-page/xai'\nimport { extractRuntimeCredentials, isOAuthAccessToken, resolveOAuthApiKey } from './oauth'\nimport { openaiCompat } from './openai-compat'\n\nconst DEFAULT_MODEL = 'grok-4.3'\n\n/**\n * xAI Grok provider — supports both a static API key (`XAI_API_KEY`) and the\n * SuperGrok / X Premium+ OAuth subscription.\n *\n * Both auth modes hit the same OpenAI-compatible endpoint (`https://api.x.ai/v1`),\n * so the wire handling is entirely {@link openaiCompat}. The only difference is\n * the bearer:\n *\n * - **API key**: `XAI_API_KEY` (or `params.apiKey`) → used verbatim.\n * - **OAuth**: no static key → {@link resolveOAuthApiKey} resolves + lazily\n * refreshes the stored `xai-oauth` token on every turn.\n *\n * `resolveOAuthApiKey` unifies the two: a real API key in `XAI_API_KEY` is\n * returned as-is, while an OAuth access token (or no env value at all) falls\n * through to the refreshable stored credentials. So this single factory serves\n * both paths without branching at construction time.\n */\nexport interface XaiParams extends OAuthParams {\n defaultModel?: string\n /**\n * Provider capability flags. Grok models are vision-capable; default\n * `{ vision: true, imageInToolResult: false }`. Override for text-only\n * deployments.\n */\n capabilities?: ProviderCapabilities\n /** Extra HTTP headers sent on every request. */\n extraHeaders?: Record<string, string>\n}\n\nexport function xai(params?: XaiParams): Provider {\n const defaultModel = params?.defaultModel || DEFAULT_MODEL\n const baseURL = xaiBaseUrl()\n const capabilities: ProviderCapabilities = params?.capabilities ?? {\n vision: true,\n imageInToolResult: false,\n }\n const baseCredentials = extractRuntimeCredentials(params)\n let runtimeCredentials = baseCredentials\n\n // Factory-time snapshot for the static `meta.isOAuth` display flag only (the\n // terminal header's `oauth`/`key` badge). The real bearer resolves per turn\n // in `stream`, so this is best-effort, mirroring anthropic's pattern. It's\n // OAuth unless a genuine static API key is present: a `xai-…` key → key mode;\n // runtime OAuth creds, a JWT access token, or no static key (stored creds\n // used) → oauth mode.\n const staticKey = params?.apiKey ?? process.env.XAI_API_KEY\n const isOAuth = baseCredentials !== undefined || !staticKey || isOAuthAccessToken(staticKey)\n\n /**\n * Build the openai-compat delegate for a resolved bearer. Cheap — the factory\n * only wires closures — so we re-create it per turn with the freshly resolved\n * (possibly refreshed) OAuth token rather than baking a stale key in once.\n */\n function delegateFor(apiKey: string): Provider {\n return openaiCompat({\n name: 'xai',\n apiKey,\n baseURL,\n defaultModel,\n capabilities,\n extraHeaders: params?.extraHeaders,\n })\n }\n\n // A delegate built with a placeholder key supplies the non-stream methods\n // (formatTools, userMessage, message builders, classifyError). Only `stream`\n // resolves the real bearer, so the placeholder never reaches the wire.\n const skeleton = delegateFor('placeholder-resolved-at-stream')\n\n return {\n ...skeleton,\n name: 'xai',\n // Reuse the delegate's normalized capability flags (openaiCompat fills the\n // audio/video/documents defaults) so the advertised shape matches the wire.\n meta: { ...skeleton.meta, defaultModel, isOAuth },\n\n async stream(options: StreamOptions, callbacks: StreamCallbacks): Promise<TurnResult> {\n const apiKey = await resolveOAuthApiKey(\n {\n provider: 'xai',\n providerId: XAI_OAUTH_PROVIDER_ID,\n params: runtimeCredentials ? { ...params, ...runtimeCredentials } : params,\n envKey: 'XAI_API_KEY',\n missingError: 'No xAI credentials found. Set XAI_API_KEY or run `bun run auth --xai` first.',\n refreshError: reason => `xAI OAuth token refresh failed. Run \\`bun run auth --xai\\` again. ${reason}`,\n },\n {\n ...callbacks,\n async onOAuthRefresh(ctx) {\n // Keep param-sourced credentials current so a long-lived provider\n // instance reuses the rotated token instead of re-refreshing.\n if (ctx.source === 'params') {\n runtimeCredentials = {\n access: ctx.credentials.access,\n refresh: ctx.credentials.refresh,\n expires: ctx.credentials.expires,\n }\n }\n await callbacks.onOAuthRefresh?.(ctx)\n },\n },\n )\n\n return delegateFor(apiKey).stream(options, callbacks)\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"xai-Du6Wfnlb.js","names":[],"sources":["../src/chat/oauth-page/pkce.ts","../src/chat/oauth-page/xai.ts","../src/providers/xai.ts"],"sourcesContent":["/**\n * PKCE helper. Inlined here (rather than imported from pi-ai) because\n * `@earendil-works/pi-ai/oauth` does not re-export it, and we don't want\n * to reach into `node_modules/.../utils/oauth/pkce.js` — that's an\n * implementation-detail path that breaks on minor upstream rearrangements.\n *\n * Web Crypto API only. Works under Bun + Node 22+ without any node:crypto\n * import (matches pi-ai's own behavior). Output is base64url per RFC 7636.\n */\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes)\n binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '')\n}\n\nexport interface PkcePair {\n /** Random verifier used in the token-exchange step. */\n verifier: string\n /** SHA-256(verifier) sent on the authorize URL. */\n challenge: string\n}\n\nexport async function generatePkce(): Promise<PkcePair> {\n const verifierBytes = new Uint8Array(32)\n crypto.getRandomValues(verifierBytes)\n const verifier = base64UrlEncode(verifierBytes)\n\n const data = new TextEncoder().encode(verifier)\n const hashBuffer = await crypto.subtle.digest('SHA-256', data)\n const challenge = base64UrlEncode(new Uint8Array(hashBuffer))\n\n return { verifier, challenge }\n}\n","/**\n * xAI Grok OAuth flow (SuperGrok / X Premium+).\n *\n * xAI is **not** built into pi-ai, so — like Cursor — we implement the flow\n * here and register it with pi-ai's OAuth registry (`registerXaiOAuthProvider`)\n * so `getOAuthApiKey('xai-oauth', …)` resolves to it during lazy token refresh.\n *\n * Auth shape: OAuth 2.0 + PKCE with a loopback callback server, against xAI's\n * OIDC issuer (`https://auth.x.ai`). Differs from the Anthropic / Codex flows\n * (which use the shared `startCallbackServer`) on three points, so it gets its\n * own login impl rather than reusing that primitive:\n *\n * 1. The `redirect_uri` must be `http://127.0.0.1:56121/callback` verbatim\n * (xAI matches the registered loopback IP, not `localhost`).\n * 2. Endpoints come from OIDC discovery, not hard-coded constants.\n * 3. Token exchange + refresh are `application/x-www-form-urlencoded`\n * (the Anthropic flow posts JSON).\n *\n * The callback page still routes through {@link OAuthCallbackPageRenderer} so\n * the post-redirect HTML matches the rest of zidane's themed flows.\n *\n * Flow + constants adapted from the public reference implementations\n * (`stnly/pi-grok`, `BlockedPath/pi-xai-oauth`) and the Hermes Agent docs.\n * xAI's OAuth surface is unofficial/undocumented — re-verify the client id,\n * scopes, and callback port if login starts failing.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n} from '@earendil-works/pi-ai/oauth'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { OAuthProviderInterface } from '../../providers/oauth-registry'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { createServer } from 'node:http'\nimport { generatePkce } from './pkce'\n\n/** pi-ai / zidane OAuth provider id. Also the credentials-file key. */\nexport const XAI_OAUTH_PROVIDER_ID = 'xai-oauth'\n\nconst DEFAULT_BASE_URL = 'https://api.x.ai/v1'\nconst ISSUER = 'https://auth.x.ai'\nconst DISCOVERY_URL = `${ISSUER}/.well-known/openid-configuration`\n/** Public client id used by the Grok CLI OAuth surface. */\nconst DEFAULT_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828'\nconst DEFAULT_SCOPE = 'openid profile email offline_access grok-cli:access api:access'\n/** xAI matches the registered loopback IP exactly — `localhost` is rejected. */\nconst DEFAULT_CALLBACK_HOST = '127.0.0.1'\nconst DEFAULT_CALLBACK_PORT = 56121\nconst CALLBACK_PATH = '/callback'\nconst PROVIDER_NAME = 'xAI Grok'\n/** Refresh slightly early so requests don't race expiry. */\nconst REFRESH_SKEW_MS = 120_000\nconst CALLBACK_TIMEOUT_MS = 180_000\n\nfunction xaiOAuthClientId(): string {\n return process.env.PI_XAI_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID\n}\n\nfunction xaiOAuthScope(): string {\n return process.env.PI_XAI_OAUTH_SCOPE || DEFAULT_SCOPE\n}\n\nfunction xaiOAuthCallbackHost(): string {\n return process.env.PI_XAI_OAUTH_CALLBACK_HOST || DEFAULT_CALLBACK_HOST\n}\n\nfunction xaiOAuthCallbackPort(): number {\n const parsed = Number.parseInt(process.env.PI_XAI_OAUTH_CALLBACK_PORT || String(DEFAULT_CALLBACK_PORT), 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CALLBACK_PORT\n}\n\n/** Resolve the xAI API base URL (env override → default). */\nexport function xaiBaseUrl(): string {\n return (process.env.PI_XAI_BASE_URL || process.env.XAI_BASE_URL || DEFAULT_BASE_URL)\n .replace(/\\/+$/, '')\n}\n\ninterface XaiDiscovery {\n authorization_endpoint: string\n token_endpoint: string\n}\n\n/**\n * xAI OAuth credentials. Extends the base `{ access, refresh, expires }` with\n * the resolved `token_endpoint` so refresh doesn't need a second discovery\n * round-trip, plus the cached discovery doc for validation.\n */\nexport interface XaiOAuthCredentials extends OAuthCredentials {\n tokenEndpoint?: string\n discovery?: XaiDiscovery\n}\n\nfunction base64Url(bytes: Uint8Array): string {\n let binary = ''\n for (const b of bytes)\n binary += String.fromCharCode(b)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction randomToken(byteLength = 16): string {\n return base64Url(crypto.getRandomValues(new Uint8Array(byteLength)))\n}\n\n/**\n * Refuse any OIDC endpoint that isn't HTTPS on an xAI origin.\n *\n * The discovery doc is cached long-term in `credentials.json`. A single MITM\n * during initial login could otherwise pin a malicious `token_endpoint` that\n * receives every subsequent refresh token. Validating scheme + host keeps the\n * endpoint on `x.ai` / `*.x.ai`.\n */\nfunction validateEndpoint(value: string, field: string): string {\n let url: URL\n try {\n url = new URL(value)\n }\n catch {\n throw new Error(`xAI OAuth discovery returned an invalid ${field}: ${value}`)\n }\n if (url.protocol !== 'https:')\n throw new Error(`xAI OAuth ${field} must use HTTPS: ${value}`)\n const host = url.hostname.toLowerCase()\n if (host !== 'x.ai' && !host.endsWith('.x.ai'))\n throw new Error(`Refusing non-xAI OAuth ${field}: ${value}`)\n return url.toString()\n}\n\nasync function discover(): Promise<XaiDiscovery> {\n let response: Response\n try {\n response = await fetch(DISCOVERY_URL, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n })\n }\n catch (cause) {\n throw new Error(`xAI OIDC discovery failed: ${cause instanceof Error ? cause.message : String(cause)}`)\n }\n if (!response.ok)\n throw new Error(`xAI OIDC discovery returned ${response.status}`)\n\n const payload = await response.json() as Record<string, unknown>\n return {\n authorization_endpoint: validateEndpoint(String(payload.authorization_endpoint ?? ''), 'authorization_endpoint'),\n token_endpoint: validateEndpoint(String(payload.token_endpoint ?? ''), 'token_endpoint'),\n }\n}\n\ninterface CallbackResult {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\ninterface XaiCallbackServer {\n redirectUri: string\n waitForCallback: (timeoutMs: number) => Promise<CallbackResult>\n close: () => void\n}\n\n/**\n * Start the loopback callback server on `127.0.0.1:56121`. Falls back to an\n * OS-assigned port if 56121 is taken — xAI accepts any loopback port as long\n * as the `redirect_uri` we send on the authorize URL matches the listener.\n */\nasync function startXaiCallbackServer(renderPage: OAuthCallbackPageRenderer): Promise<XaiCallbackServer> {\n const callbackHost = xaiOAuthCallbackHost()\n const callbackPort = xaiOAuthCallbackPort()\n let settle: ((value: CallbackResult) => void) | undefined\n let settled = false\n const callbackPromise = new Promise<CallbackResult>((resolve) => {\n settle = (value) => {\n if (settled)\n return\n settled = true\n resolve(value)\n }\n })\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n try {\n // accounts.x.ai may issue a CORS preflight against the loopback listener.\n const origin = req.headers.origin\n if (origin === 'https://accounts.x.ai' || origin === 'https://auth.x.ai') {\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type')\n res.setHeader('Access-Control-Allow-Private-Network', 'true')\n res.setHeader('Vary', 'Origin')\n }\n if (req.method === 'OPTIONS') {\n res.statusCode = 204\n res.end()\n return\n }\n\n const url = new URL(req.url ?? '/', `http://${callbackHost}`)\n if (url.pathname !== CALLBACK_PATH) {\n res.statusCode = 404\n res.end('Not found')\n return\n }\n\n const result: CallbackResult = {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n error: url.searchParams.get('error') ?? undefined,\n errorDescription: url.searchParams.get('error_description') ?? undefined,\n }\n\n res.statusCode = result.error ? 400 : 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(renderPage(result.error\n ? {\n kind: 'error',\n provider: PROVIDER_NAME,\n message: `${PROVIDER_NAME} authentication did not complete.`,\n details: result.errorDescription ?? result.error,\n }\n : {\n kind: 'success',\n provider: PROVIDER_NAME,\n message: 'xAI authentication completed. You can close this window.',\n }))\n settle?.(result)\n }\n catch {\n res.statusCode = 500\n res.end('Internal error')\n }\n })\n\n const listen = (port: number) => new Promise<number>((resolve, reject) => {\n server.once('error', reject)\n server.listen(port, callbackHost, () => {\n server.removeListener('error', reject)\n const addr = server.address()\n resolve(typeof addr === 'object' && addr ? addr.port : port)\n })\n })\n\n let actualPort: number\n try {\n actualPort = await listen(callbackPort)\n }\n catch {\n actualPort = await listen(0)\n }\n\n return {\n redirectUri: `http://${callbackHost}:${actualPort}${CALLBACK_PATH}`,\n waitForCallback: timeoutMs => Promise.race([\n callbackPromise,\n new Promise<CallbackResult>(resolve =>\n setTimeout(\n resolve,\n timeoutMs,\n { error: 'timeout', errorDescription: 'Timed out waiting for the xAI OAuth callback.' },\n )),\n ]),\n close: () => {\n try { server.close() }\n catch { /* already closed */ }\n },\n }\n}\n\nfunction expiryFromPayload(payload: Record<string, unknown>): number {\n const expiresIn = typeof payload.expires_in === 'number'\n ? payload.expires_in\n : Number(payload.expires_in ?? 3600)\n return Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS\n}\n\nasync function exchangeCode(\n tokenEndpoint: string,\n code: string,\n redirectUri: string,\n verifier: string,\n discovery: XaiDiscovery,\n): Promise<XaiOAuthCredentials> {\n const clientId = xaiOAuthClientId()\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: clientId,\n code,\n redirect_uri: redirectUri,\n code_verifier: verifier,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token exchange failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n const refresh = String(payload.refresh_token ?? '')\n if (!access)\n throw new Error('xAI token exchange did not return an access_token.')\n if (!refresh)\n throw new Error('xAI token exchange did not return a refresh_token.')\n\n return { access, refresh, expires: expiryFromPayload(payload), tokenEndpoint, discovery }\n}\n\nexport async function loginXai(\n renderPage: OAuthCallbackPageRenderer,\n callbacks: OAuthLoginCallbacks,\n): Promise<XaiOAuthCredentials> {\n const discovery = await discover()\n const { verifier, challenge } = await generatePkce()\n const state = randomToken()\n const nonce = randomToken()\n const server = await startXaiCallbackServer(renderPage)\n\n try {\n const authUrl = new URL(discovery.authorization_endpoint)\n authUrl.searchParams.set('response_type', 'code')\n authUrl.searchParams.set('client_id', xaiOAuthClientId())\n authUrl.searchParams.set('redirect_uri', server.redirectUri)\n authUrl.searchParams.set('scope', xaiOAuthScope())\n authUrl.searchParams.set('code_challenge', challenge)\n authUrl.searchParams.set('code_challenge_method', 'S256')\n authUrl.searchParams.set('state', state)\n authUrl.searchParams.set('nonce', nonce)\n // Opt into xAI's generic OAuth plan tier (SuperGrok / X Premium+).\n authUrl.searchParams.set('plan', 'generic')\n authUrl.searchParams.set('referrer', 'zidane')\n\n callbacks.onAuth({\n url: authUrl.toString(),\n instructions: `Sign in to xAI and approve access. If the browser is on another machine, the callback listener is ${server.redirectUri}.`,\n })\n\n const result = await server.waitForCallback(CALLBACK_TIMEOUT_MS)\n if (result.error)\n throw new Error(result.errorDescription ?? result.error)\n if (result.state !== state)\n throw new Error('xAI OAuth state mismatch — possible CSRF. Please retry.')\n if (!result.code)\n throw new Error('xAI OAuth callback did not include an authorization code.')\n\n callbacks.onProgress?.('Exchanging authorization code for tokens...')\n return await exchangeCode(discovery.token_endpoint, result.code, server.redirectUri, verifier, discovery)\n }\n finally {\n server.close()\n }\n}\n\nexport async function refreshXaiToken(credentials: OAuthCredentials): Promise<XaiOAuthCredentials> {\n const xai = credentials as XaiOAuthCredentials\n const clientId = xaiOAuthClientId()\n if (!credentials.refresh)\n throw new Error('xAI credentials are missing a refresh token — re-login required.')\n\n const tokenEndpoint = xai.tokenEndpoint\n ?? xai.discovery?.token_endpoint\n ?? (await discover()).token_endpoint\n validateEndpoint(tokenEndpoint, 'token_endpoint')\n\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: clientId,\n refresh_token: credentials.refresh,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token refresh failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n if (!access)\n throw new Error('xAI token refresh did not return an access_token.')\n\n return {\n ...xai,\n access,\n // xAI may omit a rotated refresh token — keep the existing one.\n refresh: String(payload.refresh_token ?? credentials.refresh),\n expires: expiryFromPayload(payload),\n tokenEndpoint,\n }\n}\n\n/**\n * Build an xAI `OAuthProviderInterface`. Shape matches Anthropic / Codex /\n * Cursor so it drops into `BUILTIN_PROVIDERS` and `src/auth.ts` unchanged.\n *\n * `usesCallbackServer: true` — the loopback flow has a real callback server,\n * so the TUI doesn't need to prompt for a manual code paste.\n */\nexport function createXaiOAuthProvider(renderPage: OAuthCallbackPageRenderer): OAuthProviderInterface {\n return {\n id: XAI_OAUTH_PROVIDER_ID,\n name: 'xAI Grok (SuperGrok / X Premium+)',\n usesCallbackServer: true,\n login: callbacks => loginXai(renderPage, callbacks),\n refreshToken: refreshXaiToken,\n getApiKey: credentials => credentials.access,\n }\n}\n","import type { Provider, ProviderCapabilities, StreamCallbacks, StreamOptions, TurnResult } from '.'\nimport type { OAuthParams } from './oauth'\nimport { XAI_OAUTH_PROVIDER_ID, xaiBaseUrl } from '../chat/oauth-page/xai'\nimport { extractRuntimeCredentials, isOAuthAccessToken, resolveOAuthApiKey } from './oauth'\nimport { openaiCompat } from './openai-compat'\n\nconst DEFAULT_MODEL = 'grok-4.3'\n\n/**\n * xAI Grok provider — supports both a static API key (`XAI_API_KEY`) and the\n * SuperGrok / X Premium+ OAuth subscription.\n *\n * Both auth modes hit the same OpenAI-compatible endpoint (`https://api.x.ai/v1`),\n * so the wire handling is entirely {@link openaiCompat}. The only difference is\n * the bearer:\n *\n * - **API key**: `XAI_API_KEY` (or `params.apiKey`) → used verbatim.\n * - **OAuth**: no static key → {@link resolveOAuthApiKey} resolves + lazily\n * refreshes the stored `xai-oauth` token on every turn.\n *\n * `resolveOAuthApiKey` unifies the two: a real API key in `XAI_API_KEY` is\n * returned as-is, while an OAuth access token (or no env value at all) falls\n * through to the refreshable stored credentials. So this single factory serves\n * both paths without branching at construction time.\n */\nexport interface XaiParams extends OAuthParams {\n defaultModel?: string\n /**\n * Provider capability flags. Grok models are vision-capable; default\n * `{ vision: true, imageInToolResult: false }`. Override for text-only\n * deployments.\n */\n capabilities?: ProviderCapabilities\n /** Extra HTTP headers sent on every request. */\n extraHeaders?: Record<string, string>\n}\n\nexport function xai(params?: XaiParams): Provider {\n const defaultModel = params?.defaultModel || DEFAULT_MODEL\n const baseURL = xaiBaseUrl()\n const capabilities: ProviderCapabilities = params?.capabilities ?? {\n vision: true,\n imageInToolResult: false,\n }\n const baseCredentials = extractRuntimeCredentials(params)\n let runtimeCredentials = baseCredentials\n\n // Factory-time snapshot for the static `meta.isOAuth` display flag only (the\n // terminal header's `oauth`/`key` badge). The real bearer resolves per turn\n // in `stream`, so this is best-effort, mirroring anthropic's pattern. It's\n // OAuth unless a genuine static API key is present: a `xai-…` key → key mode;\n // runtime OAuth creds, a JWT access token, or no static key (stored creds\n // used) → oauth mode.\n const staticKey = params?.apiKey ?? process.env.XAI_API_KEY\n const isOAuth = baseCredentials !== undefined || !staticKey || isOAuthAccessToken(staticKey)\n\n /**\n * Build the openai-compat delegate for a resolved bearer. Cheap — the factory\n * only wires closures — so we re-create it per turn with the freshly resolved\n * (possibly refreshed) OAuth token rather than baking a stale key in once.\n */\n function delegateFor(apiKey: string): Provider {\n return openaiCompat({\n name: 'xai',\n apiKey,\n baseURL,\n defaultModel,\n capabilities,\n extraHeaders: params?.extraHeaders,\n })\n }\n\n // A delegate built with a placeholder key supplies the non-stream methods\n // (formatTools, userMessage, message builders, classifyError). Only `stream`\n // resolves the real bearer, so the placeholder never reaches the wire.\n const skeleton = delegateFor('placeholder-resolved-at-stream')\n\n return {\n ...skeleton,\n name: 'xai',\n // Reuse the delegate's normalized capability flags (openaiCompat fills the\n // audio/video/documents defaults) so the advertised shape matches the wire.\n meta: { ...skeleton.meta, defaultModel, isOAuth },\n\n async stream(options: StreamOptions, callbacks: StreamCallbacks): Promise<TurnResult> {\n const apiKey = await resolveOAuthApiKey(\n {\n provider: 'xai',\n providerId: XAI_OAUTH_PROVIDER_ID,\n params: runtimeCredentials ? { ...params, ...runtimeCredentials } : params,\n envKey: 'XAI_API_KEY',\n missingError: 'No xAI credentials found. Set XAI_API_KEY or run `bun run auth --xai` first.',\n refreshError: reason => `xAI OAuth token refresh failed. Run \\`bun run auth --xai\\` again. ${reason}`,\n },\n {\n ...callbacks,\n async onOAuthRefresh(ctx) {\n // Keep param-sourced credentials current so a long-lived provider\n // instance reuses the rotated token instead of re-refreshing.\n if (ctx.source === 'params') {\n runtimeCredentials = {\n access: ctx.credentials.access,\n refresh: ctx.credentials.refresh,\n expires: ctx.credentials.expires,\n }\n }\n await callbacks.onOAuthRefresh?.(ctx)\n },\n },\n )\n\n return delegateFor(apiKey).stream(options, callbacks)\n },\n }\n}\n"],"mappings":"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"}
|
package/docs/CHAT.md
CHANGED
|
@@ -783,10 +783,51 @@ A categorized view of what's consuming the context window — system prompt, rul
|
|
|
783
783
|
- **`buildContextBreakdown(snapshot)`** (`zidane/chat` + `zidane/chat/pure`, node-free) is the pure shaper. Live categories always reconcile to the real last-turn total (`conversation = used − Σ(other live)`), so the bar can never contradict the footer's `ctx` gauge. Free space and the autocompact buffer are exact; per-section counts are heuristic (`estimateTokens`) unless the provider supplies exact counts.
|
|
784
784
|
- **Exact counts (optional).** When a provider implements `Provider.countTokens(payload)` (Anthropic `messages.countTokens`, OpenAI `responses.input_tokens`), `getContextBreakdown` differences a system-only and a system+tools probe to attribute the System prompt + Tool definitions buckets exactly; categories with no exact source render with a `~` prefix. `countTokens` returns `null` when unavailable (no endpoint / wrong auth) and the heuristic fills in — it can never break the breakdown.
|
|
785
785
|
- **Autocompact zone.** Pass `opts.compactThreshold` (the `Settings.autoCompactThreshold` fraction) and the "Autocompact buffer" bucket becomes the whole tail from the threshold to the end of the window, so the bar shows exactly where compaction will fire.
|
|
786
|
-
- **Rules attribution.** The agent can't isolate the rules block from the opaque host-composed system string, so the host passes `AgentRunOptions.contextSections = { rulesBlock, rulesFiles }` (the rendered AGENTS.md block + its source files) for a dedicated, per-file "Rules" row.
|
|
786
|
+
- **Rules attribution.** The agent can't isolate the rules block from the opaque host-composed system string, so the host passes `AgentRunOptions.contextSections = { rulesBlock, rulesFiles }` (the rendered AGENTS.md block + its source files) for a dedicated, per-file "Rules" row. Both hosts discover rules via `discoverAgentsMd`, but only the **GUI** currently threads `contextSections` into `agent.run()` — the TUI does not, so its breakdown has no "Rules" bucket. (The TUI's Settings → Instructions tab gets its per-file rows from the host side instead; see below.)
|
|
787
787
|
|
|
788
788
|
`ContextBreakdown`, `ContextBreakdownOptions`, `ContextSnapshot`, `ContextCategory`, `ContextItem`, `ContextUsageSplit`, `ContextExactCounts`, `ContextMcpGroup`, and `buildContextBreakdown` are all exported from both `zidane/chat` and `zidane/chat/pure`.
|
|
789
789
|
|
|
790
|
+
## `SYSTEM.md` — overriding the built-in prompt
|
|
791
|
+
|
|
792
|
+
`discoverSystemMd` (`chat/system-md`) finds a `SYSTEM.md` that **replaces** the built-in doctrine. Deliberately inverted semantics vs `AGENTS.md`:
|
|
793
|
+
|
|
794
|
+
| | `AGENTS.md` | `SYSTEM.md` |
|
|
795
|
+
|------------|-----------------------------|-----------------------------------|
|
|
796
|
+
| effect | appends to the doctrine | replaces the doctrine |
|
|
797
|
+
| multiples | every match loads, in order | exactly one wins |
|
|
798
|
+
| precedence | broad → narrow, all present | project beats user, rest shadowed |
|
|
799
|
+
|
|
800
|
+
Discovery honors the same `userInstructionsScope` setting as `AGENTS.md`, with the same per-scope rules — `DiscoverSystemMdOptions.scope` takes the `AgentsMdScope` itself, not a boolean. `'none'` skips discovery before any filesystem probe (it exists to debug the base prompt "without any user doctrine bleeding in", and an override is the most invasive form of user doctrine there is). `'user'` drops the project candidate and `'project'` drops the user one, mirroring how `discoverAgentsMd` builds its list. That matters most for `'user'`: it is how someone says "do not trust this checkout", so a repo-root `SYSTEM.md` must not load there.
|
|
801
|
+
|
|
802
|
+
> **A project-scope `SYSTEM.md` deletes safety doctrine.** `AGENTS.md` can only append, and is rendered under a header marking it as user-authored guidance. An override REPLACES the built-in prompt — `ACTIONS_WITH_CARE_DOCTRINE` and `GIT_DOCTRINE` included — so a file that merely exists at the root of a checkout can drop "destructive actions need explicit confirmation" and "never force-push without confirmation". Appending to doctrine and deleting doctrine are different risk profiles. Because discovery is automatic at session activation, the TUI paints a one-line transcript warning naming the file whenever a *project*-scope override is in force; the user-scope file stays quiet, since the user wrote it in their own home directory. Set `userInstructionsScope` to `'user'` or `'none'` to refuse repo-supplied overrides entirely.
|
|
803
|
+
|
|
804
|
+
Search order, winner first: `<project root>/SYSTEM.md`, then `~/.{prefix}/SYSTEM.md`. Concatenating overrides would be incoherent — two files each claiming to *be* the system prompt can't both be it — so one wins and the losers come back in `SystemMdResult.shadowed` for the UI to mark inactive. An empty or whitespace-only file counts as absent, so a created-but-unwritten file can't hand the model a blank prompt.
|
|
805
|
+
|
|
806
|
+
`BuildSystemOptions.systemOverride` is what applies it: `buildBuildSystem` / `buildPlanSystem` swap it in for identity + doctrine + guidance. Two things still compose around it:
|
|
807
|
+
|
|
808
|
+
- **`userInstructions` is appended below it** — `AGENTS.md` stays an additive layer, so overriding the prompt doesn't silently disable your project instructions.
|
|
809
|
+
- **The `<env>` block still lands below the cache boundary** — it carries cwd + project root, which the file tools' path resolution depends on. Dropping it would break `read_file` in ways that look like model error rather than configuration.
|
|
810
|
+
|
|
811
|
+
`allowInteraction` is not consulted for the override text (the built-in fragments swap in no-prompts variants of themselves; your text is yours). One file overrides whichever profile is active — there is no plan-specific override.
|
|
812
|
+
|
|
813
|
+
A host-supplied custom profile **cannot** be overridden: it keeps its own `preset.system`, so there is no built-in doctrine for the file to replace and the file is never read. Hosts must not report such a file as the prompt's source — the TUI threads a `supportsOverride` flag out of `composeHostContext` for exactly this, and refuses to create an override on those profiles rather than writing one that could never apply.
|
|
814
|
+
|
|
815
|
+
`renderSystemMdSeed(staticPrompt)` builds the contents for a newly created override: an HTML-comment header explaining the semantics, followed by the doctrine that was in effect. Seeding with the real prompt makes creating the file a behavioral no-op until edited — an empty seed would strip ~1.2k tokens of doctrine the instant the file appeared, with no visible cause. Callers pass `splitSystemPrompt(system).static` so the per-turn `<env>` block never gets frozen into the file.
|
|
816
|
+
|
|
817
|
+
## Context segments
|
|
818
|
+
|
|
819
|
+
Where the breakdown above answers "how many tokens is each part of my context", **context segments** answer "what IS each part, and where did it come from" — the data behind the TUI's Settings → Instructions tab.
|
|
820
|
+
|
|
821
|
+
- **`agent.getContextSegments(opts?)`** returns `ContextSegment[] | null` from the same assembly the breakdown uses (live run snapshot, falling back to the pre-run assembly), so it's populated before the first message and `null` only after `destroy()`. No provider round-trip and no tool serialization — just the assembled strings.
|
|
822
|
+
- **`buildContextSegments(input)`** (`zidane/chat` + `zidane/chat/pure`, node-free) is the pure shaper. Each `ContextSegment` carries `{ id, label, origin, detail?, text, path?, tokens }`. The base "System prompt" segment is `baseSystem` minus `rulesBlock`, so instruction text is never reported twice; empty / whitespace-only pieces are dropped.
|
|
823
|
+
- **Origins are honest about openability.** `'file'` is the only origin that carries a `path`, and therefore the only one a UI can hand to an editor. `'built-in'` is compiled in (base doctrine, subagent defs), `'generated'` is assembled at prompt time (the skills catalog), `'mcp'` is served by a connected server.
|
|
824
|
+
- **`systemPath` promotes the base prompt to a file segment.** Pass it when a `SYSTEM.md` override produced the prompt and the "System prompt" segment flips from `'built-in'` to `'file'`, becoming openable. Host-supplied for the same reason as `rulesFiles`: the agent receives an already-composed system string with no provenance attached.
|
|
825
|
+
- **`rulesBlock` is what makes the de-dup work.** The base-prompt segment subtracts it so instruction text isn't reported twice (once inline, once as its own file rows). `getContextSegments` falls back to the assembly's copy, but that only exists when the host threaded `AgentRunOptions.contextSections` into `run()` — which the TUI does not — so a host that renders its own instruction block should pass it explicitly.
|
|
826
|
+
- **Instruction files are host-supplied.** Pass `opts.rulesFiles` — a host that already called `discoverAgentsMd` passes its `files` straight through, since `AgentsMdFile` is structurally compatible with `ContextSegmentFile`. The agent never reads those files itself (it receives the rendered block as an opaque substring), so without this it knows paths at best and never text.
|
|
827
|
+
- **Skill bodies are not here.** Activating a skill puts its body in the *conversation* as a tool result, not the system prompt; only the catalog blurbs are default context. `ContextBreakdown.activeSkills` reports which skills are live.
|
|
828
|
+
|
|
829
|
+
`ContextSegment`, `ContextSegmentFile`, `ContextSegmentOrigin`, `ContextSegmentsInput`, and `buildContextSegments` are exported from `zidane/chat` and `zidane/chat/pure` (types also from the root barrel).
|
|
830
|
+
|
|
790
831
|
## Edit-diff rendering
|
|
791
832
|
|
|
792
833
|
Renderer-agnostic helpers for turning `edit` / `multi_edit` / `write_file` tool calls into a unified diff. Two tiers:
|
package/docs/INTERACTIONS.md
CHANGED
|
@@ -88,11 +88,50 @@ In the question wizard (multi-question batches):
|
|
|
88
88
|
the whole selection, at least one pick required unless `required: false`)
|
|
89
89
|
- `shift+↹` — back one question (preserves your answers, pre-fills the
|
|
90
90
|
input with the prior value)
|
|
91
|
+
- `pagedown` / `pageup` — scroll the form down / up by one viewport, keeping a
|
|
92
|
+
1-row overlap so the line at the fold stays visible. The form's scrollbox is
|
|
93
|
+
deliberately not focusable (the option list and text inputs own the keyboard),
|
|
94
|
+
so these are how you read a long intro or peek at a later question without
|
|
95
|
+
reaching for the mouse. Advancing a question also scrolls it into view
|
|
96
|
+
automatically. The plan panel uses the same keys and the same page size.
|
|
97
|
+
|
|
98
|
+
Not `ctrl+d` / `ctrl+u`: OpenTUI's textarea bindings map those to
|
|
99
|
+
delete-to-line-start and delete, and a textarea question is focused whenever
|
|
100
|
+
this handler is, so they would destroy your typed answer as they scrolled.
|
|
101
|
+
While a form is up the transcript releases these keys, so one press moves one
|
|
102
|
+
surface.
|
|
91
103
|
- `esc` — leave the form
|
|
92
104
|
- Live (agent waiting): aborts the run
|
|
93
105
|
- Resumed (you reopened the session): leaves the form, the session
|
|
94
106
|
re-enqueues the same interaction on next activation
|
|
95
107
|
|
|
108
|
+
### Sizing
|
|
109
|
+
|
|
110
|
+
The wizard is content-sized inside a ceiling, and all three numbers come from
|
|
111
|
+
the terminal rather than from percentages of a parent:
|
|
112
|
+
|
|
113
|
+
- **Ceiling** — `wizardMaxRows(terminalRows)`, roughly 75% of the terminal,
|
|
114
|
+
never below 8 rows and never within 8 rows of the top so some transcript stays
|
|
115
|
+
visible. It used to be a `maxHeight: '60%'`, which resolves against the
|
|
116
|
+
parent's height; the wizard's parent is content-sized, so Yoga had nothing to
|
|
117
|
+
take 60% of and the cap silently never applied — the shell grew to whatever
|
|
118
|
+
its content estimate claimed and drew its border there, leaving questions
|
|
119
|
+
rendered below a closed frame.
|
|
120
|
+
- **Content height** — `introRows` + `estimateQuestionRowHeight` per question,
|
|
121
|
+
both **wrap-aware** (`wrappedRows`) and both given the panel's content width.
|
|
122
|
+
They previously counted `\n` only and ignored choice descriptions, so any
|
|
123
|
+
wrapped markdown intro or wrapped description under-sized the panel.
|
|
124
|
+
- **Floor** — the shell takes `minHeight` equal to the rows its content needs.
|
|
125
|
+
The shell is `flexShrink: 1` (it was `0`, which is why it could neither cap
|
|
126
|
+
nor shrink), and shrinkability alone let a `flexGrow` transcript sibling
|
|
127
|
+
squeeze the form to a fraction of its content; the floor makes the transcript
|
|
128
|
+
yield instead.
|
|
129
|
+
|
|
130
|
+
The height is also **monotonic**: answering a question collapses its row to a
|
|
131
|
+
one-line summary, which would otherwise shrink the panel mid-form. A high-water
|
|
132
|
+
mark keyed by request id means the panel settles at the largest size it has
|
|
133
|
+
needed and stays there.
|
|
134
|
+
|
|
96
135
|
In the plan picker:
|
|
97
136
|
|
|
98
137
|
- Decision picker → `revise` / `reject` → comment textarea
|
package/docs/RESTATE.md
CHANGED
|
@@ -401,11 +401,106 @@ Two consequences of the turn-boundary check: a child stuck inside ONE long tool
|
|
|
401
401
|
|
|
402
402
|
### Limits
|
|
403
403
|
|
|
404
|
-
- **Sequential spawns only
|
|
404
|
+
- **Sequential spawns only** with `createSpawnTool` — for parallel children use `restateSpawnTool` (next section), which sidesteps both reasons below by giving each child its own journal. The weaker one: parallel children race on shared session state — the `child-N` label counter and the run-id re-sync both read `session.runs` at mint time — so label / run-id assignment can differ between attempts. The stronger one: children share the parent's *clock instance*, and `restateClock` carries mutable draw state (the `randomUUID` sequence counter, plus the anchor / call counter in `coalesce` mode). Concurrent children interleave those draws, so the same logical turn can draw a different id or timestamp ordinal on a replay — the `JournalMismatchError` class the clock module exists to prevent. `restateBehavior`'s default `maxConcurrentTools: 1` already gives you the safe posture; don't raise it for a turn that can spawn more than once.
|
|
405
405
|
- **Anything that reaches the transcript must come from `ctx.clock`.** The spawn result string is persisted in the parent's turns, and a state write's payload IS compared across attempts — so the elapsed figure in `[sub-agent child-1] Completed in 2 turns (35ms)` is sampled from the clock, not from `stats.elapsed` (which stays live `Date.now()` by design, for measurement accuracy). Getting this wrong fails the invocation at `set state` with `RestateError 570`, and only on an attempt that fails AFTER a child completed — a crash mid-child never produces the string, so the failure hides until a retry happens later in the run. The fixture's retry scenario exists for exactly that reason. The same rule is why the child inherits the clock at all.
|
|
406
406
|
- **The parent's spawn tool call is not itself journaled**, so it re-executes on replay: agent construction, MCP re-use, hook bubbling, and result-string formatting all run again. Keep host `onChildStart` / `spawn:*` handlers idempotent.
|
|
407
407
|
- **Don't set `behavior.maxWallMs` on a Restate-backed agent** (parent or child) unless you mean it: the provider stream watchdogs are clamped by it, so a tight budget aborts an in-flight stream from inside its journal entry rather than stopping cleanly at a turn boundary. Reach for the runtime's own invocation timeout, or `createSpawnTool({ timeoutMs })` for a child.
|
|
408
|
-
|
|
408
|
+
|
|
409
|
+
## Parallel subagents
|
|
410
|
+
|
|
411
|
+
`createSpawnTool` runs children in the parent's journal, which is what makes them sequential. `restateSpawnTool` dispatches each child to **its own invocation**, so children fan out through the durable fleet — and a crash in one child retries only that child.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const spawn = restateSpawnTool({
|
|
415
|
+
// Return the runtime's promise UNTOUCHED — no await, no .then/.catch.
|
|
416
|
+
dispatch: req => ctx.serviceClient(childService).runChild(req),
|
|
417
|
+
// Stop in-flight children when the parent's own loop aborts.
|
|
418
|
+
onAbort: () => ctx.invocation(ctx.request().id).cancel(),
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
createAgent({
|
|
422
|
+
tools: { ...wrapAgentTools(tools, ctx), spawn },
|
|
423
|
+
clock: restateClock(ctx),
|
|
424
|
+
session,
|
|
425
|
+
behavior: restateBehavior({
|
|
426
|
+
maxConcurrentTools: 4, // defaults to 1 — without this there is no fan-out
|
|
427
|
+
toolBatchExecutor: restateToolBatchExecutor({ promises: RestatePromise }),
|
|
428
|
+
}),
|
|
429
|
+
})
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Choosing between the two
|
|
433
|
+
|
|
434
|
+
| | `createSpawnTool` | `restateSpawnTool` |
|
|
435
|
+
|---|---|---|
|
|
436
|
+
| Children per turn | one at a time | parallel (fleet-capped) |
|
|
437
|
+
| Journal | shares the parent's | one per child |
|
|
438
|
+
| Crash in a child | replays the parent frame, re-drives the child | retries only that child |
|
|
439
|
+
| `child:*` events | live (in-process bubbling) | replayed onto the parent bus when the child merges |
|
|
440
|
+
| Host code needed | none | a child handler |
|
|
441
|
+
|
|
442
|
+
### Host obligations
|
|
443
|
+
|
|
444
|
+
- **The child handler must be a `service`**, or an object on a *different* key. A virtual object serializes per key, so children sharing the parent's key queue behind the parent that still holds it — no parallelism, and a deadlock risk.
|
|
445
|
+
- **Raise `maxConcurrentTools` and install the durable batch executor.** The fleet gate is all-or-nothing (`fleet.every(canExecuteDurably)`), so with the default cap of 1 the children serialize silently.
|
|
446
|
+
- **The child runs sessionless** and returns a `ChildRunResult` (`status`, `text`, `turns`, `stats`). The parent stays the single writer: it merges each child's turns under a `SessionRun` labeled in submission order, so the persisted tree matches the in-process tool's.
|
|
447
|
+
- **Enforce depth in the handler** (`req.depth`). The tool always dispatches, because short-circuiting would mean returning a non-durable promise and the fleet rejects those.
|
|
448
|
+
|
|
449
|
+
### What you give up
|
|
450
|
+
|
|
451
|
+
- **`child:*` events fire, but not live** — see the section below.
|
|
452
|
+
- **`spawn:before` is fired without awaiting** — `durableExecute` must return the durable promise synchronously, so listeners on it must not perform journaled I/O.
|
|
453
|
+
- **Merge ordering**: children are merged on `tool-results:after` (with `turn:after` as a backstop for turns that pack no results), so their turns land just *after* the parent's tool-result turn rather than just before it. `createdAt` still orders them correctly for consumers that sort chronologically. If the invocation is torn down before the turn ends at all — a cancellation, say — a child that already settled is never merged and its `spawn:complete` never fires; its work still happened, it just isn't recorded.
|
|
454
|
+
- **The child should report its own `startedAt` / `endedAt`** (measured on its clock, e.g. `restateClock(ctx)`). Those land in the merged `SessionRun`, which is persisted through a journaled state write, so they have to be replay-stable — the parent never samples a clock for them. Omit them and it falls back to the child's first/last turn timestamps, then to the parent's run start.
|
|
455
|
+
- **Per-child timeouts are the callee's job.** Racing the call promise is the forbidden native-combinator pattern; give the child handler its own budget, or use the runtime's `.orTimeout()`.
|
|
456
|
+
- **Cancelling the parent INVOCATION stops the children** — Restate cancels the call tree, so each child halts at its next journal interaction, at any depth. Verified: with two children mid-fan-out, no further work steps started after a `PATCH /invocations/{id}/cancel`. A step already inside a `ctx.run` finishes, since run bodies are not interruptible.
|
|
457
|
+
- **An in-agent `agent.abort()` needs `onAbort`.** On its own, `abort()` unwinds the parent's loop while its invocation keeps running, so nothing tells the children to stop. `onAbort` fires once with the in-flight set; cancel the parent's own invocation from it and the runtime cancels the tree. The cost is that the parent invocation ends too — an in-agent abort becomes "stop everything", so persist any summary BEFORE aborting.
|
|
458
|
+
- **Cancelling individual children is not possible in this tool shape.** The `invocationId` on the runtime's call handle is itself a journal-backed promise, so reading it while the call is in flight registers a second consumer on the SDK's single-consumer progress channel: `awaitNext already pending`, a failed attempt, and a replay on *every* fan-out (measured — 13 spurious attempts across the fixture suite before this was reverted). Per-child cancellation needs a two-phase `send → id → attach` tool, where the id is consumed before the result promise exists.
|
|
459
|
+
|
|
460
|
+
### Child events
|
|
461
|
+
|
|
462
|
+
Hook bubbling can't cross an invocation boundary, so a parallel child's events can't reach the parent's bus *as they happen*. Instead, each child's transcript is **replayed onto the parent's bus when it merges** — so `src/output/terminal.ts`, the GUI engine, and any `child:tool:*` listener render parallel children with no changes. Turn it off with `emitChildEvents: false`.
|
|
463
|
+
|
|
464
|
+
| Fires | From |
|
|
465
|
+
|---|---|
|
|
466
|
+
| `child:stream:text` | assistant text, one chunk per turn |
|
|
467
|
+
| `child:tool:before` | each `tool_call` block, with a run-cumulative `runToolCounts` tally |
|
|
468
|
+
| `child:tool:after` / `child:tool:error` | the matching `tool_result`, split on `isError` |
|
|
469
|
+
| `child:turn:after` | each assistant turn, `usage` straight off the turn |
|
|
470
|
+
| `child:stream:end` | end of each turn |
|
|
471
|
+
|
|
472
|
+
Every event carries `childId` (the reserved label), `depth`, and — on the tool events, which extend `ToolHookContext` — `runId`, `parentRunId`, and `parentCallId` (the spawn call that owns the child). Per-child blocks stay contiguous and follow **submission** order, not settlement order.
|
|
473
|
+
|
|
474
|
+
**Not synthesized**: `tool:gate`, `tool:transform`, `tool:dispatched`, `tool:cancelled`, `background:*`. Those describe decisions taken inside another process; inventing them would be fiction.
|
|
475
|
+
|
|
476
|
+
**They re-fire on a parent replay**, exactly as the in-process tool's `child:tool:*` does — the drain runs live on every attempt. Dedupe by `callId` / `turnId`, or gate on `createReplayTracker`. The fixture pins this rather than asserting it in prose: a post-merge replay emits 24 events for 12 events' worth of work, measured through a file sink that outlives the attempt.
|
|
477
|
+
|
|
478
|
+
#### Nested fan-out
|
|
479
|
+
|
|
480
|
+
A child dispatched to a service runs **sessionless**, so it has nowhere to persist its own children. Left alone, a depth-2 fan-out loses everything below depth 1 — the parent sees the child's `spawn` tool calls and none of the work they did. Hand the subtree up instead:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
const merged: MergedChild[] = []
|
|
484
|
+
const spawn = restateSpawnTool({
|
|
485
|
+
onChildMerged: child => merged.push(child),
|
|
486
|
+
dispatch: req => ctx.serviceClient(childService).runChild(req),
|
|
487
|
+
})
|
|
488
|
+
// … and return it from the handler
|
|
489
|
+
return { status: 'completed', text, turns: agent.turns, stats, children: merged }
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
The merging parent walks that subtree depth-first, **re-labeling** each descendant from its own session counter (every level mints `child-1` locally, so the arriving labels collide), and emits their `child:*` events at the right depth. Verified end to end: parent → one child → two grandchildren lands four runs at depths 0/1/2/2 with labels `child-1` / `child-2` / `child-3`, and the grandchildren's tool events arrive at `depth: 2`.
|
|
493
|
+
|
|
494
|
+
#### If you need it live
|
|
495
|
+
|
|
496
|
+
Publish from the child's own bus inside your handler; the child is an ordinary agent there and its hooks fire normally. `ChildRunRequest` carries `parentRunId`, `parentCallId` and `sessionId` so the child can tag what it publishes and your UI can group it with the parent's own events. Two caveats:
|
|
497
|
+
|
|
498
|
+
- `restateProvider(..., { streamCallbacks: 'suppress' })` no-ops `onText`, so text deltas never fire even inside the child. Drop `suppress` and you get `maxRetryAttempts: 1` instead — otherwise an entry retry re-emits deltas and duplicates text.
|
|
499
|
+
- A child's events re-fire on the *child's* own replay, so the sink must be idempotent.
|
|
500
|
+
|
|
501
|
+
What's deliberately not offered is journaling each child event so the parent can consume it as a journal entry. That is the only live option that stays inside the determinism envelope, and it costs a journal entry per event plus a parent that must stay awake to read them — which defeats the suspension that makes long children cheap. Defensible at coarse milestones, not per token.
|
|
502
|
+
|
|
503
|
+
Every property here is pinned by the fixture against a real server: two children overlapping by ~394ms of their 400ms of work; four children at a cap of two running as two clean waves (max concurrency exactly 2, wall time 1031ms versus 2090ms for the same fan-out at cap 1); a crash inside one child leaving its sibling with exactly one execution; and both children's transcripts arriving on the parent's bus as `child:*` events, in contiguous per-child blocks.
|
|
409
504
|
|
|
410
505
|
## Durable tool state
|
|
411
506
|
|
|
@@ -649,12 +744,16 @@ A runtime with a hard per-invocation CPU/wall cap (Cloudflare's ~5-minute limit)
|
|
|
649
744
|
|
|
650
745
|
`test/tool-state.test.ts` covers the runtime-agnostic half — snapshot hydration, staging, commit-on-settle, the size guard, the commit-failure hook, the in-memory default, and the loop round-trip.
|
|
651
746
|
|
|
652
|
-
`bun run restate:fixture:test` is the end-to-end suite against a **real** Restate server in Docker, and is deliberately **not** part of CI — run it locally when you touch the adapter. It boots the container, supervises the fixture service, and drives
|
|
747
|
+
`bun run restate:fixture:test` is the end-to-end suite against a **real** Restate server in Docker, and is deliberately **not** part of CI — run it locally when you touch the adapter. It boots the container, supervises the fixture service, and drives nineteen scenarios through the real ingress.
|
|
653
748
|
|
|
654
749
|
Five cover the adapter itself: baseline agent run; crash → replay (the service process is killed mid-invocation and respawned, so the journal, not a mock, decides what re-runs); per-object-key state isolation; commit-after-soft-failure; and an awakeable suspend/resume.
|
|
655
750
|
|
|
656
751
|
Five cover **subagents**, each pinned on an on-disk ledger that has to outlive the process: a crash inside the child; an awakeable park inside the child (suspend → external resume, with the child's `register` phase required to fire exactly once, or the resumed awakeable id would be one nobody resolves); a depth-2 grandchild crash, two un-journaled spawn frames deep; a journaled-deadline trip; and an attempt that fails **without** a process death, so the journal replays against warm module state — that one asserts an unchanged boot count to prove it took the retry path and not the crash path.
|
|
657
752
|
|
|
753
|
+
Eight cover **parallel** subagents (`restateSpawnTool`). Two children as separate invocations, asserted to have *overlapping* work intervals — counting executions can't tell parallel from sequential. A crash inside one of them, where the surviving sibling must show exactly one execution: proof that the children really are journal-isolated. Then the scheduling pair: four children in one turn against a fleet cap of two, where the interval ledger must show a maximum of exactly **two** open at once (3+ means the cap is ignored, 1 means the fleet silently fell back to sequential), and the same fan-out at cap 1 as a control, where nothing may overlap. Measured: 1031ms wall at cap 2 versus 2090ms at cap 1 for identical work.
|
|
754
|
+
|
|
755
|
+
The remaining three cover the failure modes that decide whether this is production-safe. **A parent crash mid-fan-out**: the parent dies while both children are still working, and on replay must re-attach to the running invocations rather than dispatch new ones — exactly one execution per child, even though the parent frame ran twice. **Depth-2 fan-out**: a child that itself spawns two grandchildren, whose intervals must overlap, so parallelism survives nesting. **Cancellation**, twice: once from outside (the parent invocation is cancelled mid-flight and no child may start another work step) and once from inside (`agent.abort()` with `onAbort` wired, where children finish the step in flight and never begin the next). **A post-merge replay**: the fan-out completes and merges, then the handler fails once, so the parent replays *after* the merge — the only scenario that re-executes the merge path, and the one that caught wall-clock timestamps leaking into the merged run records.
|
|
756
|
+
|
|
658
757
|
Both subagent bugs found in review — the `maxWallMs` watchdog clamp and the live `elapsed` inside a persisted string — were caught here and nowhere else. The suite fails if the crash never forced a respawn, so it cannot pass without actually exercising replay. This is the only coverage where replay, suspension, and virtual-object state semantics come from the SDK rather than from our reading of it.
|
|
659
758
|
|
|
660
759
|
`test/start.test.ts` covers the local `bun start --restate` runner helpers: option parsing, deployment registration, ingress invocation timeout wiring, readiness/service probing, runtime-state cleanup, cached completion, changed-input rejection, and `status` / `history` handlers.
|
package/docs/TUI.md
CHANGED
|
@@ -99,7 +99,7 @@ import {
|
|
|
99
99
|
| `Modal`, `ModalRoot`, `useModal`, `useModalAwareFocus` | Overlay layer. `Modal` owns its own esc-to-close handler. `useModalAwareFocus(preferred)` returns `false` while a modal is open so background inputs blur. **Single-slot:** `modal.open(node)` replaces the active node; modals do not stack. |
|
|
100
100
|
| `ModelPickerModal` | Cross-provider searchable model picker. Two commit modes: **single pick** (`onPick`) — ↵ returns `{ providerKey, modelId }`, used by the compaction-model picker; **ordered multi-select** (`onApplyChain`) — ↵ toggles the row in a numbered fallback chain (the number is the row's 1-based position; pick order = fallback order), esc applies. Entry 1 is the primary (provider swap, effort, options); entries 2+ become run-level fallback targets (`agent.run({ model: […] })`). Esc with an unchanged or emptied chain is a plain cancel. Cross-provider chains trigger a provider swap + session re-activation in the host; the footer model chip shows `+N` while fallbacks are configured. When a run actually falls back (`model:fallback`), the chip switches to the model NOW serving the run, warn-colored as the "temporary" cue, and a transcript info line records the switch; both snap back at the next run (fallback is run-sticky per run). |
|
|
101
101
|
| `EffortPickerModal` | Reasoning-effort picker. Lists `'off' / 'minimal' / 'low' / 'medium' / 'high'` plus `'adaptive'` when `supportsAdaptive` is true (Anthropic only). Surfaced via `ctrl+l` when `modelSupportsReasoning(descriptor, model)`. |
|
|
102
|
-
| `SettingsModal` | Tabbed: **Agent** (behaviour, persistence), **UI** (display, theming, animations), **Keybindings** (read-only catalog · `↵` opens `keybindings.json`), **Authentication** (current provider + detected providers · `↵` pops the AuthScreen), **Skills**, **MCP servers**. Category tab order is data-driven by `SETTINGS_CATEGORIES` from `zidane/chat`. Keybindings + Authentication tabs only appear when the host wires the `keybindings` / `authentication` data props; otherwise the legacy "open keybindings file" / "re-authenticate" rows render in the UI / Agent tabs as a fallback. One shared search input filters the active tab; `←/→` cycle tabs (preventDefault keeps the input cursor put), `↑/↓` navigate, `↵` toggle/cycle/pick/edit. MCP tab adds `ctrl+L` login, `ctrl+O` logout, and `esc` cancel-authorizing (Modal's esc-close still fires alongside). Sized at `maxWidth: 160, maxHeight: 50` — wider than the legacy modal so the Keybindings catalog + Authentication status block sit comfortably. |
|
|
102
|
+
| `SettingsModal` | Tabbed: **Agent** (behaviour, persistence), **UI** (display, theming, animations), **Keybindings** (read-only catalog · `↵` opens `keybindings.json`), **Authentication** (current provider + detected providers · `↵` pops the AuthScreen), **Instructions** (live default-context segments · `tab` reads inline, `↵` opens file-backed sources), **Skills**, **MCP servers**. Category tab order is data-driven by `SETTINGS_CATEGORIES` from `zidane/chat`. Keybindings + Authentication tabs only appear when the host wires the `keybindings` / `authentication` data props; otherwise the legacy "open keybindings file" / "re-authenticate" rows render in the UI / Agent tabs as a fallback. One shared search input filters the active tab; `←/→` cycle tabs (preventDefault keeps the input cursor put), `↑/↓` navigate, `↵` toggle/cycle/pick/edit. MCP tab adds `ctrl+L` login, `ctrl+O` logout, and `esc` cancel-authorizing (Modal's esc-close still fires alongside). Sized at `maxWidth: 160, maxHeight: 50` — wider than the legacy modal so the Keybindings catalog + Authentication status block sit comfortably. |
|
|
103
103
|
| `AgentPickerModal` | Multi-agent profile picker. Hidden when only one profile is registered. |
|
|
104
104
|
| `SkillsSettingsModal`, `McpsSettingsModal` | Standalone surfaces re-exported for embedders. The main `SettingsModal` integrates the same lists as tabs — prefer that for the in-app flow. `McpsSettingsModal` uses single-letter shortcuts (`l` / `o`); the integrated tab uses `ctrl+L` / `ctrl+O`. |
|
|
105
105
|
| `ToggleListModal` | Generic checkbox-list modal for `settings.enabledX` allowlists. Powers both the Skills + MCP server pickers; reuse with `keyOf` + `settingKey` + `renderDetail` to add a third toggle surface. |
|
|
@@ -378,6 +378,28 @@ Because top-level runs share `session.metadata.todos.session`, a list written in
|
|
|
378
378
|
|
|
379
379
|
The Settings modal is tabbed. The **Agent** and **UI** tabs each render three row kinds (toggle / choice / action) sourced from the chat-level tables, filtered by their `category` field. The **Keybindings** tab embeds the action catalog from `chat/keybindings.ts` (shared with the standalone `KeybindingsModal` via `groupBindings` + `KEYBINDING_KEY_COL_WIDTH`); `↵` opens the JSON file. The **Authentication** tab shows the active provider + a list of detected ones (from `detectAuth`); `↵` pops the full-screen `AuthScreen` (the multi-step wizard doesn't embed cleanly inside a modal). **Skills** and **MCP servers** render the discovered catalogs as enable/disable checklists.
|
|
380
380
|
|
|
381
|
+
The **Instructions** tab answers "what text is in my context, and where did it come from". Each row is a `ContextSegment` (`chat/context-segments`) — the base system prompt, each discovered `AGENTS.md` / `CLAUDE.md`, the skills catalog, the MCP instructions block, the subagent definitions — labelled with its origin (`built-in` / `file` / `generated` / `mcp server`) and heuristic token size. `tab` expands a row to read its text inline (clamped to `INSTRUCTION_PREVIEW_LINE_CAP` lines); `↵` opens the source in `$EDITOR`, but only for `origin: 'file'` rows — the other origins have no file, and the tab says so rather than no-op'ing. The shared search box matches each segment's full body, so it doubles as "which instruction source told the agent this?".
|
|
382
|
+
|
|
383
|
+
The data is split by who knows what: the agent supplies the segments it assembled via `agent.getContextSegments()`, while the host supplies the instruction files (path **and** text) it loaded at session activation — the agent only ever sees the rendered rules block as an opaque substring of the system prompt, so it can't attribute text back to a path. `App` holds those files in `sessionInstructionFilesRef`, captured alongside the prompt they were rendered into, so the tab reports what the session is actually running on rather than what happens to be on disk now. Because `SettingsModal` is opened with snapshot props, the fetch is an *action* (`actions.getContextSegments`) that the modal calls on every activation of the tab and whose loading / error state it owns; wiring that action is what makes the tab appear.
|
|
384
|
+
|
|
385
|
+
**`SYSTEM.md` override.** When a `SYSTEM.md` is active (see `docs/CHAT.md`), the System prompt row reports `origin: 'file'` and `↵` opens it like any other file row. When none exists, `↵` **creates** `~/.{prefix}/SYSTEM.md` seeded with the doctrine currently in effect and opens that — the one Instructions action that writes, so the footer hint reads `create SYSTEM.md override` rather than `open file`. Creation is idempotent (an existing file is opened, never clobbered) and the seed makes it a behavioral no-op until edited. A `SYSTEM.md` that exists on disk but isn't loaded — beaten on precedence, or found on a profile that ignores overrides — is reported in a note above the list, so editing an inactive file doesn't look ignored for no reason.
|
|
386
|
+
|
|
387
|
+
Two things the seed gets right, both of which were bugs worth naming:
|
|
388
|
+
|
|
389
|
+
- **It seeds from the DOCTRINE-ONLY prompt** (`composeHostContext().doctrineOnlySystem()`), not from the composed one. The composed prompt already contains the rendered `AGENTS.md` block in its static half, and an override composes *above* a freshly-appended copy of that block — so seeding from it would duplicate every instruction on the next session.
|
|
390
|
+
- **It refuses on profiles that can't be overridden.** `composeHostContext` returns `system: null` for any profile that isn't `build` or `plan`, which would seed a comment-only file. That file is still a *valid* override, so switching to `build` later would silently reduce the entire doctrine to an HTML comment. The create path throws instead, and the row explains that the profile supplies its own prompt.
|
|
391
|
+
|
|
392
|
+
`supportsOverride` also gates the hint and the row action, so a profile that ignores overrides never advertises a create action or claims a file is the prompt's source.
|
|
393
|
+
|
|
394
|
+
**`ctrl+r` reload.** Matches the refresh key on the Skills and MCPs tabs. Re-discovery is implicit — `composeHostContext` hits the filesystem on every call, so re-fetching picks up new files — and the real work is `onRefreshInstructions` rebuilding the live agent through the existing `rebuildActiveAgent()` so a running session adopts the new prompt without a TUI restart. While a run is in flight the rebuild is deferred onto `pendingAgentRebuildRef` and applied by `drainFollowUps` once the turn ends, because swapping the system prompt underneath a streaming turn is not safe. The row notice reports which happened (`agent rebuilt` vs `rebuild queued until the current run finishes`) since the consequences differ.
|
|
395
|
+
|
|
396
|
+
A mid-session rebuild costs more than it looks like it should, which is why this is an explicit keypress rather than a filesystem watcher:
|
|
397
|
+
|
|
398
|
+
- It replaces the system prompt on an existing conversation, guaranteeing a **prompt-cache miss** on the next turn, and the model sees different doctrine mid-conversation.
|
|
399
|
+
- `rebuildActiveAgent` destroys the stale agent, which **closes its MCP connections**. The fresh agent reconnects lazily on its next run, so the turn after a reload pays that reconnect. The notice says so (`MCP servers reconnect on the next turn`).
|
|
400
|
+
|
|
401
|
+
**No active session.** Settings opens on every screen but `auth`, so the tab is reachable from home / sessions / model-picker before `agentRef.current` exists. The action then composes a preview of the *next* session instead of reporting an empty context: `composeHostContext(profile, allowInteraction)` — the same helper `buildAgent` uses at activation, so the two can't drift — supplies the system prompt and the discovered instruction files, and `buildCatalog` over the enabled discovered skills supplies the catalog. The result carries `live: false` and the tab renders a note, because this view necessarily omits what the agent appends at run time (MCP instructions, extension system fragments, subagent definitions) and extension-contributed skills, which are only merged during `buildAgent`.
|
|
402
|
+
|
|
381
403
|
| Kind | Source | Activation |
|
|
382
404
|
|---|---|---|
|
|
383
405
|
| `toggle` | `SETTINGS_TOGGLES` (`BooleanSettingKey`) | `enter` flips the boolean |
|
|
@@ -389,6 +411,8 @@ The Settings modal is tabbed. The **Agent** and **UI** tabs each render three ro
|
|
|
389
411
|
|
|
390
412
|
`App` wires two general actions: `Keybindings` (open `keybindings.json` in `$EDITOR`) and `Authentication` (open `AuthScreen`). Authentication is gated on `!busy && !pendingApproval` so re-config can't tear down an in-flight run.
|
|
391
413
|
|
|
414
|
+
**Editor launching.** `launchEditor(path)` resolves `$VISUAL` / `$EDITOR`, falling back to the platform opener (`open` / `xdg-open` / `cmd /c start`). Those variables hold a command *line*, not a bare executable — `cursor --wait`, `code --wait`, `subl -w` — so `parseEditorCommand` (`src/tui/editor-command.ts`, quote-aware, no backslash escaping so Windows paths survive) splits the flags off and keeps them before the path. The promise resolves on the child's `'spawn'` event and rejects on `'error'`: `child_process.spawn` reports ENOENT through an event rather than a throw, so without that funnel a missing binary escaped every caller's `try`/`catch` as an unhandled emitter error and crashed into OpenTUI's error console. Callers therefore get a real rejection — the Instructions tab renders it as a row notice, the keybindings action logs it under `ZIDANE_DEBUG`.
|
|
415
|
+
|
|
392
416
|
A single shared search input filters the active tab's list (`label + description` for general items, `name + description` for skills, `name + transport + command/URL` for MCP servers). Typing resets the cursor on the active tab to 0; tab switches (`←/→`) preserve each tab's cursor (clamped against the current filtered size).
|
|
393
417
|
|
|
394
418
|
For the live source of truth, see `SETTINGS_TOGGLES` / `SETTINGS_CHOICES` in `src/chat/settings-context.tsx`. The current shape includes:
|
|
@@ -630,7 +654,9 @@ src/tui/
|
|
|
630
654
|
modal.tsx ModalRoot + Modal + useModalAwareFocus
|
|
631
655
|
model-picker.tsx Cross-provider searchable model list modal
|
|
632
656
|
effort-picker.tsx Reasoning-effort modal
|
|
633
|
-
settings-modal.tsx Tabbed settings panel (Agent / UI / Keybindings / Authentication / Skills / MCPs)
|
|
657
|
+
settings-modal.tsx Tabbed settings panel (Agent / UI / Keybindings / Authentication / Instructions / Skills / MCPs)
|
|
658
|
+
instruction-rows.ts Instructions-tab row model + preview clamping (pure)
|
|
659
|
+
editor-command.ts $EDITOR / $VISUAL command-line tokenizer (pure)
|
|
634
660
|
agent-picker.tsx Multi-agent profile modal (accentColor re-exported from zidane/chat)
|
|
635
661
|
skills-settings.tsx Skills list + toggle modal (standalone for embedders)
|
|
636
662
|
mcps-settings.tsx MCP servers list + toggle modal (standalone for embedders)
|