@oh-my-pi/pi-coding-agent 14.2.1 → 14.4.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 +143 -1
- package/package.json +19 -19
- package/src/autoresearch/prompt.md +1 -1
- package/src/cli/args.ts +10 -1
- package/src/cli/shell-cli.ts +15 -3
- package/src/commit/agentic/prompts/analyze-file.md +1 -1
- package/src/config/model-registry.ts +67 -15
- package/src/config/prompt-templates.ts +5 -5
- package/src/config/settings-schema.ts +63 -4
- package/src/cursor.ts +3 -8
- package/src/debug/system-info.ts +6 -2
- package/src/discovery/claude.ts +58 -36
- package/src/discovery/helpers.ts +3 -3
- package/src/discovery/opencode.ts +20 -2
- package/src/edit/diff.ts +50 -47
- package/src/edit/index.ts +87 -57
- package/src/edit/line-hash.ts +735 -19
- package/src/edit/modes/apply-patch.ts +0 -9
- package/src/edit/modes/atom.ts +658 -0
- package/src/edit/modes/chunk.ts +144 -78
- package/src/edit/modes/hashline.ts +223 -146
- package/src/edit/modes/patch.ts +5 -9
- package/src/edit/modes/replace.ts +6 -11
- package/src/edit/renderer.ts +112 -143
- package/src/edit/streaming.ts +385 -0
- package/src/exec/bash-executor.ts +58 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +4 -12
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/pi-protocol.ts +0 -2
- package/src/lsp/client.ts +8 -1
- package/src/lsp/defaults.json +2 -1
- package/src/lsp/index.ts +1 -1
- package/src/mcp/render.ts +1 -8
- package/src/modes/acp/acp-agent.ts +76 -2
- package/src/modes/components/assistant-message.ts +5 -34
- package/src/modes/components/diff.ts +23 -14
- package/src/modes/components/footer.ts +21 -16
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/settings-defs.ts +6 -1
- package/src/modes/components/todo-reminder.ts +1 -8
- package/src/modes/components/tool-execution.ts +112 -105
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +0 -2
- package/src/modes/print-mode.ts +8 -0
- package/src/modes/theme/mermaid-cache.ts +13 -52
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +4 -4
- package/src/prompts/ci-green-request.md +1 -1
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +3 -3
- package/src/prompts/system/subagent-yield-reminder.md +11 -0
- package/src/prompts/system/system-prompt.md +4 -1
- package/src/prompts/tools/ask.md +3 -2
- package/src/prompts/tools/ast-edit.md +15 -19
- package/src/prompts/tools/ast-grep.md +18 -24
- package/src/prompts/tools/atom.md +96 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/chunk-edit.md +58 -179
- package/src/prompts/tools/debug.md +4 -5
- package/src/prompts/tools/exit-plan-mode.md +4 -5
- package/src/prompts/tools/find.md +4 -8
- package/src/prompts/tools/github.md +18 -0
- package/src/prompts/tools/grep.md +8 -8
- package/src/prompts/tools/hashline.md +22 -89
- package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
- package/src/prompts/tools/inspect-image.md +6 -6
- package/src/prompts/tools/lsp.md +6 -0
- package/src/prompts/tools/patch.md +12 -19
- package/src/prompts/tools/python.md +3 -2
- package/src/prompts/tools/read-chunk.md +46 -8
- package/src/prompts/tools/read.md +9 -6
- package/src/prompts/tools/ssh.md +8 -17
- package/src/prompts/tools/todo-write.md +54 -41
- package/src/sdk.ts +22 -14
- package/src/session/agent-session.ts +61 -22
- 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 +44 -48
- package/src/task/render.ts +11 -13
- package/src/tools/ask.ts +7 -7
- package/src/tools/ast-edit.ts +45 -41
- package/src/tools/ast-grep.ts +77 -85
- package/src/tools/bash.ts +21 -9
- package/src/tools/browser.ts +32 -30
- package/src/tools/calculator.ts +4 -4
- package/src/tools/cancel-job.ts +1 -1
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/debug.ts +41 -37
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/find.ts +4 -4
- package/src/tools/gh-renderer.ts +12 -4
- package/src/tools/gh.ts +514 -712
- package/src/tools/grep.ts +115 -130
- package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
- package/src/tools/index.ts +14 -32
- package/src/tools/inspect-image.ts +3 -3
- package/src/tools/json-tree.ts +114 -114
- package/src/tools/match-line-format.ts +9 -8
- package/src/tools/notebook.ts +8 -7
- package/src/tools/poll-tool.ts +2 -1
- package/src/tools/python.ts +9 -23
- package/src/tools/read.ts +32 -21
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/render-utils.ts +18 -0
- package/src/tools/renderers.ts +2 -2
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +12 -10
- package/src/tools/search-tool-bm25.ts +2 -4
- package/src/tools/sqlite-reader.ts +116 -3
- package/src/tools/ssh.ts +4 -4
- package/src/tools/todo-write.ts +172 -147
- package/src/tools/vim.ts +14 -15
- package/src/tools/write.ts +4 -4
- package/src/tools/{submit-result.ts → yield.ts} +11 -13
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/file-display-mode.ts +10 -5
- package/src/utils/git.ts +9 -5
- package/src/utils/shell-snapshot.ts +2 -3
- package/src/vim/render.ts +4 -4
- package/src/web/search/providers/codex.ts +129 -6
- package/src/prompts/system/subagent-submit-reminder.md +0 -11
- package/src/prompts/tools/gh-issue-view.md +0 -11
- package/src/prompts/tools/gh-pr-checkout.md +0 -12
- package/src/prompts/tools/gh-pr-diff.md +0 -12
- package/src/prompts/tools/gh-pr-push.md +0 -11
- package/src/prompts/tools/gh-pr-view.md +0 -11
- package/src/prompts/tools/gh-repo-view.md +0 -11
- package/src/prompts/tools/gh-run-watch.md +0 -12
- package/src/prompts/tools/gh-search-issues.md +0 -11
- package/src/prompts/tools/gh-search-prs.md +0 -11
package/src/sdk.ts
CHANGED
|
@@ -128,7 +128,7 @@ import {
|
|
|
128
128
|
warmupLspServers,
|
|
129
129
|
} from "./tools";
|
|
130
130
|
import { ToolContextStore } from "./tools/context";
|
|
131
|
-
import {
|
|
131
|
+
import { getImageGenTools } from "./tools/image-gen";
|
|
132
132
|
import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
133
133
|
import { queueResolveHandler } from "./tools/resolve";
|
|
134
134
|
import { EventBus } from "./utils/event-bus";
|
|
@@ -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;
|
|
@@ -207,8 +209,8 @@ export interface CreateAgentSessionOptions {
|
|
|
207
209
|
|
|
208
210
|
/** Output schema for structured completion (subagents) */
|
|
209
211
|
outputSchema?: unknown;
|
|
210
|
-
/** Whether to include the
|
|
211
|
-
|
|
212
|
+
/** Whether to include the yield tool by default */
|
|
213
|
+
requireYieldTool?: boolean;
|
|
212
214
|
/** Task recursion depth (for subagent sessions). Default: 0 */
|
|
213
215
|
taskDepth?: number;
|
|
214
216
|
/** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
|
|
@@ -671,7 +673,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
671
673
|
}
|
|
672
674
|
|
|
673
675
|
const imageProvider = settings.get("providers.image");
|
|
674
|
-
if (
|
|
676
|
+
if (
|
|
677
|
+
imageProvider === "auto" ||
|
|
678
|
+
imageProvider === "openai" ||
|
|
679
|
+
imageProvider === "gemini" ||
|
|
680
|
+
imageProvider === "openrouter"
|
|
681
|
+
) {
|
|
675
682
|
setPreferredImageProvider(imageProvider);
|
|
676
683
|
}
|
|
677
684
|
|
|
@@ -914,7 +921,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
914
921
|
skills,
|
|
915
922
|
eventBus,
|
|
916
923
|
outputSchema: options.outputSchema,
|
|
917
|
-
|
|
924
|
+
requireYieldTool: options.requireYieldTool,
|
|
918
925
|
taskDepth: options.taskDepth ?? 0,
|
|
919
926
|
getSessionFile: () => sessionManager.getSessionFile() ?? null,
|
|
920
927
|
getPythonKernelOwnerId: () => pythonKernelOwnerId,
|
|
@@ -1005,10 +1012,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1005
1012
|
const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
|
|
1006
1013
|
|
|
1007
1014
|
// Discover MCP tools from .mcp.json files
|
|
1008
|
-
let mcpManager: MCPManager | undefined;
|
|
1015
|
+
let mcpManager: MCPManager | undefined = options.mcpManager;
|
|
1009
1016
|
const enableMCP = options.enableMCP ?? true;
|
|
1010
1017
|
const customTools: CustomTool[] = [];
|
|
1011
|
-
if (enableMCP) {
|
|
1018
|
+
if (enableMCP && !mcpManager) {
|
|
1012
1019
|
const mcpResult = await logger.time("discoverAndLoadMCPTools", discoverAndLoadMCPTools, cwd, {
|
|
1013
1020
|
onConnecting: serverNames => {
|
|
1014
1021
|
if (options.hasUI && serverNames.length > 0) {
|
|
@@ -1024,7 +1031,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1024
1031
|
authStorage,
|
|
1025
1032
|
});
|
|
1026
1033
|
mcpManager = mcpResult.manager;
|
|
1027
|
-
toolSession.mcpManager = mcpManager;
|
|
1028
1034
|
|
|
1029
1035
|
if (settings.get("mcp.notifications")) {
|
|
1030
1036
|
mcpManager.setNotificationsEnabled(true);
|
|
@@ -1044,11 +1050,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1044
1050
|
customTools.push(...mcpResult.tools.map(loaded => loaded.tool));
|
|
1045
1051
|
}
|
|
1046
1052
|
}
|
|
1053
|
+
toolSession.mcpManager = mcpManager;
|
|
1047
1054
|
|
|
1048
|
-
// Add
|
|
1049
|
-
const
|
|
1050
|
-
if (
|
|
1051
|
-
customTools.push(...(
|
|
1055
|
+
// Add image tools when the active model or configured image providers can generate images.
|
|
1056
|
+
const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
|
|
1057
|
+
if (imageGenTools.length > 0) {
|
|
1058
|
+
customTools.push(...(imageGenTools as unknown as CustomTool[]));
|
|
1052
1059
|
}
|
|
1053
1060
|
|
|
1054
1061
|
// Add web search tools
|
|
@@ -1663,8 +1670,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1663
1670
|
}),
|
|
1664
1671
|
);
|
|
1665
1672
|
|
|
1666
|
-
// Wire MCP manager callbacks to session for reactive tool updates
|
|
1667
|
-
|
|
1673
|
+
// Wire MCP manager callbacks to session for reactive tool updates.
|
|
1674
|
+
// Skip when reusing a parent's manager — the parent owns the callbacks.
|
|
1675
|
+
if (mcpManager && !options.mcpManager) {
|
|
1668
1676
|
mcpManager.setOnToolsChanged(tools => {
|
|
1669
1677
|
void session.refreshMCPTools(tools);
|
|
1670
1678
|
});
|
|
@@ -525,6 +525,7 @@ export class AgentSession {
|
|
|
525
525
|
#obfuscator: SecretObfuscator | undefined;
|
|
526
526
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
527
527
|
#pendingRewindReport: string | undefined = undefined;
|
|
528
|
+
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
528
529
|
#promptGeneration = 0;
|
|
529
530
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
530
531
|
|
|
@@ -789,6 +790,9 @@ export class AgentSession {
|
|
|
789
790
|
this.#toolChoiceQueue.resolve();
|
|
790
791
|
}
|
|
791
792
|
}
|
|
793
|
+
if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
|
|
794
|
+
this.#lastSuccessfulYieldToolCallId = event.toolCallId;
|
|
795
|
+
}
|
|
792
796
|
if (event.type === "turn_end" && this.#pendingRewindReport) {
|
|
793
797
|
const report = this.#pendingRewindReport;
|
|
794
798
|
this.#pendingRewindReport = undefined;
|
|
@@ -1026,7 +1030,10 @@ export class AgentSession {
|
|
|
1026
1030
|
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
1027
1031
|
const msg = this.#lastAssistantMessage ?? fallbackAssistant;
|
|
1028
1032
|
this.#lastAssistantMessage = undefined;
|
|
1029
|
-
if (!msg)
|
|
1033
|
+
if (!msg) {
|
|
1034
|
+
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1030
1037
|
|
|
1031
1038
|
// Invalidate GitHub Copilot credentials on auth failure so stale tokens
|
|
1032
1039
|
// aren't reused on the next request
|
|
@@ -1040,9 +1047,16 @@ export class AgentSession {
|
|
|
1040
1047
|
|
|
1041
1048
|
if (this.#skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
|
|
1042
1049
|
this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
|
|
1050
|
+
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1043
1051
|
return;
|
|
1044
1052
|
}
|
|
1045
1053
|
|
|
1054
|
+
if (this.#assistantEndedWithSuccessfulYield(msg)) {
|
|
1055
|
+
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1059
|
+
|
|
1046
1060
|
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
1047
1061
|
if (this.#isRetryableError(msg)) {
|
|
1048
1062
|
const didRetry = await this.#handleRetryableError(msg);
|
|
@@ -1179,6 +1193,29 @@ export class AgentSession {
|
|
|
1179
1193
|
);
|
|
1180
1194
|
}
|
|
1181
1195
|
|
|
1196
|
+
#scheduleAutoContinuePrompt(generation: number): void {
|
|
1197
|
+
const continuePrompt = async () => {
|
|
1198
|
+
await this.#promptWithMessage(
|
|
1199
|
+
{
|
|
1200
|
+
role: "developer",
|
|
1201
|
+
content: [{ type: "text", text: "Continue if you have next steps." }],
|
|
1202
|
+
attribution: "agent",
|
|
1203
|
+
timestamp: Date.now(),
|
|
1204
|
+
},
|
|
1205
|
+
"Continue if you have next steps.",
|
|
1206
|
+
{ skipPostPromptRecoveryWait: true },
|
|
1207
|
+
);
|
|
1208
|
+
};
|
|
1209
|
+
this.#schedulePostPromptTask(
|
|
1210
|
+
async signal => {
|
|
1211
|
+
await Promise.resolve();
|
|
1212
|
+
if (signal.aborted) return;
|
|
1213
|
+
await continuePrompt();
|
|
1214
|
+
},
|
|
1215
|
+
{ generation },
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1182
1219
|
#cancelPostPromptTasks(): void {
|
|
1183
1220
|
this.#postPromptTasksAbortController.abort();
|
|
1184
1221
|
this.#postPromptTasksAbortController = new AbortController();
|
|
@@ -3204,7 +3241,6 @@ export class AgentSession {
|
|
|
3204
3241
|
id: task.id,
|
|
3205
3242
|
content: task.content,
|
|
3206
3243
|
status: task.status,
|
|
3207
|
-
notes: task.notes,
|
|
3208
3244
|
})),
|
|
3209
3245
|
}));
|
|
3210
3246
|
}
|
|
@@ -4207,6 +4243,16 @@ export class AgentSession {
|
|
|
4207
4243
|
}
|
|
4208
4244
|
}
|
|
4209
4245
|
}
|
|
4246
|
+
#assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
|
|
4247
|
+
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
4248
|
+
if (!toolCallId) return false;
|
|
4249
|
+
const lastToolCall = assistantMessage.content
|
|
4250
|
+
.slice()
|
|
4251
|
+
.reverse()
|
|
4252
|
+
.find((content): content is ToolCall => content.type === "toolCall");
|
|
4253
|
+
return lastToolCall?.name === "yield" && lastToolCall.id === toolCallId;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4210
4256
|
#enforceRewindBeforeYield(): boolean {
|
|
4211
4257
|
if (!this.#checkpointState || this.#pendingRewindReport) {
|
|
4212
4258
|
return false;
|
|
@@ -4813,6 +4859,9 @@ export class AgentSession {
|
|
|
4813
4859
|
aborted: false,
|
|
4814
4860
|
willRetry: false,
|
|
4815
4861
|
});
|
|
4862
|
+
if (!autoCompactionSignal.aborted && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
4863
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
4864
|
+
}
|
|
4816
4865
|
return;
|
|
4817
4866
|
}
|
|
4818
4867
|
}
|
|
@@ -5064,26 +5113,7 @@ export class AgentSession {
|
|
|
5064
5113
|
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
5065
5114
|
|
|
5066
5115
|
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
|
-
);
|
|
5116
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
5087
5117
|
}
|
|
5088
5118
|
|
|
5089
5119
|
if (willRetry) {
|
|
@@ -5604,6 +5634,14 @@ export class AgentSession {
|
|
|
5604
5634
|
// Bash Execution
|
|
5605
5635
|
// =========================================================================
|
|
5606
5636
|
|
|
5637
|
+
async #saveBashOriginalArtifact(originalText: string): Promise<string | undefined> {
|
|
5638
|
+
try {
|
|
5639
|
+
return await this.sessionManager.saveArtifact(originalText, "bash-original");
|
|
5640
|
+
} catch {
|
|
5641
|
+
return undefined;
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5607
5645
|
/**
|
|
5608
5646
|
* Execute a bash command.
|
|
5609
5647
|
* Adds result to agent context and session.
|
|
@@ -5640,6 +5678,7 @@ export class AgentSession {
|
|
|
5640
5678
|
signal: this.#bashAbortController.signal,
|
|
5641
5679
|
sessionKey: this.sessionId,
|
|
5642
5680
|
timeout: clampTimeout("bash") * 1000,
|
|
5681
|
+
onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
|
|
5643
5682
|
});
|
|
5644
5683
|
|
|
5645
5684
|
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
|
];
|