@oh-my-pi/pi-coding-agent 17.3.0 → 17.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/{CHANGELOG-66nakf5b.md → CHANGELOG-fr2awajz.md} +20 -0
- package/dist/cli.js +2823 -2823
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/extension-flags.d.ts +3 -3
- package/dist/types/cli/flag-tables.d.ts +0 -1
- package/dist/types/cli/setup-cli.d.ts +10 -0
- package/dist/types/cli/update-cli.d.ts +48 -9
- package/dist/types/commands/completions.d.ts +3 -0
- package/dist/types/config/claude-paths.d.ts +7 -0
- package/dist/types/discovery/agents.d.ts +6 -6
- package/dist/types/discovery/helpers.d.ts +3 -4
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +4 -0
- package/dist/types/launch/broker.d.ts +5 -1
- package/dist/types/main.d.ts +1 -1
- package/dist/types/mcp/transports/stdio.d.ts +6 -3
- package/dist/types/modes/components/footer.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/runtime-init.d.ts +3 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/utils/git.d.ts +19 -0
- package/dist/types/utils/shell-snapshot.d.ts +4 -1
- package/package.json +13 -13
- package/src/async/job-manager.ts +33 -4
- package/src/cli/args.ts +14 -3
- package/src/cli/extension-flags.ts +6 -10
- package/src/cli/flag-tables.ts +2 -10
- package/src/cli/gc-cli.ts +13 -3
- package/src/cli/setup-cli.ts +2 -2
- package/src/cli/update-cli.ts +246 -107
- package/src/commands/completions.ts +16 -14
- package/src/config/claude-paths.ts +18 -0
- package/src/config/model-registry.ts +2 -2
- package/src/config.ts +4 -3
- package/src/discovery/agents.ts +7 -7
- package/src/discovery/claude.ts +5 -6
- package/src/discovery/helpers.ts +12 -11
- package/src/extensibility/extensions/runner.ts +5 -0
- package/src/extensibility/extensions/types.ts +5 -0
- package/src/extensibility/legacy-typebox.ts +45 -4
- package/src/launch/broker.ts +26 -4
- package/src/lsp/mux/server.ts +7 -1
- package/src/main.ts +30 -3
- package/src/mcp/transports/stdio.ts +7 -3
- package/src/modes/acp/acp-agent.ts +1 -0
- package/src/modes/components/footer.ts +17 -35
- package/src/modes/components/status-line/component.ts +14 -27
- package/src/modes/controllers/extension-ui-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +16 -9
- package/src/modes/print-mode.ts +1 -0
- package/src/modes/rpc/rpc-client.ts +4 -2
- package/src/modes/rpc/rpc-input.ts +27 -0
- package/src/modes/rpc/rpc-mode.ts +11 -19
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/ui-helpers.ts +74 -53
- package/src/session/agent-session.ts +24 -9
- package/src/session/claude-session-store.ts +4 -3
- package/src/task/executor.ts +8 -4
- package/src/tools/browser/launch.ts +9 -0
- package/src/tools/read-format.ts +11 -10
- package/src/tools/run-scope.ts +4 -2
- package/src/utils/external-editor.ts +10 -11
- package/src/utils/git.ts +27 -0
- package/src/utils/shell-snapshot.ts +5 -1
- package/src/web/search/providers/gemini.ts +4 -9
|
@@ -283,7 +283,7 @@ export class ExtensionUiController {
|
|
|
283
283
|
},
|
|
284
284
|
};
|
|
285
285
|
|
|
286
|
-
extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
|
|
286
|
+
extensionRunner.initialize(actions, contextActions, commandActions, uiContext, "tui");
|
|
287
287
|
|
|
288
288
|
// Subscribe to extension errors
|
|
289
289
|
extensionRunner.onError((error: ExtensionError) => {
|
|
@@ -512,7 +512,7 @@ export class ExtensionUiController {
|
|
|
512
512
|
},
|
|
513
513
|
};
|
|
514
514
|
|
|
515
|
-
extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
|
|
515
|
+
extensionRunner.initialize(actions, contextActions, commandActions, uiContext, "tui");
|
|
516
516
|
}
|
|
517
517
|
|
|
518
518
|
/**
|
|
@@ -359,6 +359,20 @@ function readPersistedToolNames(value: unknown): string[] | undefined {
|
|
|
359
359
|
return value as string[];
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
export function shouldEnterPlanModeOnStartup(
|
|
363
|
+
sessionManager: Pick<SessionManager, "buildSessionContext" | "getEntries">,
|
|
364
|
+
sessionSettings: Pick<Settings, "get">,
|
|
365
|
+
): boolean {
|
|
366
|
+
const hasConversationContext = sessionManager.buildSessionContext().messages.length > 0;
|
|
367
|
+
const hasExplicitMode = sessionManager.getEntries().some(entry => entry.type === "mode_change");
|
|
368
|
+
return (
|
|
369
|
+
!hasConversationContext &&
|
|
370
|
+
!hasExplicitMode &&
|
|
371
|
+
sessionSettings.get("plan.defaultOnStartup") &&
|
|
372
|
+
sessionSettings.get("plan.enabled")
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
362
376
|
/** Options for creating an InteractiveMode instance (for future API use) */
|
|
363
377
|
export interface InteractiveModeOptions {
|
|
364
378
|
/** Providers that were migrated during startup */
|
|
@@ -1140,14 +1154,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1140
1154
|
// execution handoff clear never get dragged back into plan mode. #enterPlanMode
|
|
1141
1155
|
// is idempotent and self-guards against an already-active plan/goal mode; it
|
|
1142
1156
|
// does not check plan.enabled itself.
|
|
1143
|
-
|
|
1144
|
-
const hasExplicitMode = this.sessionManager.getEntries().some(entry => entry.type === "mode_change");
|
|
1145
|
-
const isFreshSession = !hasConversationContext && !hasExplicitMode;
|
|
1146
|
-
if (
|
|
1147
|
-
isFreshSession &&
|
|
1148
|
-
this.session.settings.get("plan.defaultOnStartup") &&
|
|
1149
|
-
this.session.settings.get("plan.enabled")
|
|
1150
|
-
) {
|
|
1157
|
+
if (shouldEnterPlanModeOnStartup(this.sessionManager, this.session.settings)) {
|
|
1151
1158
|
await this.#enterPlanMode();
|
|
1152
1159
|
}
|
|
1153
1160
|
|
|
@@ -4597,7 +4604,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4597
4604
|
this.#uiHelpers.renderSessionContext(sessionContext, options);
|
|
4598
4605
|
}
|
|
4599
4606
|
|
|
4600
|
-
/**
|
|
4607
|
+
/** Build a session context in bounded chunks so terminal input runs between event-loop turns. */
|
|
4601
4608
|
async renderSessionContextIncrementally(
|
|
4602
4609
|
sessionContext: SessionContext,
|
|
4603
4610
|
options: RenderSessionContextOptions,
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -118,6 +118,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
118
118
|
}
|
|
119
119
|
// Set up extensions for print mode (no UI, no command context)
|
|
120
120
|
await initializeExtensions(session, {
|
|
121
|
+
mode: mode === "json" ? "json" : "print",
|
|
121
122
|
reportSendError: (action, err) => {
|
|
122
123
|
process.stderr.write(
|
|
123
124
|
`Extension ${action === "extension_send" ? "sendMessage" : "sendUserMessage"} failed: ${err.message}\n`,
|
|
@@ -62,6 +62,8 @@ export interface RpcClientOptions {
|
|
|
62
62
|
sessionDir?: string;
|
|
63
63
|
/** Additional CLI arguments */
|
|
64
64
|
args?: string[];
|
|
65
|
+
/** Grace period before escalating process termination (default: process utility default, 1000ms) */
|
|
66
|
+
terminationGraceMs?: number;
|
|
65
67
|
/** Custom tools owned by the embedding host and exposed over the RPC transport */
|
|
66
68
|
customTools?: RpcClientCustomTool[];
|
|
67
69
|
}
|
|
@@ -324,7 +326,7 @@ export class RpcClient {
|
|
|
324
326
|
this.#pendingHostToolCalls.clear();
|
|
325
327
|
|
|
326
328
|
try {
|
|
327
|
-
child.kill();
|
|
329
|
+
child.kill(undefined, this.options.terminationGraceMs);
|
|
328
330
|
} catch {
|
|
329
331
|
// The process may already have exited.
|
|
330
332
|
}
|
|
@@ -440,7 +442,7 @@ export class RpcClient {
|
|
|
440
442
|
|
|
441
443
|
const error = new Error("Client stopped");
|
|
442
444
|
const child = this.#process;
|
|
443
|
-
child.kill();
|
|
445
|
+
child.kill(undefined, this.options.terminationGraceMs);
|
|
444
446
|
this.#abortController.abort(error);
|
|
445
447
|
this.#process = null;
|
|
446
448
|
for (const request of this.#pendingRequests.values()) request.reject(error);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readLines } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Claims Bun's singleton stdin reader immediately and exposes a separately readable stream.
|
|
3
5
|
* RPC startup uses this before extension discovery so in-process modules cannot steal protocol input.
|
|
@@ -36,3 +38,28 @@ export function claimRpcInput(): ReadableStream<Uint8Array> {
|
|
|
36
38
|
},
|
|
37
39
|
});
|
|
38
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parses newline-delimited RPC input without letting one malformed line stop
|
|
44
|
+
* subsequent protocol frames.
|
|
45
|
+
*/
|
|
46
|
+
export async function readRpcInputFrames(
|
|
47
|
+
input: ReadableStream<Uint8Array>,
|
|
48
|
+
onFrame: (frame: unknown) => void,
|
|
49
|
+
onParseError: (message: string) => void,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const decoder = new TextDecoder();
|
|
52
|
+
for await (const line of readLines(input)) {
|
|
53
|
+
const text = decoder.decode(line).trim();
|
|
54
|
+
if (!text) continue;
|
|
55
|
+
let parsed: unknown;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(text);
|
|
58
|
+
} catch (error: unknown) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
onParseError(`Failed to parse command: ${message}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
onFrame(parsed);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { once } from "node:events";
|
|
14
14
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
15
15
|
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
16
|
-
import { $env, isRecord,
|
|
16
|
+
import { $env, isRecord, Snowflake } from "@oh-my-pi/pi-utils";
|
|
17
17
|
import { reset as resetCapabilities } from "../../capability";
|
|
18
18
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
19
19
|
import {
|
|
@@ -37,7 +37,7 @@ import { initializeExtensions } from "../runtime-init";
|
|
|
37
37
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
38
38
|
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
39
39
|
import { MAX_RPC_FRAME_BYTES, MAX_RPC_REASSEMBLED_BYTES, RpcFrameEncoder } from "./rpc-frame";
|
|
40
|
-
import { claimRpcInput } from "./rpc-input";
|
|
40
|
+
import { claimRpcInput, readRpcInputFrames } from "./rpc-input";
|
|
41
41
|
import { pageRpcMessages, RPC_MESSAGES_PAGE_BUSY_ERROR, RpcMessagesPageError } from "./rpc-messages";
|
|
42
42
|
import { RpcSubagentRegistry, readRpcSubagentTranscript } from "./rpc-subagents";
|
|
43
43
|
import type {
|
|
@@ -933,6 +933,7 @@ export async function runRpcMode(
|
|
|
933
933
|
|
|
934
934
|
// Set up extensions with RPC-based UI context
|
|
935
935
|
await initializeExtensions(session, {
|
|
936
|
+
mode: "rpc",
|
|
936
937
|
reportSendError: (action, err) => {
|
|
937
938
|
output(error(undefined, action, err.message));
|
|
938
939
|
},
|
|
@@ -1484,23 +1485,14 @@ export async function runRpcMode(
|
|
|
1484
1485
|
// Keep the stdin reader moving: side-channel frames dispatch immediately,
|
|
1485
1486
|
// ordinary commands serialize through inputDispatcher, and bash remains
|
|
1486
1487
|
// background-dispatched so abort_bash can overtake it. Frames are read
|
|
1487
|
-
// line-by-line
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
try {
|
|
1496
|
-
parsed = JSON.parse(text);
|
|
1497
|
-
} catch (e: unknown) {
|
|
1498
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
1499
|
-
output(error(undefined, "parse", `Failed to parse command: ${message}`));
|
|
1500
|
-
continue;
|
|
1501
|
-
}
|
|
1502
|
-
inputDispatcher.dispatch(parsed);
|
|
1503
|
-
}
|
|
1488
|
+
// line-by-line by readRpcInputFrames so a single malformed line is reported
|
|
1489
|
+
// as an error frame and the loop keeps running instead of throwing out of
|
|
1490
|
+
// the reader and killing the whole process (issue #5194).
|
|
1491
|
+
await readRpcInputFrames(
|
|
1492
|
+
input ?? Bun.stdin.stream(),
|
|
1493
|
+
parsed => inputDispatcher.dispatch(parsed),
|
|
1494
|
+
message => output(error(undefined, "parse", message)),
|
|
1495
|
+
);
|
|
1504
1496
|
|
|
1505
1497
|
// stdin closed — RPC client is gone. Fail pending side-channel requests
|
|
1506
1498
|
// first so active/queued commands can settle, then drain accepted work.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
10
10
|
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
11
|
-
import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
|
|
11
|
+
import type { ExtensionError, ExtensionMode, ExtensionUIContext } from "../extensibility/extensions/types";
|
|
12
12
|
import type { AgentSession } from "../session/agent-session";
|
|
13
13
|
import { USER_INTERRUPT_LABEL } from "../session/messages";
|
|
14
14
|
|
|
@@ -22,6 +22,8 @@ export interface InitializeExtensionsOptions {
|
|
|
22
22
|
reportRuntimeError: (error: ExtensionError) => void;
|
|
23
23
|
/** Optional shutdown hook (rpc mode signals its loop; print mode is a no-op). */
|
|
24
24
|
onShutdown?: () => void;
|
|
25
|
+
/** Pi-compatible mode exposed to extension contexts. Defaults to `"print"`. */
|
|
26
|
+
mode?: ExtensionMode;
|
|
25
27
|
/** Optional UI context (rpc supplies one; print runs headless). */
|
|
26
28
|
uiContext?: ExtensionUIContext;
|
|
27
29
|
/** Optional lifecycle hook for extension-originated messages that can start an agent turn. */
|
|
@@ -44,6 +46,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
|
|
44
46
|
reportSendError,
|
|
45
47
|
reportRuntimeError,
|
|
46
48
|
onShutdown,
|
|
49
|
+
mode = "print",
|
|
47
50
|
uiContext,
|
|
48
51
|
markAgentInvokingMessage,
|
|
49
52
|
trackAgentInvokingMessage,
|
|
@@ -137,6 +140,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
|
|
137
140
|
compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
|
|
138
141
|
},
|
|
139
142
|
uiContext,
|
|
143
|
+
mode,
|
|
140
144
|
);
|
|
141
145
|
|
|
142
146
|
runner.onError(reportRuntimeError);
|
|
@@ -33,8 +33,8 @@ import {
|
|
|
33
33
|
import { SkillMessageComponent } from "../../modes/components/skill-message";
|
|
34
34
|
import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder";
|
|
35
35
|
import { ToolActivityContainer } from "../../modes/components/tool-activity";
|
|
36
|
-
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
37
|
-
import { TranscriptBlock } from "../../modes/components/transcript-container";
|
|
36
|
+
import { ToolExecutionComponent, type ToolExecutionHandle } from "../../modes/components/tool-execution";
|
|
37
|
+
import { TranscriptBlock, TranscriptContainer } from "../../modes/components/transcript-container";
|
|
38
38
|
import { createUsageRowBlock } from "../../modes/components/usage-row";
|
|
39
39
|
import { UserMessageComponent } from "../../modes/components/user-message";
|
|
40
40
|
import { decodeStreamedToolArgs, streamingStringKeysForTool } from "../../modes/controllers/tool-args-reveal";
|
|
@@ -325,7 +325,7 @@ export class UiHelpers {
|
|
|
325
325
|
while (!steps.next().done) {}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
/**
|
|
328
|
+
/** Build a session context in bounded chunks so terminal input runs between event-loop turns. */
|
|
329
329
|
async renderSessionContextIncrementally(
|
|
330
330
|
sessionContext: SessionContext,
|
|
331
331
|
options: RenderSessionContextOptions,
|
|
@@ -729,18 +729,23 @@ export class UiHelpers {
|
|
|
729
729
|
}
|
|
730
730
|
|
|
731
731
|
async renderInitialMessages(options: RenderInitialMessagesOptions = {}): Promise<void> {
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
732
|
+
// Build against a detached container. Incremental construction still yields
|
|
733
|
+
// to terminal input, while paints keep using the complete visible transcript
|
|
734
|
+
// until the replacement is ready to swap in.
|
|
735
|
+
const visibleChatContainer = this.ctx.chatContainer;
|
|
736
|
+
const stagedChatContainer = new TranscriptContainer();
|
|
737
|
+
stagedChatContainer.setToolActivityVisible(!this.ctx.hideToolActivity);
|
|
738
|
+
const preservedChatChildren = options.preserveExistingChat ? [...visibleChatContainer.children] : undefined;
|
|
739
|
+
const previousTranscriptMessageComponents = this.ctx.transcriptMessageComponents;
|
|
740
|
+
const previousPendingTools = this.ctx.pendingTools;
|
|
741
|
+
const previousPendingBashComponents = this.ctx.pendingBashComponents;
|
|
742
|
+
const previousPendingPythonComponents = this.ctx.pendingPythonComponents;
|
|
743
|
+
const previousLastAssistantUsage = this.ctx.lastAssistantUsage;
|
|
738
744
|
const chatWasAlreadyRendered = this.ctx.initialChatRendered;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
745
|
+
|
|
746
|
+
this.ctx.chatContainer = stagedChatContainer;
|
|
747
|
+
this.ctx.transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
|
|
748
|
+
this.ctx.pendingTools = new Map<string, ToolExecutionHandle>();
|
|
744
749
|
this.ctx.pendingMessagesContainer.disposeChildren();
|
|
745
750
|
this.ctx.pendingBashComponents = [];
|
|
746
751
|
this.ctx.pendingPythonComponents = [];
|
|
@@ -751,13 +756,6 @@ export class UiHelpers {
|
|
|
751
756
|
// (focus attach/unfocus while a tool executes) keep dangling toolCalls so
|
|
752
757
|
// the in-flight call re-renders as pending instead of vanishing;
|
|
753
758
|
// renderSessionContext then keeps it in `pendingTools` for live routing.
|
|
754
|
-
let terminalHistoryCleared = false;
|
|
755
|
-
const renderChunk = options.clearTerminalHistory
|
|
756
|
-
? () => {
|
|
757
|
-
this.ctx.ui.requestRender(true, { clearScrollback: !terminalHistoryCleared });
|
|
758
|
-
terminalHistoryCleared = true;
|
|
759
|
-
}
|
|
760
|
-
: undefined;
|
|
761
759
|
let context = this.ctx.viewSession.buildTranscriptSessionContext({
|
|
762
760
|
collapseCompactedHistory: settings.get("display.collapseCompacted"),
|
|
763
761
|
keepDanglingToolCalls: this.ctx.viewSession.isStreaming,
|
|
@@ -770,6 +768,7 @@ export class UiHelpers {
|
|
|
770
768
|
// every attempt.
|
|
771
769
|
populateHistory: false,
|
|
772
770
|
};
|
|
771
|
+
let committed = false;
|
|
773
772
|
this.ctx.initialChatRendered = false;
|
|
774
773
|
try {
|
|
775
774
|
while (true) {
|
|
@@ -777,8 +776,6 @@ export class UiHelpers {
|
|
|
777
776
|
// Live events mutate the same component maps; keep their replay atomic so
|
|
778
777
|
// a delta cannot land halfway through rebuilding its pending tool block.
|
|
779
778
|
this.ctx.renderSessionContext(context, renderOptions);
|
|
780
|
-
} else if (renderChunk) {
|
|
781
|
-
await this.ctx.renderSessionContextIncrementally(context, renderOptions, renderChunk);
|
|
782
779
|
} else {
|
|
783
780
|
await this.ctx.renderSessionContextIncrementally(context, renderOptions);
|
|
784
781
|
}
|
|
@@ -790,49 +787,73 @@ export class UiHelpers {
|
|
|
790
787
|
// yielded. The display callback stayed gated by initialChatRendered;
|
|
791
788
|
// discard the stale partial tree and replay the current session once
|
|
792
789
|
// more instead of letting a reentrant synchronous rebuild interleave.
|
|
793
|
-
|
|
790
|
+
stagedChatContainer.disposeChildren();
|
|
791
|
+
this.ctx.transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
|
|
792
|
+
this.ctx.pendingTools.clear();
|
|
794
793
|
this.ctx.pendingBashComponents = [];
|
|
795
794
|
this.ctx.pendingPythonComponents = [];
|
|
796
|
-
terminalHistoryCleared = false;
|
|
797
795
|
context = this.ctx.viewSession.buildTranscriptSessionContext({
|
|
798
796
|
collapseCompactedHistory: settings.get("display.collapseCompacted"),
|
|
799
797
|
keepDanglingToolCalls: this.ctx.viewSession.isStreaming,
|
|
800
798
|
});
|
|
801
799
|
replayEntryCount = this.ctx.viewSession.sessionManager.getEntries().length;
|
|
802
800
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
801
|
+
|
|
802
|
+
const replayedChatChildren = [...stagedChatContainer.children];
|
|
803
|
+
stagedChatContainer.clear();
|
|
804
|
+
this.ctx.chatContainer = visibleChatContainer;
|
|
805
|
+
if (preservedChatChildren) {
|
|
806
|
+
visibleChatContainer.clear();
|
|
807
|
+
} else {
|
|
808
|
+
visibleChatContainer.disposeChildren();
|
|
811
809
|
}
|
|
812
|
-
|
|
810
|
+
for (const child of replayedChatChildren) {
|
|
811
|
+
visibleChatContainer.addChild(child);
|
|
812
|
+
}
|
|
813
|
+
if (preservedChatChildren) {
|
|
814
|
+
for (const child of preservedChatChildren) {
|
|
815
|
+
visibleChatContainer.addChild(child);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
committed = true;
|
|
813
819
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
+
if (!this.ctx.focusedAgentId) {
|
|
821
|
+
for (const message of context.messages) {
|
|
822
|
+
if (message.role !== "user" || message.synthetic) continue;
|
|
823
|
+
const text = this.getUserMessageText(message);
|
|
824
|
+
if (text) this.ctx.editor.addToHistory(text);
|
|
825
|
+
}
|
|
820
826
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
if (preservedChatChildren && preservedChatChildren.length > 0) {
|
|
830
|
-
for (const child of preservedChatChildren) {
|
|
831
|
-
this.ctx.chatContainer.addChild(child);
|
|
827
|
+
|
|
828
|
+
// Show compaction info if session was compacted.
|
|
829
|
+
const allEntries = this.ctx.viewSession.sessionManager.getEntries();
|
|
830
|
+
let compactionCount = 0;
|
|
831
|
+
for (const entry of allEntries) {
|
|
832
|
+
if (entry.type === "compaction") {
|
|
833
|
+
compactionCount++;
|
|
834
|
+
}
|
|
832
835
|
}
|
|
833
|
-
|
|
836
|
+
if (compactionCount > 0) {
|
|
837
|
+
const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
|
|
838
|
+
this.ctx.showStatus(`Session compacted ${times}`);
|
|
839
|
+
}
|
|
840
|
+
if (options.clearTerminalHistory) {
|
|
841
|
+
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
842
|
+
} else {
|
|
843
|
+
this.ctx.ui.requestRender();
|
|
844
|
+
}
|
|
845
|
+
} finally {
|
|
846
|
+
if (!committed) {
|
|
847
|
+
this.ctx.chatContainer = visibleChatContainer;
|
|
848
|
+
this.ctx.transcriptMessageComponents = previousTranscriptMessageComponents;
|
|
849
|
+
this.ctx.pendingTools = previousPendingTools;
|
|
850
|
+
this.ctx.pendingBashComponents = previousPendingBashComponents;
|
|
851
|
+
this.ctx.pendingPythonComponents = previousPendingPythonComponents;
|
|
852
|
+
this.ctx.lastAssistantUsage = previousLastAssistantUsage;
|
|
853
|
+
stagedChatContainer.disposeChildren();
|
|
854
|
+
}
|
|
855
|
+
this.ctx.initialChatRendered = committed ? true : chatWasAlreadyRendered;
|
|
834
856
|
}
|
|
835
|
-
this.ctx.initialChatRendered = true;
|
|
836
857
|
}
|
|
837
858
|
|
|
838
859
|
clearEditor(): void {
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type AsideMessage,
|
|
38
38
|
type BeforeToolCallContext,
|
|
39
39
|
type BeforeToolCallResult,
|
|
40
|
+
EventLoopKeepalive,
|
|
40
41
|
resolveTelemetry,
|
|
41
42
|
type StreamFn,
|
|
42
43
|
TERMINAL_TOOL_RESULT_ABORT_REASON,
|
|
@@ -1213,15 +1214,28 @@ export class AgentSession {
|
|
|
1213
1214
|
}
|
|
1214
1215
|
},
|
|
1215
1216
|
scheduleIdleFlush: run => {
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1217
|
+
const keepalive = new EventLoopKeepalive();
|
|
1218
|
+
try {
|
|
1219
|
+
this.#schedulePostPromptTask(
|
|
1220
|
+
async () => {
|
|
1221
|
+
try {
|
|
1222
|
+
await run();
|
|
1223
|
+
} finally {
|
|
1224
|
+
keepalive[Symbol.dispose]();
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
delayMs: 1,
|
|
1229
|
+
onSkip: () => {
|
|
1230
|
+
keepalive[Symbol.dispose]();
|
|
1231
|
+
this.yieldQueue.cancelIdleFlushScheduling();
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
);
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
keepalive[Symbol.dispose]();
|
|
1237
|
+
throw error;
|
|
1238
|
+
}
|
|
1225
1239
|
},
|
|
1226
1240
|
});
|
|
1227
1241
|
this.yieldQueue.register<LaunchCompletionEntry>(LAUNCH_COMPLETION_MESSAGE_TYPE, {
|
|
@@ -5647,6 +5661,7 @@ export class AgentSession {
|
|
|
5647
5661
|
|
|
5648
5662
|
return {
|
|
5649
5663
|
ui: noOpUIContext,
|
|
5664
|
+
mode: "print",
|
|
5650
5665
|
hasUI: false,
|
|
5651
5666
|
cwd: this.sessionManager.getCwd(),
|
|
5652
5667
|
sessionManager: this.sessionManager,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type * as fsTypes from "node:fs";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import type {
|
|
6
5
|
AssistantMessage,
|
|
@@ -13,6 +12,7 @@ import type {
|
|
|
13
12
|
UserMessage,
|
|
14
13
|
} from "@oh-my-pi/pi-ai";
|
|
15
14
|
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import { resolveClaudePaths } from "../config/claude-paths";
|
|
16
16
|
import { collectForeignJsonRecords, type ForeignJsonRecord, readForeignJsonRecords } from "./foreign-session-jsonl";
|
|
17
17
|
import type { ForeignSessionInfo, ForeignSessionStore } from "./foreign-session-store";
|
|
18
18
|
import type { ModelChangeEntry, SessionMessageEntry } from "./session-entries";
|
|
@@ -99,7 +99,8 @@ async function readHistoryIndex(file: string): Promise<Map<string, ClaudeHistory
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
async function readRegisteredProjects(root: string): Promise<string[]> {
|
|
102
|
-
const
|
|
102
|
+
const { configDir, configFile } = resolveClaudePaths();
|
|
103
|
+
const config = root === configDir ? configFile : path.join(path.dirname(root), ".claude.json");
|
|
103
104
|
try {
|
|
104
105
|
const parsed: unknown = await Bun.file(config).json();
|
|
105
106
|
if (!isRecord(parsed) || !isRecord(parsed.projects)) return [];
|
|
@@ -306,7 +307,7 @@ export class ClaudeSessionStore implements ForeignSessionStore {
|
|
|
306
307
|
readonly #root: string;
|
|
307
308
|
|
|
308
309
|
/** Creates a store rooted at Claude's data directory, or at a fixture root when supplied. */
|
|
309
|
-
constructor(root: string =
|
|
310
|
+
constructor(root: string = resolveClaudePaths().configDir) {
|
|
310
311
|
this.#root = path.resolve(root);
|
|
311
312
|
}
|
|
312
313
|
|
package/src/task/executor.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
|
|
9
|
-
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
9
|
+
import { EventLoopKeepalive, recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
10
|
import type { Api, Model, ServiceTierByFamily, Usage } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import { ASYNC_JOB_MANAGER_SHUTDOWN_REASON, AsyncJobManager } from "../async";
|
|
@@ -493,6 +493,8 @@ export interface ExecutorOptions {
|
|
|
493
493
|
keepAlive?: boolean;
|
|
494
494
|
/** Internal ownership handoff for cleanup that outlives the visible Task result. */
|
|
495
495
|
onCleanupDeferred?: (completion: Promise<void>) => void;
|
|
496
|
+
/** Internal cleanup grace override for deterministic lifecycle tests. */
|
|
497
|
+
cleanupGraceMs?: number;
|
|
496
498
|
}
|
|
497
499
|
|
|
498
500
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -1866,6 +1868,7 @@ async function driveSessionToYield(
|
|
|
1866
1868
|
monitor: SubagentRunMonitor,
|
|
1867
1869
|
task: string,
|
|
1868
1870
|
): Promise<DriveOutcome> {
|
|
1871
|
+
using _keepalive = new EventLoopKeepalive();
|
|
1869
1872
|
const abortSignal = monitor.abortSignal;
|
|
1870
1873
|
let exitCode = 0;
|
|
1871
1874
|
let error: string | undefined;
|
|
@@ -2657,6 +2660,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2657
2660
|
signal,
|
|
2658
2661
|
onProgress,
|
|
2659
2662
|
} = options;
|
|
2663
|
+
const cleanupGraceMs = options.cleanupGraceMs ?? TASK_ABORT_CLEANUP_GRACE_MS;
|
|
2660
2664
|
const startTime = Date.now();
|
|
2661
2665
|
// Set by the session's onFirstChatDispatch hook the first time the agent
|
|
2662
2666
|
// loop dispatches a chat request to the provider — the launch-complete boundary.
|
|
@@ -3328,7 +3332,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
3328
3332
|
error = err instanceof Error ? err.stack || err.message : String(err);
|
|
3329
3333
|
}
|
|
3330
3334
|
} finally {
|
|
3331
|
-
const cleanupDeadlineAt = Date.now() +
|
|
3335
|
+
const cleanupDeadlineAt = Date.now() + cleanupGraceMs;
|
|
3332
3336
|
const cleanupChangeStatus =
|
|
3333
3337
|
worktree === undefined
|
|
3334
3338
|
? "This task was not isolated, so its changes may remain in the working directory."
|
|
@@ -3339,8 +3343,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
3339
3343
|
lateCleanups.push(completion);
|
|
3340
3344
|
exitCode = 1;
|
|
3341
3345
|
aborted = true;
|
|
3342
|
-
abortReasonText = `cleanup exceeded ${
|
|
3343
|
-
error ??= `Task aborted. Cleanup did not finish within ${
|
|
3346
|
+
abortReasonText = `cleanup exceeded ${cleanupGraceMs} ms`;
|
|
3347
|
+
error ??= `Task aborted. Cleanup did not finish within ${cleanupGraceMs} ms. ${cleanupChangeStatus}`;
|
|
3344
3348
|
};
|
|
3345
3349
|
if (abortSignal.aborted) {
|
|
3346
3350
|
aborted = monitor.isAbortedRun();
|
|
@@ -204,6 +204,15 @@ function isExecutableFile(p: string): boolean {
|
|
|
204
204
|
|
|
205
205
|
async function isChromiumExecutable(p: string): Promise<boolean> {
|
|
206
206
|
if (!isExecutableFile(p)) return false;
|
|
207
|
+
// The version probe below launches the candidate. It exists to reject
|
|
208
|
+
// non-Chromium `chrome`/`chromium` wrapper scripts that appear on a Linux
|
|
209
|
+
// PATH (ecb22957, "validate Linux browser executables"). On Windows and
|
|
210
|
+
// macOS the candidates are fixed GUI application paths, not PATH wrappers,
|
|
211
|
+
// and executing them is harmful: a GUI `chrome.exe --version` does not print
|
|
212
|
+
// to a detached stdout and can hand off to the user's running instance,
|
|
213
|
+
// opening/activating a normal browser window (#8445). Confine the probe to
|
|
214
|
+
// Linux and trust the executable-file check elsewhere.
|
|
215
|
+
if (process.platform !== "linux") return true;
|
|
207
216
|
try {
|
|
208
217
|
const probeTimeoutMs = 3000;
|
|
209
218
|
const proc = Bun.spawn([p, "--version"], {
|
package/src/tools/read-format.ts
CHANGED
|
@@ -37,16 +37,17 @@ export interface HashlineHeaderContext {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function formatReadHashlineHeader(displayPath: string, tag: string): string {
|
|
40
|
-
// In-workspace reads
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
40
|
+
// In-workspace reads keep their workspace-relative path (e.g.
|
|
41
|
+
// `src/settings.json`), not just the basename: collapsing to the bare name
|
|
42
|
+
// made a header ambiguous whenever another same-named file exists at cwd —
|
|
43
|
+
// the edit tool would resolve the bare name against cwd, hit the wrong
|
|
44
|
+
// file, and reject the valid edit via the snapshot-tag guard (the authored
|
|
45
|
+
// path exists, so Patcher's tag-path recovery never runs). The relative
|
|
46
|
+
// path stays directly resolvable against cwd and names the file uniquely.
|
|
47
|
+
// Out-of-workspace reads use an absolute displayPath; `shortenPath` keeps
|
|
48
|
+
// `~/.claude/...` (round-trips through resolveToCwd's ~ expansion) instead
|
|
49
|
+
// of leaking the full home path into the read output.
|
|
50
|
+
const anchor = path.isAbsolute(displayPath) ? shortenPath(displayPath) : displayPath;
|
|
50
51
|
return formatHashlineHeader(anchor, tag);
|
|
51
52
|
}
|
|
52
53
|
|
package/src/tools/run-scope.ts
CHANGED
|
@@ -38,7 +38,7 @@ interface ObservedPromiseState {
|
|
|
38
38
|
const observedBrowserPromises = new WeakMap<Promise<unknown>, ObservedPromiseState>();
|
|
39
39
|
const observedPromiseConstructor = { [Symbol.species]: Promise };
|
|
40
40
|
|
|
41
|
-
type PromiseCombinatorName = "all" | "race";
|
|
41
|
+
type PromiseCombinatorName = "all" | "race" | "allSettled" | "any";
|
|
42
42
|
type PromiseCombinator = (this: PromiseConstructor, values: Iterable<unknown>) => Promise<unknown>;
|
|
43
43
|
|
|
44
44
|
interface PromiseCombinatorTrackingContext {
|
|
@@ -46,11 +46,13 @@ interface PromiseCombinatorTrackingContext {
|
|
|
46
46
|
onFloatingRejection: FloatingRejectionHandler;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const PROMISE_COMBINATORS: readonly PromiseCombinatorName[] = ["all", "race"];
|
|
49
|
+
const PROMISE_COMBINATORS: readonly PromiseCombinatorName[] = ["all", "race", "allSettled", "any"];
|
|
50
50
|
const NativePromise = Promise;
|
|
51
51
|
const nativePromiseCombinators: Record<PromiseCombinatorName, PromiseCombinator> = {
|
|
52
52
|
all: Promise.all,
|
|
53
53
|
race: Promise.race,
|
|
54
|
+
allSettled: Promise.allSettled,
|
|
55
|
+
any: Promise.any,
|
|
54
56
|
};
|
|
55
57
|
const promiseCombinatorTracking = new AsyncLocalStorage<PromiseCombinatorTrackingContext>();
|
|
56
58
|
let previousPromiseDescriptor: PropertyDescriptor | undefined;
|