@oh-my-pi/pi-coding-agent 15.11.8 → 15.12.1
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 +46 -2
- package/dist/cli.js +8095 -7704
- package/dist/types/collab/crypto.d.ts +1 -6
- package/dist/types/collab/guest.d.ts +2 -0
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +14 -1
- package/dist/types/config/settings-schema.d.ts +52 -6
- package/dist/types/export/custom-share.d.ts +1 -2
- package/dist/types/export/html/index.d.ts +39 -1
- package/dist/types/export/share.d.ts +43 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +19 -1
- package/dist/types/modes/components/status-line/component.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +7 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/session-observer-registry.d.ts +7 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/codex-auto-reset.d.ts +8 -4
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/types.d.ts +9 -0
- package/dist/types/tools/tool-result.d.ts +2 -0
- package/package.json +13 -14
- package/scripts/build-binary.ts +4 -0
- package/scripts/bundle-dist.ts +4 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/src/collab/crypto.ts +10 -4
- package/src/collab/guest.ts +31 -2
- package/src/collab/host.ts +73 -11
- package/src/collab/protocol.ts +48 -7
- package/src/commands/join.ts +1 -1
- package/src/config/settings-schema.ts +54 -5
- package/src/config/settings.ts +12 -0
- package/src/export/custom-share.ts +1 -1
- package/src/export/html/index.ts +122 -17
- package/src/export/html/share-loader.js +102 -0
- package/src/export/html/template.css +745 -459
- package/src/export/html/template.html +6 -3
- package/src/export/html/template.js +240 -915
- package/src/export/html/tool-views.generated.js +38 -0
- package/src/export/share.ts +268 -0
- package/src/internal-urls/docs-index.generated.ts +73 -73
- package/src/lsp/index.ts +11 -0
- package/src/main.ts +22 -9
- package/src/modes/components/agent-hub.ts +541 -410
- package/src/modes/components/status-line/component.ts +38 -5
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tips.txt +3 -1
- package/src/modes/controllers/command-controller.ts +55 -96
- package/src/modes/controllers/event-controller.ts +45 -16
- package/src/modes/controllers/input-controller.ts +104 -4
- package/src/modes/controllers/selector-controller.ts +11 -15
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/interactive-mode.ts +44 -2
- package/src/modes/session-observer-registry.ts +11 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +16 -13
- package/src/prompts/tools/job.md +1 -1
- package/src/session/agent-session.ts +87 -19
- package/src/session/codex-auto-reset.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +62 -35
- package/src/task/executor.ts +14 -0
- package/src/task/index.ts +5 -1
- package/src/task/render.ts +76 -5
- package/src/task/types.ts +9 -0
- package/src/tiny/worker.ts +17 -95
- package/src/tools/ast-grep.ts +3 -1
- package/src/tools/find.ts +3 -1
- package/src/tools/gh.ts +20 -6
- package/src/tools/irc.ts +4 -0
- package/src/tools/job.ts +18 -13
- package/src/tools/memory-recall.ts +2 -0
- package/src/tools/search.ts +3 -1
- package/src/tools/tool-result.ts +8 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/export/html/template.macro.d.ts +0 -5
- package/dist/types/tiny/compiled-runtime.d.ts +0 -35
- package/scripts/generate-template.ts +0 -33
- package/src/bun-imports.d.ts +0 -28
- package/src/export/html/template.generated.ts +0 -2
- package/src/export/html/template.macro.ts +0 -25
- package/src/tiny/compiled-runtime.ts +0 -179
|
@@ -841,19 +841,6 @@ export class SelectorController {
|
|
|
841
841
|
});
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
#clearTransientSessionUi(): void {
|
|
845
|
-
if (this.ctx.loadingAnimation) {
|
|
846
|
-
this.ctx.loadingAnimation.stop();
|
|
847
|
-
this.ctx.loadingAnimation = undefined;
|
|
848
|
-
}
|
|
849
|
-
this.ctx.statusContainer.clear();
|
|
850
|
-
this.ctx.pendingMessagesContainer.clear();
|
|
851
|
-
this.ctx.compactionQueuedMessages = [];
|
|
852
|
-
this.ctx.streamingComponent = undefined;
|
|
853
|
-
this.ctx.streamingMessage = undefined;
|
|
854
|
-
this.ctx.pendingTools.clear();
|
|
855
|
-
}
|
|
856
|
-
|
|
857
844
|
#refreshSessionTerminalTitle(): void {
|
|
858
845
|
const sessionManager = this.ctx.sessionManager as {
|
|
859
846
|
getSessionName?: () => string | undefined;
|
|
@@ -875,7 +862,7 @@ export class SelectorController {
|
|
|
875
862
|
}
|
|
876
863
|
this.#refreshSessionTerminalTitle();
|
|
877
864
|
|
|
878
|
-
this
|
|
865
|
+
this.ctx.clearTransientSessionUi();
|
|
879
866
|
this.ctx.statusLine.invalidate();
|
|
880
867
|
this.ctx.statusLine.setSessionStartTime(Date.now());
|
|
881
868
|
this.ctx.updateEditorTopBorder();
|
|
@@ -887,7 +874,7 @@ export class SelectorController {
|
|
|
887
874
|
}
|
|
888
875
|
|
|
889
876
|
async handleResumeSession(sessionPath: string): Promise<void> {
|
|
890
|
-
this
|
|
877
|
+
this.ctx.clearTransientSessionUi();
|
|
891
878
|
|
|
892
879
|
const previousCwd = this.ctx.sessionManager.getCwd();
|
|
893
880
|
// Switch session via AgentSession (emits hook and tool session events). The
|
|
@@ -1180,16 +1167,25 @@ export class SelectorController {
|
|
|
1180
1167
|
const done = () => {
|
|
1181
1168
|
hub?.dispose();
|
|
1182
1169
|
overlayHandle?.hide();
|
|
1170
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
1183
1171
|
this.ctx.ui.requestRender();
|
|
1184
1172
|
};
|
|
1185
1173
|
|
|
1186
1174
|
hub = new AgentHubOverlayComponent({
|
|
1187
1175
|
observers,
|
|
1188
1176
|
hubKeys,
|
|
1177
|
+
expandKeys: this.ctx.keybindings.getKeys("app.tools.expand"),
|
|
1189
1178
|
onDone: done,
|
|
1190
1179
|
requestRender: () => this.ctx.ui.requestRender(),
|
|
1191
1180
|
registry: this.ctx.collabGuest?.agentRegistry,
|
|
1192
1181
|
remote: this.ctx.collabGuest?.hubRemote,
|
|
1182
|
+
ui: this.ctx.ui,
|
|
1183
|
+
getTool: name => this.ctx.session.getToolByName(name),
|
|
1184
|
+
getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
|
|
1185
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
1186
|
+
hideThinkingBlock: () => this.ctx.hideThinkingBlock,
|
|
1187
|
+
focusAgent: id => this.ctx.focusAgentSession(id),
|
|
1188
|
+
sessionFile: this.ctx.sessionManager.getSessionFile() ?? null,
|
|
1193
1189
|
});
|
|
1194
1190
|
|
|
1195
1191
|
overlayHandle = this.ctx.ui.showOverlay(hub, {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionFocusController - Weak retargeting primitive between the rendering/
|
|
3
|
+
* input layer and the AgentSession it displays.
|
|
4
|
+
*
|
|
5
|
+
* Focusing re-points the transcript, streaming event subscription, status
|
|
6
|
+
* line, and editor prompt/interrupt at a subagent's live AgentSession (from
|
|
7
|
+
* AgentRegistry) without touching the main session underneath; unfocusing
|
|
8
|
+
* re-attaches the main session and rebuilds the transcript from its
|
|
9
|
+
* authoritative state.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AgentLifecycleManager } from "../../registry/agent-lifecycle";
|
|
13
|
+
import { AgentRegistry, MAIN_AGENT_ID, type RegistryEvent } from "../../registry/agent-registry";
|
|
14
|
+
import type { AgentSession } from "../../session/agent-session";
|
|
15
|
+
import type { InteractiveModeContext } from "../types";
|
|
16
|
+
|
|
17
|
+
export class SessionFocusController {
|
|
18
|
+
#focusedAgentId: string | undefined;
|
|
19
|
+
/** Session currently attached while focused; undefined when unfocused. */
|
|
20
|
+
#attachedSession: AgentSession | undefined;
|
|
21
|
+
#registryUnsubscribe: (() => void) | undefined;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private ctx: InteractiveModeContext,
|
|
25
|
+
private registry: AgentRegistry = AgentRegistry.global(),
|
|
26
|
+
private lifecycle: () => AgentLifecycleManager = () => AgentLifecycleManager.global(),
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
get focusedAgentId(): string | undefined {
|
|
30
|
+
return this.#focusedAgentId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Focused live session, undefined when unfocused. */
|
|
34
|
+
get target(): AgentSession | undefined {
|
|
35
|
+
return this.#attachedSession;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Focus the main view on an agent's live session. Throws an Error with a user-displayable message. */
|
|
39
|
+
async focusAgent(id: string): Promise<void> {
|
|
40
|
+
if (this.ctx.collabGuest) throw new Error("Viewing agents is unavailable in a collab session.");
|
|
41
|
+
if (id === MAIN_AGENT_ID) return this.unfocus();
|
|
42
|
+
const session = await this.lifecycle().ensureLive(id);
|
|
43
|
+
if (id === this.#focusedAgentId && session === this.#attachedSession) return;
|
|
44
|
+
this.#focusedAgentId = id;
|
|
45
|
+
this.#attachedSession = session;
|
|
46
|
+
this.#registryUnsubscribe ??= this.registry.onChange(e => this.#onRegistryEvent(e));
|
|
47
|
+
await this.#attach(session);
|
|
48
|
+
this.ctx.showStatus(`Viewing agent ${id} — Esc returns to main, ←← hops to parent`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Focus the focused agent's parent agent, falling back to the main session. No-op when unfocused. */
|
|
52
|
+
async focusParent(): Promise<void> {
|
|
53
|
+
if (!this.#focusedAgentId) return;
|
|
54
|
+
const parentId = this.registry.get(this.#focusedAgentId)?.parentId;
|
|
55
|
+
if (parentId && parentId !== MAIN_AGENT_ID && this.registry.get(parentId)) {
|
|
56
|
+
return this.focusAgent(parentId);
|
|
57
|
+
}
|
|
58
|
+
return this.unfocus();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Return to the main session. No-op when unfocused. */
|
|
62
|
+
async unfocus(): Promise<void> {
|
|
63
|
+
if (!this.#focusedAgentId) return;
|
|
64
|
+
this.#focusedAgentId = undefined;
|
|
65
|
+
this.#attachedSession = undefined;
|
|
66
|
+
await this.#attach(this.ctx.session);
|
|
67
|
+
this.ctx.showStatus("Returned to main session");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
dispose(): void {
|
|
71
|
+
this.#registryUnsubscribe?.();
|
|
72
|
+
this.#registryUnsubscribe = undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#onRegistryEvent(event: RegistryEvent): void {
|
|
76
|
+
if (event.ref.id !== this.#focusedAgentId) return;
|
|
77
|
+
const gone = event.type === "removed";
|
|
78
|
+
const dead = event.type === "status_changed" && (event.ref.status === "parked" || event.ref.status === "aborted");
|
|
79
|
+
if (!gone && !dead) return;
|
|
80
|
+
void this.unfocus().then(() => {
|
|
81
|
+
this.ctx.showStatus(`Agent ${event.ref.id} is ${gone ? "gone" : event.ref.status}; returned to main session`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Retarget core, both directions: swap subscription, transcript, and status line onto `target`. */
|
|
86
|
+
async #attach(target: AgentSession): Promise<void> {
|
|
87
|
+
this.ctx.unsubscribe?.();
|
|
88
|
+
this.ctx.clearTransientSessionUi();
|
|
89
|
+
this.ctx.eventController.resetTranscriptAnchors();
|
|
90
|
+
// Orphan-delta guard: when attaching mid-turn the message_start for the
|
|
91
|
+
// in-flight assistant message predates the attach. message_update carries
|
|
92
|
+
// the full accumulating message, so synthesize the missing start before
|
|
93
|
+
// the first orphaned update; every other handler is tolerant of unknown
|
|
94
|
+
// anchors (guarded by streamingComponent/pendingTools lookups).
|
|
95
|
+
let assistantStreamSynced = false;
|
|
96
|
+
this.ctx.unsubscribe = target.subscribe(async event => {
|
|
97
|
+
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
98
|
+
assistantStreamSynced = true;
|
|
99
|
+
} else if (event.type === "message_update" && event.message.role === "assistant" && !assistantStreamSynced) {
|
|
100
|
+
assistantStreamSynced = true;
|
|
101
|
+
await this.ctx.eventController.handleEvent({ type: "message_start", message: event.message });
|
|
102
|
+
}
|
|
103
|
+
await this.ctx.eventController.handleEvent(event);
|
|
104
|
+
});
|
|
105
|
+
this.ctx.statusLine.setSession(target, this.#focusedAgentId);
|
|
106
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
107
|
+
// Mid-turn attach: no agent_start will arrive; arm the loader/turn state manually.
|
|
108
|
+
if (target.isStreaming) await this.ctx.eventController.handleEvent({ type: "agent_start" });
|
|
109
|
+
this.ctx.updateEditorBorderColor();
|
|
110
|
+
this.ctx.ui.requestRender();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -122,6 +122,7 @@ import { InputController } from "./controllers/input-controller";
|
|
|
122
122
|
import { MCPCommandController } from "./controllers/mcp-command-controller";
|
|
123
123
|
import { OmfgController } from "./controllers/omfg-controller";
|
|
124
124
|
import { SelectorController } from "./controllers/selector-controller";
|
|
125
|
+
import { SessionFocusController } from "./controllers/session-focus-controller";
|
|
125
126
|
import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
126
127
|
import { TanCommandController } from "./controllers/tan-command-controller";
|
|
127
128
|
import { TodoCommandController } from "./controllers/todo-command-controller";
|
|
@@ -285,10 +286,15 @@ class StatusContainer extends Container implements NativeScrollbackLiveRegion {
|
|
|
285
286
|
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
|
286
287
|
* one hooked row per running agent in the same `Id: description` shape the
|
|
287
288
|
* inline task rows use (muted task preview when no description was given).
|
|
289
|
+
* Only detached background spawns are listed: a sync task call blocks the
|
|
290
|
+
* parent turn and its inline tool block already renders progress live, and
|
|
291
|
+
* eval `agent()` spawns are rendered by their own eval cell tree.
|
|
288
292
|
* Returns an empty array when nothing is running so the container can clear.
|
|
289
293
|
*/
|
|
290
294
|
export function renderSubagentHudLines(sessions: ObservableSession[], columns: number): string[] {
|
|
291
|
-
const running = sessions.filter(
|
|
295
|
+
const running = sessions.filter(
|
|
296
|
+
session => session.kind === "subagent" && session.status === "active" && session.detached === true,
|
|
297
|
+
);
|
|
292
298
|
if (running.length === 0) return [];
|
|
293
299
|
|
|
294
300
|
const indent = " ";
|
|
@@ -435,6 +441,34 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
435
441
|
readonly #extensionUiController: ExtensionUiController;
|
|
436
442
|
readonly #inputController: InputController;
|
|
437
443
|
readonly #selectorController: SelectorController;
|
|
444
|
+
readonly #focusController: SessionFocusController;
|
|
445
|
+
get viewSession(): AgentSession {
|
|
446
|
+
return this.#focusController.target ?? this.session;
|
|
447
|
+
}
|
|
448
|
+
get focusedAgentId(): string | undefined {
|
|
449
|
+
return this.#focusController.focusedAgentId;
|
|
450
|
+
}
|
|
451
|
+
focusAgentSession(id: string): Promise<void> {
|
|
452
|
+
return this.#focusController.focusAgent(id);
|
|
453
|
+
}
|
|
454
|
+
focusParentSession(): Promise<void> {
|
|
455
|
+
return this.#focusController.focusParent();
|
|
456
|
+
}
|
|
457
|
+
unfocusSession(): Promise<void> {
|
|
458
|
+
return this.#focusController.unfocus();
|
|
459
|
+
}
|
|
460
|
+
clearTransientSessionUi(): void {
|
|
461
|
+
if (this.loadingAnimation) {
|
|
462
|
+
this.loadingAnimation.stop();
|
|
463
|
+
this.loadingAnimation = undefined;
|
|
464
|
+
}
|
|
465
|
+
this.statusContainer.clear();
|
|
466
|
+
this.pendingMessagesContainer.clear();
|
|
467
|
+
this.compactionQueuedMessages = [];
|
|
468
|
+
this.streamingComponent = undefined;
|
|
469
|
+
this.streamingMessage = undefined;
|
|
470
|
+
this.pendingTools.clear();
|
|
471
|
+
}
|
|
438
472
|
readonly #uiHelpers: UiHelpers;
|
|
439
473
|
#sttController: STTController | undefined;
|
|
440
474
|
#voiceAnimationInterval: NodeJS.Timeout | undefined;
|
|
@@ -560,6 +594,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
560
594
|
this.#commandController = new CommandController(this);
|
|
561
595
|
this.#todoCommandController = new TodoCommandController(this);
|
|
562
596
|
this.#selectorController = new SelectorController(this);
|
|
597
|
+
this.#focusController = new SessionFocusController(this);
|
|
563
598
|
this.#inputController = new InputController(this);
|
|
564
599
|
this.#observerRegistry = new SessionObserverRegistry();
|
|
565
600
|
}
|
|
@@ -1155,6 +1190,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1155
1190
|
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
1156
1191
|
}
|
|
1157
1192
|
}
|
|
1193
|
+
if (this.focusedAgentId) {
|
|
1194
|
+
// Focused subagent view: faint the outline so the borrowed session is
|
|
1195
|
+
// visually distinct from the main one.
|
|
1196
|
+
const base = this.editor.borderColor;
|
|
1197
|
+
this.editor.borderColor = (str: string) => `\x1b[2m${base(str)}\x1b[22m`;
|
|
1198
|
+
}
|
|
1158
1199
|
this.updateEditorTopBorder();
|
|
1159
1200
|
this.ui.requestRender();
|
|
1160
1201
|
}
|
|
@@ -1169,7 +1210,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1169
1210
|
this.chatContainer.clear();
|
|
1170
1211
|
// Full-history transcript: compactions render as inline dividers instead
|
|
1171
1212
|
// of restarting the visible conversation (the LLM context still resets).
|
|
1172
|
-
const context = this.
|
|
1213
|
+
const context = this.viewSession.buildTranscriptSessionContext();
|
|
1173
1214
|
this.renderSessionContext(context);
|
|
1174
1215
|
// During the pre-streaming window — after `startPendingSubmission` has
|
|
1175
1216
|
// optimistically rendered the user's message but before the user
|
|
@@ -2695,6 +2736,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2695
2736
|
}
|
|
2696
2737
|
this.#btwController.dispose();
|
|
2697
2738
|
this.#omfgController.dispose();
|
|
2739
|
+
this.#focusController.dispose();
|
|
2698
2740
|
|
|
2699
2741
|
// Emit shutdown event to hooks
|
|
2700
2742
|
await this.session.dispose();
|
|
@@ -11,6 +11,13 @@ export interface ObservableSession {
|
|
|
11
11
|
status: "active" | "completed" | "failed" | "aborted";
|
|
12
12
|
sessionFile?: string;
|
|
13
13
|
parentToolCallId?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Spawn runs as a detached background job (parent turn not blocked on it).
|
|
16
|
+
* The anchored subagent HUD only lists detached spawns: sync task spawns
|
|
17
|
+
* and eval `agent()` spawns are already rendered live by their own inline
|
|
18
|
+
* tool block / eval cell.
|
|
19
|
+
*/
|
|
20
|
+
detached?: boolean;
|
|
14
21
|
index?: number;
|
|
15
22
|
lastUpdate: number;
|
|
16
23
|
/** Latest progress snapshot from the subagent executor */
|
|
@@ -146,6 +153,7 @@ export class SessionObserverRegistry {
|
|
|
146
153
|
existing.lastUpdate = Date.now();
|
|
147
154
|
existing.index = payload.index;
|
|
148
155
|
existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
|
|
156
|
+
existing.detached = payload.detached ?? existing.detached;
|
|
149
157
|
if (payload.description) existing.description = payload.description;
|
|
150
158
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
151
159
|
} else {
|
|
@@ -158,6 +166,7 @@ export class SessionObserverRegistry {
|
|
|
158
166
|
status,
|
|
159
167
|
sessionFile: payload.sessionFile,
|
|
160
168
|
parentToolCallId: payload.parentToolCallId,
|
|
169
|
+
detached: payload.detached,
|
|
161
170
|
index: payload.index,
|
|
162
171
|
lastUpdate: Date.now(),
|
|
163
172
|
});
|
|
@@ -179,6 +188,7 @@ export class SessionObserverRegistry {
|
|
|
179
188
|
existing.lastUpdate = Date.now();
|
|
180
189
|
existing.index = payload.index;
|
|
181
190
|
existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
|
|
191
|
+
existing.detached = payload.detached ?? existing.detached;
|
|
182
192
|
existing.progress = progress;
|
|
183
193
|
if (progress.description) existing.description = progress.description;
|
|
184
194
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
@@ -192,6 +202,7 @@ export class SessionObserverRegistry {
|
|
|
192
202
|
status: "active",
|
|
193
203
|
sessionFile: payload.sessionFile,
|
|
194
204
|
parentToolCallId: payload.parentToolCallId,
|
|
205
|
+
detached: payload.detached,
|
|
195
206
|
index: payload.index,
|
|
196
207
|
lastUpdate: Date.now(),
|
|
197
208
|
progress,
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -107,6 +107,7 @@ export type SymbolKey =
|
|
|
107
107
|
| "icon.cost"
|
|
108
108
|
| "icon.time"
|
|
109
109
|
| "icon.pi"
|
|
110
|
+
| "icon.ghost"
|
|
110
111
|
| "icon.agents"
|
|
111
112
|
| "icon.job"
|
|
112
113
|
| "icon.cache"
|
|
@@ -305,6 +306,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
305
306
|
"icon.cost": "💲",
|
|
306
307
|
"icon.time": "⏱",
|
|
307
308
|
"icon.pi": "π",
|
|
309
|
+
"icon.ghost": "👻",
|
|
308
310
|
"icon.agents": "👥",
|
|
309
311
|
"icon.job": "⚙",
|
|
310
312
|
"icon.cache": "💾",
|
|
@@ -569,6 +571,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
569
571
|
"icon.time": "\uf017",
|
|
570
572
|
// pick: | alt: π ∏ ∑
|
|
571
573
|
"icon.pi": "\ue22c",
|
|
574
|
+
// pick: (nf-md-ghost) | alt: 👻
|
|
575
|
+
"icon.ghost": "\u{f02a0}",
|
|
572
576
|
// pick: | alt:
|
|
573
577
|
"icon.agents": "\uf0c0",
|
|
574
578
|
// pick: (nf-fa-gear) | alt: ⚙
|
|
@@ -802,6 +806,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
802
806
|
"icon.cost": "$",
|
|
803
807
|
"icon.time": "t:",
|
|
804
808
|
"icon.pi": "pi",
|
|
809
|
+
"icon.ghost": "@",
|
|
805
810
|
"icon.agents": "AG",
|
|
806
811
|
"icon.job": "bg",
|
|
807
812
|
"icon.cache": "cache",
|
|
@@ -1769,6 +1774,7 @@ export class Theme {
|
|
|
1769
1774
|
cost: this.#symbols["icon.cost"],
|
|
1770
1775
|
time: this.#symbols["icon.time"],
|
|
1771
1776
|
pi: this.#symbols["icon.pi"],
|
|
1777
|
+
ghost: this.#symbols["icon.ghost"],
|
|
1772
1778
|
agents: this.#symbols["icon.agents"],
|
|
1773
1779
|
job: this.#symbols["icon.job"],
|
|
1774
1780
|
cache: this.#symbols["icon.cache"],
|
package/src/modes/types.ts
CHANGED
|
@@ -98,6 +98,18 @@ export interface InteractiveModeContext {
|
|
|
98
98
|
// Session access
|
|
99
99
|
session: AgentSession;
|
|
100
100
|
sessionManager: SessionManager;
|
|
101
|
+
/** Session the transcript/editor/status are attached to: the focused agent's, else `session`. */
|
|
102
|
+
readonly viewSession: AgentSession;
|
|
103
|
+
/** Id of the focused agent, undefined when the main session is attached. */
|
|
104
|
+
readonly focusedAgentId: string | undefined;
|
|
105
|
+
/** Focus the main view on an agent's live session (delegates to SessionFocusController.focusAgent). */
|
|
106
|
+
focusAgentSession(id: string): Promise<void>;
|
|
107
|
+
/** Focus the focused agent's parent session, falling back to main (delegates to focusParent). */
|
|
108
|
+
focusParentSession(): Promise<void>;
|
|
109
|
+
/** Return the view to the main session (delegates to SessionFocusController.unfocus). */
|
|
110
|
+
unfocusSession(): Promise<void>;
|
|
111
|
+
/** Clear loader, status/pending containers, streaming state, and pending tools. */
|
|
112
|
+
clearTransientSessionUi(): void;
|
|
101
113
|
settings: Settings;
|
|
102
114
|
keybindings: KeybindingsManager;
|
|
103
115
|
agent: AgentSession["agent"];
|
|
@@ -233,7 +233,7 @@ export class UiHelpers {
|
|
|
233
233
|
this.ctx.chatContainer.addChild(card);
|
|
234
234
|
return [card];
|
|
235
235
|
}
|
|
236
|
-
const renderer = this.ctx.
|
|
236
|
+
const renderer = this.ctx.viewSession.extensionRunner?.getMessageRenderer(message.customType);
|
|
237
237
|
// Both HookMessage and CustomMessage have the same structure, cast for compatibility
|
|
238
238
|
const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
|
|
239
239
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -284,7 +284,10 @@ export class UiHelpers {
|
|
|
284
284
|
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
285
285
|
const imageLinks =
|
|
286
286
|
options?.imageLinks ??
|
|
287
|
-
imageLinksForMessage(
|
|
287
|
+
imageLinksForMessage(
|
|
288
|
+
message,
|
|
289
|
+
this.ctx.viewSession.sessionManager.putBlobSync.bind(this.ctx.viewSession.sessionManager),
|
|
290
|
+
);
|
|
288
291
|
const userComponent = new UserMessageComponent(textContent, isSynthetic, imageLinks);
|
|
289
292
|
this.ctx.chatContainer.addChild(userComponent);
|
|
290
293
|
if (options?.populateHistory && message.role === "user" && !isSynthetic) {
|
|
@@ -298,7 +301,7 @@ export class UiHelpers {
|
|
|
298
301
|
message,
|
|
299
302
|
this.ctx.hideThinkingBlock,
|
|
300
303
|
() => this.ctx.ui.requestRender(),
|
|
301
|
-
this.ctx.
|
|
304
|
+
this.ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
|
|
302
305
|
this.ctx.ui.imageBudget,
|
|
303
306
|
);
|
|
304
307
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
@@ -379,7 +382,7 @@ export class UiHelpers {
|
|
|
379
382
|
!isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
380
383
|
const errorMessage = hasErrorStop
|
|
381
384
|
? message.stopReason === "aborted"
|
|
382
|
-
? resolveAbortLabel(message.errorMessage, this.ctx.
|
|
385
|
+
? resolveAbortLabel(message.errorMessage, this.ctx.viewSession.retryAttempt)
|
|
383
386
|
: message.errorMessage || "Error"
|
|
384
387
|
: null;
|
|
385
388
|
|
|
@@ -424,7 +427,7 @@ export class UiHelpers {
|
|
|
424
427
|
|
|
425
428
|
readGroup?.seal();
|
|
426
429
|
readGroup = null;
|
|
427
|
-
const tool = this.ctx.
|
|
430
|
+
const tool = this.ctx.viewSession.getToolByName(content.name);
|
|
428
431
|
const renderArgs =
|
|
429
432
|
"partialJson" in content
|
|
430
433
|
? { ...content.arguments, __partialJson: content.partialJson }
|
|
@@ -433,7 +436,7 @@ export class UiHelpers {
|
|
|
433
436
|
content.name,
|
|
434
437
|
renderArgs,
|
|
435
438
|
{
|
|
436
|
-
snapshots: getFileSnapshotStore(this.ctx.
|
|
439
|
+
snapshots: getFileSnapshotStore(this.ctx.viewSession),
|
|
437
440
|
showImages: settings.get("terminal.showImages"),
|
|
438
441
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
439
442
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
@@ -441,7 +444,7 @@ export class UiHelpers {
|
|
|
441
444
|
},
|
|
442
445
|
tool,
|
|
443
446
|
this.ctx.ui,
|
|
444
|
-
this.ctx.sessionManager.getCwd(),
|
|
447
|
+
this.ctx.viewSession.sessionManager.getCwd(),
|
|
445
448
|
content.id,
|
|
446
449
|
);
|
|
447
450
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -550,14 +553,14 @@ export class UiHelpers {
|
|
|
550
553
|
|
|
551
554
|
// Display always uses the full-history transcript: compactions show as
|
|
552
555
|
// inline dividers instead of restarting the visible conversation.
|
|
553
|
-
const context = this.ctx.
|
|
556
|
+
const context = this.ctx.viewSession.buildTranscriptSessionContext();
|
|
554
557
|
this.ctx.renderSessionContext(context, {
|
|
555
558
|
updateFooter: true,
|
|
556
|
-
populateHistory:
|
|
559
|
+
populateHistory: !this.ctx.focusedAgentId,
|
|
557
560
|
});
|
|
558
561
|
|
|
559
562
|
// Show compaction info if session was compacted
|
|
560
|
-
const allEntries = this.ctx.sessionManager.getEntries();
|
|
563
|
+
const allEntries = this.ctx.viewSession.sessionManager.getEntries();
|
|
561
564
|
let compactionCount = 0;
|
|
562
565
|
for (const entry of allEntries) {
|
|
563
566
|
if (entry.type === "compaction") {
|
|
@@ -614,7 +617,7 @@ export class UiHelpers {
|
|
|
614
617
|
|
|
615
618
|
updatePendingMessagesDisplay(): void {
|
|
616
619
|
this.ctx.pendingMessagesContainer.clear();
|
|
617
|
-
const queuedMessages = this.ctx.
|
|
620
|
+
const queuedMessages = this.ctx.viewSession.getQueuedMessages() as QueuedMessages;
|
|
618
621
|
|
|
619
622
|
const steeringMessages: Array<{ message: string; label: string }> = [];
|
|
620
623
|
for (const message of queuedMessages.steering) {
|
|
@@ -800,8 +803,8 @@ export class UiHelpers {
|
|
|
800
803
|
}
|
|
801
804
|
|
|
802
805
|
findLastAssistantMessage(): AssistantMessage | undefined {
|
|
803
|
-
for (let i = this.ctx.
|
|
804
|
-
const message = this.ctx.
|
|
806
|
+
for (let i = this.ctx.viewSession.messages.length - 1; i >= 0; i--) {
|
|
807
|
+
const message = this.ctx.viewSession.messages[i];
|
|
805
808
|
if (message?.role === "assistant") {
|
|
806
809
|
return message as AssistantMessage;
|
|
807
810
|
}
|
package/src/prompts/tools/job.md
CHANGED
|
@@ -8,7 +8,7 @@ Background job results are delivered automatically when complete. Reach for this
|
|
|
8
8
|
Use to inspect what's running.
|
|
9
9
|
|
|
10
10
|
## `poll: [id, …]`
|
|
11
|
-
Block until the specified jobs finish or the wait window elapses.
|
|
11
|
+
Block until the specified jobs finish or the wait window elapses. Omit `poll` (with no `list`/`cancel`) to wait on ALL running jobs — NEVER enumerate ids you don't need to filter.
|
|
12
12
|
- Use when you are genuinely blocked on a result and have no other work to do.
|
|
13
13
|
- Returns the current snapshot when the timer elapses; running jobs remain running.
|
|
14
14
|
- Completed jobs include their final output in the returned snapshot.
|
|
@@ -100,6 +100,7 @@ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
|
100
100
|
import { countTokens, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
101
101
|
import {
|
|
102
102
|
extractRetryHint,
|
|
103
|
+
formatDuration,
|
|
103
104
|
getAgentDbPath,
|
|
104
105
|
getInstallId,
|
|
105
106
|
isBunTestRuntime,
|
|
@@ -240,7 +241,13 @@ import { normalizeModelContextImages } from "../utils/image-loading";
|
|
|
240
241
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
241
242
|
import type { AuthStorage } from "./auth-storage";
|
|
242
243
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
243
|
-
import {
|
|
244
|
+
import {
|
|
245
|
+
type CodexAutoRedeemRedeemDecision,
|
|
246
|
+
defaultCodexAutoRedeemCoordinator,
|
|
247
|
+
evaluateCodexAutoRedeem,
|
|
248
|
+
shouldEvaluateCodexAutoRedeem,
|
|
249
|
+
shouldPromptCodexAutoRedeem,
|
|
250
|
+
} from "./codex-auto-reset";
|
|
244
251
|
import {
|
|
245
252
|
type BashExecutionMessage,
|
|
246
253
|
type CustomMessage,
|
|
@@ -1418,6 +1425,11 @@ export class AgentSession {
|
|
|
1418
1425
|
return this.#ttsrManager;
|
|
1419
1426
|
}
|
|
1420
1427
|
|
|
1428
|
+
/** Secret obfuscator, when secrets are configured; /share redaction reuses it. */
|
|
1429
|
+
get obfuscator(): SecretObfuscator | undefined {
|
|
1430
|
+
return this.#obfuscator;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1421
1433
|
/** Whether a TTSR abort is pending (stream was aborted to inject rules) */
|
|
1422
1434
|
get isTtsrAbortPending(): boolean {
|
|
1423
1435
|
return this.#ttsrAbortPending;
|
|
@@ -6143,7 +6155,13 @@ export class AgentSession {
|
|
|
6143
6155
|
|
|
6144
6156
|
async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
6145
6157
|
const branchEntries = this.sessionManager.getBranch();
|
|
6146
|
-
const result = pruneToolOutputs(
|
|
6158
|
+
const result = pruneToolOutputs(
|
|
6159
|
+
branchEntries,
|
|
6160
|
+
this.#withPlanProtection({
|
|
6161
|
+
...DEFAULT_PRUNE_CONFIG,
|
|
6162
|
+
pruneUseless: this.settings.getGroup("compaction").dropUseless,
|
|
6163
|
+
}),
|
|
6164
|
+
);
|
|
6147
6165
|
if (result.prunedCount === 0) {
|
|
6148
6166
|
return undefined;
|
|
6149
6167
|
}
|
|
@@ -6157,19 +6175,22 @@ export class AgentSession {
|
|
|
6157
6175
|
}
|
|
6158
6176
|
|
|
6159
6177
|
/**
|
|
6160
|
-
* Per-turn
|
|
6161
|
-
* the same file has made stale
|
|
6162
|
-
*
|
|
6163
|
-
*
|
|
6164
|
-
*
|
|
6178
|
+
* Per-turn stale-result pass: prune older `read` results that a newer read
|
|
6179
|
+
* of the same file has made stale, plus results their tool flagged
|
|
6180
|
+
* contextually useless. Cache-aware (only fires when the suffix after a
|
|
6181
|
+
* candidate is small or the session has been idle long enough that the
|
|
6182
|
+
* provider prompt cache is cold), so it is cheap to run every turn. Gated
|
|
6183
|
+
* on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
|
|
6165
6184
|
*/
|
|
6166
|
-
async #
|
|
6167
|
-
|
|
6185
|
+
async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
6186
|
+
const { supersedeReads, dropUseless } = this.settings.getGroup("compaction");
|
|
6187
|
+
if (!supersedeReads && !dropUseless) return undefined;
|
|
6168
6188
|
const branchEntries = this.sessionManager.getBranch();
|
|
6169
6189
|
const result = pruneSupersededToolResults(
|
|
6170
6190
|
branchEntries,
|
|
6171
6191
|
this.#withPlanProtection({
|
|
6172
|
-
supersedeKey: readToolSupersedeKey,
|
|
6192
|
+
supersedeKey: supersedeReads ? readToolSupersedeKey : undefined,
|
|
6193
|
+
pruneUseless: dropUseless,
|
|
6173
6194
|
protectedTools: [...DEFAULT_PRUNE_CONFIG.protectedTools],
|
|
6174
6195
|
}),
|
|
6175
6196
|
);
|
|
@@ -6849,9 +6870,10 @@ export class AgentSession {
|
|
|
6849
6870
|
return false;
|
|
6850
6871
|
}
|
|
6851
6872
|
|
|
6852
|
-
//
|
|
6853
|
-
// (bails when no candidate) and independent of the compaction
|
|
6854
|
-
|
|
6873
|
+
// Stale-result pass runs every turn, before any threshold gating: it is
|
|
6874
|
+
// cheap (bails when no candidate) and independent of the compaction
|
|
6875
|
+
// setting.
|
|
6876
|
+
const supersedeResult = await this.#pruneStaleToolResults();
|
|
6855
6877
|
|
|
6856
6878
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6857
6879
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
@@ -10193,20 +10215,63 @@ export class AgentSession {
|
|
|
10193
10215
|
signal,
|
|
10194
10216
|
});
|
|
10195
10217
|
}
|
|
10218
|
+
async #confirmCodexAutoRedeem(decision: CodexAutoRedeemRedeemDecision): Promise<boolean> {
|
|
10219
|
+
const runner = this.#extensionRunner;
|
|
10220
|
+
if (!runner?.hasUI()) {
|
|
10221
|
+
this.emitNotice(
|
|
10222
|
+
"warning",
|
|
10223
|
+
"Codex saved reset is eligible, but auto-redeem is unset and no prompt UI is available. Run `/usage reset` or set codexResets.autoRedeem.",
|
|
10224
|
+
"codex-auto-reset",
|
|
10225
|
+
);
|
|
10226
|
+
return false;
|
|
10227
|
+
}
|
|
10228
|
+
|
|
10229
|
+
const who = decision.target.email ?? decision.target.accountId ?? "the active account";
|
|
10230
|
+
const resetLabel = decision.availableCount === 1 ? "reset" : "resets";
|
|
10231
|
+
try {
|
|
10232
|
+
const choice = await runner
|
|
10233
|
+
.getUIContext()
|
|
10234
|
+
.select(
|
|
10235
|
+
`Do you wanna redeem your reset?\n${who} is blocked by the weekly Codex limit for about ${formatDuration(decision.remainingMs)}. Spend 1 of ${decision.availableCount} saved ${resetLabel}?`,
|
|
10236
|
+
[
|
|
10237
|
+
{
|
|
10238
|
+
label: "Yes",
|
|
10239
|
+
description: "Redeem now and remember yes for future eligible Codex weekly blocks.",
|
|
10240
|
+
},
|
|
10241
|
+
{
|
|
10242
|
+
label: "No",
|
|
10243
|
+
description: "Do not auto-redeem saved Codex resets.",
|
|
10244
|
+
},
|
|
10245
|
+
],
|
|
10246
|
+
);
|
|
10247
|
+
if (choice === "Yes") {
|
|
10248
|
+
this.settings.set("codexResets.autoRedeem", "yes");
|
|
10249
|
+
return true;
|
|
10250
|
+
}
|
|
10251
|
+
if (choice === "No") {
|
|
10252
|
+
this.settings.set("codexResets.autoRedeem", "no");
|
|
10253
|
+
}
|
|
10254
|
+
} catch (error) {
|
|
10255
|
+
logger.warn("codex-auto-reset prompt failed", { error: String(error) });
|
|
10256
|
+
}
|
|
10257
|
+
return false;
|
|
10258
|
+
}
|
|
10196
10259
|
|
|
10197
10260
|
/**
|
|
10198
10261
|
* Auto-redeem hook for {@link AgentSession.#handleRetryableError}'s
|
|
10199
10262
|
* usage-limit branch. Returns `true` only when a saved Codex reset was
|
|
10200
|
-
* actually spent (so the caller retries immediately).
|
|
10201
|
-
*
|
|
10202
|
-
*
|
|
10203
|
-
*
|
|
10263
|
+
* actually spent (so the caller retries immediately). The "unset" mode is
|
|
10264
|
+
* reactive but asks before spending; "yes" skips that prompt, and "no" avoids
|
|
10265
|
+
* the eligibility IO entirely. The decision remains heavily gated — see
|
|
10266
|
+
* `./codex-auto-reset` and the design in `local://autoreset-spec.md`.
|
|
10267
|
+
* Per-account in-flight dedup lets concurrent sessions adopt one redeem
|
|
10268
|
+
* instead of double-spending.
|
|
10204
10269
|
*/
|
|
10205
10270
|
async #maybeAutoRedeemCodexReset(coordinator = defaultCodexAutoRedeemCoordinator): Promise<boolean> {
|
|
10206
10271
|
const cfg = this.settings.getGroup("codexResets");
|
|
10207
10272
|
const model = this.model;
|
|
10208
10273
|
// Cheap exits before any IO.
|
|
10209
|
-
if (!cfg.autoRedeem || !model || model.provider !== "openai-codex") return false;
|
|
10274
|
+
if (!shouldEvaluateCodexAutoRedeem(cfg.autoRedeem) || !model || model.provider !== "openai-codex") return false;
|
|
10210
10275
|
const authStorage = this.#modelRegistry.authStorage;
|
|
10211
10276
|
// Capture identity BEFORE awaits: markUsageLimitReached leaves the
|
|
10212
10277
|
// usage-limit session credential sticky, so this names the blocked account.
|
|
@@ -10223,7 +10288,7 @@ export class AgentSession {
|
|
|
10223
10288
|
provider: model.provider,
|
|
10224
10289
|
modelId: model.id,
|
|
10225
10290
|
settings: {
|
|
10226
|
-
autoRedeem:
|
|
10291
|
+
autoRedeem: true,
|
|
10227
10292
|
minBlockedMinutes: Math.max(0, cfg.minBlockedMinutes),
|
|
10228
10293
|
keepCredits: Math.max(0, Math.trunc(cfg.keepCredits)),
|
|
10229
10294
|
},
|
|
@@ -10236,6 +10301,9 @@ export class AgentSession {
|
|
|
10236
10301
|
logger.debug("codex-auto-reset: skipped", { reason: decision.reason });
|
|
10237
10302
|
return false;
|
|
10238
10303
|
}
|
|
10304
|
+
if (shouldPromptCodexAutoRedeem(cfg.autoRedeem) && !(await this.#confirmCodexAutoRedeem(decision))) {
|
|
10305
|
+
return false;
|
|
10306
|
+
}
|
|
10239
10307
|
// Commit the attempt BEFORE acting so this block can never re-enter.
|
|
10240
10308
|
coordinator.attemptedBlockKeys.add(decision.blockKey);
|
|
10241
10309
|
coordinator.lastAttemptAtByAccount.set(decision.accountKey, Date.now());
|