@oh-my-pi/pi-coding-agent 12.14.2 → 12.15.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 +18 -1
- package/package.json +8 -8
- package/scripts/generate-docs-index.ts +21 -11
- package/src/config/settings-schema.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/rpc/rpc-client.ts +39 -12
- package/src/modes/rpc/rpc-mode.ts +2 -0
- package/src/patch/hashline.ts +2 -4
- package/src/prompts/tools/browser.md +12 -5
- package/src/session/agent-storage.ts +48 -6
- package/src/session/auth-storage.ts +5 -4
- package/src/task/executor.ts +8 -1
- package/src/task/render.ts +6 -4
- package/src/task/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Updated browser tool prompt to bias towards `observe` over `screenshot` by default
|
|
9
|
+
|
|
10
|
+
## [12.15.0] - 2026-02-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `includeDisabled` parameter to `listAuthCredentials()` to optionally retrieve disabled credentials
|
|
15
|
+
- Added `disableAuthCredential()` method for soft-deleting auth credentials while preserving database records
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Changed auth credential removal to use soft-delete (disable) instead of hard-delete when OAuth refresh fails, keeping credentials in database for audit purposes
|
|
20
|
+
- Changed default value of `tools.intentTracing` setting from false to true
|
|
21
|
+
|
|
5
22
|
## [12.14.1] - 2026-02-19
|
|
6
23
|
|
|
7
24
|
### Fixed
|
|
@@ -4854,4 +4871,4 @@ Initial public release.
|
|
|
4854
4871
|
- Git branch display in footer
|
|
4855
4872
|
- Message queueing during streaming responses
|
|
4856
4873
|
- OAuth integration for Gmail and Google Calendar access
|
|
4857
|
-
- HTML export with syntax highlighting and collapsible sections
|
|
4874
|
+
- HTML export with syntax highlighting and collapsible sections
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.15.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -70,7 +70,6 @@
|
|
|
70
70
|
"files": [
|
|
71
71
|
"src",
|
|
72
72
|
"scripts",
|
|
73
|
-
"docs",
|
|
74
73
|
"examples",
|
|
75
74
|
"README.md",
|
|
76
75
|
"CHANGELOG.md"
|
|
@@ -79,18 +78,19 @@
|
|
|
79
78
|
"check": "tsgo -p tsconfig.json",
|
|
80
79
|
"format-prompts": "bun scripts/format-prompts.ts",
|
|
81
80
|
"generate-docs-index": "bun scripts/generate-docs-index.ts",
|
|
81
|
+
"prepack": "bun scripts/generate-docs-index.ts",
|
|
82
82
|
"build:binary": "cd ../.. && bun --cwd=packages/stats scripts/generate-client-bundle.ts && bun --cwd=packages/natives run embed:native && bun build --compile --define PI_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp && bun --cwd=packages/natives run embed:native --reset && bun --cwd=packages/stats scripts/generate-client-bundle.ts --reset",
|
|
83
83
|
"generate-template": "bun scripts/generate-template.ts",
|
|
84
84
|
"test": "bun test"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
87
|
"@mozilla/readability": "0.6.0",
|
|
88
|
-
"@oh-my-pi/omp-stats": "12.
|
|
89
|
-
"@oh-my-pi/pi-agent-core": "12.
|
|
90
|
-
"@oh-my-pi/pi-ai": "12.
|
|
91
|
-
"@oh-my-pi/pi-natives": "12.
|
|
92
|
-
"@oh-my-pi/pi-tui": "12.
|
|
93
|
-
"@oh-my-pi/pi-utils": "12.
|
|
88
|
+
"@oh-my-pi/omp-stats": "12.15.0",
|
|
89
|
+
"@oh-my-pi/pi-agent-core": "12.15.0",
|
|
90
|
+
"@oh-my-pi/pi-ai": "12.15.0",
|
|
91
|
+
"@oh-my-pi/pi-natives": "12.15.0",
|
|
92
|
+
"@oh-my-pi/pi-tui": "12.15.0",
|
|
93
|
+
"@oh-my-pi/pi-utils": "12.15.0",
|
|
94
94
|
"@sinclair/typebox": "^0.34.48",
|
|
95
95
|
"@xterm/headless": "^6.0.0",
|
|
96
96
|
"ajv": "^8.18.0",
|
|
@@ -6,7 +6,6 @@ import * as path from "node:path";
|
|
|
6
6
|
const docsDir = new URL("../../../docs/", import.meta.url).pathname;
|
|
7
7
|
const outputPath = new URL("../src/internal-urls/docs-index.generated.ts", import.meta.url).pathname;
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
const glob = new Glob("**/*.md");
|
|
11
10
|
const entries: string[] = [];
|
|
12
11
|
for await (const relativePath of glob.scan(docsDir)) {
|
|
@@ -14,17 +13,28 @@ for await (const relativePath of glob.scan(docsDir)) {
|
|
|
14
13
|
}
|
|
15
14
|
entries.sort();
|
|
16
15
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return `\t"${relativePath}": ${JSON.stringify(content)},`;
|
|
23
|
-
}),
|
|
16
|
+
const docsWithContent = await Promise.all(
|
|
17
|
+
entries.map(async (relativePath) => ({
|
|
18
|
+
relativePath,
|
|
19
|
+
content: await Bun.file(path.join(docsDir, relativePath)).text(),
|
|
20
|
+
}))
|
|
24
21
|
);
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const filenamesLiteral = JSON.stringify(entries);
|
|
24
|
+
|
|
25
|
+
const mapEntries = docsWithContent
|
|
26
|
+
.map(({ relativePath, content }) => `\t${JSON.stringify(relativePath)}: ${JSON.stringify(content)},`)
|
|
27
|
+
.join("\n");
|
|
28
|
+
const output = [
|
|
29
|
+
"// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT",
|
|
30
|
+
"",
|
|
31
|
+
`export const EMBEDDED_DOC_FILENAMES: readonly string[] = ${filenamesLiteral};`,
|
|
32
|
+
"",
|
|
33
|
+
`export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {`,
|
|
34
|
+
`${mapEntries}`,
|
|
35
|
+
`};`,
|
|
36
|
+
"",
|
|
37
|
+
].join("\n");
|
|
28
38
|
|
|
29
39
|
await Bun.write(outputPath, output);
|
|
30
|
-
console.log(`Generated ${path.relative(process.cwd(), outputPath)} (${
|
|
40
|
+
console.log(`Generated ${path.relative(process.cwd(), outputPath)} (${entries.length} docs)`);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT
|
|
2
2
|
|
|
3
|
+
export const EMBEDDED_DOC_FILENAMES: readonly string[] = ["bash-tool-runtime.md","blob-artifact-architecture.md","compaction.md","config-usage.md","custom-tools.md","environment-variables.md","extension-loading.md","extensions.md","fs-scan-cache-architecture.md","gemini-manifest-extensions.md","handoff-generation-pipeline.md","hooks.md","mcp-protocol-transports.md","mcp-runtime-lifecycle.md","mcp-server-tool-authoring.md","models.md","natives-addon-loader-runtime.md","natives-architecture.md","natives-binding-contract.md","natives-build-release-debugging.md","natives-media-system-utils.md","natives-rust-task-cancellation.md","natives-shell-pty-process.md","natives-text-search-pipeline.md","non-compaction-retry-policy.md","notebook-tool-runtime.md","plugin-manager-installer-plumbing.md","porting-from-pi-mono.md","porting-to-natives.md","provider-streaming-internals.md","python-repl.md","rpc.md","rulebook-matching-pipeline.md","sdk.md","secrets.md","session-operations-export-share-fork-resume.md","session-switching-and-recent-listing.md","session-tree-plan.md","session.md","skills.md","slash-command-internals.md","task-agent-discovery.md","theme.md","tree.md","ttsr-injection-lifecycle.md","tui-runtime-internals.md","tui.md"];
|
|
4
|
+
|
|
3
5
|
export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
4
6
|
"bash-tool-runtime.md": "# Bash tool runtime\n\nThis document describes the **`bash` tool** runtime path used by agent tool calls, from command normalization to execution, truncation/artifacts, and rendering.\n\nIt also calls out where behavior diverges in interactive TUI, print mode, RPC mode, and user-initiated bang (`!`) shell execution.\n\n## Scope and runtime surfaces\n\nThere are two different bash execution surfaces in coding-agent:\n\n1. **Tool-call surface** (`toolName: \"bash\"`): used when the model calls the bash tool.\n - Entry point: `BashTool.execute()`.\n2. **User bang-command surface** (`!cmd` from interactive input or RPC `bash` command): session-level helper path.\n - Entry point: `AgentSession.executeBash()`.\n\nBoth eventually use `executeBash()` in `src/exec/bash-executor.ts` for non-PTY execution, but only the tool-call path runs normalization/interception and tool renderer logic.\n\n## End-to-end tool-call pipeline\n\n## 1) Input normalization and parameter merge\n\n`BashTool.execute()` first normalizes the raw command via `normalizeBashCommand()`:\n\n- extracts trailing `| head -n N`, `| head -N`, `| tail -n N`, `| tail -N` into structured limits,\n- trims trailing/leading whitespace,\n- keeps internal whitespace intact.\n\nThen it merges extracted limits with explicit tool args:\n\n- explicit `head`/`tail` args override extracted values,\n- extracted values are fallback only.\n\n### Caveat\n\n`bash-normalize.ts` comments mention stripping `2>&1`, but current implementation does not remove it. Runtime behavior is still correct (stdout/stderr are already merged), but the normalization behavior is narrower than comments suggest.\n\n## 2) Optional interception (blocked-command path)\n\nIf `bashInterceptor.enabled` is true, `BashTool` loads rules from settings and runs `checkBashInterception()` against the normalized command.\n\nInterception behavior:\n\n- command is blocked **only** when:\n - regex rule matches, and\n - the suggested tool is present in `ctx.toolNames`.\n- invalid regex rules are silently skipped.\n- on block, `BashTool` throws `ToolError` with message:\n - `Blocked: ...`\n - original command included.\n\nDefault rule patterns (defined in code) target common misuses:\n\n- file readers (`cat`, `head`, `tail`, ...)\n- search tools (`grep`, `rg`, ...)\n- file finders (`find`, `fd`, ...)\n- in-place editors (`sed -i`, `perl -i`, `awk -i inplace`)\n- shell redirection writes (`echo ... > file`, heredoc redirection)\n\n### Caveat\n\n`InterceptionResult` includes `suggestedTool`, but `BashTool` currently surfaces only the message text (no structured suggested-tool field in `details`).\n\n## 3) CWD validation and timeout clamping\n\n`cwd` is resolved relative to session cwd (`resolveToCwd`), then validated via `stat`:\n\n- missing path -> `ToolError(\"Working directory does not exist: ...\")`\n- non-directory -> `ToolError(\"Working directory is not a directory: ...\")`\n\nTimeout is clamped to `[1, 3600]` seconds and converted to milliseconds.\n\n## 4) Artifact allocation + environment injection\n\nBefore execution, the tool allocates an artifact path/id (best-effort) and injects `$ARTIFACTS` env when session artifacts dir is available.\n\n- artifact allocation failure is non-fatal (execution continues without artifact spill file),\n- artifact id/path are passed into execution path for full-output persistence on truncation.\n\n## 5) PTY vs non-PTY execution selection\n\n`BashTool` chooses PTY execution only when all are true:\n\n- `bash.virtualTerminal === \"on\"`\n- `PI_NO_PTY !== \"1\"`\n- tool context has UI (`ctx.hasUI === true` and `ctx.ui` set)\n\nOtherwise it uses non-interactive `executeBash()`.\n\nThat means print mode and non-UI RPC/tool contexts always use non-PTY.\n\n## Non-interactive execution engine (`executeBash`)\n\n## Shell session reuse model\n\n`executeBash()` caches native `Shell` instances in a process-global map keyed by:\n\n- shell path,\n- configured command prefix,\n- snapshot path,\n- serialized shell env,\n- optional agent session key.\n\nFor session-level executions, `AgentSession.executeBash()` passes `sessionKey: this.sessionId`, isolating reuse per session.\n\nTool-call path does **not** pass `sessionKey`, so reuse scope is based on shell config/snapshot/env.\n\n## Shell config and snapshot behavior\n\nAt each call, executor loads settings shell config (`shell`, `env`, optional `prefix`).\n\nIf selected shell includes `bash`, it attempts `getOrCreateSnapshot()`:\n\n- snapshot captures aliases/functions/options from user rc,\n- snapshot creation is best-effort,\n- failure falls back to no snapshot.\n\nIf `prefix` is configured, command becomes:\n\n```text\n<prefix> <command>\n```\n\n## Streaming and cancellation\n\n`Shell.run()` streams chunks to callback. Executor pipes each chunk into `OutputSink` and optional `onChunk` callback.\n\nCancellation:\n\n- aborted signal triggers `shellSession.abort(...)`,\n- timeout from native result is mapped to `cancelled: true` + annotation text,\n- explicit cancellation similarly returns `cancelled: true` + annotation.\n\nNo exception is thrown inside executor for timeout/cancel; it returns structured `BashResult` and lets caller map error semantics.\n\n## Interactive PTY path (`runInteractiveBashPty`)\n\nWhen PTY is enabled, tool runs `runInteractiveBashPty()` which opens an overlay console component and drives a native `PtySession`.\n\nBehavior highlights:\n\n- xterm-headless virtual terminal renders viewport in overlay,\n- keyboard input is normalized (including Kitty sequences and application cursor mode handling),\n- `esc` while running kills the PTY session,\n- terminal resize propagates to PTY (`session.resize(cols, rows)`).\n\nEnvironment hardening defaults are injected for unattended runs:\n\n- pagers disabled (`PAGER=cat`, `GIT_PAGER=cat`, etc.),\n- editor prompts disabled (`GIT_EDITOR=true`, `EDITOR=true`, ...),\n- terminal/auth prompts reduced (`GIT_TERMINAL_PROMPT=0`, `SSH_ASKPASS=/usr/bin/false`, `CI=1`),\n- package-manager/tool automation flags for non-interactive behavior.\n\nPTY output is normalized (`CRLF`/`CR` to `LF`, `sanitizeText`) and written into `OutputSink`, including artifact spill support.\n\nOn PTY startup/runtime error, sink receives `PTY error: ...` line and command finalizes with undefined exit code.\n\n## Output handling: streaming, truncation, artifact spill\n\nBoth PTY and non-PTY paths use `OutputSink`.\n\n## OutputSink semantics\n\n- keeps an in-memory UTF-8-safe tail buffer (`DEFAULT_MAX_BYTES`, currently 50KB),\n- tracks total bytes/lines seen,\n- if artifact path exists and output overflows (or file already active), writes full stream to artifact file,\n- when memory threshold overflows, trims in-memory buffer to tail (UTF-8 boundary safe),\n- marks `truncated` when overflow/file spill occurs.\n\n`dump()` returns:\n\n- `output` (possibly annotated prefix),\n- `truncated`,\n- `totalLines/totalBytes`,\n- `outputLines/outputBytes`,\n- `artifactId` if artifact file was active.\n\n### Long-output caveat\n\nRuntime truncation is byte-threshold based in `OutputSink` (50KB default). It does not enforce a hard 2000-line cap in this code path.\n\n## Live tool updates\n\nFor non-PTY execution, `BashTool` uses a separate `TailBuffer` for partial updates and emits `onUpdate` snapshots while command is running.\n\nFor PTY execution, live rendering is handled by custom UI overlay, not by `onUpdate` text chunks.\n\n## Result shaping, metadata, and error mapping\n\nAfter execution:\n\n1. `cancelled` handling:\n - if abort signal is aborted -> throw `ToolAbortError` (abort semantics),\n - else -> throw `ToolError` (treated as tool failure).\n2. PTY `timedOut` -> throw `ToolError`.\n3. apply head/tail filters to final output text (`applyHeadTail`, head then tail).\n4. empty output becomes `(no output)`.\n5. attach truncation metadata via `toolResult(...).truncationFromSummary(result, { direction: \"tail\" })`.\n6. exit-code mapping:\n - missing exit code -> `ToolError(\"... missing exit status\")`\n - non-zero exit -> `ToolError(\"... Command exited with code N\")`\n - zero exit -> success result.\n\nSuccess payload structure:\n\n- `content`: text output,\n- `details.meta.truncation` when truncated, including:\n - `direction`, `truncatedBy`, total/output line+byte counts,\n - `shownRange`,\n - `artifactId` when available.\n\nBecause built-in tools are wrapped with `wrapToolWithMetaNotice()`, truncation notice text is appended to final text content automatically (for example: `Full: artifact://<id>`).\n\n## Rendering paths\n\n## Tool-call renderer (`bashToolRenderer`)\n\n`bashToolRenderer` is used for tool-call messages (`toolCall` / `toolResult`):\n\n- collapsed mode shows visual-line-truncated preview,\n- expanded mode shows all currently available output text,\n- warning line includes truncation reason and `artifact://<id>` when truncated,\n- timeout value (from args) is shown in footer metadata line.\n\n### Caveat: full artifact expansion\n\n`BashRenderContext` has `isFullOutput`, but current renderer context builder does not set it for bash tool results. Expanded view still uses the text already in result content (tail/truncated output) unless another caller provides full artifact content.\n\n## User bang-command component (`BashExecutionComponent`)\n\n`BashExecutionComponent` is for user `!` commands in interactive mode (not model tool calls):\n\n- streams chunks live,\n- collapsed preview keeps last 20 logical lines,\n- line clamp at 4000 chars per line,\n- shows truncation + artifact warnings when metadata is present,\n- marks cancelled/error/exit state separately.\n\nThis component is wired by `CommandController.handleBashCommand()` and fed from `AgentSession.executeBash()`.\n\n## Mode-specific behavior differences\n\n| Surface | Entry path | PTY eligible | Live output UX | Error surfacing |\n| ------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------ |\n| Interactive tool call | `BashTool.execute` | Yes, when `bash.virtualTerminal=on` and UI exists and `PI_NO_PTY!=1` | PTY overlay (interactive) or streamed tail updates | Tool errors become `toolResult.isError` |\n| Print mode tool call | `BashTool.execute` | No (no UI context) | No TUI overlay; output appears in event stream/final assistant text flow | Same tool error mapping |\n| RPC tool call (agent tooling) | `BashTool.execute` | Usually no UI -> non-PTY | Structured tool events/results | Same tool error mapping |\n| Interactive bang command (`!`) | `AgentSession.executeBash` + `BashExecutionComponent` | No (uses executor directly) | Dedicated bash execution component | Controller catches exceptions and shows UI error |\n| RPC `bash` command | `rpc-mode` -> `session.executeBash` | No | Returns `BashResult` directly | Consumer handles returned fields |\n\n## Operational caveats\n\n- Interceptor only blocks commands when suggested tool is currently available in context.\n- If artifact allocation fails, truncation still occurs but no `artifact://` back-reference is available.\n- Shell session cache has no explicit eviction in this module; lifetime is process-scoped.\n- PTY and non-PTY timeout surfaces differ:\n - PTY exposes explicit `timedOut` result field,\n - non-PTY maps timeout into `cancelled + annotation` summary.\n\n## Implementation files\n\n- [`src/tools/bash.ts`](../packages/coding-agent/src/tools/bash.ts) — tool entrypoint, normalization/interception, PTY/non-PTY selection, result/error mapping, bash tool renderer.\n- [`src/tools/bash-normalize.ts`](../packages/coding-agent/src/tools/bash-normalize.ts) — command normalization and post-run head/tail filtering.\n- [`src/tools/bash-interceptor.ts`](../packages/coding-agent/src/tools/bash-interceptor.ts) — interceptor rule matching and blocked-command messages.\n- [`src/exec/bash-executor.ts`](../packages/coding-agent/src/exec/bash-executor.ts) — non-PTY executor, shell session reuse, cancellation wiring, output sink integration.\n- [`src/tools/bash-interactive.ts`](../packages/coding-agent/src/tools/bash-interactive.ts) — PTY runtime, overlay UI, input normalization, non-interactive env defaults.\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) — `OutputSink` truncation/artifact spill and summary metadata.\n- [`src/tools/output-utils.ts`](../packages/coding-agent/src/tools/output-utils.ts) — artifact allocation helpers and streaming tail buffer.\n- [`src/tools/output-meta.ts`](../packages/coding-agent/src/tools/output-meta.ts) — truncation metadata shape + notice injection wrapper.\n- [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — session-level `executeBash`, message recording, abort lifecycle.\n- [`src/modes/components/bash-execution.ts`](../packages/coding-agent/src/modes/components/bash-execution.ts) — interactive `!` command execution component.\n- [`src/modes/controllers/command-controller.ts`](../packages/coding-agent/src/modes/controllers/command-controller.ts) — wiring for interactive `!` command UI stream/update completion.\n- [`src/modes/rpc/rpc-mode.ts`](../packages/coding-agent/src/modes/rpc/rpc-mode.ts) — RPC `bash` and `abort_bash` command surface.\n- [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://<id>` resolution.\n",
|
|
5
7
|
"blob-artifact-architecture.md": "# Blob and artifact storage architecture\n\nThis document describes how coding-agent stores large/binary payloads outside session JSONL, how truncated tool output is persisted, and how internal URLs (`artifact://`, `agent://`) resolve back to stored data.\n\n## Why two storage systems exist\n\nThe runtime uses two different persistence mechanisms for different data shapes:\n\n- **Content-addressed blobs** (`blob:sha256:<hash>`): global, binary-oriented storage used to externalize large image base64 payloads from persisted session entries.\n- **Session-scoped artifacts** (files under `<sessionFile-without-.jsonl>/`): per-session text files used for full tool outputs and subagent outputs.\n\nThey are intentionally separate:\n\n- blob storage optimizes deduplication and stable references by content hash,\n- artifact storage optimizes append-only session tooling and human/tool retrieval by local IDs.\n\n## Storage boundaries and on-disk layout\n\n## Blob store boundary (global)\n\n`SessionManager` constructs `BlobStore(getBlobsDir())`, so blob files live in a shared global blob directory (not in a session folder).\n\nBlob file naming:\n\n- file path: `<blobsDir>/<sha256-hex>`\n- no extension\n- reference string stored in entries: `blob:sha256:<sha256-hex>`\n\nImplications:\n\n- same binary content across sessions resolves to the same hash/path,\n- writes are idempotent at the content level,\n- blobs can outlive any individual session file.\n\n## Artifact boundary (session-local)\n\n`ArtifactManager` derives artifact directory from session file path:\n\n- session file: `.../<timestamp>_<sessionId>.jsonl`\n- artifacts directory: `.../<timestamp>_<sessionId>/` (strip `.jsonl`)\n\nArtifact types share this directory:\n\n- truncated tool output files: `<numericId>.<toolType>.log` (for `artifact://`)\n- subagent output files: `<outputId>.md` (for `agent://`)\n\n## ID and name allocation schemes\n\n## Blob IDs: content hash\n\n`BlobStore.put()` computes SHA-256 over raw binary bytes and returns:\n\n- `hash`: hex digest,\n- `path`: `<blobsDir>/<hash>`,\n- `ref`: `blob:sha256:<hash>`.\n\nNo session-local counter is used.\n\n## Artifact IDs: session-local monotonic integer\n\n`ArtifactManager` scans existing `*.log` artifact files on first use to find max existing numeric ID and sets `nextId = max + 1`.\n\nAllocation behavior:\n\n- file format: `{id}.{toolType}.log`\n- IDs are sequential strings (`\"0\"`, `\"1\"`, ...)\n- resume does not overwrite existing artifacts because scan happens before allocation.\n\nIf artifact directory is missing, scanning yields empty list and allocation starts from `0`.\n\n## Agent output IDs (`agent://`)\n\n`AgentOutputManager` allocates IDs for subagent outputs as `<index>-<requestedId>` (optionally nested under parent prefix, e.g. `0-Parent.1-Child`). It scans existing `.md` files on initialization to continue from the next index on resume.\n\n## Persistence dataflow\n\n## 1) Session entry persistence rewrite path\n\nBefore session entries are written (`#rewriteFile` / incremental persist), `SessionManager` calls `prepareEntryForPersistence()` (via `truncateForPersistence`).\n\nKey behaviors:\n\n1. **Large string truncation**: oversized strings are cut and suffixed with `\"[Session persistence truncated large content]\"`.\n2. **Transient field stripping**: `partialJson` and `jsonlEvents` are removed from persisted entries.\n3. **Image externalization to blobs**:\n - only applies to image blocks in `content` arrays,\n - only when `data` is not already a blob ref,\n - only when base64 length is at least threshold (`BLOB_EXTERNALIZE_THRESHOLD = 1024`),\n - replaces inline base64 with `blob:sha256:<hash>`.\n\nThis keeps session JSONL compact while preserving recoverability.\n\n## 2) Session load rehydration path\n\nWhen opening a session (`setSessionFile`), after migrations, `SessionManager` runs `resolveBlobRefsInEntries()`.\n\nFor each message/custom-message image block with `blob:sha256:<hash>`:\n\n- reads blob bytes from blob store,\n- converts bytes back to base64,\n- mutates in-memory entry to inline base64 for runtime consumers.\n\nIf blob is missing:\n\n- `resolveImageData()` logs warning,\n- returns original ref string unchanged,\n- load continues (no hard crash).\n\n## 3) Tool output spill/truncation path\n\n`OutputSink` powers streaming output in bash/python/ssh and related executors.\n\nBehavior:\n\n1. Every chunk is sanitized and appended to in-memory tail buffer.\n2. When in-memory bytes exceed spill threshold (`DEFAULT_MAX_BYTES`, 50KB), sink marks output truncated.\n3. If an artifact path is available, sink opens a file writer and writes:\n - existing buffered content once,\n - all subsequent chunks.\n4. In-memory buffer is always trimmed to tail window for display.\n5. `dump()` returns summary including `artifactId` only when file sink was successfully created.\n\nPractical effect:\n\n- UI/tool return shows truncated tail,\n- full output is preserved in artifact file and referenced as `artifact://<id>`.\n\nIf file sink creation fails (I/O error, missing path, etc.), sink silently falls back to in-memory truncation only; full output is not persisted.\n\n## URL access model\n\n## `blob:` references\n\n`blob:sha256:<hash>` is a persistence reference inside session entry payloads, not an internal URL scheme handled by the router. Resolution is done by `SessionManager` during session load.\n\n## `artifact://<id>`\n\nHandled by `ArtifactProtocolHandler`:\n\n- requires active session artifact directory,\n- ID must be numeric,\n- resolves by matching filename prefix `<id>.`,\n- returns raw text (`text/plain`) from the matched `.log` file,\n- when missing, error includes list of available artifact IDs.\n\nMissing directory behavior:\n\n- if artifacts directory does not exist, throws `No artifacts directory found`.\n\n## `agent://<id>`\n\nHandled by `AgentProtocolHandler` over `<artifactsDir>/<id>.md`:\n\n- plain form returns markdown text,\n- `/path` or `?q=` forms perform JSON extraction,\n- path and query extraction cannot be combined,\n- if extraction requested, file content must parse as JSON.\n\nMissing directory behavior:\n\n- throws `No artifacts directory found`.\n\nMissing output behavior:\n\n- throws `Not found: <id>` with available IDs from existing `.md` files.\n\nRead tool integration:\n\n- `read` supports offset/limit pagination for non-extraction internal URL reads,\n- rejects `offset/limit` when `agent://` extraction is used.\n\n## Resume, fork, and move semantics\n\n## Resume\n\n- `ArtifactManager` scans existing `{id}.*.log` files on first allocation and continues numbering.\n- `AgentOutputManager` scans existing `.md` output IDs and continues numbering.\n- `SessionManager` rehydrates blob refs to base64 on load.\n\n## Fork\n\n`SessionManager.fork()` creates a new session file with new session ID and `parentSession` link, then returns old/new file paths. Artifact copying is handled by `AgentSession.fork()`:\n\n- attempts recursive copy of old artifact directory to new artifact directory,\n- missing old directory is tolerated,\n- non-ENOENT copy errors are logged as warnings and fork still completes.\n\nID implications after fork:\n\n- if copy succeeded, artifact counters in new session continue after max copied ID,\n- if copy failed/skipped, new session artifact IDs start from `0`.\n\nBlob implications after fork:\n\n- blobs are global and content-addressed, so no blob directory copy is required.\n\n## Move to new cwd\n\n`SessionManager.moveTo()` renames both session file and artifact directory to the new default session directory, with rollback logic if a later step fails. This preserves artifact identity while relocating session scope.\n\n## Failure handling and fallback paths\n\n| Case | Behavior |\n| --- | --- |\n| Blob file missing during rehydration | Warn and keep `blob:sha256:` ref string in-memory |\n| Blob read ENOENT via `BlobStore.get` | Returns `null` |\n| Artifact directory missing (`ArtifactManager.listFiles`) | Returns empty list (allocation can start fresh) |\n| Artifact directory missing (`artifact://` / `agent://`) | Throws explicit `No artifacts directory found` |\n| Artifact ID not found | Throws with available IDs listing |\n| OutputSink artifact writer init fails | Continues with tail-only truncation (no full-output artifact) |\n| No session file (some task paths) | Task tool falls back to temp artifacts directory for subagent outputs |\n\n## Binary blob externalization vs text-output artifacts\n\n- **Blob externalization** is for binary image payloads inside persisted session entry content; it replaces inline base64 in JSONL with stable content refs.\n- **Artifacts** are plain text files for execution output and subagent output; they are addressable by session-local IDs through internal URLs.\n\nThe two systems intersect only indirectly (both reduce session JSONL bloat) but have different identity, lifetime, and retrieval paths.\n\n## Implementation files\n\n- [`src/session/blob-store.ts`](../packages/coding-agent/src/session/blob-store.ts) — blob reference format, hashing, put/get, externalize/resolve helpers.\n- [`src/session/artifacts.ts`](../packages/coding-agent/src/session/artifacts.ts) — session artifact directory model and numeric artifact ID allocation.\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) — `OutputSink` truncation/spill-to-file behavior and summary metadata.\n- [`src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts) — persistence transforms, blob rehydration on load, session fork/move interactions.\n- [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — artifact directory copy during interactive fork.\n- [`src/tools/output-utils.ts`](../packages/coding-agent/src/tools/output-utils.ts) — tool artifact manager bootstrap and per-tool artifact path allocation.\n- [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://` resolver.\n- [`src/internal-urls/agent-protocol.ts`](../packages/coding-agent/src/internal-urls/agent-protocol.ts) — `agent://` resolver + JSON extraction.\n- [`src/sdk.ts`](../packages/coding-agent/src/sdk.ts) — internal URL router wiring and artifacts-dir resolver.\n- [`src/task/output-manager.ts`](../packages/coding-agent/src/task/output-manager.ts) — session-scoped agent output ID allocation for `agent://`.\n- [`src/task/executor.ts`](../packages/coding-agent/src/task/executor.ts) — subagent output artifact writes (`<id>.md`) and temp artifact directory fallback.",
|
|
@@ -49,5 +51,3 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
49
51
|
"tui-runtime-internals.md": "# TUI runtime internals\n\nThis document maps the non-theme runtime path from terminal input to rendered output in interactive mode. It focuses on behavior in `packages/tui` and its integration from `packages/coding-agent` controllers.\n\n## Runtime layers and ownership\n\n- **`packages/tui` engine**: terminal lifecycle, stdin normalization, focus routing, render scheduling, differential painting, overlay composition, hardware cursor placement.\n- **`packages/coding-agent` interactive mode**: builds component tree, binds editor callbacks and keymaps, reacts to agent/session events, and translates domain state (streaming, tool execution, retries, plan mode) into UI components.\n\nBoundary rule: the TUI engine is message-agnostic. It only knows `Component.render(width)`, `handleInput(data)`, focus, and overlays. Agent semantics stay in interactive controllers.\n\n## Implementation files\n\n- [`../src/modes/interactive-mode.ts`](../packages/coding-agent/src/modes/interactive-mode.ts)\n- [`../src/modes/controllers/event-controller.ts`](../packages/coding-agent/src/modes/controllers/event-controller.ts)\n- [`../src/modes/controllers/input-controller.ts`](../packages/coding-agent/src/modes/controllers/input-controller.ts)\n- [`../src/modes/components/custom-editor.ts`](../packages/coding-agent/src/modes/components/custom-editor.ts)\n- [`../../tui/src/tui.ts`](../packages/tui/src/tui.ts)\n- [`../../tui/src/terminal.ts`](../packages/tui/src/terminal.ts)\n- [`../../tui/src/editor-component.ts`](../packages/tui/src/editor-component.ts)\n- [`../../tui/src/stdin-buffer.ts`](../packages/tui/src/stdin-buffer.ts)\n- [`../../tui/src/components/loader.ts`](../packages/tui/src/components/loader.ts)\n\n## Boot and component tree assembly\n\n`InteractiveMode` constructs `TUI(new ProcessTerminal(), showHardwareCursor)` and creates persistent containers:\n\n- `chatContainer`\n- `pendingMessagesContainer`\n- `statusContainer`\n- `todoContainer`\n- `statusLine`\n- `editorContainer` (holds `CustomEditor`)\n\n`init()` wires the tree in that order, focuses the editor, registers input handlers via `InputController`, starts TUI, and requests a forced render.\n\nA forced render (`requestRender(true)`) resets previous-line caches and cursor bookkeeping before repainting.\n\n## Terminal lifecycle and stdin normalization\n\n`ProcessTerminal.start()`:\n\n1. Enables raw mode and bracketed paste.\n2. Attaches resize handler.\n3. Creates a `StdinBuffer` to split partial escape chunks into complete sequences.\n4. Queries Kitty keyboard protocol support (`CSI ? u`), then enables protocol flags if supported.\n5. On Windows, attempts VT input enablement via `kernel32` mode flags.\n\n`StdinBuffer` behavior:\n\n- Buffers fragmented escape sequences (CSI/OSC/DCS/APC/SS3).\n- Emits `data` only when a sequence is complete or timeout-flushed.\n- Detects bracketed paste and emits a `paste` event with raw pasted text.\n\nThis prevents partial escape chunks from being misinterpreted as normal keypresses.\n\n## Input routing and focus model\n\nInput path:\n\n`stdin -> ProcessTerminal -> StdinBuffer -> TUI.#handleInput -> focusedComponent.handleInput`\n\nRouting details:\n\n1. TUI runs registered input listeners first (`addInputListener`), allowing consume/transform behavior.\n2. TUI handles global debug shortcut (`shift+ctrl+d`) before component dispatch.\n3. If focused component belongs to an overlay that is now hidden/invisible, TUI reassigns focus to next visible overlay or saved pre-overlay focus.\n4. Key release events are filtered unless focused component sets `wantsKeyRelease = true`.\n5. After dispatch, TUI schedules render.\n\n`setFocus()` also toggles `Focusable.focused`, which controls whether components emit `CURSOR_MARKER` for hardware cursor placement.\n\n## Key handling split: editor vs controller\n\n`CustomEditor` intercepts high-priority combos first (escape, ctrl-c/d/z, ctrl-v, ctrl-p variants, ctrl-t, alt-up, extension custom keys) and delegates the rest to base `Editor` behavior (text editing, history, autocomplete, cursor movement).\n\n`InputController.setupKeyHandlers()` then binds editor callbacks to mode actions:\n\n- cancellation / mode exits on `Escape`\n- shutdown on double `Ctrl+C` or empty-editor `Ctrl+D`\n- suspend/resume on `Ctrl+Z`\n- slash-command and selector hotkeys\n- follow-up/dequeue toggles and expansion toggles\n\nThis keeps key parsing/editor mechanics in `packages/tui` and mode semantics in coding-agent controllers.\n\n## Render loop and diffing strategy\n\n`TUI.requestRender()` is debounced to one render per tick using `process.nextTick`. Multiple state changes in the same turn coalesce.\n\n`#doRender()` pipeline:\n\n1. Render root component tree to `newLines`.\n2. Composite visible overlays (if any).\n3. Extract and strip `CURSOR_MARKER` from visible viewport lines.\n4. Append segment reset suffixes for non-image lines.\n5. Choose full repaint vs differential patch:\n - first frame\n - width change\n - shrink with `clearOnShrink` enabled and no overlays\n - edits above previous viewport\n6. For differential updates, patch only changed line range and clear stale trailing lines when needed.\n7. Reposition hardware cursor for IME support.\n\nRender writes use synchronized output mode (`CSI ? 2026 h/l`) to reduce flicker/tearing.\n\n## Render safety constraints\n\nCritical safety checks in `TUI`:\n\n- Non-image rendered lines must not exceed terminal width; overflow throws and writes crash diagnostics.\n- Overlay compositing includes defensive truncation and post-composite width verification.\n- Width changes force full redraw because wrapping semantics change.\n- Cursor position is clamped before movement.\n\nThese constraints are runtime enforcement, not just conventions.\n\n## Resize handling\n\nResize events are event-driven from `ProcessTerminal` to `TUI.requestRender()`.\n\nEffects:\n\n- Any width change triggers full redraw.\n- Viewport/top tracking (`#previousViewportTop`, `#maxLinesRendered`) avoids invalid relative cursor math when content or terminal size changes.\n- Overlay visibility can depend on terminal dimensions (`OverlayOptions.visible`); focus is corrected when overlays become non-visible after resize.\n\n## Streaming and incremental UI updates\n\n`EventController` subscribes to `AgentSessionEvent` and updates UI incrementally:\n\n- `agent_start`: starts loader in `statusContainer`.\n- `message_start` assistant: creates `streamingComponent` and mounts it.\n- `message_update`: updates streaming assistant content; creates/updates tool execution components as tool calls appear.\n- `tool_execution_update/end`: updates tool result components and completion state.\n- `message_end`: finalizes assistant stream, handles aborted/error annotations, marks pending tool args complete on normal stop.\n- `agent_end`: stops loaders, clears transient stream state, flushes deferred model switch, issues completion notification if backgrounded.\n\nRead-tool grouping is intentionally stateful (`#lastReadGroup`) to coalesce consecutive read tool calls into one visual block until a non-read break occurs.\n\n## Status and loader orchestration\n\nStatus lane ownership:\n\n- `statusContainer` holds transient loaders (`loadingAnimation`, `autoCompactionLoader`, `retryLoader`).\n- `statusLine` renders persistent status/hooks/plan indicators and drives editor top border updates.\n\nLoader behavior:\n\n- `Loader` updates every 80ms via interval and requests render each frame.\n- Escape handlers are temporarily overridden during auto-compaction and auto-retry to cancel those operations.\n- On end/cancel paths, controllers restore prior escape handlers and stop/clear loader components.\n\n## Mode transitions and backgrounding\n\n### Bash/Python input modes\n\nInput text prefixes toggle editor border mode flags:\n\n- `!` -> bash mode\n- `$` (non-template literal prefix) -> python mode\n\nEscape exits inactive mode by clearing editor text and restoring border color; when execution is active, escape aborts the running task instead.\n\n### Plan mode\n\n`InteractiveMode` tracks plan mode flags, status-line state, active tools, and model switching. Enter/exit updates session mode entries and status/UI state, including deferred model switch if streaming is active.\n\n### Suspend/resume (`Ctrl+Z`)\n\n`InputController.handleCtrlZ()`:\n\n1. Registers one-shot `SIGCONT` handler to restart TUI and force render.\n2. Stops TUI before suspend.\n3. Sends `SIGTSTP` to process group.\n\n### Background mode (`/background` or `/bg`)\n\n`handleBackgroundCommand()`:\n\n- Rejects when idle.\n- Switches tool UI context to non-interactive (`hasUI=false`) so interactive UI tools fail fast.\n- Stops loaders/status line and unsubscribes foreground event handler.\n- Subscribes background event handler (primarily waits for `agent_end`).\n- Stops TUI and sends `SIGTSTP` (POSIX job control path).\n\nOn `agent_end` in background with no queued work, controller sends completion notification and shuts down.\n\n## Cancellation paths\n\nPrimary cancellation inputs:\n\n- `Escape` during active stream loader: restores queued messages to editor and aborts agent.\n- `Escape` during bash/python execution: aborts running command.\n- `Escape` during auto-compaction/retry: invokes dedicated abort methods through temporary escape handlers.\n- `Ctrl+C` single press: clear editor; double press within 500ms: shutdown.\n\nCancellation is state-conditional; same key can mean abort, mode-exit, selector trigger, or no-op depending on runtime state.\n\n## Event-driven vs throttled behavior\n\nEvent-driven updates:\n\n- Agent session events (`EventController`)\n- Key input callbacks (`InputController`)\n- terminal resize callback\n- theme/branch watchers in `InteractiveMode`\n\nThrottled/debounced paths:\n\n- TUI rendering is tick-debounced (`requestRender` coalescing).\n- Loader animation is fixed-interval (80ms), each frame requesting render.\n- Editor autocomplete updates (inside `Editor`) use debounce timers, reducing recompute churn during typing.\n\nThe runtime therefore mixes event-driven state transitions with bounded render cadence to keep interactivity responsive without repaint storms.\n",
|
|
50
52
|
"tui.md": "# TUI integration for extensions and custom tools\n\nThis document covers the **current** TUI contract used by `packages/coding-agent` and `packages/tui` for extension UI, custom tool UI, and custom renderers.\n\n## What this subsystem is\n\nThe runtime has two layers:\n\n- **Rendering engine (`packages/tui`)**: differential terminal renderer, input dispatch, focus, overlays, cursor placement.\n- **Integration layer (`packages/coding-agent`)**: mounts extension/custom-tool components, wires keybindings/theme, and restores editor state.\n\n## Runtime behavior by mode\n\n| Mode | `ctx.ui.custom(...)` availability | Notes |\n| --- | --- | --- |\n| Interactive TUI | Supported | Component is mounted in the editor area, focused, and must call `done(result)` to resolve. |\n| Background/headless | Not interactive | UI context is no-op (`hasUI === false`). |\n| RPC mode | Not supported | `custom()` returns `Promise<never>` and does not mount TUI components. |\n\nIf your extension/tool can run in non-interactive mode, guard with `ctx.hasUI` / `pi.hasUI`.\n\n## Core component contract (`@oh-my-pi/pi-tui`)\n\n`packages/tui/src/tui.ts` defines:\n\n```ts\nexport interface Component {\n render(width: number): string[];\n handleInput?(data: string): void;\n wantsKeyRelease?: boolean;\n invalidate(): void;\n}\n```\n\n`Focusable` is separate:\n\n```ts\nexport interface Focusable {\n focused: boolean;\n}\n```\n\nCursor behavior uses `CURSOR_MARKER` (not `getCursorPosition`). Focused components emit the marker in rendered text; `TUI` extracts it and positions the hardware cursor.\n\n## Rendering constraints (terminal safety)\n\nYour `render(width)` output must be terminal-safe:\n\n1. **Never exceed `width` on any line**. The renderer throws if a non-image line overflows.\n2. **Measure visual width**, not string length: use `visibleWidth()`.\n3. **Truncate/wrap ANSI-aware text** with `truncateToWidth()` / `wrapTextWithAnsi()`.\n4. **Sanitize tabs/content** from external sources using `replaceTabs()` (and higher-level sanitizers in coding-agent render paths).\n\nMinimal pattern:\n\n```ts\nimport { replaceTabs, truncateToWidth } from \"@oh-my-pi/pi-tui\";\n\nrender(width: number): string[] {\n return this.lines.map(line => truncateToWidth(replaceTabs(line), width));\n}\n```\n\n## Input handling and keybindings\n\n### Raw key matching\n\nUse `matchesKey(data, \"...\")` for navigation keys and combos.\n\n### Respect user-configured app keybindings\n\nExtension UI factories receive a `KeybindingsManager` (interactive mode) so you can honor mapped actions instead of hardcoding keys:\n\n```ts\nif (keybindings.matches(data, \"interrupt\")) {\n done(undefined);\n return;\n}\n```\n\n### Key release/repeat events\n\nKey release events are filtered unless your component sets:\n\n```ts\nwantsKeyRelease = true;\n```\n\nThen use `isKeyRelease()` / `isKeyRepeat()` if needed.\n\n## Focus, overlays, and cursor\n\n- `TUI.setFocus(component)` routes input to that component.\n- Overlay APIs exist in `TUI` (`showOverlay`, `OverlayHandle`), but extension `ctx.ui.custom` mounting in interactive mode currently replaces the editor component area directly.\n- The `custom(..., options?: { overlay?: boolean })` option exists in extension types; interactive extension mounting currently ignores this option.\n\n## Mount points and return contracts\n\n## 1) Extension UI (`ExtensionUIContext`)\n\nCurrent signature (`extensibility/extensions/types.ts`):\n\n```ts\ncustom<T>(\n factory: (\n tui: TUI,\n theme: Theme,\n keybindings: KeybindingsManager,\n done: (result: T) => void,\n ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,\n options?: { overlay?: boolean },\n): Promise<T>\n```\n\nBehavior in interactive mode (`extension-ui-controller.ts`):\n\n- Saves editor text.\n- Replaces editor component with your component.\n- Focuses your component.\n- On `done(result)`: calls `component.dispose?.()`, restores editor + text, focuses editor, resolves promise.\n\nSo `done(...)` is mandatory for completion.\n\n## 2) Hook/custom-tool UI context (legacy typing)\n\n`HookUIContext.custom` is typed as `(tui, theme, done)` in hook/custom-tool types.\nUnderlying interactive implementation calls factories with `(tui, theme, keybindings, done)`. JS consumers can use the extra arg; type-level compatibility still reflects the 3-arg legacy signature.\n\nCustom tools typically use the same UI entrypoint via the factory-scoped `pi.ui` object, then return the selected value in normal tool content:\n\n```ts\nasync execute(toolCallId, params, onUpdate, ctx, signal) {\n if (!pi.hasUI) {\n return { content: [{ type: \"text\", text: \"UI unavailable\" }] };\n }\n\n const picked = await pi.ui.custom<string | undefined>((tui, theme, done) => {\n const component = new MyPickerComponent(done, signal);\n return component;\n });\n\n return { content: [{ type: \"text\", text: picked ? `Picked: ${picked}` : \"Cancelled\" }] };\n}\n```\n\n\n## 3) Custom tool call/result renderers\n\nCustom tools and extension tools can return components from:\n\n- `renderCall(args, theme)`\n- `renderResult(result, options, theme, args?)`\n\n`options` currently includes:\n\n- `expanded: boolean`\n- `isPartial: boolean`\n- `spinnerFrame?: number`\n\nThese renderers are mounted by `ToolExecutionComponent`.\n\n## Lifecycle and cancellation\n\n- `dispose()` is optional at type level but should be implemented when you own timers, subprocesses, watchers, sockets, or overlays.\n- `done(...)` should be called exactly once from your component flow.\n- For cancellable long-running UI, pair `CancellableLoader` with `AbortSignal` and call `done(...)` from `onAbort`.\n\nExample cancellation pattern:\n\n```ts\nconst loader = new CancellableLoader(tui, theme.fg(\"accent\"), theme.fg(\"muted\"), \"Working...\");\nloader.onAbort = () => done(undefined);\nvoid doWork(loader.signal).then(result => done(result));\nreturn loader;\n```\n\n## Realistic custom component example (extension command)\n\n```ts\nimport type { Component } from \"@oh-my-pi/pi-tui\";\nimport { SelectList, matchesKey, replaceTabs, truncateToWidth } from \"@oh-my-pi/pi-tui\";\nimport { getSelectListTheme, type ExtensionAPI } from \"@oh-my-pi/pi-coding-agent\";\n\nclass Picker implements Component {\n list: SelectList;\n keybindings: any;\n done: (value: string | undefined) => void;\n\n constructor(\n items: Array<{ value: string; label: string }>,\n keybindings: any,\n done: (value: string | undefined) => void,\n ) {\n this.list = new SelectList(items, 8, getSelectListTheme());\n this.keybindings = keybindings;\n this.done = done;\n this.list.onSelect = item => this.done(item.value);\n this.list.onCancel = () => this.done(undefined);\n }\n\n handleInput(data: string): void {\n if (this.keybindings.matches(data, \"interrupt\")) {\n this.done(undefined);\n return;\n }\n this.list.handleInput(data);\n }\n\n render(width: number): string[] {\n return this.list.render(width).map(line => truncateToWidth(replaceTabs(line), width));\n }\n\n invalidate(): void {\n this.list.invalidate();\n }\n}\n\nexport default function extension(pi: ExtensionAPI): void {\n pi.registerCommand(\"pick-model\", {\n description: \"Pick a model profile\",\n handler: async (_args, ctx) => {\n if (!ctx.hasUI) return;\n\n const selected = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {\n const items = [\n { value: \"fast\", label: theme.fg(\"accent\", \"Fast\") },\n { value: \"balanced\", label: \"Balanced\" },\n { value: \"quality\", label: \"Quality\" },\n ];\n return new Picker(items, keybindings, done);\n });\n\n if (selected) ctx.ui.notify(`Selected profile: ${selected}`, \"info\");\n },\n });\n}\n```\n\n## Key implementation files\n\n- `packages/tui/src/tui.ts` — `Component`, `Focusable`, cursor marker, focus, overlay, input dispatch.\n- `packages/tui/src/utils.ts` — width/truncation/sanitization primitives.\n- `packages/tui/src/keys.ts` / `keybindings.ts` — key parsing and configurable action mapping.\n- `packages/coding-agent/src/modes/controllers/extension-ui-controller.ts` — interactive mounting/unmounting for extension/hook/custom-tool UI.\n- `packages/coding-agent/src/extensibility/extensions/types.ts` — extension UI and renderer contracts.\n- `packages/coding-agent/src/extensibility/hooks/types.ts` — hook UI contract (legacy custom signature).\n- `packages/coding-agent/src/extensibility/custom-tools/types.ts` — custom tool execute/render contracts.\n- `packages/coding-agent/src/modes/components/tool-execution.ts` — mounting `renderCall`/`renderResult` components and partial-state options.\n- `packages/coding-agent/src/tools/context.ts` — tool UI context propagation (`hasUI`, `ui`).\n",
|
|
51
53
|
};
|
|
52
|
-
|
|
53
|
-
export const EMBEDDED_DOC_FILENAMES = Object.keys(EMBEDDED_DOCS).sort();
|
|
@@ -123,27 +123,54 @@ export class RpcClient {
|
|
|
123
123
|
stdin: "pipe",
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
//
|
|
126
|
+
// Wait for the "ready" signal or process exit
|
|
127
|
+
const { promise: readyPromise, resolve: readyResolve, reject: readyReject } = Promise.withResolvers<void>();
|
|
128
|
+
let readySettled = false;
|
|
129
|
+
|
|
130
|
+
// Process lines in background, intercepting the ready signal
|
|
127
131
|
const lines = readJsonl(this.#process.stdout, this.#abortController.signal);
|
|
128
132
|
void (async () => {
|
|
129
133
|
for await (const line of lines) {
|
|
134
|
+
if (!readySettled && isRecord(line) && line.type === "ready") {
|
|
135
|
+
readySettled = true;
|
|
136
|
+
readyResolve();
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
130
139
|
this.#handleLine(line);
|
|
131
140
|
}
|
|
132
|
-
|
|
141
|
+
// Stream ended without ready signal — process exited
|
|
142
|
+
if (!readySettled) {
|
|
143
|
+
readySettled = true;
|
|
144
|
+
readyReject(new Error(`Agent process exited before ready. Stderr: ${this.#process?.peekStderr() ?? ""}`));
|
|
145
|
+
}
|
|
146
|
+
})().catch((err: Error) => {
|
|
147
|
+
if (!readySettled) {
|
|
148
|
+
readySettled = true;
|
|
149
|
+
readyReject(err);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
133
152
|
|
|
134
|
-
//
|
|
135
|
-
|
|
153
|
+
// Also race against process exit (in case stdout closes before we read it)
|
|
154
|
+
void this.#process.exited.then((exitCode: number) => {
|
|
155
|
+
if (!readySettled) {
|
|
156
|
+
readySettled = true;
|
|
157
|
+
readyReject(
|
|
158
|
+
new Error(`Agent process exited with code ${exitCode}. Stderr: ${this.#process?.peekStderr() ?? ""}`),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
136
162
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
// Timeout to prevent hanging forever
|
|
164
|
+
void Bun.sleep(30000).then(() => {
|
|
165
|
+
if (!readySettled) {
|
|
166
|
+
readySettled = true;
|
|
167
|
+
readyReject(
|
|
168
|
+
new Error(`Timeout waiting for agent to become ready. Stderr: ${this.#process?.peekStderr() ?? ""}`),
|
|
142
169
|
);
|
|
143
170
|
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await readyPromise;
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
/**
|
|
@@ -36,6 +36,8 @@ export type {
|
|
|
36
36
|
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
|
37
37
|
*/
|
|
38
38
|
export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
39
|
+
// Signal to RPC clients that the server is ready to accept commands
|
|
40
|
+
process.stdout.write(`${JSON.stringify({ type: "ready" })}\n`);
|
|
39
41
|
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
|
|
40
42
|
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
41
43
|
};
|
package/src/patch/hashline.ts
CHANGED
|
@@ -663,10 +663,8 @@ export function applyHashlineEdits(
|
|
|
663
663
|
if (edit.content.length === 0) {
|
|
664
664
|
throw new Error('Insert-between edit (src "A#HH.. B#HH..") requires non-empty dst');
|
|
665
665
|
}
|
|
666
|
-
if (edit.before.line
|
|
667
|
-
throw new Error(
|
|
668
|
-
`insert requires adjacent anchors (after ${edit.after.line}, before ${edit.before.line})`,
|
|
669
|
-
);
|
|
666
|
+
if (edit.before.line <= edit.after.line) {
|
|
667
|
+
throw new Error(`insert requires after (${edit.after.line}) < before (${edit.before.line})`);
|
|
670
668
|
}
|
|
671
669
|
const afterValid = validateRef(edit.after);
|
|
672
670
|
const beforeValid = validateRef(edit.before);
|
|
@@ -6,21 +6,28 @@ Use this tool to navigate, click, type, scroll, drag, query DOM content, and cap
|
|
|
6
6
|
- Use `action: "open"` to start a new headless browser session (or implicitly launch on first action)
|
|
7
7
|
- Use `action: "goto"` with `url` to navigate
|
|
8
8
|
- Use `action: "observe"` to capture a numbered accessibility snapshot with URL/title/viewport/scroll info
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
- Prefer `click_id`, `type_id`, or `fill_id` actions using the returned `element_id` values
|
|
10
|
+
- Optional flags: `include_all` to include non-interactive nodes, `viewport_only` to limit to visible elements
|
|
11
11
|
- Use `action: "click"`, `"type"`, `"fill"`, `"press"`, `"scroll"`, or `"drag"` for selector-based interactions
|
|
12
|
-
|
|
12
|
+
- Prefer ARIA or text selectors (e.g. `p-aria/[name="Sign in"]`, `p-text/Continue`) over brittle CSS
|
|
13
13
|
- Use `action: "click_id"`, `"type_id"`, or `"fill_id"` to interact with observed elements without selectors
|
|
14
14
|
- Use `action: "wait_for_selector"` before interacting when the page is dynamic
|
|
15
15
|
- Use `action: "evaluate"` with `script` to run a JavaScript expression in the page context
|
|
16
16
|
- Use `action: "get_text"`, `"get_html"`, or `"get_attribute"` for DOM queries
|
|
17
|
-
|
|
17
|
+
- For batch queries, pass `args: [{ selector, attribute? }]` to get an array of results (attribute required for `get_attribute`)
|
|
18
18
|
- Use `action: "extract_readable"` to return reader-mode content (title/byline/excerpt/text or markdown)
|
|
19
|
-
|
|
19
|
+
- Set `format` to `"markdown"` (default) or `"text"`
|
|
20
20
|
- Use `action: "screenshot"` to capture images (optionally with `selector` to capture a single element)
|
|
21
21
|
- Use `action: "close"` to release the browser when done
|
|
22
22
|
</instruction>
|
|
23
23
|
|
|
24
|
+
<critical>
|
|
25
|
+
**Default to `observe`, not `screenshot`.**
|
|
26
|
+
- `observe` is cheaper, faster, and returns structured data — use it to understand page state, find elements, and plan interactions.
|
|
27
|
+
- Only use `screenshot` when visual appearance matters (verifying layout, debugging CSS, capturing a visual artifact for the user).
|
|
28
|
+
- Never screenshot just to "see what's on the page" — `observe` gives you that with element IDs you can act on immediately.
|
|
29
|
+
</critical>
|
|
30
|
+
|
|
24
31
|
<output>
|
|
25
32
|
Returns text output for navigation and DOM queries, and image output for screenshots. Screenshots can optionally be saved to disk via the `path` parameter.
|
|
26
33
|
</output>
|
|
@@ -37,7 +37,7 @@ export interface StoredAuthCredential {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** Bump when schema changes require migration */
|
|
40
|
-
const SCHEMA_VERSION =
|
|
40
|
+
const SCHEMA_VERSION = 4;
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Type guard for plain objects.
|
|
@@ -124,11 +124,14 @@ export class AgentStorage {
|
|
|
124
124
|
#deleteExpiredCacheStmt: Statement;
|
|
125
125
|
#listAuthStmt: Statement;
|
|
126
126
|
#listAuthByProviderStmt: Statement;
|
|
127
|
+
#listActiveAuthStmt: Statement;
|
|
128
|
+
#listActiveAuthByProviderStmt: Statement;
|
|
127
129
|
#insertAuthStmt: Statement;
|
|
128
130
|
#updateAuthStmt: Statement;
|
|
129
131
|
#deleteAuthStmt: Statement;
|
|
130
132
|
#deleteAuthByProviderStmt: Statement;
|
|
131
133
|
#countAuthStmt: Statement;
|
|
134
|
+
#disableAuthStmt: Statement;
|
|
132
135
|
#upsertModelUsageStmt: Statement;
|
|
133
136
|
#listModelUsageStmt: Statement;
|
|
134
137
|
#modelUsageCache: string[] | null = null;
|
|
@@ -165,6 +168,12 @@ export class AgentStorage {
|
|
|
165
168
|
this.#listAuthByProviderStmt = this.#db.prepare(
|
|
166
169
|
"SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? ORDER BY id ASC",
|
|
167
170
|
);
|
|
171
|
+
this.#listActiveAuthStmt = this.#db.prepare(
|
|
172
|
+
"SELECT id, provider, credential_type, data FROM auth_credentials WHERE disabled = 0 ORDER BY id ASC",
|
|
173
|
+
);
|
|
174
|
+
this.#listActiveAuthByProviderStmt = this.#db.prepare(
|
|
175
|
+
"SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? AND disabled = 0 ORDER BY id ASC",
|
|
176
|
+
);
|
|
168
177
|
this.#insertAuthStmt = this.#db.prepare(
|
|
169
178
|
"INSERT INTO auth_credentials (provider, credential_type, data) VALUES (?, ?, ?) RETURNING id",
|
|
170
179
|
);
|
|
@@ -173,6 +182,9 @@ export class AgentStorage {
|
|
|
173
182
|
);
|
|
174
183
|
this.#deleteAuthStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE id = ?");
|
|
175
184
|
this.#deleteAuthByProviderStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE provider = ?");
|
|
185
|
+
this.#disableAuthStmt = this.#db.prepare(
|
|
186
|
+
"UPDATE auth_credentials SET disabled = 1, updated_at = unixepoch() WHERE id = ?",
|
|
187
|
+
);
|
|
176
188
|
this.#countAuthStmt = this.#db.prepare("SELECT COUNT(*) as count FROM auth_credentials");
|
|
177
189
|
|
|
178
190
|
this.#upsertModelUsageStmt = this.#db.prepare(
|
|
@@ -198,6 +210,7 @@ CREATE TABLE IF NOT EXISTS auth_credentials (
|
|
|
198
210
|
provider TEXT NOT NULL,
|
|
199
211
|
credential_type TEXT NOT NULL,
|
|
200
212
|
data TEXT NOT NULL,
|
|
213
|
+
disabled INTEGER NOT NULL DEFAULT 0,
|
|
201
214
|
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
202
215
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
203
216
|
);
|
|
@@ -281,10 +294,21 @@ CREATE TABLE settings (
|
|
|
281
294
|
current: versionRow.version,
|
|
282
295
|
expected: SCHEMA_VERSION,
|
|
283
296
|
});
|
|
297
|
+
this.#migrateSchema(versionRow.version);
|
|
284
298
|
}
|
|
285
299
|
this.#db.prepare("INSERT OR REPLACE INTO schema_version(version) VALUES (?)").run(SCHEMA_VERSION);
|
|
286
300
|
}
|
|
287
301
|
|
|
302
|
+
#migrateSchema(fromVersion: number): void {
|
|
303
|
+
if (fromVersion < 4) {
|
|
304
|
+
// v3 → v4: Add disabled column to auth_credentials
|
|
305
|
+
const cols = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
|
|
306
|
+
if (!cols.some(c => c.name === "disabled")) {
|
|
307
|
+
this.#db.exec("ALTER TABLE auth_credentials ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
288
312
|
/**
|
|
289
313
|
* Returns singleton instance for the given database path, creating if needed.
|
|
290
314
|
* Retries on SQLITE_BUSY with exponential backoff.
|
|
@@ -429,14 +453,19 @@ CREATE TABLE settings (
|
|
|
429
453
|
|
|
430
454
|
/**
|
|
431
455
|
* Lists auth credentials, optionally filtered by provider.
|
|
456
|
+
* Only returns active (non-disabled) credentials by default.
|
|
432
457
|
* @param provider - Optional provider name to filter by
|
|
458
|
+
* @param includeDisabled - If true, includes disabled credentials
|
|
433
459
|
* @returns Array of stored credentials with their database IDs
|
|
434
460
|
*/
|
|
435
|
-
listAuthCredentials(provider?: string): StoredAuthCredential[] {
|
|
436
|
-
const rows =
|
|
437
|
-
(provider
|
|
438
|
-
|
|
439
|
-
|
|
461
|
+
listAuthCredentials(provider?: string, includeDisabled = false): StoredAuthCredential[] {
|
|
462
|
+
const rows = includeDisabled
|
|
463
|
+
? ((provider
|
|
464
|
+
? (this.#listAuthByProviderStmt.all(provider) as AuthRow[])
|
|
465
|
+
: (this.#listAuthStmt.all() as AuthRow[])) ?? [])
|
|
466
|
+
: ((provider
|
|
467
|
+
? (this.#listActiveAuthByProviderStmt.all(provider) as AuthRow[])
|
|
468
|
+
: (this.#listActiveAuthStmt.all() as AuthRow[])) ?? []);
|
|
440
469
|
|
|
441
470
|
const results: StoredAuthCredential[] = [];
|
|
442
471
|
for (const row of rows) {
|
|
@@ -498,6 +527,19 @@ CREATE TABLE settings (
|
|
|
498
527
|
}
|
|
499
528
|
}
|
|
500
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Disables an auth credential by ID (soft-delete).
|
|
532
|
+
* Disabled credentials are excluded from normal listing but remain in the database.
|
|
533
|
+
* @param id - Database row ID of the credential to disable
|
|
534
|
+
*/
|
|
535
|
+
disableAuthCredential(id: number): void {
|
|
536
|
+
try {
|
|
537
|
+
this.#disableAuthStmt.run(id);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
logger.warn("AgentStorage disableAuthCredential failed", { id, error: String(error) });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
501
543
|
/**
|
|
502
544
|
* Deletes all auth credentials for a provider.
|
|
503
545
|
* @param provider - Provider name whose credentials should be deleted
|
|
@@ -547,13 +547,14 @@ export class AuthStorage {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
/**
|
|
550
|
-
*
|
|
551
|
-
*
|
|
550
|
+
* Disables credential at index (used when OAuth refresh fails).
|
|
551
|
+
* The credential remains in the database but is excluded from active queries.
|
|
552
|
+
* Cleans up provider entry if last credential disabled.
|
|
552
553
|
*/
|
|
553
554
|
#removeCredentialAt(provider: string, index: number): void {
|
|
554
555
|
const entries = this.#getStoredCredentials(provider);
|
|
555
556
|
if (index < 0 || index >= entries.length) return;
|
|
556
|
-
this.storage.
|
|
557
|
+
this.storage.disableAuthCredential(entries[index].id);
|
|
557
558
|
const updated = entries.filter((_value, idx) => idx !== index);
|
|
558
559
|
this.#setStoredCredentials(provider, updated);
|
|
559
560
|
this.#resetProviderAssignments(provider);
|
|
@@ -1339,7 +1340,7 @@ export class AuthStorage {
|
|
|
1339
1340
|
// Keep credentials for transient errors (network, 5xx) and block temporarily
|
|
1340
1341
|
const isDefinitiveFailure =
|
|
1341
1342
|
/invalid_grant|invalid_token|revoked|unauthorized|expired.*refresh|refresh.*expired/i.test(errorMsg) ||
|
|
1342
|
-
(
|
|
1343
|
+
(/\b(401|403)\b/.test(errorMsg) && !/timeout|network|fetch failed|ECONNREFUSED/i.test(errorMsg));
|
|
1343
1344
|
|
|
1344
1345
|
logger.warn("OAuth token refresh failed", {
|
|
1345
1346
|
provider,
|
package/src/task/executor.ts
CHANGED
|
@@ -379,6 +379,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
379
379
|
status: "running",
|
|
380
380
|
task,
|
|
381
381
|
description: options.description,
|
|
382
|
+
lastIntent: undefined,
|
|
382
383
|
recentTools: [],
|
|
383
384
|
recentOutput: [],
|
|
384
385
|
toolCount: 0,
|
|
@@ -645,14 +646,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
645
646
|
}
|
|
646
647
|
break;
|
|
647
648
|
|
|
648
|
-
case "tool_execution_start":
|
|
649
|
+
case "tool_execution_start": {
|
|
649
650
|
progress.toolCount++;
|
|
650
651
|
progress.currentTool = event.toolName;
|
|
651
652
|
progress.currentToolArgs = extractToolArgsPreview(
|
|
652
653
|
(event as { toolArgs?: Record<string, unknown> }).toolArgs || event.args || {},
|
|
653
654
|
);
|
|
654
655
|
progress.currentToolStartMs = now;
|
|
656
|
+
const intent = event.intent?.trim();
|
|
657
|
+
if (intent) {
|
|
658
|
+
progress.lastIntent = intent;
|
|
659
|
+
}
|
|
655
660
|
break;
|
|
661
|
+
}
|
|
656
662
|
|
|
657
663
|
case "tool_execution_end": {
|
|
658
664
|
if (progress.currentTool) {
|
|
@@ -1174,6 +1180,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1174
1180
|
agentSource: agent.source,
|
|
1175
1181
|
task,
|
|
1176
1182
|
description: options.description,
|
|
1183
|
+
lastIntent: progress.lastIntent,
|
|
1177
1184
|
exitCode,
|
|
1178
1185
|
output: truncatedOutput,
|
|
1179
1186
|
stderr,
|
package/src/task/render.ts
CHANGED
|
@@ -535,8 +535,9 @@ function renderAgentProgress(
|
|
|
535
535
|
if (progress.status === "running") {
|
|
536
536
|
if (progress.currentTool) {
|
|
537
537
|
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
|
|
538
|
-
|
|
539
|
-
|
|
538
|
+
const toolDetail = progress.lastIntent ?? progress.currentToolArgs;
|
|
539
|
+
if (toolDetail) {
|
|
540
|
+
toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
|
|
540
541
|
}
|
|
541
542
|
if (progress.currentToolStartMs) {
|
|
542
543
|
const elapsed = Date.now() - progress.currentToolStartMs;
|
|
@@ -549,8 +550,9 @@ function renderAgentProgress(
|
|
|
549
550
|
// Show most recent completed tool when idle between tools
|
|
550
551
|
const recent = progress.recentTools[0];
|
|
551
552
|
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", recent.tool)}`;
|
|
552
|
-
|
|
553
|
-
|
|
553
|
+
const toolDetail = progress.lastIntent ?? recent.args;
|
|
554
|
+
if (toolDetail) {
|
|
555
|
+
toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
|
|
554
556
|
}
|
|
555
557
|
lines.push(toolLine);
|
|
556
558
|
}
|
package/src/task/types.ts
CHANGED
|
@@ -140,6 +140,7 @@ export interface AgentProgress {
|
|
|
140
140
|
status: "pending" | "running" | "completed" | "failed" | "aborted";
|
|
141
141
|
task: string;
|
|
142
142
|
description?: string;
|
|
143
|
+
lastIntent?: string;
|
|
143
144
|
currentTool?: string;
|
|
144
145
|
currentToolArgs?: string;
|
|
145
146
|
currentToolStartMs?: number;
|
|
@@ -161,6 +162,7 @@ export interface SingleResult {
|
|
|
161
162
|
agentSource: AgentSource;
|
|
162
163
|
task: string;
|
|
163
164
|
description?: string;
|
|
165
|
+
lastIntent?: string;
|
|
164
166
|
exitCode: number;
|
|
165
167
|
output: string;
|
|
166
168
|
stderr: string;
|