@oh-my-pi/pi-coding-agent 14.2.0 → 14.3.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 +59 -0
- package/package.json +19 -19
- package/src/cli/args.ts +10 -1
- package/src/cli/shell-cli.ts +15 -3
- package/src/config/settings-schema.ts +60 -1
- package/src/dap/session.ts +8 -2
- package/src/debug/system-info.ts +6 -2
- package/src/discovery/claude.ts +58 -36
- package/src/discovery/opencode.ts +20 -2
- package/src/edit/index.ts +3 -1
- package/src/edit/modes/chunk.ts +133 -53
- package/src/edit/modes/hashline.ts +36 -11
- package/src/edit/renderer.ts +98 -133
- package/src/edit/streaming.ts +351 -0
- package/src/exec/bash-executor.ts +60 -5
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/pi-protocol.ts +0 -2
- package/src/lsp/client.ts +22 -6
- package/src/lsp/defaults.json +2 -1
- package/src/lsp/index.ts +53 -10
- package/src/lsp/types.ts +2 -0
- package/src/modes/acp/acp-agent.ts +76 -2
- package/src/modes/components/assistant-message.ts +1 -34
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/tool-execution.ts +111 -101
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +0 -2
- package/src/modes/theme/mermaid-cache.ts +13 -52
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/chunk-edit.md +25 -22
- package/src/prompts/tools/gh-pr-push.md +2 -1
- package/src/prompts/tools/grep.md +4 -3
- package/src/prompts/tools/lsp.md +6 -0
- package/src/prompts/tools/read-chunk.md +46 -7
- package/src/prompts/tools/read.md +7 -4
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +36 -20
- package/src/session/session-manager.ts +228 -57
- package/src/session/streaming-output.ts +11 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +1 -0
- package/src/tools/ast-edit.ts +37 -2
- package/src/tools/bash.ts +75 -12
- package/src/tools/find.ts +19 -26
- package/src/tools/gh.ts +6 -16
- package/src/tools/grep.ts +94 -37
- package/src/tools/path-utils.ts +31 -3
- package/src/tools/resolve.ts +12 -3
- package/src/tools/sqlite-reader.ts +116 -3
- package/src/tools/vim.ts +1 -1
- package/src/web/search/providers/codex.ts +129 -6
|
@@ -1,23 +1,54 @@
|
|
|
1
|
-
Reads files using syntax-aware chunks.
|
|
1
|
+
Reads files using syntax-aware chunks. Also inspects directories, archives, SQLite databases, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), **and URLs**.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
|
-
- `
|
|
5
|
-
|
|
4
|
+
The chunk-aware `read` variant returns AST-scoped chunks with current checksum IDs for structural editing, and otherwise behaves like `open` for non-code content.
|
|
5
|
+
|
|
6
|
+
- You **MUST** parallelize calls when exploring related files
|
|
7
|
+
- For URLs, `read` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `read` — not a browser/puppeteer tool — for fetching and inspecting web content.
|
|
8
|
+
|
|
9
|
+
## Parameters
|
|
10
|
+
- `path` — file path or URL; may include `:selector` suffix (required)
|
|
11
|
+
- `sel` — optional selector for chunks, line ranges, listing, or raw mode
|
|
6
12
|
- `timeout` — seconds, for URLs only
|
|
7
13
|
|
|
14
|
+
## Selectors
|
|
15
|
+
|
|
16
|
+
|`sel` value|Behavior|
|
|
17
|
+
|---|---|
|
|
18
|
+
|*(omitted)*|Read full file as chunks (up to {{DEFAULT_LIMIT}} lines)|
|
|
19
|
+
|`class_Foo`|Read a specific chunk|
|
|
20
|
+
|`class_Foo.fn_bar#ABCD~`|Read a chunk region (body `~` / head `^`) by ID|
|
|
21
|
+
|`?`|List all chunk paths with IDs|
|
|
22
|
+
|`L50`|Read from line 50 onward (shorthand for L50 to EOF)|
|
|
23
|
+
|`L50-L120`|Read lines 50 through 120|
|
|
24
|
+
|`L20-L20`|Read exactly one line|
|
|
25
|
+
|`raw`|Raw content without transformations (for URLs: untouched HTML)|
|
|
26
|
+
|
|
27
|
+
Max {{DEFAULT_MAX_LINES}} lines per call.
|
|
28
|
+
|
|
29
|
+
# Chunks
|
|
8
30
|
Each anchor `@full.chunk.path#CCCC` (with `-` prefixes for nesting depth) in the output identifies a chunk. Use `full.chunk.path#CCCC` as-is to read truncated chunks.
|
|
9
|
-
If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with IDs.
|
|
31
|
+
If you need a canonical target list, run `read(path="file", sel="?")`. That listing shows chunk paths with IDs and is the safest structural discovery mode. Summary lines in this listing are orientation hints; follow a selector with `read(path="file", sel="chunk#ID")` or use `raw` when you need exact source.
|
|
10
32
|
Line numbers in the gutter are absolute file line numbers.
|
|
11
33
|
|
|
12
|
-
`L20` (single line, no explicit end) is shorthand for `L20` to end-of-file. Use `L20-L20` for a one-line window.
|
|
13
|
-
|
|
14
34
|
{{#if chunkAutoIndent}}
|
|
15
35
|
Chunk reads normalize leading indentation so copied content round-trips cleanly into chunk edits.
|
|
16
36
|
{{else}}
|
|
17
37
|
Chunk reads preserve literal leading tabs/spaces from the file. When editing, keep the same whitespace characters you see here.
|
|
18
38
|
{{/if}}
|
|
39
|
+
`raw` shows the file's literal whitespace. Structured chunk views may normalize or display indentation for edit round-tripping, so use `raw` when exact tabs/spaces matter, especially inside markdown fenced code blocks.
|
|
40
|
+
|
|
41
|
+
IDs change after every edit. Use the new IDs from the edit response or refresh with `sel="?"` before the next `write`/`delete`. `insert` selectors may omit IDs, but still prefer fresh paths after structural edits.
|
|
42
|
+
|
|
43
|
+
Parser boundaries vary by language: TypeScript/JavaScript decorators and JSDoc above decorated methods may appear as sibling `chunk#ID` entries, Python decorators are part of the function/class head, Python docstrings are body lines, and Python enum members or nested closures may remain opaque inside their parent chunk. Decorated Python `^` writes and Python `^` deletes are rejected for safety.
|
|
44
|
+
Markdown sections, lists, and tables are structural chunks. Recognized pipe tables expose `row_N` children for row-level edits; list items and table cells are not independently addressable. Fenced code blocks with a declared language are parsed again when possible, so functions inside a markdown fence can appear as addressable nested chunks.
|
|
19
45
|
|
|
20
46
|
Chunk trees: JS, TS, TSX, Python, Rust, Go. Others use blank-line fallback.
|
|
47
|
+
# Inspection
|
|
48
|
+
Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook files. Can inspect images.
|
|
49
|
+
|
|
50
|
+
# Directories & Archives
|
|
51
|
+
Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents.
|
|
21
52
|
|
|
22
53
|
# SQLite Databases
|
|
23
54
|
When used against a SQLite database (`.sqlite`, `.sqlite3`, `.db`, `.db3`), returns structured database content.
|
|
@@ -27,9 +58,17 @@ When used against a SQLite database (`.sqlite`, `.sqlite3`, `.db`, `.db3`), retu
|
|
|
27
58
|
- `file.db:table?limit=50&offset=100` — paginated rows
|
|
28
59
|
- `file.db:table?where=status='active'&order=created:desc` — filtered rows
|
|
29
60
|
- `file.db?q=SELECT …` — read-only SELECT query
|
|
61
|
+
|
|
62
|
+
# URLs
|
|
63
|
+
Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling).
|
|
30
64
|
</instruction>
|
|
31
65
|
|
|
32
66
|
<critical>
|
|
33
|
-
- **MUST** `read` before editing — never invent chunk names or IDs.
|
|
67
|
+
- You **MUST** `read` before editing — never invent chunk names or IDs.
|
|
34
68
|
- Chunk names are truncated (e.g., `handleRequest` becomes `fn_handleRequ`). Always copy chunk paths from `read` or `?` output — never construct them from source identifiers.
|
|
69
|
+
- You **MUST** use `read` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads.
|
|
70
|
+
- You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `read` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows.
|
|
71
|
+
- You **MUST** always include the `path` parameter; never call with `{}`.
|
|
72
|
+
- For specific line ranges, use `sel`: `read(path="file", sel="L50-L150")` — not `cat -n file | sed`.
|
|
73
|
+
- You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
|
|
35
74
|
</critical>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Reads the content at the specified path or URL.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
|
-
The `read` tool is multi-purpose — inspects files, directories, archives, SQLite databases, and URLs
|
|
4
|
+
The `read` tool is multi-purpose and more capable than it looks — inspects files, directories, archives, SQLite databases, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), **and URLs**.
|
|
5
5
|
- You **MUST** parallelize reads when exploring related files
|
|
6
|
+
- For URLs, `read` fetches the page and returns clean extracted text/markdown by default (reader-mode). It handles HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs, etc. You **SHOULD** reach for `read` — not a browser/puppeteer tool — for fetching and inspecting web content.
|
|
6
7
|
|
|
7
8
|
## Parameters
|
|
8
9
|
- `path` — file path or URL (required)
|
|
@@ -14,8 +15,9 @@ The `read` tool is multi-purpose — inspects files, directories, archives, SQLi
|
|
|
14
15
|
|`sel` value|Behavior|
|
|
15
16
|
|---|---|
|
|
16
17
|
|*(omitted)*|Read full file (up to {{DEFAULT_LIMIT}} lines)|
|
|
17
|
-
|`L50`|Read from line 50 onward|
|
|
18
|
+
|`L50`|Read from line 50 onward (shorthand for L50 to EOF)|
|
|
18
19
|
|`L50-L120`|Read lines 50 through 120|
|
|
20
|
+
|`L20-L20`|Read exactly one line|
|
|
19
21
|
|`raw`|Raw content without transformations (for URLs: untouched HTML)|
|
|
20
22
|
|
|
21
23
|
Max {{DEFAULT_MAX_LINES}} lines per call.
|
|
@@ -45,11 +47,12 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
45
47
|
- `file.db?q=SELECT …` — read-only SELECT query
|
|
46
48
|
|
|
47
49
|
# URLs
|
|
48
|
-
Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, and similar text-based resources. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout.
|
|
50
|
+
Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling).
|
|
49
51
|
</instruction>
|
|
50
52
|
|
|
51
53
|
<critical>
|
|
52
|
-
- You **MUST** use `read` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`) for all file, directory, and
|
|
54
|
+
- You **MUST** use `read` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads.
|
|
55
|
+
- You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content — `read` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows.
|
|
53
56
|
- You **MUST** always include the `path` parameter; never call with `{}`.
|
|
54
57
|
- For specific line ranges, use `sel`: `read(path="file", sel="L50-L150")` — not `cat -n file | sed`.
|
|
55
58
|
- You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
|
package/src/sdk.ts
CHANGED
|
@@ -194,6 +194,8 @@ export interface CreateAgentSessionOptions {
|
|
|
194
194
|
|
|
195
195
|
/** Enable MCP server discovery from .mcp.json files. Default: true */
|
|
196
196
|
enableMCP?: boolean;
|
|
197
|
+
/** Existing MCP manager to reuse (skips discovery, propagates to toolSession). */
|
|
198
|
+
mcpManager?: MCPManager;
|
|
197
199
|
|
|
198
200
|
/** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
|
|
199
201
|
enableLsp?: boolean;
|
|
@@ -1005,10 +1007,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1005
1007
|
const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
|
|
1006
1008
|
|
|
1007
1009
|
// Discover MCP tools from .mcp.json files
|
|
1008
|
-
let mcpManager: MCPManager | undefined;
|
|
1010
|
+
let mcpManager: MCPManager | undefined = options.mcpManager;
|
|
1009
1011
|
const enableMCP = options.enableMCP ?? true;
|
|
1010
1012
|
const customTools: CustomTool[] = [];
|
|
1011
|
-
if (enableMCP) {
|
|
1013
|
+
if (enableMCP && !mcpManager) {
|
|
1012
1014
|
const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
|
|
1013
1015
|
onConnecting: serverNames => {
|
|
1014
1016
|
if (options.hasUI && serverNames.length > 0) {
|
|
@@ -1024,7 +1026,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1024
1026
|
authStorage,
|
|
1025
1027
|
});
|
|
1026
1028
|
mcpManager = mcpResult.manager;
|
|
1027
|
-
toolSession.mcpManager = mcpManager;
|
|
1028
1029
|
|
|
1029
1030
|
if (settings.get("mcp.notifications")) {
|
|
1030
1031
|
mcpManager.setNotificationsEnabled(true);
|
|
@@ -1044,6 +1045,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1044
1045
|
customTools.push(...mcpResult.tools.map(loaded => loaded.tool));
|
|
1045
1046
|
}
|
|
1046
1047
|
}
|
|
1048
|
+
toolSession.mcpManager = mcpManager;
|
|
1047
1049
|
|
|
1048
1050
|
// Add Gemini image tools if GEMINI_API_KEY (or GOOGLE_API_KEY) is available
|
|
1049
1051
|
const geminiImageTools = await logger.time("getGeminiImageTools", getGeminiImageTools);
|
|
@@ -1663,8 +1665,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1663
1665
|
}),
|
|
1664
1666
|
);
|
|
1665
1667
|
|
|
1666
|
-
// Wire MCP manager callbacks to session for reactive tool updates
|
|
1667
|
-
|
|
1668
|
+
// Wire MCP manager callbacks to session for reactive tool updates.
|
|
1669
|
+
// Skip when reusing a parent's manager — the parent owns the callbacks.
|
|
1670
|
+
if (mcpManager && !options.mcpManager) {
|
|
1668
1671
|
mcpManager.setOnToolsChanged(tools => {
|
|
1669
1672
|
void session.refreshMCPTools(tools);
|
|
1670
1673
|
});
|
|
@@ -1179,6 +1179,29 @@ export class AgentSession {
|
|
|
1179
1179
|
);
|
|
1180
1180
|
}
|
|
1181
1181
|
|
|
1182
|
+
#scheduleAutoContinuePrompt(generation: number): void {
|
|
1183
|
+
const continuePrompt = async () => {
|
|
1184
|
+
await this.#promptWithMessage(
|
|
1185
|
+
{
|
|
1186
|
+
role: "developer",
|
|
1187
|
+
content: [{ type: "text", text: "Continue if you have next steps." }],
|
|
1188
|
+
attribution: "agent",
|
|
1189
|
+
timestamp: Date.now(),
|
|
1190
|
+
},
|
|
1191
|
+
"Continue if you have next steps.",
|
|
1192
|
+
{ skipPostPromptRecoveryWait: true },
|
|
1193
|
+
);
|
|
1194
|
+
};
|
|
1195
|
+
this.#schedulePostPromptTask(
|
|
1196
|
+
async signal => {
|
|
1197
|
+
await Promise.resolve();
|
|
1198
|
+
if (signal.aborted) return;
|
|
1199
|
+
await continuePrompt();
|
|
1200
|
+
},
|
|
1201
|
+
{ generation },
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1182
1205
|
#cancelPostPromptTasks(): void {
|
|
1183
1206
|
this.#postPromptTasksAbortController.abort();
|
|
1184
1207
|
this.#postPromptTasksAbortController = new AbortController();
|
|
@@ -4813,6 +4836,9 @@ export class AgentSession {
|
|
|
4813
4836
|
aborted: false,
|
|
4814
4837
|
willRetry: false,
|
|
4815
4838
|
});
|
|
4839
|
+
if (!autoCompactionSignal.aborted && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
4840
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
4841
|
+
}
|
|
4816
4842
|
return;
|
|
4817
4843
|
}
|
|
4818
4844
|
}
|
|
@@ -5064,26 +5090,7 @@ export class AgentSession {
|
|
|
5064
5090
|
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
5065
5091
|
|
|
5066
5092
|
if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
5067
|
-
|
|
5068
|
-
await this.#promptWithMessage(
|
|
5069
|
-
{
|
|
5070
|
-
role: "developer",
|
|
5071
|
-
content: [{ type: "text", text: "Continue if you have next steps." }],
|
|
5072
|
-
attribution: "agent",
|
|
5073
|
-
timestamp: Date.now(),
|
|
5074
|
-
},
|
|
5075
|
-
"Continue if you have next steps.",
|
|
5076
|
-
{ skipPostPromptRecoveryWait: true },
|
|
5077
|
-
);
|
|
5078
|
-
};
|
|
5079
|
-
this.#schedulePostPromptTask(
|
|
5080
|
-
async signal => {
|
|
5081
|
-
await Promise.resolve();
|
|
5082
|
-
if (signal.aborted) return;
|
|
5083
|
-
await continuePrompt();
|
|
5084
|
-
},
|
|
5085
|
-
{ generation },
|
|
5086
|
-
);
|
|
5093
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
5087
5094
|
}
|
|
5088
5095
|
|
|
5089
5096
|
if (willRetry) {
|
|
@@ -5604,6 +5611,14 @@ export class AgentSession {
|
|
|
5604
5611
|
// Bash Execution
|
|
5605
5612
|
// =========================================================================
|
|
5606
5613
|
|
|
5614
|
+
async #saveBashOriginalArtifact(originalText: string): Promise<string | undefined> {
|
|
5615
|
+
try {
|
|
5616
|
+
return await this.sessionManager.saveArtifact(originalText, "bash-original");
|
|
5617
|
+
} catch {
|
|
5618
|
+
return undefined;
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5607
5622
|
/**
|
|
5608
5623
|
* Execute a bash command.
|
|
5609
5624
|
* Adds result to agent context and session.
|
|
@@ -5640,6 +5655,7 @@ export class AgentSession {
|
|
|
5640
5655
|
signal: this.#bashAbortController.signal,
|
|
5641
5656
|
sessionKey: this.sessionId,
|
|
5642
5657
|
timeout: clampTimeout("bash") * 1000,
|
|
5658
|
+
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
|
|
5643
5659
|
});
|
|
5644
5660
|
|
|
5645
5661
|
this.recordBashResult(command, result, options);
|
|
@@ -1264,74 +1264,245 @@ function extractTextFromContent(content: Message["content"]): string {
|
|
|
1264
1264
|
.join(" ");
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1267
|
+
const SESSION_LIST_PREFIX_BYTES = 1024;
|
|
1268
|
+
const SESSION_LIST_PARALLEL_THRESHOLD = 64;
|
|
1269
|
+
const SESSION_LIST_MAX_WORKERS = 16;
|
|
1270
|
+
const sessionListPrefixDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
1269
1271
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1272
|
+
async function readSessionListPrefix(file: string, storage: SessionStorage, buffer: Buffer): Promise<string> {
|
|
1273
|
+
if (!(storage instanceof FileSessionStorage)) {
|
|
1274
|
+
return storage.readTextPrefix(file, buffer.byteLength);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const handle = await fs.promises.open(file, "r");
|
|
1278
|
+
try {
|
|
1279
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, 0);
|
|
1280
|
+
return sessionListPrefixDecoder.decode(buffer.subarray(0, bytesRead));
|
|
1281
|
+
} finally {
|
|
1282
|
+
await handle.close();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function decodeJsonStringFragment(value: string): string {
|
|
1287
|
+
const safeValue = value.endsWith("\\") ? value.slice(0, -1) : value;
|
|
1288
|
+
try {
|
|
1289
|
+
return JSON.parse(`"${safeValue}"`) as string;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return safeValue
|
|
1292
|
+
.replace(/\\n/g, "\n")
|
|
1293
|
+
.replace(/\\r/g, "\r")
|
|
1294
|
+
.replace(/\\t/g, "\t")
|
|
1295
|
+
.replace(/\\"/g, '"')
|
|
1296
|
+
.replace(/\\\\/g, "\\");
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1289
1299
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
let shortSummary: string | undefined;
|
|
1300
|
+
function extractStringProperty(source: string, name: string, startIndex = 0): string | undefined {
|
|
1301
|
+
const propertyIndex = source.indexOf(`"${name}"`, startIndex);
|
|
1302
|
+
if (propertyIndex === -1) return undefined;
|
|
1294
1303
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1304
|
+
const colonIndex = source.indexOf(":", propertyIndex + name.length + 2);
|
|
1305
|
+
if (colonIndex === -1) return undefined;
|
|
1297
1306
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1307
|
+
let valueIndex = colonIndex + 1;
|
|
1308
|
+
while (valueIndex < source.length) {
|
|
1309
|
+
const char = source.charCodeAt(valueIndex);
|
|
1310
|
+
if (char !== 32 && char !== 9 && char !== 10 && char !== 13) break;
|
|
1311
|
+
valueIndex++;
|
|
1312
|
+
}
|
|
1313
|
+
if (source.charCodeAt(valueIndex) !== 34) return undefined;
|
|
1314
|
+
|
|
1315
|
+
const valueStart = valueIndex + 1;
|
|
1316
|
+
let escaped = false;
|
|
1317
|
+
for (let i = valueStart; i < source.length; i++) {
|
|
1318
|
+
const char = source.charCodeAt(i);
|
|
1319
|
+
if (escaped) {
|
|
1320
|
+
escaped = false;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
if (char === 92) {
|
|
1324
|
+
escaped = true;
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
if (char === 34) {
|
|
1328
|
+
return decodeJsonStringFragment(source.slice(valueStart, i));
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
return decodeJsonStringFragment(source.slice(valueStart));
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function countMessageMarkers(content: string): number {
|
|
1336
|
+
let count = 0;
|
|
1337
|
+
let index = 0;
|
|
1338
|
+
while (index < content.length) {
|
|
1339
|
+
const typeIndex = content.indexOf('"type"', index);
|
|
1340
|
+
if (typeIndex === -1) break;
|
|
1341
|
+
const colonIndex = content.indexOf(":", typeIndex + 6);
|
|
1342
|
+
if (colonIndex === -1) break;
|
|
1343
|
+
const type = extractStringProperty(content, "type", typeIndex);
|
|
1344
|
+
if (type === "message") count++;
|
|
1345
|
+
index = colonIndex + 1;
|
|
1346
|
+
}
|
|
1347
|
+
return count;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function extractFirstUserMessageFromPrefix(content: string): string | undefined {
|
|
1351
|
+
const roleIndex = content.indexOf('"role"');
|
|
1352
|
+
if (roleIndex === -1) return undefined;
|
|
1353
|
+
|
|
1354
|
+
let index = roleIndex;
|
|
1355
|
+
while (index !== -1) {
|
|
1356
|
+
const role = extractStringProperty(content, "role", index);
|
|
1357
|
+
if (role === "user") {
|
|
1358
|
+
return extractStringProperty(content, "content", index) ?? extractStringProperty(content, "text", index);
|
|
1359
|
+
}
|
|
1360
|
+
index = content.indexOf('"role"', index + 6);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
return undefined;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
interface SessionListHeader {
|
|
1367
|
+
type: "session";
|
|
1368
|
+
id: string;
|
|
1369
|
+
cwd?: string;
|
|
1370
|
+
title?: string;
|
|
1371
|
+
parentSession?: string;
|
|
1372
|
+
timestamp?: string;
|
|
1373
|
+
}
|
|
1301
1374
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1375
|
+
function parseSessionListHeader(
|
|
1376
|
+
content: string,
|
|
1377
|
+
entries: Array<Record<string, unknown>>,
|
|
1378
|
+
): SessionListHeader | undefined {
|
|
1379
|
+
const parsedHeader = entries[0];
|
|
1380
|
+
if (parsedHeader?.type === "session" && typeof parsedHeader.id === "string") {
|
|
1381
|
+
return {
|
|
1382
|
+
type: "session",
|
|
1383
|
+
id: parsedHeader.id,
|
|
1384
|
+
cwd: typeof parsedHeader.cwd === "string" ? parsedHeader.cwd : undefined,
|
|
1385
|
+
title: typeof parsedHeader.title === "string" ? parsedHeader.title : undefined,
|
|
1386
|
+
parentSession: typeof parsedHeader.parentSession === "string" ? parsedHeader.parentSession : undefined,
|
|
1387
|
+
timestamp: typeof parsedHeader.timestamp === "string" ? parsedHeader.timestamp : undefined,
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const firstLineEnd = content.indexOf("\n");
|
|
1392
|
+
const firstLine = firstLineEnd === -1 ? content : content.slice(0, firstLineEnd);
|
|
1393
|
+
if (extractStringProperty(firstLine, "type") !== "session") return undefined;
|
|
1394
|
+
|
|
1395
|
+
const id = extractStringProperty(firstLine, "id");
|
|
1396
|
+
if (!id) return undefined;
|
|
1397
|
+
|
|
1398
|
+
return {
|
|
1399
|
+
type: "session",
|
|
1400
|
+
id,
|
|
1401
|
+
cwd: extractStringProperty(firstLine, "cwd"),
|
|
1402
|
+
title: extractStringProperty(firstLine, "title"),
|
|
1403
|
+
parentSession: extractStringProperty(firstLine, "parentSession"),
|
|
1404
|
+
timestamp: extractStringProperty(firstLine, "timestamp"),
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function getSessionListWorkerCount(fileCount: number): number {
|
|
1409
|
+
if (fileCount <= SESSION_LIST_PARALLEL_THRESHOLD) return 1;
|
|
1410
|
+
return Math.min(
|
|
1411
|
+
SESSION_LIST_MAX_WORKERS,
|
|
1412
|
+
os.availableParallelism(),
|
|
1413
|
+
Math.ceil(fileCount / SESSION_LIST_PARALLEL_THRESHOLD),
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async function collectSessionFromFile(
|
|
1418
|
+
file: string,
|
|
1419
|
+
storage: SessionStorage,
|
|
1420
|
+
buffer: Buffer,
|
|
1421
|
+
): Promise<SessionInfo | undefined> {
|
|
1422
|
+
try {
|
|
1423
|
+
const content = await readSessionListPrefix(file, storage, buffer);
|
|
1424
|
+
const entries = parseJsonlLenient<Record<string, unknown>>(content);
|
|
1425
|
+
const header = parseSessionListHeader(content, entries);
|
|
1426
|
+
if (!header) return undefined;
|
|
1427
|
+
|
|
1428
|
+
let parsedMessageCount = 0;
|
|
1429
|
+
let firstMessage = "";
|
|
1430
|
+
const allMessages: string[] = [];
|
|
1431
|
+
let shortSummary: string | undefined;
|
|
1432
|
+
|
|
1433
|
+
for (let i = 1; i < entries.length; i++) {
|
|
1434
|
+
const entry = entries[i] as { type?: string; message?: Message; shortSummary?: string };
|
|
1435
|
+
|
|
1436
|
+
if (entry.type === "compaction" && typeof entry.shortSummary === "string") {
|
|
1437
|
+
shortSummary = entry.shortSummary;
|
|
1438
|
+
}
|
|
1304
1439
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1440
|
+
if (entry.type === "message" && entry.message) {
|
|
1441
|
+
parsedMessageCount++;
|
|
1307
1442
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1443
|
+
if (entry.message.role === "user" || entry.message.role === "assistant") {
|
|
1444
|
+
const textContent = extractTextFromContent(entry.message.content);
|
|
1310
1445
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1446
|
+
if (textContent) {
|
|
1447
|
+
allMessages.push(textContent);
|
|
1448
|
+
|
|
1449
|
+
if (!firstMessage && entry.message.role === "user") {
|
|
1450
|
+
firstMessage = textContent;
|
|
1315
1451
|
}
|
|
1316
1452
|
}
|
|
1317
1453
|
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1318
1456
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1457
|
+
firstMessage ||= extractFirstUserMessageFromPrefix(content) ?? "";
|
|
1458
|
+
const messageCount = Math.max(parsedMessageCount, countMessageMarkers(content));
|
|
1459
|
+
const stats = storage.statSync(file);
|
|
1460
|
+
return {
|
|
1461
|
+
path: file,
|
|
1462
|
+
id: header.id,
|
|
1463
|
+
cwd: header.cwd ?? "",
|
|
1464
|
+
title: header.title ?? shortSummary,
|
|
1465
|
+
parentSessionPath: header.parentSession,
|
|
1466
|
+
created: new Date(header.timestamp ?? ""),
|
|
1467
|
+
modified: stats.mtime,
|
|
1468
|
+
messageCount,
|
|
1469
|
+
firstMessage: firstMessage || "(no messages)",
|
|
1470
|
+
allMessagesText: allMessages.length > 0 ? allMessages.join(" ") : firstMessage,
|
|
1471
|
+
};
|
|
1472
|
+
} catch {
|
|
1473
|
+
return undefined;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async function collectSessionsFromFileStride(
|
|
1478
|
+
files: string[],
|
|
1479
|
+
storage: SessionStorage,
|
|
1480
|
+
startIndex: number,
|
|
1481
|
+
stride: number,
|
|
1482
|
+
): Promise<SessionInfo[]> {
|
|
1483
|
+
const sessions: SessionInfo[] = [];
|
|
1484
|
+
const buffer = Buffer.allocUnsafe(SESSION_LIST_PREFIX_BYTES);
|
|
1485
|
+
|
|
1486
|
+
for (let i = startIndex; i < files.length; i += stride) {
|
|
1487
|
+
const session = await collectSessionFromFile(files[i], storage, buffer);
|
|
1488
|
+
if (session) sessions.push(session);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
return sessions;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
async function collectSessionsFromFiles(files: string[], storage: SessionStorage): Promise<SessionInfo[]> {
|
|
1495
|
+
const workerCount = getSessionListWorkerCount(files.length);
|
|
1496
|
+
const sessions =
|
|
1497
|
+
workerCount === 1
|
|
1498
|
+
? await collectSessionsFromFileStride(files, storage, 0, 1)
|
|
1499
|
+
: (
|
|
1500
|
+
await Promise.all(
|
|
1501
|
+
Array.from({ length: workerCount }, (_, workerIndex) =>
|
|
1502
|
+
collectSessionsFromFileStride(files, storage, workerIndex, workerCount),
|
|
1503
|
+
),
|
|
1504
|
+
)
|
|
1505
|
+
).flat();
|
|
1335
1506
|
|
|
1336
1507
|
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1337
1508
|
return sessions;
|
|
@@ -2771,7 +2942,7 @@ export class SessionManager {
|
|
|
2771
2942
|
static async listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
|
|
2772
2943
|
const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
|
|
2773
2944
|
try {
|
|
2774
|
-
const files = Array.
|
|
2945
|
+
const files = await Array.fromAsync(new Bun.Glob("*/*.jsonl").scan(sessionsRoot), name =>
|
|
2775
2946
|
path.join(sessionsRoot, name),
|
|
2776
2947
|
);
|
|
2777
2948
|
return await collectSessionsFromFiles(files, storage);
|
|
@@ -680,6 +680,17 @@ export class OutputSink {
|
|
|
680
680
|
});
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Replace the in-memory buffer with the given text while preserving the
|
|
685
|
+
* streaming counters (totalLines/totalBytes reflect the raw chunks that
|
|
686
|
+
* already reached the sink). Used when an upstream minimizer rewrites the
|
|
687
|
+
* captured output after the raw bytes have already been streamed.
|
|
688
|
+
*/
|
|
689
|
+
replace(text: string): void {
|
|
690
|
+
this.#buffer = text;
|
|
691
|
+
this.#bufferBytes = Buffer.byteLength(text, "utf-8");
|
|
692
|
+
}
|
|
693
|
+
|
|
683
694
|
async dump(notice?: string): Promise<OutputSummary> {
|
|
684
695
|
const noticeLine = notice ? `[${notice}]\n` : "";
|
|
685
696
|
const outputLines = this.#buffer.length > 0 ? countNewlines(this.#buffer) + 1 : 0;
|
package/src/system-prompt.ts
CHANGED
|
@@ -275,13 +275,18 @@ async function getCachedGpu(): Promise<string | undefined> {
|
|
|
275
275
|
}
|
|
276
276
|
async function getEnvironmentInfo(): Promise<Array<{ label: string; value: string }>> {
|
|
277
277
|
const gpu = await getCachedGpu();
|
|
278
|
-
|
|
278
|
+
let cpuModel: string | undefined;
|
|
279
|
+
try {
|
|
280
|
+
cpuModel = os.cpus()[0]?.model;
|
|
281
|
+
} catch {
|
|
282
|
+
cpuModel = undefined;
|
|
283
|
+
}
|
|
279
284
|
const entries: Array<{ label: string; value: string | undefined }> = [
|
|
280
285
|
{ label: "OS", value: `${os.platform()} ${os.release()}` },
|
|
281
286
|
{ label: "Distro", value: os.type() },
|
|
282
287
|
{ label: "Kernel", value: os.version() },
|
|
283
288
|
{ label: "Arch", value: os.arch() },
|
|
284
|
-
{ label: "CPU", value:
|
|
289
|
+
{ label: "CPU", value: cpuModel },
|
|
285
290
|
{ label: "GPU", value: gpu },
|
|
286
291
|
{ label: "Terminal", value: getTerminalName() },
|
|
287
292
|
];
|
package/src/task/executor.ts
CHANGED
|
@@ -975,6 +975,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
975
975
|
enableLsp: lspEnabled,
|
|
976
976
|
skipPythonPreflight,
|
|
977
977
|
enableMCP,
|
|
978
|
+
mcpManager: options.mcpManager,
|
|
978
979
|
customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
|
|
979
980
|
});
|
|
980
981
|
|