apex-code 0.1.1 → 0.2.0
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/CHANGELOG.md +22 -0
- package/README.md +3 -4
- package/dist/core/permissions/gate.d.ts.map +1 -1
- package/dist/core/permissions/gate.js +5 -2
- package/dist/core/permissions/gate.js.map +1 -1
- package/dist/core/permissions/modes.d.ts +1 -1
- package/dist/core/permissions/modes.d.ts.map +1 -1
- package/dist/core/permissions/modes.js +12 -2
- package/dist/core/permissions/modes.js.map +1 -1
- package/dist/core/permissions/protected-paths.d.ts +23 -0
- package/dist/core/permissions/protected-paths.d.ts.map +1 -0
- package/dist/core/permissions/protected-paths.js +41 -0
- package/dist/core/permissions/protected-paths.js.map +1 -0
- package/dist/core/removed-settings.d.ts +26 -0
- package/dist/core/removed-settings.d.ts.map +1 -0
- package/dist/core/removed-settings.js +40 -0
- package/dist/core/removed-settings.js.map +1 -0
- package/dist/core/settings-diagnostics.d.ts.map +1 -1
- package/dist/core/settings-diagnostics.js +16 -1
- package/dist/core/settings-diagnostics.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/contract.d.ts +15 -0
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts.map +1 -1
- package/dist/core/tools/path-permission.js +8 -1
- package/dist/core/tools/path-permission.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +10 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/message-spine.d.ts +14 -0
- package/dist/modes/interactive/components/message-spine.d.ts.map +1 -0
- package/dist/modes/interactive/components/message-spine.js +19 -0
- package/dist/modes/interactive/components/message-spine.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +4 -1
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/utils/paths.d.ts +10 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +34 -2
- package/dist/utils/paths.js.map +1 -1
- package/npm-shrinkwrap.json +5 -5
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/core/tools/contract.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The canonical tool contract. Settled by ADR 0010 and specified in full at\n * docs/architecture/contracts.md § 1 — this module implements that shape, it does\n * not redesign it. `contract` is required on every registered tool, and so is every\n * sub-field: a tool cannot compile without answering all four axes (capabilities,\n * permission, context, evidence), which is what keeps a new tool from silently\n * defaulting into \"unclassified\".\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport { type Static, type TSchema, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport type { PermissionPreview } from \"../permissions/responder.ts\";\n\n/** What class of thing a tool does. A set, not a single value — `bash` is `{exec}`. */\nexport type Capability = \"fs.read\" | \"fs.write\" | \"exec\" | \"net\" | \"delegate\" | \"ui\" | \"state\";\n\nexport const ALL_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([\n\t\"fs.read\",\n\t\"fs.write\",\n\t\"exec\",\n\t\"net\",\n\t\"delegate\",\n\t\"ui\",\n\t\"state\",\n]);\n\nexport type PermissionBehavior = \"allow\" | \"deny\" | \"ask\";\n\n/**\n * `ruleContent` is interpreted by the tool, never by the rule engine (ADR 0010).\n * That is what lets `Bash(git commit:*)` and `Read(~/.ssh/**)` mean entirely\n * different things while the engine that resolves precedence stays tool-agnostic.\n */\nexport interface PermissionSpec<TParams extends TSchema = TSchema> {\n\t/** Behavior when no rule matches. Read-only tools may default to \"allow\". */\n\tdefaultBehavior: PermissionBehavior;\n\n\t/**\n\t * Per-call behavior when no rule matches, for tools whose calls differ in\n\t * nature (bash: launching a command is \"ask\", retrieving or killing an\n\t * already-approved background command is \"allow\"). Return undefined to fall\n\t * back to `defaultBehavior`. Floors and matching rules still outrank it --\n\t * this only replaces the fallthrough, never a rule or a mode floor.\n\t */\n\tdefaultBehaviorFor?(params: Static<TParams>): PermissionBehavior | undefined;\n\n\t/** Prepare canonical operation facts on the validated call before authorization and execution. */\n\tprepareCall?(params: Static<TParams>): void;\n\n\t/**\n\t * Describe, for a human about to approve it, what this call would change.\n\t *\n\t * Called by the gate only once it has resolved to `ask`, so an allowed or denied\n\t * call never pays for it. It must read through the value `prepareCall` stored\n\t * rather than re-deriving a target, which is what keeps the diff the user\n\t * approves and the bytes execution writes the same file (ADR 0029). It must not\n\t * execute the tool, write, or widen what the gate already validated.\n\t */\n\tpreviewCall?(params: Static<TParams>): PermissionPreview;\n\t/** Does this call match this rule's content as an allow rule? */\n\tmatches(ruleContent: string, params: Static<TParams>): boolean;\n\t/** Optional deny matcher. It may identify any prohibited part of a structured call. */\n\tmatchesDeny?(ruleContent: string, params: Static<TParams>): boolean;\n\t/** True when the call contains grammar that cannot be safely authorized by an allow. */\n\tisUnknown?(params: Static<TParams>): boolean;\n\n\t/** Human-readable rendering of a rule, for prompts and denial messages. */\n\tdescribe(ruleContent: string): string;\n\n\t/**\n\t * The rule that would allow this exact call — what \"always allow this\"\n\t * persists. Return null when the call is not generalizable into a rule.\n\t */\n\truleForCall(params: Static<TParams>): string | null;\n}\n\nexport interface ContextSpec {\n\t/**\n\t * True when the result's information is recoverable — the same content can be\n\t * obtained again by re-running the tool or reading the workspace.\n\t *\n\t * ONLY recoverable results may be evicted (Phase 3). A tool whose result cannot\n\t * be regenerated (a nondeterministic command, a consumed one-shot resource)\n\t * must set this false, or eviction would silently destroy information the\n\t * transcript is the only record of.\n\t */\n\tresultRecoverable: boolean;\n\n\t/** Marker substituted for an evicted result. */\n\tevictionMarker?: string;\n\n\t/** Announce by name only; load the parameter schema on demand. */\n\tdeferSchema: boolean;\n\n\t/** Soft cap on result tokens before truncation. */\n\toutputBudgetTokens?: number;\n}\n\nexport type EvidenceKind = \"diff\" | \"test\" | \"command\" | \"manual\" | \"workflow\" | \"diagnostic\";\n\n/** Source-observed command facts. `exitCode` is absent only when execution never began. */\nexport interface CommandEvidenceRecord {\n\tkind: \"command\";\n\tcommand: string;\n\tcwd?: string;\n\texecutable?: string;\n\targv?: string[];\n\texitCode?: number | null;\n}\n\n/** A file mutation is identified by hashes and paths, never raw file or patch contents. */\nexport interface DiffEvidenceRecord {\n\tkind: \"diff\";\n\tpath: string;\n\tpatchHash?: string;\n\tcontentHash?: string;\n\tbyteCount?: number;\n}\n\n/** Zero-filled diagnostic severity buckets for one bounded language-server outcome. */\nexport interface DiagnosticSeverityCounts {\n\terror: number;\n\twarning: number;\n\tinformation: number;\n\thint: number;\n\tunspecified: number;\n\tother: number;\n}\n\n/** Stable durable classification. Free-form server and process errors never enter evidence. */\nexport type DiagnosticUnavailableKind =\n\t| \"no-server\"\n\t| \"disposed\"\n\t| \"unsupported-sync\"\n\t| \"timed-out\"\n\t| \"aborted\"\n\t| \"superseded\"\n\t| \"server-failed\";\n\n/** Bounded post-mutation diagnostic facts, never diagnostic messages or rendered failure reasons. */\nexport type DiagnosticEvidenceRecord =\n\t| {\n\t\t\tkind: \"diagnostic\";\n\t\t\tpath: string;\n\t\t\tstatus: \"ok\";\n\t\t\tserverId: string;\n\t\t\tdiagnosticCount: number;\n\t\t\tseverityCounts: DiagnosticSeverityCounts;\n\t\t\ttruncated: boolean;\n\t }\n\t| {\n\t\t\tkind: \"diagnostic\";\n\t\t\tpath: string;\n\t\t\tstatus: \"unavailable\";\n\t\t\tserverId?: string;\n\t\t\tunavailableKind: DiagnosticUnavailableKind;\n\t };\n\n/** How a `test` run actually ended. */\nexport type TestProcessOutcome = \"exit\" | \"signal\" | \"timeout\" | \"cancelled\" | \"spawn-failed\";\n\n/** Bounded argv-based test execution facts. Full output never enters evidence; it lives in artifacts. */\nexport interface TestEvidenceRecord {\n\tkind: \"test\";\n\tcwd: string;\n\texecutable: string;\n\targv: string[];\n\texitCode: number | null;\n\t/** How the run ended. Unset means a legacy record; derive from `exitCode` when reading. */\n\toutcome?: TestProcessOutcome;\n\t/** True when the model-facing view omitted part of a stream (full output lives in artifacts). */\n\toutputTruncated?: boolean;\n}\n\n/** Explicit human attestation; policy decides how, or whether, it is sufficient. */\nexport interface ManualEvidenceRecord {\n\tkind: \"manual\";\n\tvalue?: unknown;\n\tobserved?: unknown;\n\tstatus?: string;\n}\n\n/** Source facts from approval and delegation workflows. */\nexport interface WorkflowEvidenceRecord {\n\tkind: \"workflow\";\n\tplan?: string;\n\tapproved?: boolean;\n\tagentType?: string;\n\ttask?: string;\n\thandle?: string;\n}\n\n/** A structured source record. Policy is a separate consumer, not an additional variant. */\nexport type EvidenceRecord =\n\t| CommandEvidenceRecord\n\t| DiffEvidenceRecord\n\t| DiagnosticEvidenceRecord\n\t| TestEvidenceRecord\n\t| ManualEvidenceRecord\n\t| WorkflowEvidenceRecord;\n\n/** Durable destination for source-level tool evidence. Policy is deliberately not part of this boundary. */\nexport interface EvidenceSink {\n\trecord(entry: { toolName: string; records: EvidenceRecord[] }): void;\n\trecordDiagnostic?(diagnostic: EvidenceCaptureDiagnostic): void;\n}\n\n/** A capture failure is observable but never changes an already-completed tool result. */\nexport interface EvidenceCaptureDiagnostic {\n\ttoolName: string;\n\treason: string;\n}\n\nexport interface EvidenceSpec<TParams extends TSchema = TSchema, TDetails = unknown> {\n\t/** Kinds this tool can emit. Empty set is valid and explicit. */\n\temits: ReadonlySet<EvidenceKind>;\n\n\t/**\n\t * Derive evidence from a completed call. Runs inside the tool's own execution\n\t * path, with access to what actually happened, not a reconstruction from\n\t * rendered output.\n\t */\n\tcapture(params: Static<TParams>, result: AgentToolResult<TDetails>): EvidenceRecord[];\n}\n\nexport interface ToolContract<TParams extends TSchema = TSchema, TDetails = unknown> {\n\tcapabilities: ReadonlySet<Capability>;\n\tpermission: PermissionSpec<TParams>;\n\tcontext: ContextSpec;\n\tevidence: EvidenceSpec<TParams, TDetails>;\n}\n\n/**\n * Apex Code extends upstream ToolDefinition with exactly one required field.\n *\n * `TState` defaults to `any`, matching upstream `ToolDefinition` exactly (not\n * `unknown`). That default is load-bearing: TypeScript compares two instantiations\n * of the *same* generic interface (here, both rooted in `ToolDefinition`) using each\n * type parameter's declared variance, without expanding `Static<TParams>`\n * structurally — but only when every type argument lines up, including the ones a\n * caller left defaulted. A mismatched default reintroduces a full structural check,\n * where `Static<any>` does not behave like `any`, and breaks every pre-existing\n * `ToolDefinition<any, X>`-typed consumer across the codebase.\n */\nexport interface ApexToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>\n\textends ToolDefinition<TParams, TDetails, TState> {\n\tcontract: ToolContract<TParams, TDetails>;\n}\n\n/**\n * Conservative contract for a tool registered without one -- third-party extension\n * tools, which cannot supply a `contract`. MCP tools no longer land here: they reach\n * the model through the built-in `mcp` proxy, whose contract is declared in\n * `core/mcp/contract.ts`. Full capability set (so it can\n * never widen a delegation ceiling), `ask` by default, never evicted, schema\n * deferred, emits nothing. Matching is exact-argument only: a foreign tool's rule\n * authorizes exactly the call it was generated from, never a pattern.\n *\n * This must also be reported as unclassified wherever a consumer describes the tool\n * registry (contracts.md invariant 1) — a conservative default nobody can see is\n * indistinguishable from a bug.\n */\nexport const UNCLASSIFIED: ToolContract<TSchema, unknown> = {\n\tcapabilities: ALL_CAPABILITIES,\n\tpermission: {\n\t\tdefaultBehavior: \"ask\",\n\t\tmatches: (ruleContent, params) => ruleContent === JSON.stringify(params),\n\t\tdescribe: (ruleContent) => `Exact call: ${ruleContent}`,\n\t\truleForCall: (params) => JSON.stringify(params),\n\t},\n\tcontext: { resultRecoverable: false, deferSchema: false },\n\tevidence: { emits: new Set(), capture: () => [] },\n};\n\n/** Resolve a complete tool contract for enforcement consumers. */\nexport function resolveToolContract(\n\tlookup: (toolName: string) => ToolContract | undefined,\n\ttoolName: string,\n): ToolContract {\n\treturn lookup(toolName) ?? UNCLASSIFIED;\n}\n\n/** Resolve the context projection using the same canonical foreign-tool fallback. */\nexport function resolveToolContext(\n\tlookup: (toolName: string) => Pick<ToolContract, \"context\"> | undefined,\n\ttoolName: string,\n): Pick<ToolContract, \"context\"> {\n\treturn lookup(toolName) ?? UNCLASSIFIED;\n}\n\n/**\n * A tool whose call shapes differ, declared so a provider can still see its fields.\n *\n * `Type.Union` compiles to `{ anyOf: [...] }`, which carries no top-level\n * `properties`. The Anthropic request builder reads `properties` and `required`\n * directly (`anthropic-messages.js`, `legacyInputSchema`), so a union reached the\n * model as an object with no fields at all. It then guessed, and our own\n * validation rejected the guess.\n *\n * The union still decides what is valid. `properties` is advertisement, listing\n * every field across the variants so a provider that ignores `anyOf` has\n * something to work from. It deliberately carries no `required`, because no field\n * is required by every variant.\n */\nexport function toolUnion<T extends TSchema[]>(variants: [...T], properties: Record<string, TSchema>) {\n\treturn Type.Union(variants, { type: \"object\", properties });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/core/tools/contract.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The canonical tool contract. Settled by ADR 0010 and specified in full at\n * docs/architecture/contracts.md § 1 — this module implements that shape, it does\n * not redesign it. `contract` is required on every registered tool, and so is every\n * sub-field: a tool cannot compile without answering all four axes (capabilities,\n * permission, context, evidence), which is what keeps a new tool from silently\n * defaulting into \"unclassified\".\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport { type Static, type TSchema, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport type { PermissionPreview } from \"../permissions/responder.ts\";\n\n/** What class of thing a tool does. A set, not a single value — `bash` is `{exec}`. */\nexport type Capability = \"fs.read\" | \"fs.write\" | \"exec\" | \"net\" | \"delegate\" | \"ui\" | \"state\";\n\nexport const ALL_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([\n\t\"fs.read\",\n\t\"fs.write\",\n\t\"exec\",\n\t\"net\",\n\t\"delegate\",\n\t\"ui\",\n\t\"state\",\n]);\n\nexport type PermissionBehavior = \"allow\" | \"deny\" | \"ask\";\n\n/**\n * `ruleContent` is interpreted by the tool, never by the rule engine (ADR 0010).\n * That is what lets `Bash(git commit:*)` and `Read(~/.ssh/**)` mean entirely\n * different things while the engine that resolves precedence stays tool-agnostic.\n */\nexport interface PermissionSpec<TParams extends TSchema = TSchema> {\n\t/** Behavior when no rule matches. Read-only tools may default to \"allow\". */\n\tdefaultBehavior: PermissionBehavior;\n\n\t/**\n\t * Per-call behavior when no rule matches, for tools whose calls differ in\n\t * nature (bash: launching a command is \"ask\", retrieving or killing an\n\t * already-approved background command is \"allow\"). Return undefined to fall\n\t * back to `defaultBehavior`. Floors and matching rules still outrank it --\n\t * this only replaces the fallthrough, never a rule or a mode floor.\n\t */\n\tdefaultBehaviorFor?(params: Static<TParams>): PermissionBehavior | undefined;\n\n\t/**\n\t * True when this call's target resolves inside the session's workspace.\n\t *\n\t * Only `acceptEdits` consults it, to auto-allow an edit that stays in the workspace\n\t * and no other. A tool that cannot answer leaves it undefined and is not auto-allowed,\n\t * which is the fail-closed side: a tool that cannot say where it writes is not a tool\n\t * a mode named for edits should approve unattended.\n\t */\n\twithinWorkspace?(params: Static<TParams>): boolean;\n\n\t/**\n\t * Why this call's target must not be handed over without an explicit decision, or\n\t * undefined for an ordinary target. Outranks every rule source but `policy`, and is\n\t * not lifted by `bypassPermissions`.\n\t */\n\tprotectedTarget?(params: Static<TParams>): string | undefined;\n\n\t/** Prepare canonical operation facts on the validated call before authorization and execution. */\n\tprepareCall?(params: Static<TParams>): void;\n\n\t/**\n\t * Describe, for a human about to approve it, what this call would change.\n\t *\n\t * Called by the gate only once it has resolved to `ask`, so an allowed or denied\n\t * call never pays for it. It must read through the value `prepareCall` stored\n\t * rather than re-deriving a target, which is what keeps the diff the user\n\t * approves and the bytes execution writes the same file (ADR 0029). It must not\n\t * execute the tool, write, or widen what the gate already validated.\n\t */\n\tpreviewCall?(params: Static<TParams>): PermissionPreview;\n\t/** Does this call match this rule's content as an allow rule? */\n\tmatches(ruleContent: string, params: Static<TParams>): boolean;\n\t/** Optional deny matcher. It may identify any prohibited part of a structured call. */\n\tmatchesDeny?(ruleContent: string, params: Static<TParams>): boolean;\n\t/** True when the call contains grammar that cannot be safely authorized by an allow. */\n\tisUnknown?(params: Static<TParams>): boolean;\n\n\t/** Human-readable rendering of a rule, for prompts and denial messages. */\n\tdescribe(ruleContent: string): string;\n\n\t/**\n\t * The rule that would allow this exact call — what \"always allow this\"\n\t * persists. Return null when the call is not generalizable into a rule.\n\t */\n\truleForCall(params: Static<TParams>): string | null;\n}\n\nexport interface ContextSpec {\n\t/**\n\t * True when the result's information is recoverable — the same content can be\n\t * obtained again by re-running the tool or reading the workspace.\n\t *\n\t * ONLY recoverable results may be evicted (Phase 3). A tool whose result cannot\n\t * be regenerated (a nondeterministic command, a consumed one-shot resource)\n\t * must set this false, or eviction would silently destroy information the\n\t * transcript is the only record of.\n\t */\n\tresultRecoverable: boolean;\n\n\t/** Marker substituted for an evicted result. */\n\tevictionMarker?: string;\n\n\t/** Announce by name only; load the parameter schema on demand. */\n\tdeferSchema: boolean;\n\n\t/** Soft cap on result tokens before truncation. */\n\toutputBudgetTokens?: number;\n}\n\nexport type EvidenceKind = \"diff\" | \"test\" | \"command\" | \"manual\" | \"workflow\" | \"diagnostic\";\n\n/** Source-observed command facts. `exitCode` is absent only when execution never began. */\nexport interface CommandEvidenceRecord {\n\tkind: \"command\";\n\tcommand: string;\n\tcwd?: string;\n\texecutable?: string;\n\targv?: string[];\n\texitCode?: number | null;\n}\n\n/** A file mutation is identified by hashes and paths, never raw file or patch contents. */\nexport interface DiffEvidenceRecord {\n\tkind: \"diff\";\n\tpath: string;\n\tpatchHash?: string;\n\tcontentHash?: string;\n\tbyteCount?: number;\n}\n\n/** Zero-filled diagnostic severity buckets for one bounded language-server outcome. */\nexport interface DiagnosticSeverityCounts {\n\terror: number;\n\twarning: number;\n\tinformation: number;\n\thint: number;\n\tunspecified: number;\n\tother: number;\n}\n\n/** Stable durable classification. Free-form server and process errors never enter evidence. */\nexport type DiagnosticUnavailableKind =\n\t| \"no-server\"\n\t| \"disposed\"\n\t| \"unsupported-sync\"\n\t| \"timed-out\"\n\t| \"aborted\"\n\t| \"superseded\"\n\t| \"server-failed\";\n\n/** Bounded post-mutation diagnostic facts, never diagnostic messages or rendered failure reasons. */\nexport type DiagnosticEvidenceRecord =\n\t| {\n\t\t\tkind: \"diagnostic\";\n\t\t\tpath: string;\n\t\t\tstatus: \"ok\";\n\t\t\tserverId: string;\n\t\t\tdiagnosticCount: number;\n\t\t\tseverityCounts: DiagnosticSeverityCounts;\n\t\t\ttruncated: boolean;\n\t }\n\t| {\n\t\t\tkind: \"diagnostic\";\n\t\t\tpath: string;\n\t\t\tstatus: \"unavailable\";\n\t\t\tserverId?: string;\n\t\t\tunavailableKind: DiagnosticUnavailableKind;\n\t };\n\n/** How a `test` run actually ended. */\nexport type TestProcessOutcome = \"exit\" | \"signal\" | \"timeout\" | \"cancelled\" | \"spawn-failed\";\n\n/** Bounded argv-based test execution facts. Full output never enters evidence; it lives in artifacts. */\nexport interface TestEvidenceRecord {\n\tkind: \"test\";\n\tcwd: string;\n\texecutable: string;\n\targv: string[];\n\texitCode: number | null;\n\t/** How the run ended. Unset means a legacy record; derive from `exitCode` when reading. */\n\toutcome?: TestProcessOutcome;\n\t/** True when the model-facing view omitted part of a stream (full output lives in artifacts). */\n\toutputTruncated?: boolean;\n}\n\n/** Explicit human attestation; policy decides how, or whether, it is sufficient. */\nexport interface ManualEvidenceRecord {\n\tkind: \"manual\";\n\tvalue?: unknown;\n\tobserved?: unknown;\n\tstatus?: string;\n}\n\n/** Source facts from approval and delegation workflows. */\nexport interface WorkflowEvidenceRecord {\n\tkind: \"workflow\";\n\tplan?: string;\n\tapproved?: boolean;\n\tagentType?: string;\n\ttask?: string;\n\thandle?: string;\n}\n\n/** A structured source record. Policy is a separate consumer, not an additional variant. */\nexport type EvidenceRecord =\n\t| CommandEvidenceRecord\n\t| DiffEvidenceRecord\n\t| DiagnosticEvidenceRecord\n\t| TestEvidenceRecord\n\t| ManualEvidenceRecord\n\t| WorkflowEvidenceRecord;\n\n/** Durable destination for source-level tool evidence. Policy is deliberately not part of this boundary. */\nexport interface EvidenceSink {\n\trecord(entry: { toolName: string; records: EvidenceRecord[] }): void;\n\trecordDiagnostic?(diagnostic: EvidenceCaptureDiagnostic): void;\n}\n\n/** A capture failure is observable but never changes an already-completed tool result. */\nexport interface EvidenceCaptureDiagnostic {\n\ttoolName: string;\n\treason: string;\n}\n\nexport interface EvidenceSpec<TParams extends TSchema = TSchema, TDetails = unknown> {\n\t/** Kinds this tool can emit. Empty set is valid and explicit. */\n\temits: ReadonlySet<EvidenceKind>;\n\n\t/**\n\t * Derive evidence from a completed call. Runs inside the tool's own execution\n\t * path, with access to what actually happened, not a reconstruction from\n\t * rendered output.\n\t */\n\tcapture(params: Static<TParams>, result: AgentToolResult<TDetails>): EvidenceRecord[];\n}\n\nexport interface ToolContract<TParams extends TSchema = TSchema, TDetails = unknown> {\n\tcapabilities: ReadonlySet<Capability>;\n\tpermission: PermissionSpec<TParams>;\n\tcontext: ContextSpec;\n\tevidence: EvidenceSpec<TParams, TDetails>;\n}\n\n/**\n * Apex Code extends upstream ToolDefinition with exactly one required field.\n *\n * `TState` defaults to `any`, matching upstream `ToolDefinition` exactly (not\n * `unknown`). That default is load-bearing: TypeScript compares two instantiations\n * of the *same* generic interface (here, both rooted in `ToolDefinition`) using each\n * type parameter's declared variance, without expanding `Static<TParams>`\n * structurally — but only when every type argument lines up, including the ones a\n * caller left defaulted. A mismatched default reintroduces a full structural check,\n * where `Static<any>` does not behave like `any`, and breaks every pre-existing\n * `ToolDefinition<any, X>`-typed consumer across the codebase.\n */\nexport interface ApexToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>\n\textends ToolDefinition<TParams, TDetails, TState> {\n\tcontract: ToolContract<TParams, TDetails>;\n}\n\n/**\n * Conservative contract for a tool registered without one -- third-party extension\n * tools, which cannot supply a `contract`. MCP tools no longer land here: they reach\n * the model through the built-in `mcp` proxy, whose contract is declared in\n * `core/mcp/contract.ts`. Full capability set (so it can\n * never widen a delegation ceiling), `ask` by default, never evicted, schema\n * deferred, emits nothing. Matching is exact-argument only: a foreign tool's rule\n * authorizes exactly the call it was generated from, never a pattern.\n *\n * This must also be reported as unclassified wherever a consumer describes the tool\n * registry (contracts.md invariant 1) — a conservative default nobody can see is\n * indistinguishable from a bug.\n */\nexport const UNCLASSIFIED: ToolContract<TSchema, unknown> = {\n\tcapabilities: ALL_CAPABILITIES,\n\tpermission: {\n\t\tdefaultBehavior: \"ask\",\n\t\tmatches: (ruleContent, params) => ruleContent === JSON.stringify(params),\n\t\tdescribe: (ruleContent) => `Exact call: ${ruleContent}`,\n\t\truleForCall: (params) => JSON.stringify(params),\n\t},\n\tcontext: { resultRecoverable: false, deferSchema: false },\n\tevidence: { emits: new Set(), capture: () => [] },\n};\n\n/** Resolve a complete tool contract for enforcement consumers. */\nexport function resolveToolContract(\n\tlookup: (toolName: string) => ToolContract | undefined,\n\ttoolName: string,\n): ToolContract {\n\treturn lookup(toolName) ?? UNCLASSIFIED;\n}\n\n/** Resolve the context projection using the same canonical foreign-tool fallback. */\nexport function resolveToolContext(\n\tlookup: (toolName: string) => Pick<ToolContract, \"context\"> | undefined,\n\ttoolName: string,\n): Pick<ToolContract, \"context\"> {\n\treturn lookup(toolName) ?? UNCLASSIFIED;\n}\n\n/**\n * A tool whose call shapes differ, declared so a provider can still see its fields.\n *\n * `Type.Union` compiles to `{ anyOf: [...] }`, which carries no top-level\n * `properties`. The Anthropic request builder reads `properties` and `required`\n * directly (`anthropic-messages.js`, `legacyInputSchema`), so a union reached the\n * model as an object with no fields at all. It then guessed, and our own\n * validation rejected the guess.\n *\n * The union still decides what is valid. `properties` is advertisement, listing\n * every field across the variants so a provider that ignores `anyOf` has\n * something to work from. It deliberately carries no `required`, because no field\n * is required by every variant.\n */\nexport function toolUnion<T extends TSchema[]>(variants: [...T], properties: Record<string, TSchema>) {\n\treturn Type.Union(variants, { type: \"object\", properties });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/core/tools/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAA6B,IAAI,EAAE,MAAM,SAAS,CAAC;AAO1D,MAAM,CAAC,MAAM,gBAAgB,GAA4B,IAAI,GAAG,CAAa;IAC5E,SAAS;IACT,UAAU;IACV,MAAM;IACN,KAAK;IACL,UAAU;IACV,IAAI;IACJ,OAAO;CACP,CAAC,CAAC;AAiOH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,YAAY,GAAmC;IAC3D,YAAY,EAAE,gBAAgB;IAC9B,UAAU,EAAE;QACX,eAAe,EAAE,KAAK;QACtB,OAAO,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,WAAW,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;QACxE,QAAQ,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,eAAe,WAAW,EAAE;QACvD,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;KAC/C;IACD,OAAO,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE;IACzD,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE;CACjD,CAAC;AAEF,kEAAkE;AAClE,MAAM,UAAU,mBAAmB,CAClC,MAAsD,EACtD,QAAgB,EACD;IACf,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC;AAAA,CACxC;AAED,qFAAqF;AACrF,MAAM,UAAU,kBAAkB,CACjC,MAAuE,EACvE,QAAgB,EACgB;IAChC,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC;AAAA,CACxC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAsB,QAAgB,EAAE,UAAmC,EAAE;IACrG,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,CAC5D","sourcesContent":["/**\n * The canonical tool contract. Settled by ADR 0010 and specified in full at\n * docs/architecture/contracts.md § 1 — this module implements that shape, it does\n * not redesign it. `contract` is required on every registered tool, and so is every\n * sub-field: a tool cannot compile without answering all four axes (capabilities,\n * permission, context, evidence), which is what keeps a new tool from silently\n * defaulting into \"unclassified\".\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport { type Static, type TSchema, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport type { PermissionPreview } from \"../permissions/responder.ts\";\n\n/** What class of thing a tool does. A set, not a single value — `bash` is `{exec}`. */\nexport type Capability = \"fs.read\" | \"fs.write\" | \"exec\" | \"net\" | \"delegate\" | \"ui\" | \"state\";\n\nexport const ALL_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([\n\t\"fs.read\",\n\t\"fs.write\",\n\t\"exec\",\n\t\"net\",\n\t\"delegate\",\n\t\"ui\",\n\t\"state\",\n]);\n\nexport type PermissionBehavior = \"allow\" | \"deny\" | \"ask\";\n\n/**\n * `ruleContent` is interpreted by the tool, never by the rule engine (ADR 0010).\n * That is what lets `Bash(git commit:*)` and `Read(~/.ssh/**)` mean entirely\n * different things while the engine that resolves precedence stays tool-agnostic.\n */\nexport interface PermissionSpec<TParams extends TSchema = TSchema> {\n\t/** Behavior when no rule matches. Read-only tools may default to \"allow\". */\n\tdefaultBehavior: PermissionBehavior;\n\n\t/**\n\t * Per-call behavior when no rule matches, for tools whose calls differ in\n\t * nature (bash: launching a command is \"ask\", retrieving or killing an\n\t * already-approved background command is \"allow\"). Return undefined to fall\n\t * back to `defaultBehavior`. Floors and matching rules still outrank it --\n\t * this only replaces the fallthrough, never a rule or a mode floor.\n\t */\n\tdefaultBehaviorFor?(params: Static<TParams>): PermissionBehavior | undefined;\n\n\t/** Prepare canonical operation facts on the validated call before authorization and execution. */\n\tprepareCall?(params: Static<TParams>): void;\n\n\t/**\n\t * Describe, for a human about to approve it, what this call would change.\n\t *\n\t * Called by the gate only once it has resolved to `ask`, so an allowed or denied\n\t * call never pays for it. It must read through the value `prepareCall` stored\n\t * rather than re-deriving a target, which is what keeps the diff the user\n\t * approves and the bytes execution writes the same file (ADR 0029). It must not\n\t * execute the tool, write, or widen what the gate already validated.\n\t */\n\tpreviewCall?(params: Static<TParams>): PermissionPreview;\n\t/** Does this call match this rule's content as an allow rule? */\n\tmatches(ruleContent: string, params: Static<TParams>): boolean;\n\t/** Optional deny matcher. It may identify any prohibited part of a structured call. */\n\tmatchesDeny?(ruleContent: string, params: Static<TParams>): boolean;\n\t/** True when the call contains grammar that cannot be safely authorized by an allow. */\n\tisUnknown?(params: Static<TParams>): boolean;\n\n\t/** Human-readable rendering of a rule, for prompts and denial messages. */\n\tdescribe(ruleContent: string): string;\n\n\t/**\n\t * The rule that would allow this exact call — what \"always allow this\"\n\t * persists. Return null when the call is not generalizable into a rule.\n\t */\n\truleForCall(params: Static<TParams>): string | null;\n}\n\nexport interface ContextSpec {\n\t/**\n\t * True when the result's information is recoverable — the same content can be\n\t * obtained again by re-running the tool or reading the workspace.\n\t *\n\t * ONLY recoverable results may be evicted (Phase 3). A tool whose result cannot\n\t * be regenerated (a nondeterministic command, a consumed one-shot resource)\n\t * must set this false, or eviction would silently destroy information the\n\t * transcript is the only record of.\n\t */\n\tresultRecoverable: boolean;\n\n\t/** Marker substituted for an evicted result. */\n\tevictionMarker?: string;\n\n\t/** Announce by name only; load the parameter schema on demand. */\n\tdeferSchema: boolean;\n\n\t/** Soft cap on result tokens before truncation. */\n\toutputBudgetTokens?: number;\n}\n\nexport type EvidenceKind = \"diff\" | \"test\" | \"command\" | \"manual\" | \"workflow\" | \"diagnostic\";\n\n/** Source-observed command facts. `exitCode` is absent only when execution never began. */\nexport interface CommandEvidenceRecord {\n\tkind: \"command\";\n\tcommand: string;\n\tcwd?: string;\n\texecutable?: string;\n\targv?: string[];\n\texitCode?: number | null;\n}\n\n/** A file mutation is identified by hashes and paths, never raw file or patch contents. */\nexport interface DiffEvidenceRecord {\n\tkind: \"diff\";\n\tpath: string;\n\tpatchHash?: string;\n\tcontentHash?: string;\n\tbyteCount?: number;\n}\n\n/** Zero-filled diagnostic severity buckets for one bounded language-server outcome. */\nexport interface DiagnosticSeverityCounts {\n\terror: number;\n\twarning: number;\n\tinformation: number;\n\thint: number;\n\tunspecified: number;\n\tother: number;\n}\n\n/** Stable durable classification. Free-form server and process errors never enter evidence. */\nexport type DiagnosticUnavailableKind =\n\t| \"no-server\"\n\t| \"disposed\"\n\t| \"unsupported-sync\"\n\t| \"timed-out\"\n\t| \"aborted\"\n\t| \"superseded\"\n\t| \"server-failed\";\n\n/** Bounded post-mutation diagnostic facts, never diagnostic messages or rendered failure reasons. */\nexport type DiagnosticEvidenceRecord =\n\t| {\n\t\t\tkind: \"diagnostic\";\n\t\t\tpath: string;\n\t\t\tstatus: \"ok\";\n\t\t\tserverId: string;\n\t\t\tdiagnosticCount: number;\n\t\t\tseverityCounts: DiagnosticSeverityCounts;\n\t\t\ttruncated: boolean;\n\t }\n\t| {\n\t\t\tkind: \"diagnostic\";\n\t\t\tpath: string;\n\t\t\tstatus: \"unavailable\";\n\t\t\tserverId?: string;\n\t\t\tunavailableKind: DiagnosticUnavailableKind;\n\t };\n\n/** How a `test` run actually ended. */\nexport type TestProcessOutcome = \"exit\" | \"signal\" | \"timeout\" | \"cancelled\" | \"spawn-failed\";\n\n/** Bounded argv-based test execution facts. Full output never enters evidence; it lives in artifacts. */\nexport interface TestEvidenceRecord {\n\tkind: \"test\";\n\tcwd: string;\n\texecutable: string;\n\targv: string[];\n\texitCode: number | null;\n\t/** How the run ended. Unset means a legacy record; derive from `exitCode` when reading. */\n\toutcome?: TestProcessOutcome;\n\t/** True when the model-facing view omitted part of a stream (full output lives in artifacts). */\n\toutputTruncated?: boolean;\n}\n\n/** Explicit human attestation; policy decides how, or whether, it is sufficient. */\nexport interface ManualEvidenceRecord {\n\tkind: \"manual\";\n\tvalue?: unknown;\n\tobserved?: unknown;\n\tstatus?: string;\n}\n\n/** Source facts from approval and delegation workflows. */\nexport interface WorkflowEvidenceRecord {\n\tkind: \"workflow\";\n\tplan?: string;\n\tapproved?: boolean;\n\tagentType?: string;\n\ttask?: string;\n\thandle?: string;\n}\n\n/** A structured source record. Policy is a separate consumer, not an additional variant. */\nexport type EvidenceRecord =\n\t| CommandEvidenceRecord\n\t| DiffEvidenceRecord\n\t| DiagnosticEvidenceRecord\n\t| TestEvidenceRecord\n\t| ManualEvidenceRecord\n\t| WorkflowEvidenceRecord;\n\n/** Durable destination for source-level tool evidence. Policy is deliberately not part of this boundary. */\nexport interface EvidenceSink {\n\trecord(entry: { toolName: string; records: EvidenceRecord[] }): void;\n\trecordDiagnostic?(diagnostic: EvidenceCaptureDiagnostic): void;\n}\n\n/** A capture failure is observable but never changes an already-completed tool result. */\nexport interface EvidenceCaptureDiagnostic {\n\ttoolName: string;\n\treason: string;\n}\n\nexport interface EvidenceSpec<TParams extends TSchema = TSchema, TDetails = unknown> {\n\t/** Kinds this tool can emit. Empty set is valid and explicit. */\n\temits: ReadonlySet<EvidenceKind>;\n\n\t/**\n\t * Derive evidence from a completed call. Runs inside the tool's own execution\n\t * path, with access to what actually happened, not a reconstruction from\n\t * rendered output.\n\t */\n\tcapture(params: Static<TParams>, result: AgentToolResult<TDetails>): EvidenceRecord[];\n}\n\nexport interface ToolContract<TParams extends TSchema = TSchema, TDetails = unknown> {\n\tcapabilities: ReadonlySet<Capability>;\n\tpermission: PermissionSpec<TParams>;\n\tcontext: ContextSpec;\n\tevidence: EvidenceSpec<TParams, TDetails>;\n}\n\n/**\n * Apex Code extends upstream ToolDefinition with exactly one required field.\n *\n * `TState` defaults to `any`, matching upstream `ToolDefinition` exactly (not\n * `unknown`). That default is load-bearing: TypeScript compares two instantiations\n * of the *same* generic interface (here, both rooted in `ToolDefinition`) using each\n * type parameter's declared variance, without expanding `Static<TParams>`\n * structurally — but only when every type argument lines up, including the ones a\n * caller left defaulted. A mismatched default reintroduces a full structural check,\n * where `Static<any>` does not behave like `any`, and breaks every pre-existing\n * `ToolDefinition<any, X>`-typed consumer across the codebase.\n */\nexport interface ApexToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>\n\textends ToolDefinition<TParams, TDetails, TState> {\n\tcontract: ToolContract<TParams, TDetails>;\n}\n\n/**\n * Conservative contract for a tool registered without one -- third-party extension\n * tools, which cannot supply a `contract`. MCP tools no longer land here: they reach\n * the model through the built-in `mcp` proxy, whose contract is declared in\n * `core/mcp/contract.ts`. Full capability set (so it can\n * never widen a delegation ceiling), `ask` by default, never evicted, schema\n * deferred, emits nothing. Matching is exact-argument only: a foreign tool's rule\n * authorizes exactly the call it was generated from, never a pattern.\n *\n * This must also be reported as unclassified wherever a consumer describes the tool\n * registry (contracts.md invariant 1) — a conservative default nobody can see is\n * indistinguishable from a bug.\n */\nexport const UNCLASSIFIED: ToolContract<TSchema, unknown> = {\n\tcapabilities: ALL_CAPABILITIES,\n\tpermission: {\n\t\tdefaultBehavior: \"ask\",\n\t\tmatches: (ruleContent, params) => ruleContent === JSON.stringify(params),\n\t\tdescribe: (ruleContent) => `Exact call: ${ruleContent}`,\n\t\truleForCall: (params) => JSON.stringify(params),\n\t},\n\tcontext: { resultRecoverable: false, deferSchema: false },\n\tevidence: { emits: new Set(), capture: () => [] },\n};\n\n/** Resolve a complete tool contract for enforcement consumers. */\nexport function resolveToolContract(\n\tlookup: (toolName: string) => ToolContract | undefined,\n\ttoolName: string,\n): ToolContract {\n\treturn lookup(toolName) ?? UNCLASSIFIED;\n}\n\n/** Resolve the context projection using the same canonical foreign-tool fallback. */\nexport function resolveToolContext(\n\tlookup: (toolName: string) => Pick<ToolContract, \"context\"> | undefined,\n\ttoolName: string,\n): Pick<ToolContract, \"context\"> {\n\treturn lookup(toolName) ?? UNCLASSIFIED;\n}\n\n/**\n * A tool whose call shapes differ, declared so a provider can still see its fields.\n *\n * `Type.Union` compiles to `{ anyOf: [...] }`, which carries no top-level\n * `properties`. The Anthropic request builder reads `properties` and `required`\n * directly (`anthropic-messages.js`, `legacyInputSchema`), so a union reached the\n * model as an object with no fields at all. It then guessed, and our own\n * validation rejected the guess.\n *\n * The union still decides what is valid. `properties` is advertisement, listing\n * every field across the variants so a provider that ignores `anyOf` has\n * something to work from. It deliberately carries no `required`, because no field\n * is required by every variant.\n */\nexport function toolUnion<T extends TSchema[]>(variants: [...T], properties: Record<string, TSchema>) {\n\treturn Type.Union(variants, { type: \"object\", properties });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/core/tools/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAA6B,IAAI,EAAE,MAAM,SAAS,CAAC;AAO1D,MAAM,CAAC,MAAM,gBAAgB,GAA4B,IAAI,GAAG,CAAa;IAC5E,SAAS;IACT,UAAU;IACV,MAAM;IACN,KAAK;IACL,UAAU;IACV,IAAI;IACJ,OAAO;CACP,CAAC,CAAC;AAkPH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,YAAY,GAAmC;IAC3D,YAAY,EAAE,gBAAgB;IAC9B,UAAU,EAAE;QACX,eAAe,EAAE,KAAK;QACtB,OAAO,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,WAAW,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;QACxE,QAAQ,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,eAAe,WAAW,EAAE;QACvD,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;KAC/C;IACD,OAAO,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE;IACzD,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE;CACjD,CAAC;AAEF,kEAAkE;AAClE,MAAM,UAAU,mBAAmB,CAClC,MAAsD,EACtD,QAAgB,EACD;IACf,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC;AAAA,CACxC;AAED,qFAAqF;AACrF,MAAM,UAAU,kBAAkB,CACjC,MAAuE,EACvE,QAAgB,EACgB;IAChC,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC;AAAA,CACxC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CAAsB,QAAgB,EAAE,UAAmC,EAAE;IACrG,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;AAAA,CAC5D","sourcesContent":["/**\n * The canonical tool contract. Settled by ADR 0010 and specified in full at\n * docs/architecture/contracts.md § 1 — this module implements that shape, it does\n * not redesign it. `contract` is required on every registered tool, and so is every\n * sub-field: a tool cannot compile without answering all four axes (capabilities,\n * permission, context, evidence), which is what keeps a new tool from silently\n * defaulting into \"unclassified\".\n */\n\nimport type { AgentToolResult } from \"apex-code-agent-core\";\nimport { type Static, type TSchema, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\nimport type { PermissionPreview } from \"../permissions/responder.ts\";\n\n/** What class of thing a tool does. A set, not a single value — `bash` is `{exec}`. */\nexport type Capability = \"fs.read\" | \"fs.write\" | \"exec\" | \"net\" | \"delegate\" | \"ui\" | \"state\";\n\nexport const ALL_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([\n\t\"fs.read\",\n\t\"fs.write\",\n\t\"exec\",\n\t\"net\",\n\t\"delegate\",\n\t\"ui\",\n\t\"state\",\n]);\n\nexport type PermissionBehavior = \"allow\" | \"deny\" | \"ask\";\n\n/**\n * `ruleContent` is interpreted by the tool, never by the rule engine (ADR 0010).\n * That is what lets `Bash(git commit:*)` and `Read(~/.ssh/**)` mean entirely\n * different things while the engine that resolves precedence stays tool-agnostic.\n */\nexport interface PermissionSpec<TParams extends TSchema = TSchema> {\n\t/** Behavior when no rule matches. Read-only tools may default to \"allow\". */\n\tdefaultBehavior: PermissionBehavior;\n\n\t/**\n\t * Per-call behavior when no rule matches, for tools whose calls differ in\n\t * nature (bash: launching a command is \"ask\", retrieving or killing an\n\t * already-approved background command is \"allow\"). Return undefined to fall\n\t * back to `defaultBehavior`. Floors and matching rules still outrank it --\n\t * this only replaces the fallthrough, never a rule or a mode floor.\n\t */\n\tdefaultBehaviorFor?(params: Static<TParams>): PermissionBehavior | undefined;\n\n\t/**\n\t * True when this call's target resolves inside the session's workspace.\n\t *\n\t * Only `acceptEdits` consults it, to auto-allow an edit that stays in the workspace\n\t * and no other. A tool that cannot answer leaves it undefined and is not auto-allowed,\n\t * which is the fail-closed side: a tool that cannot say where it writes is not a tool\n\t * a mode named for edits should approve unattended.\n\t */\n\twithinWorkspace?(params: Static<TParams>): boolean;\n\n\t/**\n\t * Why this call's target must not be handed over without an explicit decision, or\n\t * undefined for an ordinary target. Outranks every rule source but `policy`, and is\n\t * not lifted by `bypassPermissions`.\n\t */\n\tprotectedTarget?(params: Static<TParams>): string | undefined;\n\n\t/** Prepare canonical operation facts on the validated call before authorization and execution. */\n\tprepareCall?(params: Static<TParams>): void;\n\n\t/**\n\t * Describe, for a human about to approve it, what this call would change.\n\t *\n\t * Called by the gate only once it has resolved to `ask`, so an allowed or denied\n\t * call never pays for it. It must read through the value `prepareCall` stored\n\t * rather than re-deriving a target, which is what keeps the diff the user\n\t * approves and the bytes execution writes the same file (ADR 0029). It must not\n\t * execute the tool, write, or widen what the gate already validated.\n\t */\n\tpreviewCall?(params: Static<TParams>): PermissionPreview;\n\t/** Does this call match this rule's content as an allow rule? */\n\tmatches(ruleContent: string, params: Static<TParams>): boolean;\n\t/** Optional deny matcher. It may identify any prohibited part of a structured call. */\n\tmatchesDeny?(ruleContent: string, params: Static<TParams>): boolean;\n\t/** True when the call contains grammar that cannot be safely authorized by an allow. */\n\tisUnknown?(params: Static<TParams>): boolean;\n\n\t/** Human-readable rendering of a rule, for prompts and denial messages. */\n\tdescribe(ruleContent: string): string;\n\n\t/**\n\t * The rule that would allow this exact call — what \"always allow this\"\n\t * persists. Return null when the call is not generalizable into a rule.\n\t */\n\truleForCall(params: Static<TParams>): string | null;\n}\n\nexport interface ContextSpec {\n\t/**\n\t * True when the result's information is recoverable — the same content can be\n\t * obtained again by re-running the tool or reading the workspace.\n\t *\n\t * ONLY recoverable results may be evicted (Phase 3). A tool whose result cannot\n\t * be regenerated (a nondeterministic command, a consumed one-shot resource)\n\t * must set this false, or eviction would silently destroy information the\n\t * transcript is the only record of.\n\t */\n\tresultRecoverable: boolean;\n\n\t/** Marker substituted for an evicted result. */\n\tevictionMarker?: string;\n\n\t/** Announce by name only; load the parameter schema on demand. */\n\tdeferSchema: boolean;\n\n\t/** Soft cap on result tokens before truncation. */\n\toutputBudgetTokens?: number;\n}\n\nexport type EvidenceKind = \"diff\" | \"test\" | \"command\" | \"manual\" | \"workflow\" | \"diagnostic\";\n\n/** Source-observed command facts. `exitCode` is absent only when execution never began. */\nexport interface CommandEvidenceRecord {\n\tkind: \"command\";\n\tcommand: string;\n\tcwd?: string;\n\texecutable?: string;\n\targv?: string[];\n\texitCode?: number | null;\n}\n\n/** A file mutation is identified by hashes and paths, never raw file or patch contents. */\nexport interface DiffEvidenceRecord {\n\tkind: \"diff\";\n\tpath: string;\n\tpatchHash?: string;\n\tcontentHash?: string;\n\tbyteCount?: number;\n}\n\n/** Zero-filled diagnostic severity buckets for one bounded language-server outcome. */\nexport interface DiagnosticSeverityCounts {\n\terror: number;\n\twarning: number;\n\tinformation: number;\n\thint: number;\n\tunspecified: number;\n\tother: number;\n}\n\n/** Stable durable classification. Free-form server and process errors never enter evidence. */\nexport type DiagnosticUnavailableKind =\n\t| \"no-server\"\n\t| \"disposed\"\n\t| \"unsupported-sync\"\n\t| \"timed-out\"\n\t| \"aborted\"\n\t| \"superseded\"\n\t| \"server-failed\";\n\n/** Bounded post-mutation diagnostic facts, never diagnostic messages or rendered failure reasons. */\nexport type DiagnosticEvidenceRecord =\n\t| {\n\t\t\tkind: \"diagnostic\";\n\t\t\tpath: string;\n\t\t\tstatus: \"ok\";\n\t\t\tserverId: string;\n\t\t\tdiagnosticCount: number;\n\t\t\tseverityCounts: DiagnosticSeverityCounts;\n\t\t\ttruncated: boolean;\n\t }\n\t| {\n\t\t\tkind: \"diagnostic\";\n\t\t\tpath: string;\n\t\t\tstatus: \"unavailable\";\n\t\t\tserverId?: string;\n\t\t\tunavailableKind: DiagnosticUnavailableKind;\n\t };\n\n/** How a `test` run actually ended. */\nexport type TestProcessOutcome = \"exit\" | \"signal\" | \"timeout\" | \"cancelled\" | \"spawn-failed\";\n\n/** Bounded argv-based test execution facts. Full output never enters evidence; it lives in artifacts. */\nexport interface TestEvidenceRecord {\n\tkind: \"test\";\n\tcwd: string;\n\texecutable: string;\n\targv: string[];\n\texitCode: number | null;\n\t/** How the run ended. Unset means a legacy record; derive from `exitCode` when reading. */\n\toutcome?: TestProcessOutcome;\n\t/** True when the model-facing view omitted part of a stream (full output lives in artifacts). */\n\toutputTruncated?: boolean;\n}\n\n/** Explicit human attestation; policy decides how, or whether, it is sufficient. */\nexport interface ManualEvidenceRecord {\n\tkind: \"manual\";\n\tvalue?: unknown;\n\tobserved?: unknown;\n\tstatus?: string;\n}\n\n/** Source facts from approval and delegation workflows. */\nexport interface WorkflowEvidenceRecord {\n\tkind: \"workflow\";\n\tplan?: string;\n\tapproved?: boolean;\n\tagentType?: string;\n\ttask?: string;\n\thandle?: string;\n}\n\n/** A structured source record. Policy is a separate consumer, not an additional variant. */\nexport type EvidenceRecord =\n\t| CommandEvidenceRecord\n\t| DiffEvidenceRecord\n\t| DiagnosticEvidenceRecord\n\t| TestEvidenceRecord\n\t| ManualEvidenceRecord\n\t| WorkflowEvidenceRecord;\n\n/** Durable destination for source-level tool evidence. Policy is deliberately not part of this boundary. */\nexport interface EvidenceSink {\n\trecord(entry: { toolName: string; records: EvidenceRecord[] }): void;\n\trecordDiagnostic?(diagnostic: EvidenceCaptureDiagnostic): void;\n}\n\n/** A capture failure is observable but never changes an already-completed tool result. */\nexport interface EvidenceCaptureDiagnostic {\n\ttoolName: string;\n\treason: string;\n}\n\nexport interface EvidenceSpec<TParams extends TSchema = TSchema, TDetails = unknown> {\n\t/** Kinds this tool can emit. Empty set is valid and explicit. */\n\temits: ReadonlySet<EvidenceKind>;\n\n\t/**\n\t * Derive evidence from a completed call. Runs inside the tool's own execution\n\t * path, with access to what actually happened, not a reconstruction from\n\t * rendered output.\n\t */\n\tcapture(params: Static<TParams>, result: AgentToolResult<TDetails>): EvidenceRecord[];\n}\n\nexport interface ToolContract<TParams extends TSchema = TSchema, TDetails = unknown> {\n\tcapabilities: ReadonlySet<Capability>;\n\tpermission: PermissionSpec<TParams>;\n\tcontext: ContextSpec;\n\tevidence: EvidenceSpec<TParams, TDetails>;\n}\n\n/**\n * Apex Code extends upstream ToolDefinition with exactly one required field.\n *\n * `TState` defaults to `any`, matching upstream `ToolDefinition` exactly (not\n * `unknown`). That default is load-bearing: TypeScript compares two instantiations\n * of the *same* generic interface (here, both rooted in `ToolDefinition`) using each\n * type parameter's declared variance, without expanding `Static<TParams>`\n * structurally — but only when every type argument lines up, including the ones a\n * caller left defaulted. A mismatched default reintroduces a full structural check,\n * where `Static<any>` does not behave like `any`, and breaks every pre-existing\n * `ToolDefinition<any, X>`-typed consumer across the codebase.\n */\nexport interface ApexToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>\n\textends ToolDefinition<TParams, TDetails, TState> {\n\tcontract: ToolContract<TParams, TDetails>;\n}\n\n/**\n * Conservative contract for a tool registered without one -- third-party extension\n * tools, which cannot supply a `contract`. MCP tools no longer land here: they reach\n * the model through the built-in `mcp` proxy, whose contract is declared in\n * `core/mcp/contract.ts`. Full capability set (so it can\n * never widen a delegation ceiling), `ask` by default, never evicted, schema\n * deferred, emits nothing. Matching is exact-argument only: a foreign tool's rule\n * authorizes exactly the call it was generated from, never a pattern.\n *\n * This must also be reported as unclassified wherever a consumer describes the tool\n * registry (contracts.md invariant 1) — a conservative default nobody can see is\n * indistinguishable from a bug.\n */\nexport const UNCLASSIFIED: ToolContract<TSchema, unknown> = {\n\tcapabilities: ALL_CAPABILITIES,\n\tpermission: {\n\t\tdefaultBehavior: \"ask\",\n\t\tmatches: (ruleContent, params) => ruleContent === JSON.stringify(params),\n\t\tdescribe: (ruleContent) => `Exact call: ${ruleContent}`,\n\t\truleForCall: (params) => JSON.stringify(params),\n\t},\n\tcontext: { resultRecoverable: false, deferSchema: false },\n\tevidence: { emits: new Set(), capture: () => [] },\n};\n\n/** Resolve a complete tool contract for enforcement consumers. */\nexport function resolveToolContract(\n\tlookup: (toolName: string) => ToolContract | undefined,\n\ttoolName: string,\n): ToolContract {\n\treturn lookup(toolName) ?? UNCLASSIFIED;\n}\n\n/** Resolve the context projection using the same canonical foreign-tool fallback. */\nexport function resolveToolContext(\n\tlookup: (toolName: string) => Pick<ToolContract, \"context\"> | undefined,\n\ttoolName: string,\n): Pick<ToolContract, \"context\"> {\n\treturn lookup(toolName) ?? UNCLASSIFIED;\n}\n\n/**\n * A tool whose call shapes differ, declared so a provider can still see its fields.\n *\n * `Type.Union` compiles to `{ anyOf: [...] }`, which carries no top-level\n * `properties`. The Anthropic request builder reads `properties` and `required`\n * directly (`anthropic-messages.js`, `legacyInputSchema`), so a union reached the\n * model as an object with no fields at all. It then guessed, and our own\n * validation rejected the guess.\n *\n * The union still decides what is valid. `properties` is advertisement, listing\n * every field across the variants so a provider that ignores `anyOf` has\n * something to work from. It deliberately carries no `required`, because no field\n * is required by every variant.\n */\nexport function toolUnion<T extends TSchema[]>(variants: [...T], properties: Record<string, TSchema>) {\n\treturn Type.Union(variants, { type: \"object\", properties });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-permission.d.ts","sourceRoot":"","sources":["../../../src/core/tools/path-permission.ts"],"names":[],"mappings":"AAAA,wGAAwG;AAGxG,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"path-permission.d.ts","sourceRoot":"","sources":["../../../src/core/tools/path-permission.ts"],"names":[],"mappings":"AAAA,wGAAwG;AAGxG,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGxE;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,SAAS,OAAO,EAAE,OAAO,EAAE;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,kBAAkB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IACzD,4FAA4F;IAC5F,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,iBAAiB,CAAC;CAC7D,GAAG,cAAc,CAAC,OAAO,CAAC,CAoC1B","sourcesContent":["/** Shared PermissionSpec construction for the path-shaped tools (read, grep, find, ls, write, edit). */\n\nimport { minimatch } from \"minimatch\";\nimport type { Static, TSchema } from \"typebox\";\nimport { formatPathRelativeToCwdOrAbsolute, resolvesInsideDirectory } from \"../../utils/paths.ts\";\nimport { setPreparedPathOperation } from \"../permissions/operations.ts\";\nimport { describeProtectedTarget } from \"../permissions/protected-paths.ts\";\nimport type { PermissionPreview } from \"../permissions/responder.ts\";\nimport type { PermissionBehavior, PermissionSpec } from \"./contract.ts\";\nimport { preparePathOperation, resolveToCwd } from \"./path-utils.ts\";\n\n/**\n * Builds a glob-based PermissionSpec over a single path field. `getPath` extracts\n * the workspace path a call touches (returning undefined defaults to the tool's own\n * cwd-relative root, e.g. `ls` with no `path` argument). Rule content is a\n * cwd-relative glob; `ruleForCall` returns the exact normalized path of the call, so\n * \"always allow this\" authorizes only that path — never a pattern the tool did not\n * generate itself.\n */\nexport function createPathPermissionSpec<TParams extends TSchema>(options: {\n\tcwd: string;\n\tdefaultBehavior: PermissionBehavior;\n\tverb: string;\n\tgetPath: (params: Static<TParams>) => string | undefined;\n\t/** Describes the change for a human about to approve it. See PermissionSpec.previewCall. */\n\tpreviewCall?: (params: Static<TParams>) => PermissionPreview;\n}): PermissionSpec<TParams> {\n\tconst { cwd, defaultBehavior, verb, getPath, previewCall } = options;\n\n\tconst normalize = (path: string | undefined): string => {\n\t\tconst canonical = resolveToCwd(path?.trim() ? path : \".\", cwd);\n\t\treturn formatPathRelativeToCwdOrAbsolute(canonical, cwd);\n\t};\n\tconst escapeExact = (value: string): string => `exact:${value}`;\n\n\treturn {\n\t\tdefaultBehavior,\n\t\tprepareCall(params) {\n\t\t\tsetPreparedPathOperation(params as object, preparePathOperation(getPath(params) ?? \".\", cwd));\n\t\t},\n\t\t...(previewCall ? { previewCall } : {}),\n\t\tprotectedTarget(params) {\n\t\t\treturn describeProtectedTarget(resolveToCwd(getPath(params)?.trim() ? (getPath(params) as string) : \".\", cwd));\n\t\t},\n\t\twithinWorkspace(params) {\n\t\t\treturn resolvesInsideDirectory(\n\t\t\t\tresolveToCwd(getPath(params)?.trim() ? (getPath(params) as string) : \".\", cwd),\n\t\t\t\tcwd,\n\t\t\t);\n\t\t},\n\t\tmatches(ruleContent, params) {\n\t\t\treturn ruleContent.startsWith(\"exact:\")\n\t\t\t\t? normalize(getPath(params)) === ruleContent.slice(\"exact:\".length)\n\t\t\t\t: minimatch(normalize(getPath(params)), ruleContent, { dot: true });\n\t\t},\n\t\tdescribe(ruleContent) {\n\t\t\treturn `${verb} paths matching \"${ruleContent}\"`;\n\t\t},\n\t\truleForCall(params) {\n\t\t\treturn escapeExact(normalize(getPath(params)));\n\t\t},\n\t};\n}\n"]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Shared PermissionSpec construction for the path-shaped tools (read, grep, find, ls, write, edit). */
|
|
2
2
|
import { minimatch } from "minimatch";
|
|
3
|
-
import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js";
|
|
3
|
+
import { formatPathRelativeToCwdOrAbsolute, resolvesInsideDirectory } from "../../utils/paths.js";
|
|
4
4
|
import { setPreparedPathOperation } from "../permissions/operations.js";
|
|
5
|
+
import { describeProtectedTarget } from "../permissions/protected-paths.js";
|
|
5
6
|
import { preparePathOperation, resolveToCwd } from "./path-utils.js";
|
|
6
7
|
/**
|
|
7
8
|
* Builds a glob-based PermissionSpec over a single path field. `getPath` extracts
|
|
@@ -24,6 +25,12 @@ export function createPathPermissionSpec(options) {
|
|
|
24
25
|
setPreparedPathOperation(params, preparePathOperation(getPath(params) ?? ".", cwd));
|
|
25
26
|
},
|
|
26
27
|
...(previewCall ? { previewCall } : {}),
|
|
28
|
+
protectedTarget(params) {
|
|
29
|
+
return describeProtectedTarget(resolveToCwd(getPath(params)?.trim() ? getPath(params) : ".", cwd));
|
|
30
|
+
},
|
|
31
|
+
withinWorkspace(params) {
|
|
32
|
+
return resolvesInsideDirectory(resolveToCwd(getPath(params)?.trim() ? getPath(params) : ".", cwd), cwd);
|
|
33
|
+
},
|
|
27
34
|
matches(ruleContent, params) {
|
|
28
35
|
return ruleContent.startsWith("exact:")
|
|
29
36
|
? normalize(getPath(params)) === ruleContent.slice("exact:".length)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-permission.js","sourceRoot":"","sources":["../../../src/core/tools/path-permission.ts"],"names":[],"mappings":"AAAA,wGAAwG;AAExG,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,iCAAiC,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"path-permission.js","sourceRoot":"","sources":["../../../src/core/tools/path-permission.ts"],"names":[],"mappings":"AAAA,wGAAwG;AAExG,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,iCAAiC,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAClG,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAG5E,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAA0B,OAOjE,EAA2B;IAC3B,MAAM,EAAE,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;IAErE,MAAM,SAAS,GAAG,CAAC,IAAwB,EAAU,EAAE,CAAC;QACvD,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC/D,OAAO,iCAAiC,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAAA,CACzD,CAAC;IACF,MAAM,WAAW,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,SAAS,KAAK,EAAE,CAAC;IAEhE,OAAO;QACN,eAAe;QACf,WAAW,CAAC,MAAM,EAAE;YACnB,wBAAwB,CAAC,MAAgB,EAAE,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QAAA,CAC9F;QACD,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,eAAe,CAAC,MAAM,EAAE;YACvB,OAAO,uBAAuB,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAE,OAAO,CAAC,MAAM,CAAY,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QAAA,CAC/G;QACD,eAAe,CAAC,MAAM,EAAE;YACvB,OAAO,uBAAuB,CAC7B,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAE,OAAO,CAAC,MAAM,CAAY,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAC9E,GAAG,CACH,CAAC;QAAA,CACF;QACD,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE;YAC5B,OAAO,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC;gBACtC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;gBACnE,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;QAAA,CACrE;QACD,QAAQ,CAAC,WAAW,EAAE;YACrB,OAAO,GAAG,IAAI,oBAAoB,WAAW,GAAG,CAAC;QAAA,CACjD;QACD,WAAW,CAAC,MAAM,EAAE;YACnB,OAAO,WAAW,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAAA,CAC/C;KACD,CAAC;AAAA,CACF","sourcesContent":["/** Shared PermissionSpec construction for the path-shaped tools (read, grep, find, ls, write, edit). */\n\nimport { minimatch } from \"minimatch\";\nimport type { Static, TSchema } from \"typebox\";\nimport { formatPathRelativeToCwdOrAbsolute, resolvesInsideDirectory } from \"../../utils/paths.ts\";\nimport { setPreparedPathOperation } from \"../permissions/operations.ts\";\nimport { describeProtectedTarget } from \"../permissions/protected-paths.ts\";\nimport type { PermissionPreview } from \"../permissions/responder.ts\";\nimport type { PermissionBehavior, PermissionSpec } from \"./contract.ts\";\nimport { preparePathOperation, resolveToCwd } from \"./path-utils.ts\";\n\n/**\n * Builds a glob-based PermissionSpec over a single path field. `getPath` extracts\n * the workspace path a call touches (returning undefined defaults to the tool's own\n * cwd-relative root, e.g. `ls` with no `path` argument). Rule content is a\n * cwd-relative glob; `ruleForCall` returns the exact normalized path of the call, so\n * \"always allow this\" authorizes only that path — never a pattern the tool did not\n * generate itself.\n */\nexport function createPathPermissionSpec<TParams extends TSchema>(options: {\n\tcwd: string;\n\tdefaultBehavior: PermissionBehavior;\n\tverb: string;\n\tgetPath: (params: Static<TParams>) => string | undefined;\n\t/** Describes the change for a human about to approve it. See PermissionSpec.previewCall. */\n\tpreviewCall?: (params: Static<TParams>) => PermissionPreview;\n}): PermissionSpec<TParams> {\n\tconst { cwd, defaultBehavior, verb, getPath, previewCall } = options;\n\n\tconst normalize = (path: string | undefined): string => {\n\t\tconst canonical = resolveToCwd(path?.trim() ? path : \".\", cwd);\n\t\treturn formatPathRelativeToCwdOrAbsolute(canonical, cwd);\n\t};\n\tconst escapeExact = (value: string): string => `exact:${value}`;\n\n\treturn {\n\t\tdefaultBehavior,\n\t\tprepareCall(params) {\n\t\t\tsetPreparedPathOperation(params as object, preparePathOperation(getPath(params) ?? \".\", cwd));\n\t\t},\n\t\t...(previewCall ? { previewCall } : {}),\n\t\tprotectedTarget(params) {\n\t\t\treturn describeProtectedTarget(resolveToCwd(getPath(params)?.trim() ? (getPath(params) as string) : \".\", cwd));\n\t\t},\n\t\twithinWorkspace(params) {\n\t\t\treturn resolvesInsideDirectory(\n\t\t\t\tresolveToCwd(getPath(params)?.trim() ? (getPath(params) as string) : \".\", cwd),\n\t\t\t\tcwd,\n\t\t\t);\n\t\t},\n\t\tmatches(ruleContent, params) {\n\t\t\treturn ruleContent.startsWith(\"exact:\")\n\t\t\t\t? normalize(getPath(params)) === ruleContent.slice(\"exact:\".length)\n\t\t\t\t: minimatch(normalize(getPath(params)), ruleContent, { dot: true });\n\t\t},\n\t\tdescribe(ruleContent) {\n\t\t\treturn `${verb} paths matching \"${ruleContent}\"`;\n\t\t},\n\t\truleForCall(params) {\n\t\t\treturn escapeExact(normalize(getPath(params)));\n\t\t},\n\t};\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,wBAAwB,CAAC;AAC/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAiC7E;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAqC7D;AAcD;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,oBAAoB,CAAiC;IAC7D,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAiB;IAEjC,YACC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,iBAAiB,UAAQ,EACzB,aAAa,GAAE,aAAkC,EACjD,mBAAmB,SAAgB,EACnC,SAAS,SAAI,EACb,oBAAoB,GAAE,SAAS,mBAAmB,EAAO,EAiBzD;IAEQ,UAAU,IAAI,IAAI,CAM1B;IAED,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAMxC;IAED,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAM1C;IAED,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAMlC;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CASvC;IAED,aAAa,CAAC,OAAO,EAAE,gBAAgB,EAAE,WAAW,UAAmB,GAAG,IAAI,CA0C7E;IAED,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,kBAAkB;IAyD1B,OAAO,CAAC,iBAAiB;IAqBzB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,eAAe;CA0BvB","sourcesContent":["import type { AssistantMessage } from \"@earendil-works/pi-ai\";\nimport { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { MarkdownTransformer } from \"../../../core/extensions/types.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\nimport { createMarkdownTransform } from \"./markdown-transform.ts\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\nconst FENCE_OPENER = /^(```|~~~)/;\nconst FENCE_CLOSER = /^(```|~~~)\\s*$/;\nconst LIST_MARKER = /^([-*+]\\s|\\d+[.)]\\s)/;\nconst QUOTE_MARKER = /^>\\s?/;\nconst INDENTED_CODE = /^(?:\\t| {4})/;\n\nfunction lastNonEmpty(lines: string[]): string | undefined {\n\tfor (let i = lines.length - 1; i >= 0; i--) {\n\t\tif (lines[i].trim() !== \"\") return lines[i];\n\t}\n\treturn undefined;\n}\n\nfunction shouldSplitBefore(next: string, current: string[]): boolean {\n\t// Indented lines can continue indented code blocks; never split there.\n\tif (INDENTED_CODE.test(next)) return false;\n\tconst previous = lastNonEmpty(current);\n\tif (previous && INDENTED_CODE.test(previous)) return false;\n\t// A loose list (\"item\\n\\nitem\") and a blockquote with a blank line render\n\t// as one construct; splitting them would change the output.\n\tif (LIST_MARKER.test(next) && previous && LIST_MARKER.test(previous)) return false;\n\tif (QUOTE_MARKER.test(next) && previous && QUOTE_MARKER.test(previous)) return false;\n\treturn true;\n}\n\n/**\n * Split streamed markdown into render units at boundaries that render\n * independently: closed column-0 code fences, and blank-line block\n * boundaries that cannot continue a preceding construct.\n *\n * The result is prefix-stable: appending text can only add units at the end,\n * never change earlier ones, which is what makes the units cacheable while\n * a message streams.\n */\nexport function splitStreamingMarkdown(text: string): string[] {\n\tif (!text) return [];\n\tconst lines = text.split(\"\\n\");\n\tconst units: string[] = [];\n\tlet current: string[] = [];\n\tlet inFence = false;\n\tconst flush = (): void => {\n\t\tif (current.length > 0) {\n\t\t\tunits.push(current.join(\"\\n\"));\n\t\t\tcurrent = [];\n\t\t}\n\t};\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i];\n\t\tif (!inFence && FENCE_OPENER.test(line)) {\n\t\t\tinFence = true;\n\t\t\tcurrent.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence) {\n\t\t\tcurrent.push(line);\n\t\t\tif (FENCE_CLOSER.test(line)) {\n\t\t\t\tflush();\n\t\t\t\tinFence = false;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent.push(line);\n\t\tif (line.trim() === \"\") {\n\t\t\tconst next = lines[i + 1];\n\t\t\tif (next !== undefined && next.trim() !== \"\" && shouldSplitBefore(next, current)) {\n\t\t\t\tflush();\n\t\t\t}\n\t\t}\n\t}\n\tflush();\n\treturn units;\n}\n\ntype SectionSpec =\n\t| { kind: \"spacer\" }\n\t| { kind: \"markdown-text\"; text: string }\n\t| { kind: \"markdown-thinking\"; text: string }\n\t| { kind: \"label\"; text: string };\n\ntype Section = { spec: SectionSpec; component: Container[\"children\"][number] };\n\nfunction specText(spec: SectionSpec): string | undefined {\n\treturn spec.kind === \"spacer\" ? undefined : spec.text;\n}\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate hiddenThinkingLabel: string;\n\tprivate outputPad: number;\n\tprivate markdownTransformers: readonly MarkdownTransformer[];\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\tprivate isStreaming = false;\n\tprivate forceFullRebuild = true;\n\tprivate sections: Section[] = [];\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t\toutputPad = 1,\n\t\tmarkdownTransformers: readonly MarkdownTransformer[] = [],\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\t\tthis.outputPad = outputPad;\n\t\tthis.markdownTransformers = markdownTransformers;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetOutputPad(padding: number): void {\n\t\tthis.outputPad = padding;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst lines = super.render(width);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\tlines[0] = OSC133_ZONE_START + lines[0];\n\t\tlines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];\n\t\treturn lines;\n\t}\n\n\tupdateContent(message: AssistantMessage, isStreaming = this.isStreaming): void {\n\t\tthis.lastMessage = message;\n\t\t// The transform closures capture isStreaming, and non-streaming updates\n\t\t// render whole blocks rather than split units, so an isStreaming flip\n\t\t// rebuilds every section.\n\t\tconst rebuildAll = this.forceFullRebuild || isStreaming !== this.isStreaming;\n\t\tthis.isStreaming = isStreaming;\n\n\t\tconst specs = this.buildSectionSpecs(message);\n\t\tconst sections: Section[] = [];\n\t\tfor (let i = 0; i < specs.length; i++) {\n\t\t\tconst spec = specs[i];\n\t\t\tconst previous = rebuildAll ? undefined : this.sections[i];\n\t\t\tconst sameKind = previous !== undefined && previous.spec.kind === spec.kind;\n\t\t\tif (sameKind && (spec.kind === \"spacer\" || specText(previous.spec) === specText(spec))) {\n\t\t\t\tsections.push({ spec, component: previous.component });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (sameKind && spec.kind !== \"spacer\") {\n\t\t\t\t// Same slot, new content (the growing tail of a streamed\n\t\t\t\t// message): update in place so only this unit re-parses.\n\t\t\t\tconst component = previous.component as Text;\n\t\t\t\tcomponent.setText(specText(spec) ?? \"\");\n\t\t\t\tsections.push({ spec, component: previous.component });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsections.push({ spec, component: this.createComponent(spec) });\n\t\t}\n\n\t\tthis.forceFullRebuild = false;\n\t\tthis.sections = sections;\n\n\t\tthis.contentContainer.clear();\n\t\tfor (const section of sections) {\n\t\t\tthis.contentContainer.addChild(section.component);\n\t\t}\n\n\t\t// Check if incomplete/failed - show after partial content.\n\t\t// For aborted/error tool calls, tool execution components show the error.\n\t\t// Length stops can happen before a tool call is complete, so surface them here too.\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t}\n\n\tprivate buildSectionSpecs(message: AssistantMessage): SectionSpec[] {\n\t\tconst specs: SectionSpec[] = [];\n\t\tthis.appendContentSpecs(message, specs);\n\t\tthis.appendStatusSpecs(message, specs);\n\t\treturn specs;\n\t}\n\n\tprivate appendContentSpecs(message: AssistantMessage, specs: SectionSpec[]): void {\n\t\tconst hasVisibleContent = message.content.some(\n\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t);\n\n\t\tif (hasVisibleContent) {\n\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.pushMarkdownSpecs(specs, content.text.trim(), \"markdown-text\");\n\t\t\t} else if (content.type === \"thinking\") {\n\t\t\t\tconst thinkingBlocks: string[] = [];\n\t\t\t\tfor (; i < message.content.length; i++) {\n\t\t\t\t\tconst thinkingContent = message.content[i];\n\t\t\t\t\tif (thinkingContent.type !== \"thinking\") {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tconst thinking = thinkingContent.thinking.trim();\n\t\t\t\t\tif (thinking) {\n\t\t\t\t\t\tthinkingBlocks.push(thinking);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti--;\n\n\t\t\t\tif (thinkingBlocks.length === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.some((c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()));\n\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\t// Show one static label for each run of thinking blocks when hidden.\n\t\t\t\t\tspecs.push({\n\t\t\t\t\t\tkind: \"label\",\n\t\t\t\t\t\ttext: theme.italic(theme.fg(\"thinkingText\", this.hiddenThinkingLabel)),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\t// Render each run of thinking blocks as one Markdown section.\n\t\t\t\t\tthis.pushMarkdownSpecs(specs, thinkingBlocks.join(\"\\n\\n\"), \"markdown-thinking\");\n\t\t\t\t}\n\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate appendStatusSpecs(message: AssistantMessage, specs: SectionSpec[]): void {\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tif (message.stopReason === \"length\") {\n\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", \"Response was truncated before completion.\") });\n\t\t} else if (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", abortMessage) });\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", `Error: ${errorMsg}`) });\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Push markdown sections for one content block. While streaming, the block\n\t * is split at render-independent boundaries so completed units keep their\n\t * rendered Markdown instances and only the active tail re-parses per\n\t * chunk; otherwise the block renders whole, byte-identical to the\n\t * pre-split behaviour.\n\t *\n\t * Transformers are assumed block-local (the built-in mermaid transformer\n\t * maps tokens and preserves the rest, so per-unit application is\n\t * equivalent). A transformer that genuinely needs whole-message context\n\t * still gets it: a non-streaming update always renders whole blocks.\n\t */\n\tprivate pushMarkdownSpecs(specs: SectionSpec[], text: string, kind: \"markdown-text\" | \"markdown-thinking\"): void {\n\t\tif (this.isStreaming) {\n\t\t\tfor (const unit of splitStreamingMarkdown(text)) {\n\t\t\t\tspecs.push({ kind, text: unit });\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tspecs.push({ kind, text });\n\t}\n\n\tprivate createComponent(spec: SectionSpec): Section[\"component\"] {\n\t\tswitch (spec.kind) {\n\t\t\tcase \"spacer\":\n\t\t\t\treturn new Spacer(1);\n\t\t\tcase \"label\":\n\t\t\t\treturn new Text(spec.text, this.outputPad, 0);\n\t\t\tcase \"markdown-text\":\n\t\t\t\treturn new Markdown(spec.text, this.outputPad, 0, this.markdownTheme, undefined, {\n\t\t\t\t\ttransform: createMarkdownTransform(\"assistant\", this.isStreaming, this.markdownTransformers),\n\t\t\t\t});\n\t\t\tcase \"markdown-thinking\":\n\t\t\t\treturn new Markdown(\n\t\t\t\t\tspec.text,\n\t\t\t\t\tthis.outputPad,\n\t\t\t\t\t0,\n\t\t\t\t\tthis.markdownTheme,\n\t\t\t\t\t{\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttransform: createMarkdownTransform(\"assistant-thinking\", this.isStreaming, this.markdownTransformers),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assistant-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,wBAAwB,CAAC;AAC/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAkC7E;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAqC7D;AAcD;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACvD,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,oBAAoB,CAAiC;IAC7D,OAAO,CAAC,WAAW,CAAC,CAAmB;IACvC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAiB;IAEjC,YACC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,iBAAiB,UAAQ,EACzB,aAAa,GAAE,aAAkC,EACjD,mBAAmB,SAAgB,EACnC,SAAS,SAAI,EACb,oBAAoB,GAAE,SAAS,mBAAmB,EAAO,EAiBzD;IAEQ,UAAU,IAAI,IAAI,CAM1B;IAED,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAMxC;IAED,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAM1C;IAED,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAMlC;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBvC;IAED,aAAa,CAAC,OAAO,EAAE,gBAAgB,EAAE,WAAW,UAAmB,GAAG,IAAI,CA0C7E;IAED,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,kBAAkB;IAyD1B,OAAO,CAAC,iBAAiB;IAqBzB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,eAAe;CA0BvB","sourcesContent":["import type { AssistantMessage } from \"@earendil-works/pi-ai\";\nimport { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { MarkdownTransformer } from \"../../../core/extensions/types.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\nimport { createMarkdownTransform } from \"./markdown-transform.ts\";\nimport { withAccentSpine } from \"./message-spine.ts\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\nconst FENCE_OPENER = /^(```|~~~)/;\nconst FENCE_CLOSER = /^(```|~~~)\\s*$/;\nconst LIST_MARKER = /^([-*+]\\s|\\d+[.)]\\s)/;\nconst QUOTE_MARKER = /^>\\s?/;\nconst INDENTED_CODE = /^(?:\\t| {4})/;\n\nfunction lastNonEmpty(lines: string[]): string | undefined {\n\tfor (let i = lines.length - 1; i >= 0; i--) {\n\t\tif (lines[i].trim() !== \"\") return lines[i];\n\t}\n\treturn undefined;\n}\n\nfunction shouldSplitBefore(next: string, current: string[]): boolean {\n\t// Indented lines can continue indented code blocks; never split there.\n\tif (INDENTED_CODE.test(next)) return false;\n\tconst previous = lastNonEmpty(current);\n\tif (previous && INDENTED_CODE.test(previous)) return false;\n\t// A loose list (\"item\\n\\nitem\") and a blockquote with a blank line render\n\t// as one construct; splitting them would change the output.\n\tif (LIST_MARKER.test(next) && previous && LIST_MARKER.test(previous)) return false;\n\tif (QUOTE_MARKER.test(next) && previous && QUOTE_MARKER.test(previous)) return false;\n\treturn true;\n}\n\n/**\n * Split streamed markdown into render units at boundaries that render\n * independently: closed column-0 code fences, and blank-line block\n * boundaries that cannot continue a preceding construct.\n *\n * The result is prefix-stable: appending text can only add units at the end,\n * never change earlier ones, which is what makes the units cacheable while\n * a message streams.\n */\nexport function splitStreamingMarkdown(text: string): string[] {\n\tif (!text) return [];\n\tconst lines = text.split(\"\\n\");\n\tconst units: string[] = [];\n\tlet current: string[] = [];\n\tlet inFence = false;\n\tconst flush = (): void => {\n\t\tif (current.length > 0) {\n\t\t\tunits.push(current.join(\"\\n\"));\n\t\t\tcurrent = [];\n\t\t}\n\t};\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i];\n\t\tif (!inFence && FENCE_OPENER.test(line)) {\n\t\t\tinFence = true;\n\t\t\tcurrent.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence) {\n\t\t\tcurrent.push(line);\n\t\t\tif (FENCE_CLOSER.test(line)) {\n\t\t\t\tflush();\n\t\t\t\tinFence = false;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent.push(line);\n\t\tif (line.trim() === \"\") {\n\t\t\tconst next = lines[i + 1];\n\t\t\tif (next !== undefined && next.trim() !== \"\" && shouldSplitBefore(next, current)) {\n\t\t\t\tflush();\n\t\t\t}\n\t\t}\n\t}\n\tflush();\n\treturn units;\n}\n\ntype SectionSpec =\n\t| { kind: \"spacer\" }\n\t| { kind: \"markdown-text\"; text: string }\n\t| { kind: \"markdown-thinking\"; text: string }\n\t| { kind: \"label\"; text: string };\n\ntype Section = { spec: SectionSpec; component: Container[\"children\"][number] };\n\nfunction specText(spec: SectionSpec): string | undefined {\n\treturn spec.kind === \"spacer\" ? undefined : spec.text;\n}\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate hiddenThinkingLabel: string;\n\tprivate outputPad: number;\n\tprivate markdownTransformers: readonly MarkdownTransformer[];\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\tprivate isStreaming = false;\n\tprivate forceFullRebuild = true;\n\tprivate sections: Section[] = [];\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t\toutputPad = 1,\n\t\tmarkdownTransformers: readonly MarkdownTransformer[] = [],\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\t\tthis.outputPad = outputPad;\n\t\tthis.markdownTransformers = markdownTransformers;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetOutputPad(padding: number): void {\n\t\tthis.outputPad = padding;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst lines = super.render(width);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Keep conversational messages on one visual axis. Tool panels own\n\t\t// their lifecycle spine; assistant text uses the quieter accent rail.\n\t\tif (this.outputPad > 0) {\n\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\tif (lines[i].trim().length > 0) {\n\t\t\t\t\tlines[i] = withAccentSpine(lines[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlines[0] = OSC133_ZONE_START + lines[0];\n\t\tlines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];\n\t\treturn lines;\n\t}\n\n\tupdateContent(message: AssistantMessage, isStreaming = this.isStreaming): void {\n\t\tthis.lastMessage = message;\n\t\t// The transform closures capture isStreaming, and non-streaming updates\n\t\t// render whole blocks rather than split units, so an isStreaming flip\n\t\t// rebuilds every section.\n\t\tconst rebuildAll = this.forceFullRebuild || isStreaming !== this.isStreaming;\n\t\tthis.isStreaming = isStreaming;\n\n\t\tconst specs = this.buildSectionSpecs(message);\n\t\tconst sections: Section[] = [];\n\t\tfor (let i = 0; i < specs.length; i++) {\n\t\t\tconst spec = specs[i];\n\t\t\tconst previous = rebuildAll ? undefined : this.sections[i];\n\t\t\tconst sameKind = previous !== undefined && previous.spec.kind === spec.kind;\n\t\t\tif (sameKind && (spec.kind === \"spacer\" || specText(previous.spec) === specText(spec))) {\n\t\t\t\tsections.push({ spec, component: previous.component });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (sameKind && spec.kind !== \"spacer\") {\n\t\t\t\t// Same slot, new content (the growing tail of a streamed\n\t\t\t\t// message): update in place so only this unit re-parses.\n\t\t\t\tconst component = previous.component as Text;\n\t\t\t\tcomponent.setText(specText(spec) ?? \"\");\n\t\t\t\tsections.push({ spec, component: previous.component });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsections.push({ spec, component: this.createComponent(spec) });\n\t\t}\n\n\t\tthis.forceFullRebuild = false;\n\t\tthis.sections = sections;\n\n\t\tthis.contentContainer.clear();\n\t\tfor (const section of sections) {\n\t\t\tthis.contentContainer.addChild(section.component);\n\t\t}\n\n\t\t// Check if incomplete/failed - show after partial content.\n\t\t// For aborted/error tool calls, tool execution components show the error.\n\t\t// Length stops can happen before a tool call is complete, so surface them here too.\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t}\n\n\tprivate buildSectionSpecs(message: AssistantMessage): SectionSpec[] {\n\t\tconst specs: SectionSpec[] = [];\n\t\tthis.appendContentSpecs(message, specs);\n\t\tthis.appendStatusSpecs(message, specs);\n\t\treturn specs;\n\t}\n\n\tprivate appendContentSpecs(message: AssistantMessage, specs: SectionSpec[]): void {\n\t\tconst hasVisibleContent = message.content.some(\n\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t);\n\n\t\tif (hasVisibleContent) {\n\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.pushMarkdownSpecs(specs, content.text.trim(), \"markdown-text\");\n\t\t\t} else if (content.type === \"thinking\") {\n\t\t\t\tconst thinkingBlocks: string[] = [];\n\t\t\t\tfor (; i < message.content.length; i++) {\n\t\t\t\t\tconst thinkingContent = message.content[i];\n\t\t\t\t\tif (thinkingContent.type !== \"thinking\") {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tconst thinking = thinkingContent.thinking.trim();\n\t\t\t\t\tif (thinking) {\n\t\t\t\t\t\tthinkingBlocks.push(thinking);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti--;\n\n\t\t\t\tif (thinkingBlocks.length === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.some((c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()));\n\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\t// Show one static label for each run of thinking blocks when hidden.\n\t\t\t\t\tspecs.push({\n\t\t\t\t\t\tkind: \"label\",\n\t\t\t\t\t\ttext: theme.italic(theme.fg(\"thinkingText\", this.hiddenThinkingLabel)),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\t// Render each run of thinking blocks as one Markdown section.\n\t\t\t\t\tthis.pushMarkdownSpecs(specs, thinkingBlocks.join(\"\\n\\n\"), \"markdown-thinking\");\n\t\t\t\t}\n\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate appendStatusSpecs(message: AssistantMessage, specs: SectionSpec[]): void {\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tif (message.stopReason === \"length\") {\n\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", \"Response was truncated before completion.\") });\n\t\t} else if (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", abortMessage) });\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", `Error: ${errorMsg}`) });\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Push markdown sections for one content block. While streaming, the block\n\t * is split at render-independent boundaries so completed units keep their\n\t * rendered Markdown instances and only the active tail re-parses per\n\t * chunk; otherwise the block renders whole, byte-identical to the\n\t * pre-split behaviour.\n\t *\n\t * Transformers are assumed block-local (the built-in mermaid transformer\n\t * maps tokens and preserves the rest, so per-unit application is\n\t * equivalent). A transformer that genuinely needs whole-message context\n\t * still gets it: a non-streaming update always renders whole blocks.\n\t */\n\tprivate pushMarkdownSpecs(specs: SectionSpec[], text: string, kind: \"markdown-text\" | \"markdown-thinking\"): void {\n\t\tif (this.isStreaming) {\n\t\t\tfor (const unit of splitStreamingMarkdown(text)) {\n\t\t\t\tspecs.push({ kind, text: unit });\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tspecs.push({ kind, text });\n\t}\n\n\tprivate createComponent(spec: SectionSpec): Section[\"component\"] {\n\t\tswitch (spec.kind) {\n\t\t\tcase \"spacer\":\n\t\t\t\treturn new Spacer(1);\n\t\t\tcase \"label\":\n\t\t\t\treturn new Text(spec.text, this.outputPad, 0);\n\t\t\tcase \"markdown-text\":\n\t\t\t\treturn new Markdown(spec.text, this.outputPad, 0, this.markdownTheme, undefined, {\n\t\t\t\t\ttransform: createMarkdownTransform(\"assistant\", this.isStreaming, this.markdownTransformers),\n\t\t\t\t});\n\t\t\tcase \"markdown-thinking\":\n\t\t\t\treturn new Markdown(\n\t\t\t\t\tspec.text,\n\t\t\t\t\tthis.outputPad,\n\t\t\t\t\t0,\n\t\t\t\t\tthis.markdownTheme,\n\t\t\t\t\t{\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttransform: createMarkdownTransform(\"assistant-thinking\", this.isStreaming, this.markdownTransformers),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
|
3
3
|
import { createMarkdownTransform } from "./markdown-transform.js";
|
|
4
|
+
import { withAccentSpine } from "./message-spine.js";
|
|
4
5
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
5
6
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
6
7
|
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
|
|
@@ -144,6 +145,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
144
145
|
if (this.hasToolCalls || lines.length === 0) {
|
|
145
146
|
return lines;
|
|
146
147
|
}
|
|
148
|
+
// Keep conversational messages on one visual axis. Tool panels own
|
|
149
|
+
// their lifecycle spine; assistant text uses the quieter accent rail.
|
|
150
|
+
if (this.outputPad > 0) {
|
|
151
|
+
for (let i = 0; i < lines.length; i++) {
|
|
152
|
+
if (lines[i].trim().length > 0) {
|
|
153
|
+
lines[i] = withAccentSpine(lines[i]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
147
157
|
lines[0] = OSC133_ZONE_START + lines[0];
|
|
148
158
|
lines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];
|
|
149
159
|
return lines;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAsB,MAAM,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAE/F,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAElE,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAC3C,MAAM,eAAe,GAAG,gBAAgB,CAAC;AACzC,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAE3C,MAAM,YAAY,GAAG,YAAY,CAAC;AAClC,MAAM,YAAY,GAAG,gBAAgB,CAAC;AACtC,MAAM,WAAW,GAAG,sBAAsB,CAAC;AAC3C,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,MAAM,aAAa,GAAG,cAAc,CAAC;AAErC,SAAS,YAAY,CAAC,KAAe,EAAsB;IAC1D,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,SAAS,iBAAiB,CAAC,IAAY,EAAE,OAAiB,EAAW;IACpE,uEAAuE;IACvE,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,0EAA0E;IAC1E,4DAA4D;IAC5D,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,IAAI,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IACnF,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IACrF,OAAO,IAAI,CAAC;AAAA,CACZ;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY,EAAY;IAC9D,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,KAAK,GAAG,GAAS,EAAE,CAAC;QACzB,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAC/B,OAAO,GAAG,EAAE,CAAC;QACd,CAAC;IAAA,CACD,CAAC;IACF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,SAAS;QACV,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,KAAK,EAAE,CAAC;gBACR,OAAO,GAAG,KAAK,CAAC;YACjB,CAAC;YACD,SAAS;QACV,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;gBAClF,KAAK,EAAE,CAAC;YACT,CAAC;QACF,CAAC;IACF,CAAC;IACD,KAAK,EAAE,CAAC;IACR,OAAO,KAAK,CAAC;AAAA,CACb;AAUD,SAAS,QAAQ,CAAC,IAAiB,EAAsB;IACxD,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;AAAA,CACtD;AAED;;GAEG;AACH,MAAM,OAAO,yBAA0B,SAAQ,SAAS;IAC/C,gBAAgB,CAAY;IAC5B,iBAAiB,CAAU;IAC3B,aAAa,CAAgB;IAC7B,mBAAmB,CAAS;IAC5B,SAAS,CAAS;IAClB,oBAAoB,CAAiC;IACrD,WAAW,CAAoB;IAC/B,YAAY,GAAG,KAAK,CAAC;IACrB,WAAW,GAAG,KAAK,CAAC;IACpB,gBAAgB,GAAG,IAAI,CAAC;IACxB,QAAQ,GAAc,EAAE,CAAC;IAEjC,YACC,OAA0B,EAC1B,iBAAiB,GAAG,KAAK,EACzB,aAAa,GAAkB,gBAAgB,EAAE,EACjD,mBAAmB,GAAG,aAAa,EACnC,SAAS,GAAG,CAAC,EACb,oBAAoB,GAAmC,EAAE,EACxD;QACD,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC3C,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,oBAAoB,GAAG,oBAAoB,CAAC;QAEjD,sCAAsC;QACtC,IAAI,CAAC,gBAAgB,GAAG,IAAI,SAAS,EAAE,CAAC;QACxC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAErC,IAAI,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;IAAA,CACD;IAEQ,UAAU,GAAS;QAC3B,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,oBAAoB,CAAC,IAAa,EAAQ;QACzC,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;QAC9B,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,sBAAsB,CAAC,KAAa,EAAQ;QAC3C,IAAI,CAAC,mBAAmB,GAAG,KAAK,CAAC;QACjC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,YAAY,CAAC,OAAe,EAAQ;QACnC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC;QACzB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAEQ,MAAM,CAAC,KAAa,EAAY;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,YAAY,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7C,OAAO,KAAK,CAAC;QACd,CAAC;QAED,KAAK,CAAC,CAAC,CAAC,GAAG,iBAAiB,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACxC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,eAAe,GAAG,iBAAiB,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACxF,OAAO,KAAK,CAAC;IAAA,CACb;IAED,aAAa,CAAC,OAAyB,EAAE,WAAW,GAAG,IAAI,CAAC,WAAW,EAAQ;QAC9E,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC;QAC3B,wEAAwE;QACxE,sEAAsE;QACtE,0BAA0B;QAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,IAAI,WAAW,KAAK,IAAI,CAAC,WAAW,CAAC;QAC7E,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAE/B,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC3D,MAAM,QAAQ,GAAG,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC;YAC5E,IAAI,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;gBACxF,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;gBACvD,SAAS;YACV,CAAC;YACD,IAAI,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACxC,yDAAyD;gBACzD,yDAAyD;gBACzD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAiB,CAAC;gBAC7C,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;gBACvD,SAAS;YACV,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;QAED,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAEzB,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAC9B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAChC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACnD,CAAC;QAED,2DAA2D;QAC3D,0EAA0E;QAC1E,oFAAoF;QACpF,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;QACxE,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IAAA,CACjC;IAEO,iBAAiB,CAAC,OAAyB,EAAiB;QACnE,MAAM,KAAK,GAAkB,EAAE,CAAC;QAChC,IAAI,CAAC,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACxC,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACvC,OAAO,KAAK,CAAC;IAAA,CACb;IAEO,kBAAkB,CAAC,OAAyB,EAAE,KAAoB,EAAQ;QACjF,MAAM,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAC7C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAC3F,CAAC;QAEF,IAAI,iBAAiB,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAChC,CAAC;QAED,0BAA0B;QAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACjD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACnC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBACpD,6DAA6D;gBAC7D,+DAA+D;gBAC/D,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,eAAe,CAAC,CAAC;YACrE,CAAC;iBAAM,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBACxC,MAAM,cAAc,GAAa,EAAE,CAAC;gBACpC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBACxC,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;oBAC3C,IAAI,eAAe,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;wBACzC,MAAM;oBACP,CAAC;oBACD,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;oBACjD,IAAI,QAAQ,EAAE,CAAC;wBACd,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAC/B,CAAC;gBACF,CAAC;gBACD,CAAC,EAAE,CAAC;gBAEJ,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACjC,SAAS;gBACV,CAAC;gBAED,yEAAyE;gBACzE,yFAAyF;gBACzF,MAAM,sBAAsB,GAAG,OAAO,CAAC,OAAO;qBAC5C,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC;qBACZ,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBAEpG,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;oBAC5B,qEAAqE;oBACrE,KAAK,CAAC,IAAI,CAAC;wBACV,IAAI,EAAE,OAAO;wBACb,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;qBACtE,CAAC,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACP,8DAA8D;oBAC9D,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,mBAAmB,CAAC,CAAC;gBACjF,CAAC;gBACD,IAAI,sBAAsB,EAAE,CAAC;oBAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAChC,CAAC;YACF,CAAC;QACF,CAAC;IAAA,CACD;IAEO,iBAAiB,CAAC,OAAyB,EAAE,KAAoB,EAAQ;QAChF,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;QACxE,IAAI,OAAO,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,2CAA2C,CAAC,EAAE,CAAC,CAAC;QACrG,CAAC;aAAM,IAAI,CAAC,YAAY,EAAE,CAAC;YAC1B,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;gBACtC,MAAM,YAAY,GACjB,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,KAAK,qBAAqB;oBACrE,CAAC,CAAC,OAAO,CAAC,YAAY;oBACtB,CAAC,CAAC,mBAAmB,CAAC;gBACxB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC;YACtE,CAAC;iBAAM,IAAI,OAAO,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;gBAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,IAAI,eAAe,CAAC;gBACzD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9E,CAAC;QACF,CAAC;IAAA,CACD;IAED;;;;;;;;;;;OAWG;IACK,iBAAiB,CAAC,KAAoB,EAAE,IAAY,EAAE,IAA2C,EAAQ;QAChH,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,KAAK,MAAM,IAAI,IAAI,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC;gBACjD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAClC,CAAC;YACD,OAAO;QACR,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAAA,CAC3B;IAEO,eAAe,CAAC,IAAiB,EAAwB;QAChE,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,QAAQ;gBACZ,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;YACtB,KAAK,OAAO;gBACX,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;YAC/C,KAAK,eAAe;gBACnB,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,EAAE;oBAChF,SAAS,EAAE,uBAAuB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,oBAAoB,CAAC;iBAC5F,CAAC,CAAC;YACJ,KAAK,mBAAmB;gBACvB,OAAO,IAAI,QAAQ,CAClB,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,SAAS,EACd,CAAC,EACD,IAAI,CAAC,aAAa,EAClB;oBACC,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC;oBACvD,MAAM,EAAE,IAAI;iBACZ,EACD;oBACC,SAAS,EAAE,uBAAuB,CAAC,oBAAoB,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,oBAAoB,CAAC;iBACrG,CACD,CAAC;QACJ,CAAC;IAAA,CACD;CACD","sourcesContent":["import type { AssistantMessage } from \"@earendil-works/pi-ai\";\nimport { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { MarkdownTransformer } from \"../../../core/extensions/types.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\nimport { createMarkdownTransform } from \"./markdown-transform.ts\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\nconst FENCE_OPENER = /^(```|~~~)/;\nconst FENCE_CLOSER = /^(```|~~~)\\s*$/;\nconst LIST_MARKER = /^([-*+]\\s|\\d+[.)]\\s)/;\nconst QUOTE_MARKER = /^>\\s?/;\nconst INDENTED_CODE = /^(?:\\t| {4})/;\n\nfunction lastNonEmpty(lines: string[]): string | undefined {\n\tfor (let i = lines.length - 1; i >= 0; i--) {\n\t\tif (lines[i].trim() !== \"\") return lines[i];\n\t}\n\treturn undefined;\n}\n\nfunction shouldSplitBefore(next: string, current: string[]): boolean {\n\t// Indented lines can continue indented code blocks; never split there.\n\tif (INDENTED_CODE.test(next)) return false;\n\tconst previous = lastNonEmpty(current);\n\tif (previous && INDENTED_CODE.test(previous)) return false;\n\t// A loose list (\"item\\n\\nitem\") and a blockquote with a blank line render\n\t// as one construct; splitting them would change the output.\n\tif (LIST_MARKER.test(next) && previous && LIST_MARKER.test(previous)) return false;\n\tif (QUOTE_MARKER.test(next) && previous && QUOTE_MARKER.test(previous)) return false;\n\treturn true;\n}\n\n/**\n * Split streamed markdown into render units at boundaries that render\n * independently: closed column-0 code fences, and blank-line block\n * boundaries that cannot continue a preceding construct.\n *\n * The result is prefix-stable: appending text can only add units at the end,\n * never change earlier ones, which is what makes the units cacheable while\n * a message streams.\n */\nexport function splitStreamingMarkdown(text: string): string[] {\n\tif (!text) return [];\n\tconst lines = text.split(\"\\n\");\n\tconst units: string[] = [];\n\tlet current: string[] = [];\n\tlet inFence = false;\n\tconst flush = (): void => {\n\t\tif (current.length > 0) {\n\t\t\tunits.push(current.join(\"\\n\"));\n\t\t\tcurrent = [];\n\t\t}\n\t};\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i];\n\t\tif (!inFence && FENCE_OPENER.test(line)) {\n\t\t\tinFence = true;\n\t\t\tcurrent.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence) {\n\t\t\tcurrent.push(line);\n\t\t\tif (FENCE_CLOSER.test(line)) {\n\t\t\t\tflush();\n\t\t\t\tinFence = false;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent.push(line);\n\t\tif (line.trim() === \"\") {\n\t\t\tconst next = lines[i + 1];\n\t\t\tif (next !== undefined && next.trim() !== \"\" && shouldSplitBefore(next, current)) {\n\t\t\t\tflush();\n\t\t\t}\n\t\t}\n\t}\n\tflush();\n\treturn units;\n}\n\ntype SectionSpec =\n\t| { kind: \"spacer\" }\n\t| { kind: \"markdown-text\"; text: string }\n\t| { kind: \"markdown-thinking\"; text: string }\n\t| { kind: \"label\"; text: string };\n\ntype Section = { spec: SectionSpec; component: Container[\"children\"][number] };\n\nfunction specText(spec: SectionSpec): string | undefined {\n\treturn spec.kind === \"spacer\" ? undefined : spec.text;\n}\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate hiddenThinkingLabel: string;\n\tprivate outputPad: number;\n\tprivate markdownTransformers: readonly MarkdownTransformer[];\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\tprivate isStreaming = false;\n\tprivate forceFullRebuild = true;\n\tprivate sections: Section[] = [];\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t\toutputPad = 1,\n\t\tmarkdownTransformers: readonly MarkdownTransformer[] = [],\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\t\tthis.outputPad = outputPad;\n\t\tthis.markdownTransformers = markdownTransformers;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetOutputPad(padding: number): void {\n\t\tthis.outputPad = padding;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst lines = super.render(width);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\tlines[0] = OSC133_ZONE_START + lines[0];\n\t\tlines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];\n\t\treturn lines;\n\t}\n\n\tupdateContent(message: AssistantMessage, isStreaming = this.isStreaming): void {\n\t\tthis.lastMessage = message;\n\t\t// The transform closures capture isStreaming, and non-streaming updates\n\t\t// render whole blocks rather than split units, so an isStreaming flip\n\t\t// rebuilds every section.\n\t\tconst rebuildAll = this.forceFullRebuild || isStreaming !== this.isStreaming;\n\t\tthis.isStreaming = isStreaming;\n\n\t\tconst specs = this.buildSectionSpecs(message);\n\t\tconst sections: Section[] = [];\n\t\tfor (let i = 0; i < specs.length; i++) {\n\t\t\tconst spec = specs[i];\n\t\t\tconst previous = rebuildAll ? undefined : this.sections[i];\n\t\t\tconst sameKind = previous !== undefined && previous.spec.kind === spec.kind;\n\t\t\tif (sameKind && (spec.kind === \"spacer\" || specText(previous.spec) === specText(spec))) {\n\t\t\t\tsections.push({ spec, component: previous.component });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (sameKind && spec.kind !== \"spacer\") {\n\t\t\t\t// Same slot, new content (the growing tail of a streamed\n\t\t\t\t// message): update in place so only this unit re-parses.\n\t\t\t\tconst component = previous.component as Text;\n\t\t\t\tcomponent.setText(specText(spec) ?? \"\");\n\t\t\t\tsections.push({ spec, component: previous.component });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsections.push({ spec, component: this.createComponent(spec) });\n\t\t}\n\n\t\tthis.forceFullRebuild = false;\n\t\tthis.sections = sections;\n\n\t\tthis.contentContainer.clear();\n\t\tfor (const section of sections) {\n\t\t\tthis.contentContainer.addChild(section.component);\n\t\t}\n\n\t\t// Check if incomplete/failed - show after partial content.\n\t\t// For aborted/error tool calls, tool execution components show the error.\n\t\t// Length stops can happen before a tool call is complete, so surface them here too.\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t}\n\n\tprivate buildSectionSpecs(message: AssistantMessage): SectionSpec[] {\n\t\tconst specs: SectionSpec[] = [];\n\t\tthis.appendContentSpecs(message, specs);\n\t\tthis.appendStatusSpecs(message, specs);\n\t\treturn specs;\n\t}\n\n\tprivate appendContentSpecs(message: AssistantMessage, specs: SectionSpec[]): void {\n\t\tconst hasVisibleContent = message.content.some(\n\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t);\n\n\t\tif (hasVisibleContent) {\n\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.pushMarkdownSpecs(specs, content.text.trim(), \"markdown-text\");\n\t\t\t} else if (content.type === \"thinking\") {\n\t\t\t\tconst thinkingBlocks: string[] = [];\n\t\t\t\tfor (; i < message.content.length; i++) {\n\t\t\t\t\tconst thinkingContent = message.content[i];\n\t\t\t\t\tif (thinkingContent.type !== \"thinking\") {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tconst thinking = thinkingContent.thinking.trim();\n\t\t\t\t\tif (thinking) {\n\t\t\t\t\t\tthinkingBlocks.push(thinking);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti--;\n\n\t\t\t\tif (thinkingBlocks.length === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.some((c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()));\n\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\t// Show one static label for each run of thinking blocks when hidden.\n\t\t\t\t\tspecs.push({\n\t\t\t\t\t\tkind: \"label\",\n\t\t\t\t\t\ttext: theme.italic(theme.fg(\"thinkingText\", this.hiddenThinkingLabel)),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\t// Render each run of thinking blocks as one Markdown section.\n\t\t\t\t\tthis.pushMarkdownSpecs(specs, thinkingBlocks.join(\"\\n\\n\"), \"markdown-thinking\");\n\t\t\t\t}\n\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate appendStatusSpecs(message: AssistantMessage, specs: SectionSpec[]): void {\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tif (message.stopReason === \"length\") {\n\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", \"Response was truncated before completion.\") });\n\t\t} else if (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", abortMessage) });\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", `Error: ${errorMsg}`) });\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Push markdown sections for one content block. While streaming, the block\n\t * is split at render-independent boundaries so completed units keep their\n\t * rendered Markdown instances and only the active tail re-parses per\n\t * chunk; otherwise the block renders whole, byte-identical to the\n\t * pre-split behaviour.\n\t *\n\t * Transformers are assumed block-local (the built-in mermaid transformer\n\t * maps tokens and preserves the rest, so per-unit application is\n\t * equivalent). A transformer that genuinely needs whole-message context\n\t * still gets it: a non-streaming update always renders whole blocks.\n\t */\n\tprivate pushMarkdownSpecs(specs: SectionSpec[], text: string, kind: \"markdown-text\" | \"markdown-thinking\"): void {\n\t\tif (this.isStreaming) {\n\t\t\tfor (const unit of splitStreamingMarkdown(text)) {\n\t\t\t\tspecs.push({ kind, text: unit });\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tspecs.push({ kind, text });\n\t}\n\n\tprivate createComponent(spec: SectionSpec): Section[\"component\"] {\n\t\tswitch (spec.kind) {\n\t\t\tcase \"spacer\":\n\t\t\t\treturn new Spacer(1);\n\t\t\tcase \"label\":\n\t\t\t\treturn new Text(spec.text, this.outputPad, 0);\n\t\t\tcase \"markdown-text\":\n\t\t\t\treturn new Markdown(spec.text, this.outputPad, 0, this.markdownTheme, undefined, {\n\t\t\t\t\ttransform: createMarkdownTransform(\"assistant\", this.isStreaming, this.markdownTransformers),\n\t\t\t\t});\n\t\t\tcase \"markdown-thinking\":\n\t\t\t\treturn new Markdown(\n\t\t\t\t\tspec.text,\n\t\t\t\t\tthis.outputPad,\n\t\t\t\t\t0,\n\t\t\t\t\tthis.markdownTheme,\n\t\t\t\t\t{\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttransform: createMarkdownTransform(\"assistant-thinking\", this.isStreaming, this.markdownTransformers),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assistant-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/assistant-message.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAsB,MAAM,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAE/F,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAC3C,MAAM,eAAe,GAAG,gBAAgB,CAAC;AACzC,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAE3C,MAAM,YAAY,GAAG,YAAY,CAAC;AAClC,MAAM,YAAY,GAAG,gBAAgB,CAAC;AACtC,MAAM,WAAW,GAAG,sBAAsB,CAAC;AAC3C,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,MAAM,aAAa,GAAG,cAAc,CAAC;AAErC,SAAS,YAAY,CAAC,KAAe,EAAsB;IAC1D,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,SAAS,iBAAiB,CAAC,IAAY,EAAE,OAAiB,EAAW;IACpE,uEAAuE;IACvE,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,0EAA0E;IAC1E,4DAA4D;IAC5D,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,IAAI,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IACnF,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,IAAI,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IACrF,OAAO,IAAI,CAAC;AAAA,CACZ;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY,EAAY;IAC9D,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,KAAK,GAAG,GAAS,EAAE,CAAC;QACzB,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAC/B,OAAO,GAAG,EAAE,CAAC;QACd,CAAC;IAAA,CACD,CAAC;IACF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,SAAS;QACV,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,KAAK,EAAE,CAAC;gBACR,OAAO,GAAG,KAAK,CAAC;YACjB,CAAC;YACD,SAAS;QACV,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;gBAClF,KAAK,EAAE,CAAC;YACT,CAAC;QACF,CAAC;IACF,CAAC;IACD,KAAK,EAAE,CAAC;IACR,OAAO,KAAK,CAAC;AAAA,CACb;AAUD,SAAS,QAAQ,CAAC,IAAiB,EAAsB;IACxD,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;AAAA,CACtD;AAED;;GAEG;AACH,MAAM,OAAO,yBAA0B,SAAQ,SAAS;IAC/C,gBAAgB,CAAY;IAC5B,iBAAiB,CAAU;IAC3B,aAAa,CAAgB;IAC7B,mBAAmB,CAAS;IAC5B,SAAS,CAAS;IAClB,oBAAoB,CAAiC;IACrD,WAAW,CAAoB;IAC/B,YAAY,GAAG,KAAK,CAAC;IACrB,WAAW,GAAG,KAAK,CAAC;IACpB,gBAAgB,GAAG,IAAI,CAAC;IACxB,QAAQ,GAAc,EAAE,CAAC;IAEjC,YACC,OAA0B,EAC1B,iBAAiB,GAAG,KAAK,EACzB,aAAa,GAAkB,gBAAgB,EAAE,EACjD,mBAAmB,GAAG,aAAa,EACnC,SAAS,GAAG,CAAC,EACb,oBAAoB,GAAmC,EAAE,EACxD;QACD,KAAK,EAAE,CAAC;QAER,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC3C,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,oBAAoB,GAAG,oBAAoB,CAAC;QAEjD,sCAAsC;QACtC,IAAI,CAAC,gBAAgB,GAAG,IAAI,SAAS,EAAE,CAAC;QACxC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAErC,IAAI,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;IAAA,CACD;IAEQ,UAAU,GAAS;QAC3B,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,oBAAoB,CAAC,IAAa,EAAQ;QACzC,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;QAC9B,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,sBAAsB,CAAC,KAAa,EAAQ;QAC3C,IAAI,CAAC,mBAAmB,GAAG,KAAK,CAAC;QACjC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,YAAY,CAAC,OAAe,EAAQ;QACnC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC;QACzB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAEQ,MAAM,CAAC,KAAa,EAAY;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,YAAY,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7C,OAAO,KAAK,CAAC;QACd,CAAC;QAED,mEAAmE;QACnE,sEAAsE;QACtE,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;YACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACvC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAChC,KAAK,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBACtC,CAAC;YACF,CAAC;QACF,CAAC;QACD,KAAK,CAAC,CAAC,CAAC,GAAG,iBAAiB,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACxC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,eAAe,GAAG,iBAAiB,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACxF,OAAO,KAAK,CAAC;IAAA,CACb;IAED,aAAa,CAAC,OAAyB,EAAE,WAAW,GAAG,IAAI,CAAC,WAAW,EAAQ;QAC9E,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC;QAC3B,wEAAwE;QACxE,sEAAsE;QACtE,0BAA0B;QAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,IAAI,WAAW,KAAK,IAAI,CAAC,WAAW,CAAC;QAC7E,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAE/B,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC3D,MAAM,QAAQ,GAAG,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC;YAC5E,IAAI,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;gBACxF,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;gBACvD,SAAS;YACV,CAAC;YACD,IAAI,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACxC,yDAAyD;gBACzD,yDAAyD;gBACzD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAiB,CAAC;gBAC7C,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;gBACvD,SAAS;YACV,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;QAED,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAEzB,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAC9B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAChC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACnD,CAAC;QAED,2DAA2D;QAC3D,0EAA0E;QAC1E,oFAAoF;QACpF,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;QACxE,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IAAA,CACjC;IAEO,iBAAiB,CAAC,OAAyB,EAAiB;QACnE,MAAM,KAAK,GAAkB,EAAE,CAAC;QAChC,IAAI,CAAC,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACxC,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACvC,OAAO,KAAK,CAAC;IAAA,CACb;IAEO,kBAAkB,CAAC,OAAyB,EAAE,KAAoB,EAAQ;QACjF,MAAM,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAC7C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAC3F,CAAC;QAEF,IAAI,iBAAiB,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAChC,CAAC;QAED,0BAA0B;QAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACjD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACnC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBACpD,6DAA6D;gBAC7D,+DAA+D;gBAC/D,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,eAAe,CAAC,CAAC;YACrE,CAAC;iBAAM,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBACxC,MAAM,cAAc,GAAa,EAAE,CAAC;gBACpC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBACxC,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;oBAC3C,IAAI,eAAe,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;wBACzC,MAAM;oBACP,CAAC;oBACD,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;oBACjD,IAAI,QAAQ,EAAE,CAAC;wBACd,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAC/B,CAAC;gBACF,CAAC;gBACD,CAAC,EAAE,CAAC;gBAEJ,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACjC,SAAS;gBACV,CAAC;gBAED,yEAAyE;gBACzE,yFAAyF;gBACzF,MAAM,sBAAsB,GAAG,OAAO,CAAC,OAAO;qBAC5C,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC;qBACZ,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBAEpG,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;oBAC5B,qEAAqE;oBACrE,KAAK,CAAC,IAAI,CAAC;wBACV,IAAI,EAAE,OAAO;wBACb,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;qBACtE,CAAC,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACP,8DAA8D;oBAC9D,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,mBAAmB,CAAC,CAAC;gBACjF,CAAC;gBACD,IAAI,sBAAsB,EAAE,CAAC;oBAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAChC,CAAC;YACF,CAAC;QACF,CAAC;IAAA,CACD;IAEO,iBAAiB,CAAC,OAAyB,EAAE,KAAoB,EAAQ;QAChF,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;QACxE,IAAI,OAAO,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,2CAA2C,CAAC,EAAE,CAAC,CAAC;QACrG,CAAC;aAAM,IAAI,CAAC,YAAY,EAAE,CAAC;YAC1B,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;gBACtC,MAAM,YAAY,GACjB,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,KAAK,qBAAqB;oBACrE,CAAC,CAAC,OAAO,CAAC,YAAY;oBACtB,CAAC,CAAC,mBAAmB,CAAC;gBACxB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC;YACtE,CAAC;iBAAM,IAAI,OAAO,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;gBAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,IAAI,eAAe,CAAC;gBACzD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9E,CAAC;QACF,CAAC;IAAA,CACD;IAED;;;;;;;;;;;OAWG;IACK,iBAAiB,CAAC,KAAoB,EAAE,IAAY,EAAE,IAA2C,EAAQ;QAChH,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,KAAK,MAAM,IAAI,IAAI,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC;gBACjD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAClC,CAAC;YACD,OAAO;QACR,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAAA,CAC3B;IAEO,eAAe,CAAC,IAAiB,EAAwB;QAChE,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,QAAQ;gBACZ,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;YACtB,KAAK,OAAO;gBACX,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;YAC/C,KAAK,eAAe;gBACnB,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,EAAE;oBAChF,SAAS,EAAE,uBAAuB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,oBAAoB,CAAC;iBAC5F,CAAC,CAAC;YACJ,KAAK,mBAAmB;gBACvB,OAAO,IAAI,QAAQ,CAClB,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,SAAS,EACd,CAAC,EACD,IAAI,CAAC,aAAa,EAClB;oBACC,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC;oBACvD,MAAM,EAAE,IAAI;iBACZ,EACD;oBACC,SAAS,EAAE,uBAAuB,CAAC,oBAAoB,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,oBAAoB,CAAC;iBACrG,CACD,CAAC;QACJ,CAAC;IAAA,CACD;CACD","sourcesContent":["import type { AssistantMessage } from \"@earendil-works/pi-ai\";\nimport { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@earendil-works/pi-tui\";\nimport type { MarkdownTransformer } from \"../../../core/extensions/types.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\nimport { createMarkdownTransform } from \"./markdown-transform.ts\";\nimport { withAccentSpine } from \"./message-spine.ts\";\n\nconst OSC133_ZONE_START = \"\\x1b]133;A\\x07\";\nconst OSC133_ZONE_END = \"\\x1b]133;B\\x07\";\nconst OSC133_ZONE_FINAL = \"\\x1b]133;C\\x07\";\n\nconst FENCE_OPENER = /^(```|~~~)/;\nconst FENCE_CLOSER = /^(```|~~~)\\s*$/;\nconst LIST_MARKER = /^([-*+]\\s|\\d+[.)]\\s)/;\nconst QUOTE_MARKER = /^>\\s?/;\nconst INDENTED_CODE = /^(?:\\t| {4})/;\n\nfunction lastNonEmpty(lines: string[]): string | undefined {\n\tfor (let i = lines.length - 1; i >= 0; i--) {\n\t\tif (lines[i].trim() !== \"\") return lines[i];\n\t}\n\treturn undefined;\n}\n\nfunction shouldSplitBefore(next: string, current: string[]): boolean {\n\t// Indented lines can continue indented code blocks; never split there.\n\tif (INDENTED_CODE.test(next)) return false;\n\tconst previous = lastNonEmpty(current);\n\tif (previous && INDENTED_CODE.test(previous)) return false;\n\t// A loose list (\"item\\n\\nitem\") and a blockquote with a blank line render\n\t// as one construct; splitting them would change the output.\n\tif (LIST_MARKER.test(next) && previous && LIST_MARKER.test(previous)) return false;\n\tif (QUOTE_MARKER.test(next) && previous && QUOTE_MARKER.test(previous)) return false;\n\treturn true;\n}\n\n/**\n * Split streamed markdown into render units at boundaries that render\n * independently: closed column-0 code fences, and blank-line block\n * boundaries that cannot continue a preceding construct.\n *\n * The result is prefix-stable: appending text can only add units at the end,\n * never change earlier ones, which is what makes the units cacheable while\n * a message streams.\n */\nexport function splitStreamingMarkdown(text: string): string[] {\n\tif (!text) return [];\n\tconst lines = text.split(\"\\n\");\n\tconst units: string[] = [];\n\tlet current: string[] = [];\n\tlet inFence = false;\n\tconst flush = (): void => {\n\t\tif (current.length > 0) {\n\t\t\tunits.push(current.join(\"\\n\"));\n\t\t\tcurrent = [];\n\t\t}\n\t};\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i];\n\t\tif (!inFence && FENCE_OPENER.test(line)) {\n\t\t\tinFence = true;\n\t\t\tcurrent.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence) {\n\t\t\tcurrent.push(line);\n\t\t\tif (FENCE_CLOSER.test(line)) {\n\t\t\t\tflush();\n\t\t\t\tinFence = false;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tcurrent.push(line);\n\t\tif (line.trim() === \"\") {\n\t\t\tconst next = lines[i + 1];\n\t\t\tif (next !== undefined && next.trim() !== \"\" && shouldSplitBefore(next, current)) {\n\t\t\t\tflush();\n\t\t\t}\n\t\t}\n\t}\n\tflush();\n\treturn units;\n}\n\ntype SectionSpec =\n\t| { kind: \"spacer\" }\n\t| { kind: \"markdown-text\"; text: string }\n\t| { kind: \"markdown-thinking\"; text: string }\n\t| { kind: \"label\"; text: string };\n\ntype Section = { spec: SectionSpec; component: Container[\"children\"][number] };\n\nfunction specText(spec: SectionSpec): string | undefined {\n\treturn spec.kind === \"spacer\" ? undefined : spec.text;\n}\n\n/**\n * Component that renders a complete assistant message\n */\nexport class AssistantMessageComponent extends Container {\n\tprivate contentContainer: Container;\n\tprivate hideThinkingBlock: boolean;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate hiddenThinkingLabel: string;\n\tprivate outputPad: number;\n\tprivate markdownTransformers: readonly MarkdownTransformer[];\n\tprivate lastMessage?: AssistantMessage;\n\tprivate hasToolCalls = false;\n\tprivate isStreaming = false;\n\tprivate forceFullRebuild = true;\n\tprivate sections: Section[] = [];\n\n\tconstructor(\n\t\tmessage?: AssistantMessage,\n\t\thideThinkingBlock = false,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t\thiddenThinkingLabel = \"Thinking...\",\n\t\toutputPad = 1,\n\t\tmarkdownTransformers: readonly MarkdownTransformer[] = [],\n\t) {\n\t\tsuper();\n\n\t\tthis.hideThinkingBlock = hideThinkingBlock;\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.hiddenThinkingLabel = hiddenThinkingLabel;\n\t\tthis.outputPad = outputPad;\n\t\tthis.markdownTransformers = markdownTransformers;\n\n\t\t// Container for text/thinking content\n\t\tthis.contentContainer = new Container();\n\t\tthis.addChild(this.contentContainer);\n\n\t\tif (message) {\n\t\t\tthis.updateContent(message);\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.hideThinkingBlock = hide;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetHiddenThinkingLabel(label: string): void {\n\t\tthis.hiddenThinkingLabel = label;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\tsetOutputPad(padding: number): void {\n\t\tthis.outputPad = padding;\n\t\tif (this.lastMessage) {\n\t\t\tthis.forceFullRebuild = true;\n\t\t\tthis.updateContent(this.lastMessage);\n\t\t}\n\t}\n\n\toverride render(width: number): string[] {\n\t\tconst lines = super.render(width);\n\t\tif (this.hasToolCalls || lines.length === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Keep conversational messages on one visual axis. Tool panels own\n\t\t// their lifecycle spine; assistant text uses the quieter accent rail.\n\t\tif (this.outputPad > 0) {\n\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\tif (lines[i].trim().length > 0) {\n\t\t\t\t\tlines[i] = withAccentSpine(lines[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlines[0] = OSC133_ZONE_START + lines[0];\n\t\tlines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];\n\t\treturn lines;\n\t}\n\n\tupdateContent(message: AssistantMessage, isStreaming = this.isStreaming): void {\n\t\tthis.lastMessage = message;\n\t\t// The transform closures capture isStreaming, and non-streaming updates\n\t\t// render whole blocks rather than split units, so an isStreaming flip\n\t\t// rebuilds every section.\n\t\tconst rebuildAll = this.forceFullRebuild || isStreaming !== this.isStreaming;\n\t\tthis.isStreaming = isStreaming;\n\n\t\tconst specs = this.buildSectionSpecs(message);\n\t\tconst sections: Section[] = [];\n\t\tfor (let i = 0; i < specs.length; i++) {\n\t\t\tconst spec = specs[i];\n\t\t\tconst previous = rebuildAll ? undefined : this.sections[i];\n\t\t\tconst sameKind = previous !== undefined && previous.spec.kind === spec.kind;\n\t\t\tif (sameKind && (spec.kind === \"spacer\" || specText(previous.spec) === specText(spec))) {\n\t\t\t\tsections.push({ spec, component: previous.component });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (sameKind && spec.kind !== \"spacer\") {\n\t\t\t\t// Same slot, new content (the growing tail of a streamed\n\t\t\t\t// message): update in place so only this unit re-parses.\n\t\t\t\tconst component = previous.component as Text;\n\t\t\t\tcomponent.setText(specText(spec) ?? \"\");\n\t\t\t\tsections.push({ spec, component: previous.component });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsections.push({ spec, component: this.createComponent(spec) });\n\t\t}\n\n\t\tthis.forceFullRebuild = false;\n\t\tthis.sections = sections;\n\n\t\tthis.contentContainer.clear();\n\t\tfor (const section of sections) {\n\t\t\tthis.contentContainer.addChild(section.component);\n\t\t}\n\n\t\t// Check if incomplete/failed - show after partial content.\n\t\t// For aborted/error tool calls, tool execution components show the error.\n\t\t// Length stops can happen before a tool call is complete, so surface them here too.\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tthis.hasToolCalls = hasToolCalls;\n\t}\n\n\tprivate buildSectionSpecs(message: AssistantMessage): SectionSpec[] {\n\t\tconst specs: SectionSpec[] = [];\n\t\tthis.appendContentSpecs(message, specs);\n\t\tthis.appendStatusSpecs(message, specs);\n\t\treturn specs;\n\t}\n\n\tprivate appendContentSpecs(message: AssistantMessage, specs: SectionSpec[]): void {\n\t\tconst hasVisibleContent = message.content.some(\n\t\t\t(c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()),\n\t\t);\n\n\t\tif (hasVisibleContent) {\n\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t}\n\n\t\t// Render content in order\n\t\tfor (let i = 0; i < message.content.length; i++) {\n\t\t\tconst content = message.content[i];\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\t// Assistant text messages with no background - trim the text\n\t\t\t\t// Set paddingY=0 to avoid extra spacing before tool executions\n\t\t\t\tthis.pushMarkdownSpecs(specs, content.text.trim(), \"markdown-text\");\n\t\t\t} else if (content.type === \"thinking\") {\n\t\t\t\tconst thinkingBlocks: string[] = [];\n\t\t\t\tfor (; i < message.content.length; i++) {\n\t\t\t\t\tconst thinkingContent = message.content[i];\n\t\t\t\t\tif (thinkingContent.type !== \"thinking\") {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tconst thinking = thinkingContent.thinking.trim();\n\t\t\t\t\tif (thinking) {\n\t\t\t\t\t\tthinkingBlocks.push(thinking);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti--;\n\n\t\t\t\tif (thinkingBlocks.length === 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Add spacing only when another visible assistant content block follows.\n\t\t\t\t// This avoids a superfluous blank line before separately-rendered tool execution blocks.\n\t\t\t\tconst hasVisibleContentAfter = message.content\n\t\t\t\t\t.slice(i + 1)\n\t\t\t\t\t.some((c) => (c.type === \"text\" && c.text.trim()) || (c.type === \"thinking\" && c.thinking.trim()));\n\n\t\t\t\tif (this.hideThinkingBlock) {\n\t\t\t\t\t// Show one static label for each run of thinking blocks when hidden.\n\t\t\t\t\tspecs.push({\n\t\t\t\t\t\tkind: \"label\",\n\t\t\t\t\t\ttext: theme.italic(theme.fg(\"thinkingText\", this.hiddenThinkingLabel)),\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\t// Render each run of thinking blocks as one Markdown section.\n\t\t\t\t\tthis.pushMarkdownSpecs(specs, thinkingBlocks.join(\"\\n\\n\"), \"markdown-thinking\");\n\t\t\t\t}\n\t\t\t\tif (hasVisibleContentAfter) {\n\t\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate appendStatusSpecs(message: AssistantMessage, specs: SectionSpec[]): void {\n\t\tconst hasToolCalls = message.content.some((c) => c.type === \"toolCall\");\n\t\tif (message.stopReason === \"length\") {\n\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", \"Response was truncated before completion.\") });\n\t\t} else if (!hasToolCalls) {\n\t\t\tif (message.stopReason === \"aborted\") {\n\t\t\t\tconst abortMessage =\n\t\t\t\t\tmessage.errorMessage && message.errorMessage !== \"Request was aborted\"\n\t\t\t\t\t\t? message.errorMessage\n\t\t\t\t\t\t: \"Operation aborted\";\n\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", abortMessage) });\n\t\t\t} else if (message.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = message.errorMessage || \"Unknown error\";\n\t\t\t\tspecs.push({ kind: \"spacer\" });\n\t\t\t\tspecs.push({ kind: \"label\", text: theme.fg(\"error\", `Error: ${errorMsg}`) });\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Push markdown sections for one content block. While streaming, the block\n\t * is split at render-independent boundaries so completed units keep their\n\t * rendered Markdown instances and only the active tail re-parses per\n\t * chunk; otherwise the block renders whole, byte-identical to the\n\t * pre-split behaviour.\n\t *\n\t * Transformers are assumed block-local (the built-in mermaid transformer\n\t * maps tokens and preserves the rest, so per-unit application is\n\t * equivalent). A transformer that genuinely needs whole-message context\n\t * still gets it: a non-streaming update always renders whole blocks.\n\t */\n\tprivate pushMarkdownSpecs(specs: SectionSpec[], text: string, kind: \"markdown-text\" | \"markdown-thinking\"): void {\n\t\tif (this.isStreaming) {\n\t\t\tfor (const unit of splitStreamingMarkdown(text)) {\n\t\t\t\tspecs.push({ kind, text: unit });\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tspecs.push({ kind, text });\n\t}\n\n\tprivate createComponent(spec: SectionSpec): Section[\"component\"] {\n\t\tswitch (spec.kind) {\n\t\t\tcase \"spacer\":\n\t\t\t\treturn new Spacer(1);\n\t\t\tcase \"label\":\n\t\t\t\treturn new Text(spec.text, this.outputPad, 0);\n\t\t\tcase \"markdown-text\":\n\t\t\t\treturn new Markdown(spec.text, this.outputPad, 0, this.markdownTheme, undefined, {\n\t\t\t\t\ttransform: createMarkdownTransform(\"assistant\", this.isStreaming, this.markdownTransformers),\n\t\t\t\t});\n\t\t\tcase \"markdown-thinking\":\n\t\t\t\treturn new Markdown(\n\t\t\t\t\tspec.text,\n\t\t\t\t\tthis.outputPad,\n\t\t\t\t\t0,\n\t\t\t\t\tthis.markdownTheme,\n\t\t\t\t\t{\n\t\t\t\t\t\tcolor: (text: string) => theme.fg(\"thinkingText\", text),\n\t\t\t\t\t\titalic: true,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttransform: createMarkdownTransform(\"assistant-thinking\", this.isStreaming, this.markdownTransformers),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put the accent spine in the first padding column of an already-rendered line.
|
|
3
|
+
*
|
|
4
|
+
* Both message components render their children with `outputPad` leading spaces, so the
|
|
5
|
+
* first column is padding and is free to take. Prefixing the spine instead makes the line
|
|
6
|
+
* one column wider than the width it was rendered for, and nothing downstream re-flows it:
|
|
7
|
+
* the user-message line then overflows the terminal by one column, and the assistant path
|
|
8
|
+
* hands it to `truncateToWidth`, which clips real content and appends an ellipsis.
|
|
9
|
+
*
|
|
10
|
+
* A line that does not start with a padding column is returned unchanged, because there is
|
|
11
|
+
* no free column to take and widening it is the defect this exists to avoid.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withAccentSpine(line: string): string;
|
|
14
|
+
//# sourceMappingURL=message-spine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-spine.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/message-spine.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGpD","sourcesContent":["import { theme } from \"../theme/theme.ts\";\n\n/**\n * Put the accent spine in the first padding column of an already-rendered line.\n *\n * Both message components render their children with `outputPad` leading spaces, so the\n * first column is padding and is free to take. Prefixing the spine instead makes the line\n * one column wider than the width it was rendered for, and nothing downstream re-flows it:\n * the user-message line then overflows the terminal by one column, and the assistant path\n * hands it to `truncateToWidth`, which clips real content and appends an ellipsis.\n *\n * A line that does not start with a padding column is returned unchanged, because there is\n * no free column to take and widening it is the defect this exists to avoid.\n */\nexport function withAccentSpine(line: string): string {\n\tif (!line.startsWith(\" \")) return line;\n\treturn theme.fg(\"accent\", \"│\") + line.slice(1);\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { theme } from "../theme/theme.js";
|
|
2
|
+
/**
|
|
3
|
+
* Put the accent spine in the first padding column of an already-rendered line.
|
|
4
|
+
*
|
|
5
|
+
* Both message components render their children with `outputPad` leading spaces, so the
|
|
6
|
+
* first column is padding and is free to take. Prefixing the spine instead makes the line
|
|
7
|
+
* one column wider than the width it was rendered for, and nothing downstream re-flows it:
|
|
8
|
+
* the user-message line then overflows the terminal by one column, and the assistant path
|
|
9
|
+
* hands it to `truncateToWidth`, which clips real content and appends an ellipsis.
|
|
10
|
+
*
|
|
11
|
+
* A line that does not start with a padding column is returned unchanged, because there is
|
|
12
|
+
* no free column to take and widening it is the defect this exists to avoid.
|
|
13
|
+
*/
|
|
14
|
+
export function withAccentSpine(line) {
|
|
15
|
+
if (!line.startsWith(" "))
|
|
16
|
+
return line;
|
|
17
|
+
return theme.fg("accent", "│") + line.slice(1);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=message-spine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-spine.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/message-spine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAU;IACrD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAAA,CAC/C","sourcesContent":["import { theme } from \"../theme/theme.ts\";\n\n/**\n * Put the accent spine in the first padding column of an already-rendered line.\n *\n * Both message components render their children with `outputPad` leading spaces, so the\n * first column is padding and is free to take. Prefixing the spine instead makes the line\n * one column wider than the width it was rendered for, and nothing downstream re-flows it:\n * the user-message line then overflows the terminal by one column, and the assistant path\n * hands it to `truncateToWidth`, which clips real content and appends an ellipsis.\n *\n * A line that does not start with a padding column is returned unchanged, because there is\n * no free column to take and widening it is the defect this exists to avoid.\n */\nexport function withAccentSpine(line: string): string {\n\tif (!line.startsWith(\" \")) return line;\n\treturn theme.fg(\"accent\", \"│\") + line.slice(1);\n}\n"]}
|