@oh-my-pi/pi-coding-agent 15.11.7 → 15.12.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 +63 -1
- package/dist/cli.js +8106 -7708
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/collab/crypto.d.ts +7 -0
- package/dist/types/collab/guest.d.ts +23 -0
- package/dist/types/collab/host.d.ts +29 -0
- package/dist/types/collab/protocol.d.ts +113 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +60 -5
- 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/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +32 -1
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/status-line/component.d.ts +10 -2
- package/dist/types/modes/components/status-line/types.d.ts +11 -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 +16 -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 +20 -0
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/session/codex-auto-reset.d.ts +8 -4
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +6 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/types.d.ts +9 -0
- package/package.json +14 -13
- package/scripts/bench-guard.ts +71 -0
- package/scripts/build-binary.ts +4 -0
- package/scripts/bundle-dist.ts +4 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/src/cli/args.ts +2 -0
- package/src/cli-commands.ts +1 -0
- package/src/collab/crypto.ts +63 -0
- package/src/collab/guest.ts +450 -0
- package/src/collab/host.ts +556 -0
- package/src/collab/protocol.ts +232 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +22 -14
- package/src/config/settings-schema.ts +67 -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/extensibility/slash-commands.ts +1 -97
- package/src/internal-urls/docs-index.generated.ts +74 -73
- package/src/main.ts +33 -11
- package/src/modes/components/agent-hub.ts +659 -431
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/status-line/component.ts +59 -6
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +18 -1
- package/src/modes/components/status-line/types.ts +12 -0
- package/src/modes/components/tips.txt +4 -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 +175 -9
- package/src/modes/controllers/selector-controller.ts +13 -15
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +56 -6
- package/src/modes/session-observer-registry.ts +11 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +20 -0
- package/src/modes/utils/ui-helpers.ts +23 -13
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk.ts +239 -36
- package/src/session/agent-session.ts +82 -7
- package/src/session/codex-auto-reset.ts +23 -11
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +9 -3
- package/src/slash-commands/builtin-registry.ts +261 -24
- 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/job.ts +6 -9
- package/src/tools/read.ts +38 -5
- package/src/tools/write.ts +13 -42
- 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,14 +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(),
|
|
1180
|
+
registry: this.ctx.collabGuest?.agentRegistry,
|
|
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,
|
|
1191
1189
|
});
|
|
1192
1190
|
|
|
1193
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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { getSegmenter } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { LRUCache } from "lru-cache/raw";
|
|
3
4
|
import type { AssistantMessageComponent } from "../components/assistant-message";
|
|
4
5
|
|
|
5
6
|
export const STREAMING_REVEAL_FRAME_MS = 1000 / 30;
|
|
@@ -15,11 +16,17 @@ type StreamingRevealControllerOptions = {
|
|
|
15
16
|
requestRender(): void;
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
const graphemeCountCache = new LRUCache<string, number>({ max: 128 });
|
|
20
|
+
|
|
18
21
|
function countGraphemes(text: string): number {
|
|
22
|
+
if (text.length === 0) return 0;
|
|
23
|
+
const cached = graphemeCountCache.get(text);
|
|
24
|
+
if (cached !== undefined) return cached;
|
|
19
25
|
let count = 0;
|
|
20
26
|
for (const _segment of getSegmenter().segment(text)) {
|
|
21
27
|
count += 1;
|
|
22
28
|
}
|
|
29
|
+
graphemeCountCache.set(text, count);
|
|
23
30
|
return count;
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -49,8 +49,9 @@ import {
|
|
|
49
49
|
} from "@oh-my-pi/pi-utils";
|
|
50
50
|
import chalk from "chalk";
|
|
51
51
|
import { reset as resetCapabilities } from "../capability";
|
|
52
|
+
import type { CollabGuestLink } from "../collab/guest";
|
|
53
|
+
import type { CollabHost } from "../collab/host";
|
|
52
54
|
import { KeybindingsManager } from "../config/keybindings";
|
|
53
|
-
import { MODEL_ROLES, type ModelRole } from "../config/model-roles";
|
|
54
55
|
import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
|
|
55
56
|
import { clearClaudePluginRootsCache } from "../discovery/helpers";
|
|
56
57
|
import type {
|
|
@@ -62,7 +63,7 @@ import type {
|
|
|
62
63
|
ExtensionWidgetOptions,
|
|
63
64
|
} from "../extensibility/extensions";
|
|
64
65
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
65
|
-
import {
|
|
66
|
+
import { loadSlashCommands } from "../extensibility/slash-commands";
|
|
66
67
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
67
68
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
68
69
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
@@ -82,7 +83,7 @@ import { HistoryStorage } from "../session/history-storage";
|
|
|
82
83
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
83
84
|
import { getRecentSessions } from "../session/session-manager";
|
|
84
85
|
import type { ShakeMode } from "../session/shake-types";
|
|
85
|
-
import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES } from "../slash-commands/builtin-registry";
|
|
86
|
+
import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, BUILTIN_SLASH_COMMANDS } from "../slash-commands/builtin-registry";
|
|
86
87
|
import { formatDuration } from "../slash-commands/helpers/format";
|
|
87
88
|
import { STTController, type SttState } from "../stt";
|
|
88
89
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
@@ -121,6 +122,7 @@ import { InputController } from "./controllers/input-controller";
|
|
|
121
122
|
import { MCPCommandController } from "./controllers/mcp-command-controller";
|
|
122
123
|
import { OmfgController } from "./controllers/omfg-controller";
|
|
123
124
|
import { SelectorController } from "./controllers/selector-controller";
|
|
125
|
+
import { SessionFocusController } from "./controllers/session-focus-controller";
|
|
124
126
|
import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
125
127
|
import { TanCommandController } from "./controllers/tan-command-controller";
|
|
126
128
|
import { TodoCommandController } from "./controllers/todo-command-controller";
|
|
@@ -284,10 +286,15 @@ class StatusContainer extends Container implements NativeScrollbackLiveRegion {
|
|
|
284
286
|
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
|
285
287
|
* one hooked row per running agent in the same `Id: description` shape the
|
|
286
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.
|
|
287
292
|
* Returns an empty array when nothing is running so the container can clear.
|
|
288
293
|
*/
|
|
289
294
|
export function renderSubagentHudLines(sessions: ObservableSession[], columns: number): string[] {
|
|
290
|
-
const running = sessions.filter(
|
|
295
|
+
const running = sessions.filter(
|
|
296
|
+
session => session.kind === "subagent" && session.status === "active" && session.detached === true,
|
|
297
|
+
);
|
|
291
298
|
if (running.length === 0) return [];
|
|
292
299
|
|
|
293
300
|
const indent = " ";
|
|
@@ -397,6 +404,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
397
404
|
fileSlashCommands: Set<string> = new Set();
|
|
398
405
|
skillCommands: Map<string, string> = new Map();
|
|
399
406
|
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
407
|
+
collabHost?: CollabHost;
|
|
408
|
+
collabGuest?: CollabGuestLink;
|
|
400
409
|
|
|
401
410
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
402
411
|
#cleanupUnsubscribe?: () => void;
|
|
@@ -423,9 +432,43 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
423
432
|
readonly #commandController: CommandController;
|
|
424
433
|
readonly #todoCommandController: TodoCommandController;
|
|
425
434
|
readonly #eventController: EventController;
|
|
435
|
+
get eventController(): EventController {
|
|
436
|
+
return this.#eventController;
|
|
437
|
+
}
|
|
438
|
+
get eventBus(): EventBus | undefined {
|
|
439
|
+
return this.#eventBus;
|
|
440
|
+
}
|
|
426
441
|
readonly #extensionUiController: ExtensionUiController;
|
|
427
442
|
readonly #inputController: InputController;
|
|
428
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
|
+
}
|
|
429
472
|
readonly #uiHelpers: UiHelpers;
|
|
430
473
|
#sttController: STTController | undefined;
|
|
431
474
|
#voiceAnimationInterval: NodeJS.Timeout | undefined;
|
|
@@ -551,6 +594,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
551
594
|
this.#commandController = new CommandController(this);
|
|
552
595
|
this.#todoCommandController = new TodoCommandController(this);
|
|
553
596
|
this.#selectorController = new SelectorController(this);
|
|
597
|
+
this.#focusController = new SessionFocusController(this);
|
|
554
598
|
this.#inputController = new InputController(this);
|
|
555
599
|
this.#observerRegistry = new SessionObserverRegistry();
|
|
556
600
|
}
|
|
@@ -1146,6 +1190,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1146
1190
|
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
1147
1191
|
}
|
|
1148
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
|
+
}
|
|
1149
1199
|
this.updateEditorTopBorder();
|
|
1150
1200
|
this.ui.requestRender();
|
|
1151
1201
|
}
|
|
@@ -1160,7 +1210,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1160
1210
|
this.chatContainer.clear();
|
|
1161
1211
|
// Full-history transcript: compactions render as inline dividers instead
|
|
1162
1212
|
// of restarting the visible conversation (the LLM context still resets).
|
|
1163
|
-
const context = this.
|
|
1213
|
+
const context = this.viewSession.buildTranscriptSessionContext();
|
|
1164
1214
|
this.renderSessionContext(context);
|
|
1165
1215
|
// During the pre-streaming window — after `startPendingSubmission` has
|
|
1166
1216
|
// optimistically rendered the user's message but before the user
|
|
@@ -2491,7 +2541,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2491
2541
|
index: startTierIndex,
|
|
2492
2542
|
segments: cycle.models.map(entry => ({
|
|
2493
2543
|
label: entry.role,
|
|
2494
|
-
color: MODEL_ROLES[entry.role as ModelRole]?.color,
|
|
2495
2544
|
detail: entry.model.name || entry.model.id,
|
|
2496
2545
|
})),
|
|
2497
2546
|
onChange: index => {
|
|
@@ -2687,6 +2736,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2687
2736
|
}
|
|
2688
2737
|
this.#btwController.dispose();
|
|
2689
2738
|
this.#omfgController.dispose();
|
|
2739
|
+
this.#focusController.dispose();
|
|
2690
2740
|
|
|
2691
2741
|
// Emit shutdown event to hooks
|
|
2692
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
|
@@ -2,6 +2,8 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
3
|
import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { Component, Container, EditorTheme, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import type { CollabGuestLink } from "../collab/guest";
|
|
6
|
+
import type { CollabHost } from "../collab/host";
|
|
5
7
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
6
8
|
import type { Settings } from "../config/settings";
|
|
7
9
|
import type {
|
|
@@ -19,6 +21,7 @@ import type { HistoryStorage } from "../session/history-storage";
|
|
|
19
21
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
20
22
|
import type { ShakeMode } from "../session/shake-types";
|
|
21
23
|
import type { LspStartupServerInfo } from "../tools";
|
|
24
|
+
import type { EventBus } from "../utils/event-bus";
|
|
22
25
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
23
26
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
24
27
|
import type { CustomEditor } from "./components/custom-editor";
|
|
@@ -29,6 +32,7 @@ import type { HookSelectorComponent, HookSelectorOptions } from "./components/ho
|
|
|
29
32
|
import type { StatusLineComponent } from "./components/status-line";
|
|
30
33
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
31
34
|
import type { TranscriptContainer } from "./components/transcript-container";
|
|
35
|
+
import type { EventController } from "./controllers/event-controller";
|
|
32
36
|
import type { LoopLimitRuntime } from "./loop-limit";
|
|
33
37
|
import type { OAuthManualInputManager } from "./oauth-manual-input";
|
|
34
38
|
import type { Theme } from "./theme/theme";
|
|
@@ -94,6 +98,18 @@ export interface InteractiveModeContext {
|
|
|
94
98
|
// Session access
|
|
95
99
|
session: AgentSession;
|
|
96
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;
|
|
97
113
|
settings: Settings;
|
|
98
114
|
keybindings: KeybindingsManager;
|
|
99
115
|
agent: AgentSession["agent"];
|
|
@@ -101,6 +117,10 @@ export interface InteractiveModeContext {
|
|
|
101
117
|
mcpManager?: MCPManager;
|
|
102
118
|
lspServers?: LspStartupServerInfo[];
|
|
103
119
|
titleSystemPrompt?: string;
|
|
120
|
+
collabHost?: CollabHost;
|
|
121
|
+
collabGuest?: CollabGuestLink;
|
|
122
|
+
eventController: EventController;
|
|
123
|
+
eventBus?: EventBus;
|
|
104
124
|
|
|
105
125
|
// State
|
|
106
126
|
isInitialized: boolean;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { type Component, Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
|
|
4
5
|
import { settings } from "../../config/settings";
|
|
5
6
|
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
6
7
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
7
8
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
8
9
|
import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
|
|
10
|
+
import { CollabPromptMessageComponent } from "../../modes/components/collab-prompt-message";
|
|
9
11
|
import { CompactionSummaryMessageComponent } from "../../modes/components/compaction-summary-message";
|
|
10
12
|
import { CustomMessageComponent } from "../../modes/components/custom-message";
|
|
11
13
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
@@ -185,6 +187,11 @@ export class UiHelpers {
|
|
|
185
187
|
this.ctx.chatContainer.addChild(component);
|
|
186
188
|
break;
|
|
187
189
|
}
|
|
190
|
+
if (message.customType === COLLAB_PROMPT_MESSAGE_TYPE) {
|
|
191
|
+
const component = new CollabPromptMessageComponent(message as CustomMessage<CollabPromptDetails>);
|
|
192
|
+
this.ctx.chatContainer.addChild(component);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
188
195
|
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
189
196
|
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
190
197
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -226,7 +233,7 @@ export class UiHelpers {
|
|
|
226
233
|
this.ctx.chatContainer.addChild(card);
|
|
227
234
|
return [card];
|
|
228
235
|
}
|
|
229
|
-
const renderer = this.ctx.
|
|
236
|
+
const renderer = this.ctx.viewSession.extensionRunner?.getMessageRenderer(message.customType);
|
|
230
237
|
// Both HookMessage and CustomMessage have the same structure, cast for compatibility
|
|
231
238
|
const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
|
|
232
239
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -277,7 +284,10 @@ export class UiHelpers {
|
|
|
277
284
|
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
278
285
|
const imageLinks =
|
|
279
286
|
options?.imageLinks ??
|
|
280
|
-
imageLinksForMessage(
|
|
287
|
+
imageLinksForMessage(
|
|
288
|
+
message,
|
|
289
|
+
this.ctx.viewSession.sessionManager.putBlobSync.bind(this.ctx.viewSession.sessionManager),
|
|
290
|
+
);
|
|
281
291
|
const userComponent = new UserMessageComponent(textContent, isSynthetic, imageLinks);
|
|
282
292
|
this.ctx.chatContainer.addChild(userComponent);
|
|
283
293
|
if (options?.populateHistory && message.role === "user" && !isSynthetic) {
|
|
@@ -291,7 +301,7 @@ export class UiHelpers {
|
|
|
291
301
|
message,
|
|
292
302
|
this.ctx.hideThinkingBlock,
|
|
293
303
|
() => this.ctx.ui.requestRender(),
|
|
294
|
-
this.ctx.
|
|
304
|
+
this.ctx.viewSession.extensionRunner?.getAssistantThinkingRenderers(),
|
|
295
305
|
this.ctx.ui.imageBudget,
|
|
296
306
|
);
|
|
297
307
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
@@ -372,7 +382,7 @@ export class UiHelpers {
|
|
|
372
382
|
!isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
373
383
|
const errorMessage = hasErrorStop
|
|
374
384
|
? message.stopReason === "aborted"
|
|
375
|
-
? resolveAbortLabel(message.errorMessage, this.ctx.
|
|
385
|
+
? resolveAbortLabel(message.errorMessage, this.ctx.viewSession.retryAttempt)
|
|
376
386
|
: message.errorMessage || "Error"
|
|
377
387
|
: null;
|
|
378
388
|
|
|
@@ -417,7 +427,7 @@ export class UiHelpers {
|
|
|
417
427
|
|
|
418
428
|
readGroup?.seal();
|
|
419
429
|
readGroup = null;
|
|
420
|
-
const tool = this.ctx.
|
|
430
|
+
const tool = this.ctx.viewSession.getToolByName(content.name);
|
|
421
431
|
const renderArgs =
|
|
422
432
|
"partialJson" in content
|
|
423
433
|
? { ...content.arguments, __partialJson: content.partialJson }
|
|
@@ -426,7 +436,7 @@ export class UiHelpers {
|
|
|
426
436
|
content.name,
|
|
427
437
|
renderArgs,
|
|
428
438
|
{
|
|
429
|
-
snapshots: getFileSnapshotStore(this.ctx.
|
|
439
|
+
snapshots: getFileSnapshotStore(this.ctx.viewSession),
|
|
430
440
|
showImages: settings.get("terminal.showImages"),
|
|
431
441
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
432
442
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
@@ -434,7 +444,7 @@ export class UiHelpers {
|
|
|
434
444
|
},
|
|
435
445
|
tool,
|
|
436
446
|
this.ctx.ui,
|
|
437
|
-
this.ctx.sessionManager.getCwd(),
|
|
447
|
+
this.ctx.viewSession.sessionManager.getCwd(),
|
|
438
448
|
content.id,
|
|
439
449
|
);
|
|
440
450
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -543,14 +553,14 @@ export class UiHelpers {
|
|
|
543
553
|
|
|
544
554
|
// Display always uses the full-history transcript: compactions show as
|
|
545
555
|
// inline dividers instead of restarting the visible conversation.
|
|
546
|
-
const context = this.ctx.
|
|
556
|
+
const context = this.ctx.viewSession.buildTranscriptSessionContext();
|
|
547
557
|
this.ctx.renderSessionContext(context, {
|
|
548
558
|
updateFooter: true,
|
|
549
|
-
populateHistory:
|
|
559
|
+
populateHistory: !this.ctx.focusedAgentId,
|
|
550
560
|
});
|
|
551
561
|
|
|
552
562
|
// Show compaction info if session was compacted
|
|
553
|
-
const allEntries = this.ctx.sessionManager.getEntries();
|
|
563
|
+
const allEntries = this.ctx.viewSession.sessionManager.getEntries();
|
|
554
564
|
let compactionCount = 0;
|
|
555
565
|
for (const entry of allEntries) {
|
|
556
566
|
if (entry.type === "compaction") {
|
|
@@ -607,7 +617,7 @@ export class UiHelpers {
|
|
|
607
617
|
|
|
608
618
|
updatePendingMessagesDisplay(): void {
|
|
609
619
|
this.ctx.pendingMessagesContainer.clear();
|
|
610
|
-
const queuedMessages = this.ctx.
|
|
620
|
+
const queuedMessages = this.ctx.viewSession.getQueuedMessages() as QueuedMessages;
|
|
611
621
|
|
|
612
622
|
const steeringMessages: Array<{ message: string; label: string }> = [];
|
|
613
623
|
for (const message of queuedMessages.steering) {
|
|
@@ -793,8 +803,8 @@ export class UiHelpers {
|
|
|
793
803
|
}
|
|
794
804
|
|
|
795
805
|
findLastAssistantMessage(): AssistantMessage | undefined {
|
|
796
|
-
for (let i = this.ctx.
|
|
797
|
-
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];
|
|
798
808
|
if (message?.role === "assistant") {
|
|
799
809
|
return message as AssistantMessage;
|
|
800
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.
|