@oh-my-pi/pi-coding-agent 16.2.13 → 16.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -7
- package/dist/cli.js +6033 -5982
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
+
import { Settings } from "../../../config/settings";
|
|
3
|
+
import type { AgentSession } from "../../../session/agent-session";
|
|
4
|
+
import { getThemeByName, setThemeInstance } from "../../theme/theme";
|
|
5
|
+
import { StatusLineComponent } from "./component";
|
|
6
|
+
|
|
7
|
+
function makeSessionWithLastMessage(lastMessage: unknown) {
|
|
8
|
+
return {
|
|
9
|
+
messages: [lastMessage],
|
|
10
|
+
model: { contextWindow: 128000 },
|
|
11
|
+
contextUsageRevision: 0,
|
|
12
|
+
systemPrompt: [],
|
|
13
|
+
agent: { state: { tools: [] } },
|
|
14
|
+
skills: [],
|
|
15
|
+
getContextUsage: () => ({ tokens: 42, contextWindow: 128000 }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
await Settings.init({ inMemory: true });
|
|
21
|
+
const loaded = await getThemeByName("dark");
|
|
22
|
+
if (!loaded) throw new Error("theme unavailable");
|
|
23
|
+
setThemeInstance(loaded);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("StatusLineComponent", () => {
|
|
27
|
+
it("fingerprints tool-call arguments containing bigint values", () => {
|
|
28
|
+
const statusLine = new StatusLineComponent(
|
|
29
|
+
makeSessionWithLastMessage({
|
|
30
|
+
role: "assistant",
|
|
31
|
+
timestamp: 1,
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "toolCall",
|
|
35
|
+
name: "read",
|
|
36
|
+
arguments: { offset: 1n, nested: { limit: 2n } },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}) as unknown as AgentSession,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(statusLine.getCachedContextBreakdown()).toEqual({ usedTokens: 42, contextWindow: 128000 });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -118,7 +118,15 @@ function messageFingerprint(msg: AgentMessage): string {
|
|
|
118
118
|
redactedLen += b.data.length;
|
|
119
119
|
} else if (b.type === "toolCall") {
|
|
120
120
|
if (typeof b.name === "string") textLen += b.name.length;
|
|
121
|
-
|
|
121
|
+
if (b.arguments !== undefined) {
|
|
122
|
+
try {
|
|
123
|
+
textLen += JSON.stringify(b.arguments, (_key, value) =>
|
|
124
|
+
typeof value === "bigint" ? value.toString() : value,
|
|
125
|
+
).length;
|
|
126
|
+
} catch {
|
|
127
|
+
textLen += String(b.arguments).length;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
132
|
}
|
|
@@ -379,7 +379,7 @@ const tokenRateSegment: StatusLineSegment = {
|
|
|
379
379
|
const { tokensPerSecond } = ctx.usageStats;
|
|
380
380
|
if (!tokensPerSecond) return { content: "", visible: false };
|
|
381
381
|
|
|
382
|
-
const content = withIcon(theme.icon.
|
|
382
|
+
const content = withIcon(theme.icon.throughput, `${tokensPerSecond.toFixed(1)}/s`);
|
|
383
383
|
return { content: theme.fg("statusLineOutput", content), visible: true };
|
|
384
384
|
},
|
|
385
385
|
};
|
|
@@ -498,7 +498,7 @@ const cacheReadSegment: StatusLineSegment = {
|
|
|
498
498
|
const { cacheRead } = ctx.usageStats;
|
|
499
499
|
if (!cacheRead) return { content: "", visible: false };
|
|
500
500
|
|
|
501
|
-
const parts = [theme.icon.cache,
|
|
501
|
+
const parts = [theme.icon.cache, formatNumber(cacheRead)].filter(Boolean);
|
|
502
502
|
const content = parts.join(" ");
|
|
503
503
|
return { content: theme.fg("statusLineSpend", content), visible: true };
|
|
504
504
|
},
|
|
@@ -510,7 +510,7 @@ const cacheWriteSegment: StatusLineSegment = {
|
|
|
510
510
|
const { cacheWrite } = ctx.usageStats;
|
|
511
511
|
if (!cacheWrite) return { content: "", visible: false };
|
|
512
512
|
|
|
513
|
-
const parts = [theme.icon.cache,
|
|
513
|
+
const parts = [theme.icon.cache, formatNumber(cacheWrite)].filter(Boolean);
|
|
514
514
|
const content = parts.join(" ");
|
|
515
515
|
return { content: theme.fg("statusLineOutput", content), visible: true };
|
|
516
516
|
},
|
|
@@ -3,13 +3,27 @@ import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
|
3
3
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** Below this the rate is nonsense (cached/instant responses yield absurd tok/s). */
|
|
7
|
+
const MIN_DURATION_MS = 100;
|
|
8
|
+
|
|
9
|
+
export function createUsageRowBlock(usage: Usage, durationMs?: number, ttftMs?: number): Container {
|
|
7
10
|
const totalInput = usage.input + usage.cacheWrite;
|
|
8
11
|
const parts: string[] = [];
|
|
9
12
|
parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
|
|
10
13
|
parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
|
|
11
14
|
if (usage.cacheRead > 0) {
|
|
12
|
-
parts.push(
|
|
15
|
+
parts.push(`${theme.icon.cache} ${formatNumber(usage.cacheRead)}`);
|
|
16
|
+
}
|
|
17
|
+
if (ttftMs && ttftMs > 0) {
|
|
18
|
+
parts.push(`${theme.icon.time} ${(ttftMs / 1000).toFixed(1)}s`);
|
|
19
|
+
}
|
|
20
|
+
if (durationMs && durationMs > MIN_DURATION_MS && usage.output > 0) {
|
|
21
|
+
// Throughput excludes TTFT — generation time is duration minus time-to-first-token.
|
|
22
|
+
const genMs = durationMs - (ttftMs ?? 0);
|
|
23
|
+
if (genMs > MIN_DURATION_MS) {
|
|
24
|
+
const tokPerSec = (usage.output / genMs) * 1000;
|
|
25
|
+
parts.push(`${theme.icon.throughput} ${tokPerSec.toFixed(1)}/s`);
|
|
26
|
+
}
|
|
13
27
|
}
|
|
14
28
|
const block = new Container();
|
|
15
29
|
block.addChild(new Spacer(1));
|
|
@@ -867,7 +867,7 @@ export class CommandController {
|
|
|
867
867
|
|
|
868
868
|
this.ctx.statusLine.invalidate();
|
|
869
869
|
this.ctx.statusLine.resetActiveTime();
|
|
870
|
-
this.ctx.
|
|
870
|
+
this.ctx.ui.requestRender();
|
|
871
871
|
this.ctx.updateEditorBorderColor();
|
|
872
872
|
this.ctx.chatContainer.clear();
|
|
873
873
|
this.ctx.pendingMessagesContainer.clear();
|
|
@@ -893,7 +893,7 @@ export class CommandController {
|
|
|
893
893
|
}
|
|
894
894
|
const stateLabel = result.closedProviderSessions === 1 ? "provider state" : "provider states";
|
|
895
895
|
this.ctx.statusLine.invalidate();
|
|
896
|
-
this.ctx.
|
|
896
|
+
this.ctx.ui.requestRender();
|
|
897
897
|
this.ctx.showStatus(`Fresh provider session started (${result.closedProviderSessions} ${stateLabel} pruned).`);
|
|
898
898
|
}
|
|
899
899
|
|
|
@@ -923,7 +923,7 @@ export class CommandController {
|
|
|
923
923
|
}
|
|
924
924
|
|
|
925
925
|
this.ctx.statusLine.invalidate();
|
|
926
|
-
this.ctx.
|
|
926
|
+
this.ctx.ui.requestRender();
|
|
927
927
|
|
|
928
928
|
const sessionFile = this.ctx.session.sessionFile;
|
|
929
929
|
const shortPath = sessionFile ? sessionFile.split("/").pop() : "new session";
|
|
@@ -1156,25 +1156,10 @@ export class CommandController {
|
|
|
1156
1156
|
}
|
|
1157
1157
|
this.ctx.rebuildChatFromMessages();
|
|
1158
1158
|
this.ctx.statusLine.invalidate();
|
|
1159
|
-
this.ctx.
|
|
1159
|
+
this.ctx.ui.requestRender();
|
|
1160
1160
|
this.ctx.showStatus(formatShakeSummary(result));
|
|
1161
1161
|
}
|
|
1162
1162
|
|
|
1163
|
-
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
|
1164
|
-
try {
|
|
1165
|
-
const content = await Bun.file(skillPath).text();
|
|
1166
|
-
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
1167
|
-
const metaLines = [`Skill: ${skillPath}`];
|
|
1168
|
-
if (args) {
|
|
1169
|
-
metaLines.push(`User: ${args}`);
|
|
1170
|
-
}
|
|
1171
|
-
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
|
1172
|
-
await this.ctx.session.prompt(message);
|
|
1173
|
-
} catch (err) {
|
|
1174
|
-
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
1163
|
async executeCompaction(
|
|
1179
1164
|
customInstructionsOrOptions?: string | CompactOptions,
|
|
1180
1165
|
isAuto = false,
|
|
@@ -1219,7 +1204,7 @@ export class CommandController {
|
|
|
1219
1204
|
this.ctx.rebuildChatFromMessages();
|
|
1220
1205
|
|
|
1221
1206
|
this.ctx.statusLine.invalidate();
|
|
1222
|
-
this.ctx.
|
|
1207
|
+
this.ctx.ui.requestRender();
|
|
1223
1208
|
} catch (error) {
|
|
1224
1209
|
if (error instanceof CompactionCancelledError) {
|
|
1225
1210
|
outcome = "cancelled";
|
|
@@ -1285,7 +1270,7 @@ export class CommandController {
|
|
|
1285
1270
|
this.ctx.rebuildChatFromMessages();
|
|
1286
1271
|
|
|
1287
1272
|
this.ctx.statusLine.invalidate();
|
|
1288
|
-
this.ctx.
|
|
1273
|
+
this.ctx.ui.requestRender();
|
|
1289
1274
|
this.ctx.updateEditorBorderColor();
|
|
1290
1275
|
await this.ctx.reloadTodos();
|
|
1291
1276
|
|
|
@@ -32,7 +32,7 @@ import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
|
32
32
|
import { interruptHint } from "../shared";
|
|
33
33
|
import { createAssistantMessageComponent } from "../utils/interactive-context-helpers";
|
|
34
34
|
import { StreamingRevealController } from "./streaming-reveal";
|
|
35
|
-
import { ToolArgsRevealController } from "./tool-args-reveal";
|
|
35
|
+
import { streamingStringKeysForTool, ToolArgsRevealController } from "./tool-args-reveal";
|
|
36
36
|
|
|
37
37
|
type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
38
38
|
|
|
@@ -287,7 +287,7 @@ export class EventController {
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
this.ctx.statusLine.invalidate();
|
|
290
|
-
this.ctx.
|
|
290
|
+
this.ctx.ui.requestRender();
|
|
291
291
|
|
|
292
292
|
const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
|
|
293
293
|
await run(event);
|
|
@@ -646,7 +646,7 @@ export class EventController {
|
|
|
646
646
|
renderArgs = this.#toolArgsReveal.setTarget(content.id, partialJson, {
|
|
647
647
|
rawInput,
|
|
648
648
|
exposeRawPartialJson: exposesRawPartialJson(content.name, rawInput, tool),
|
|
649
|
-
|
|
649
|
+
streamingStringKeys: streamingStringKeysForTool(content.name, rawInput),
|
|
650
650
|
});
|
|
651
651
|
} else {
|
|
652
652
|
this.#toolArgsReveal.finish(content.id);
|
|
@@ -786,7 +786,9 @@ export class EventController {
|
|
|
786
786
|
this.#lastAssistantComponent = this.ctx.streamingComponent;
|
|
787
787
|
this.#lastAssistantComponent.markTranscriptBlockFinalized();
|
|
788
788
|
if (settings.get("display.showTokenUsage")) {
|
|
789
|
-
this.ctx.chatContainer.addChild(
|
|
789
|
+
this.ctx.chatContainer.addChild(
|
|
790
|
+
createUsageRowBlock(event.message.usage, event.message.duration, event.message.ttft),
|
|
791
|
+
);
|
|
790
792
|
}
|
|
791
793
|
this.ctx.streamingComponent = undefined;
|
|
792
794
|
this.ctx.streamingMessage = undefined;
|
|
@@ -800,7 +802,7 @@ export class EventController {
|
|
|
800
802
|
this.ctx.showPinnedError(event.message.errorMessage);
|
|
801
803
|
}
|
|
802
804
|
this.ctx.statusLine.invalidate();
|
|
803
|
-
this.ctx.
|
|
805
|
+
this.ctx.ui.requestRender();
|
|
804
806
|
}
|
|
805
807
|
this.ctx.ui.requestRender();
|
|
806
808
|
}
|
|
@@ -1158,21 +1160,21 @@ export class EventController {
|
|
|
1158
1160
|
if (!event.skipped) {
|
|
1159
1161
|
this.ctx.rebuildChatFromMessages();
|
|
1160
1162
|
this.ctx.statusLine.invalidate();
|
|
1161
|
-
this.ctx.
|
|
1163
|
+
this.ctx.ui.requestRender();
|
|
1162
1164
|
}
|
|
1163
1165
|
this.ctx.showWarning(event.errorMessage);
|
|
1164
1166
|
} else if (!event.skipped) {
|
|
1165
1167
|
this.ctx.lastAssistantUsage = undefined;
|
|
1166
1168
|
this.ctx.rebuildChatFromMessages();
|
|
1167
1169
|
this.ctx.statusLine.invalidate();
|
|
1168
|
-
this.ctx.
|
|
1170
|
+
this.ctx.ui.requestRender();
|
|
1169
1171
|
this.ctx.showStatus("Auto-shake completed");
|
|
1170
1172
|
}
|
|
1171
1173
|
} else if (event.result) {
|
|
1172
1174
|
this.ctx.lastAssistantUsage = undefined;
|
|
1173
1175
|
this.ctx.rebuildChatFromMessages();
|
|
1174
1176
|
this.ctx.statusLine.invalidate();
|
|
1175
|
-
this.ctx.
|
|
1177
|
+
this.ctx.ui.requestRender();
|
|
1176
1178
|
} else if (event.errorMessage) {
|
|
1177
1179
|
this.ctx.showWarning(event.errorMessage);
|
|
1178
1180
|
} else if (isHandoffAction) {
|
|
@@ -1180,7 +1182,7 @@ export class EventController {
|
|
|
1180
1182
|
this.ctx.lastAssistantUsage = undefined;
|
|
1181
1183
|
this.ctx.rebuildChatFromMessages();
|
|
1182
1184
|
this.ctx.statusLine.invalidate();
|
|
1183
|
-
this.ctx.
|
|
1185
|
+
this.ctx.ui.requestRender();
|
|
1184
1186
|
await this.ctx.reloadTodos();
|
|
1185
1187
|
this.ctx.showStatus("Auto-handoff completed");
|
|
1186
1188
|
} else if (event.skipped) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import type { CollabUiRequestDraft, CollabUiSelectItem } from "@oh-my-pi/pi-wire";
|
|
3
4
|
import { KeybindingsManager } from "../../config/keybindings";
|
|
4
5
|
import type {
|
|
5
6
|
CompactOptions,
|
|
@@ -28,6 +29,21 @@ import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-gen
|
|
|
28
29
|
|
|
29
30
|
const MAX_WIDGET_LINES = 10;
|
|
30
31
|
|
|
32
|
+
interface CollabDialogWinner {
|
|
33
|
+
source: "local" | "remote";
|
|
34
|
+
value: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toWireSelectOptions(options: ExtensionUISelectItem[]): CollabUiSelectItem[] {
|
|
38
|
+
return options.map(option =>
|
|
39
|
+
typeof option === "string"
|
|
40
|
+
? option
|
|
41
|
+
: option.description
|
|
42
|
+
? { label: option.label, description: option.description }
|
|
43
|
+
: { label: option.label },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
export class ExtensionUiController {
|
|
32
48
|
#extensionTerminalInputUnsubscribers = new Set<() => void>();
|
|
33
49
|
#hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
|
|
@@ -45,7 +61,7 @@ export class ExtensionUiController {
|
|
|
45
61
|
async initHooksAndCustomTools(): Promise<void> {
|
|
46
62
|
// Create and set hook & tool UI context
|
|
47
63
|
const uiContext: ExtensionUIContext = {
|
|
48
|
-
select: (title, options, dialogOptions) => this.
|
|
64
|
+
select: (title, options, dialogOptions) => this.showCollabAwareSelector(title, options, dialogOptions),
|
|
49
65
|
confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
|
|
50
66
|
input: (title, placeholder, dialogOptions) => this.showHookInput(title, placeholder, dialogOptions),
|
|
51
67
|
notify: (message, type) => this.showHookNotify(message, type),
|
|
@@ -61,7 +77,7 @@ export class ExtensionUiController {
|
|
|
61
77
|
},
|
|
62
78
|
getEditorText: () => this.ctx.editor.getText(),
|
|
63
79
|
editor: (title, prefill, dialogOptions, editorOptions) =>
|
|
64
|
-
this.
|
|
80
|
+
this.showCollabAwareEditor(title, prefill, dialogOptions, editorOptions),
|
|
65
81
|
get theme() {
|
|
66
82
|
return theme;
|
|
67
83
|
},
|
|
@@ -171,7 +187,6 @@ export class ExtensionUiController {
|
|
|
171
187
|
// Reset and update status line
|
|
172
188
|
this.ctx.statusLine.invalidate();
|
|
173
189
|
this.ctx.statusLine.resetActiveTime();
|
|
174
|
-
this.ctx.updateEditorTopBorder();
|
|
175
190
|
this.ctx.ui.requestRender();
|
|
176
191
|
|
|
177
192
|
// Clear UI state
|
|
@@ -531,6 +546,72 @@ export class ExtensionUiController {
|
|
|
531
546
|
this.ctx.ui.requestRender();
|
|
532
547
|
}
|
|
533
548
|
|
|
549
|
+
async showCollabAwareSelector(
|
|
550
|
+
title: string,
|
|
551
|
+
options: ExtensionUISelectItem[],
|
|
552
|
+
dialogOptions?: InteractiveSelectorDialogOptions,
|
|
553
|
+
extra?: { slider?: HookSelectorSlider },
|
|
554
|
+
): Promise<string | undefined> {
|
|
555
|
+
const request: CollabUiRequestDraft = {
|
|
556
|
+
kind: "select",
|
|
557
|
+
title,
|
|
558
|
+
options: toWireSelectOptions(options),
|
|
559
|
+
initialIndex: dialogOptions?.initialIndex,
|
|
560
|
+
selectionMarker: dialogOptions?.selectionMarker,
|
|
561
|
+
checkedIndices: dialogOptions?.checkedIndices ? [...dialogOptions.checkedIndices] : undefined,
|
|
562
|
+
markableCount: dialogOptions?.markableCount,
|
|
563
|
+
helpText: dialogOptions?.helpText,
|
|
564
|
+
};
|
|
565
|
+
return this.#raceCollabDialog(request, dialogOptions?.signal, signal =>
|
|
566
|
+
this.showHookSelector(title, options, { ...dialogOptions, signal }, extra),
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async showCollabAwareEditor(
|
|
571
|
+
title: string,
|
|
572
|
+
prefill?: string,
|
|
573
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
574
|
+
editorOptions?: { promptStyle?: boolean },
|
|
575
|
+
): Promise<string | undefined> {
|
|
576
|
+
const request: CollabUiRequestDraft = { kind: "editor", title, prefill };
|
|
577
|
+
return this.#raceCollabDialog(request, dialogOptions?.signal, signal =>
|
|
578
|
+
this.showHookEditor(title, prefill, { ...dialogOptions, signal }, editorOptions),
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Race the local hook dialog against a mirrored guest ask. First *answer*
|
|
584
|
+
* wins and cancels the other side. A remote `unavailable` settlement
|
|
585
|
+
* (collab teardown, relay drop, abort) is NOT an answer: the local dialog
|
|
586
|
+
* keeps running — the host user may be mid-keystroke in it — and its
|
|
587
|
+
* eventual result is returned.
|
|
588
|
+
*/
|
|
589
|
+
async #raceCollabDialog(
|
|
590
|
+
request: CollabUiRequestDraft,
|
|
591
|
+
signal: AbortSignal | undefined,
|
|
592
|
+
local: (signal: AbortSignal | undefined) => Promise<string | undefined>,
|
|
593
|
+
): Promise<string | undefined> {
|
|
594
|
+
const host = this.ctx.collabHost;
|
|
595
|
+
if (!host) return local(signal);
|
|
596
|
+
const localAbort = new AbortController();
|
|
597
|
+
const remoteAbort = new AbortController();
|
|
598
|
+
const remote = host.requestGuestUi(
|
|
599
|
+
request,
|
|
600
|
+
signal ? AbortSignal.any([signal, remoteAbort.signal]) : remoteAbort.signal,
|
|
601
|
+
);
|
|
602
|
+
if (!remote) return local(signal);
|
|
603
|
+
const localWinner = local(signal ? AbortSignal.any([signal, localAbort.signal]) : localAbort.signal).then(
|
|
604
|
+
(value): CollabDialogWinner => ({ source: "local", value }),
|
|
605
|
+
);
|
|
606
|
+
const remoteWinner: Promise<CollabDialogWinner> = remote.then(result =>
|
|
607
|
+
result.kind === "answered" ? { source: "remote", value: result.value } : localWinner,
|
|
608
|
+
);
|
|
609
|
+
const winner = await Promise.race([localWinner, remoteWinner]);
|
|
610
|
+
if (winner.source === "remote") localAbort.abort();
|
|
611
|
+
else remoteAbort.abort();
|
|
612
|
+
return winner.value;
|
|
613
|
+
}
|
|
614
|
+
|
|
534
615
|
/**
|
|
535
616
|
* Show a selector for hooks.
|
|
536
617
|
*/
|
|
@@ -110,6 +110,17 @@ function wrapPasteInAttachmentBlock(content: string): string {
|
|
|
110
110
|
return `<attachment>\n${content}\n</attachment>`;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** Run a teardown abort that must never throw (Esc / Ctrl+C path). A thrown
|
|
114
|
+
* error is logged at debug instead of silently swallowed, so a failing abort
|
|
115
|
+
* stays diagnosable without disturbing teardown ordering. */
|
|
116
|
+
function safeAbort(label: string, fn: () => void): void {
|
|
117
|
+
try {
|
|
118
|
+
fn();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logger.debug(`Failed to abort ${label}`, { error: err instanceof Error ? err.message : String(err) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000;
|
|
114
125
|
// A cached model fires its file-load events in a short burst and then goes silent
|
|
115
126
|
// while onnxruntime builds the session; a genuine download keeps streaming progress
|
|
@@ -306,21 +317,15 @@ export class InputController {
|
|
|
306
317
|
const viewSession = this.ctx.viewSession;
|
|
307
318
|
let aborted = false;
|
|
308
319
|
if (viewSession.isCompacting) {
|
|
309
|
-
|
|
310
|
-
viewSession.abortCompaction();
|
|
311
|
-
} catch {}
|
|
320
|
+
safeAbort("compaction", () => viewSession.abortCompaction());
|
|
312
321
|
aborted = true;
|
|
313
322
|
}
|
|
314
323
|
if (viewSession.isGeneratingHandoff) {
|
|
315
|
-
|
|
316
|
-
viewSession.abortHandoff();
|
|
317
|
-
} catch {}
|
|
324
|
+
safeAbort("handoff", () => viewSession.abortHandoff());
|
|
318
325
|
aborted = true;
|
|
319
326
|
}
|
|
320
327
|
if (viewSession.isRetrying) {
|
|
321
|
-
|
|
322
|
-
viewSession.abortRetry();
|
|
323
|
-
} catch {}
|
|
328
|
+
safeAbort("retry", () => viewSession.abortRetry());
|
|
324
329
|
aborted = true;
|
|
325
330
|
}
|
|
326
331
|
if (aborted) return;
|
|
@@ -382,9 +387,7 @@ export class InputController {
|
|
|
382
387
|
} else if (this.ctx.session.isStreaming) {
|
|
383
388
|
this.#handleStreamingEscape();
|
|
384
389
|
} else if (this.ctx.editor.getText().trim()) {
|
|
385
|
-
// Esc
|
|
386
|
-
this.ctx.editor.setText("");
|
|
387
|
-
this.ctx.ui.requestRender();
|
|
390
|
+
// Esc must not destroy an in-progress draft; it only disarms a previous empty-editor Esc.
|
|
388
391
|
this.ctx.lastEscapeTime = 0;
|
|
389
392
|
this.#clearStreamingEscapeArm();
|
|
390
393
|
} else {
|
|
@@ -836,7 +839,7 @@ export class InputController {
|
|
|
836
839
|
this.ctx.session.sessionId,
|
|
837
840
|
this.ctx.session.model,
|
|
838
841
|
provider => this.ctx.session.agent.metadataForProvider(provider),
|
|
839
|
-
this.ctx.titleSystemPrompt,
|
|
842
|
+
this.ctx.session.titleSystemPrompt,
|
|
840
843
|
)
|
|
841
844
|
.then(async title => {
|
|
842
845
|
// Re-check: a concurrent attempt for an earlier message may have
|
|
@@ -157,7 +157,6 @@ export class SelectorController {
|
|
|
157
157
|
const result = await previewTheme(themeName);
|
|
158
158
|
if (result.success) {
|
|
159
159
|
this.ctx.statusLine.invalidate();
|
|
160
|
-
this.ctx.updateEditorTopBorder();
|
|
161
160
|
this.ctx.ui.invalidate();
|
|
162
161
|
this.ctx.ui.requestRender();
|
|
163
162
|
}
|
|
@@ -175,7 +174,6 @@ export class SelectorController {
|
|
|
175
174
|
compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
|
|
176
175
|
...previewSettings,
|
|
177
176
|
});
|
|
178
|
-
this.ctx.updateEditorTopBorder();
|
|
179
177
|
this.ctx.ui.requestRender();
|
|
180
178
|
},
|
|
181
179
|
getStatusLinePreview: () => {
|
|
@@ -203,7 +201,6 @@ export class SelectorController {
|
|
|
203
201
|
transparent: settings.get("statusLine.transparent"),
|
|
204
202
|
compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
|
|
205
203
|
});
|
|
206
|
-
this.ctx.updateEditorTopBorder();
|
|
207
204
|
this.ctx.ui.requestRender();
|
|
208
205
|
},
|
|
209
206
|
},
|
|
@@ -456,7 +453,6 @@ export class SelectorController {
|
|
|
456
453
|
case "tui.tight":
|
|
457
454
|
setTuiTight(value as boolean);
|
|
458
455
|
this.ctx.ui.invalidate();
|
|
459
|
-
this.ctx.updateEditorTopBorder();
|
|
460
456
|
this.ctx.ui.requestRender();
|
|
461
457
|
break;
|
|
462
458
|
|
|
@@ -472,7 +468,7 @@ export class SelectorController {
|
|
|
472
468
|
case "theme": {
|
|
473
469
|
setTheme(value as string, true).then(result => {
|
|
474
470
|
this.ctx.statusLine.invalidate();
|
|
475
|
-
this.ctx.
|
|
471
|
+
this.ctx.ui.requestRender();
|
|
476
472
|
this.ctx.ui.invalidate();
|
|
477
473
|
if (!result.success) {
|
|
478
474
|
this.ctx.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
|
|
@@ -483,7 +479,7 @@ export class SelectorController {
|
|
|
483
479
|
case "symbolPreset": {
|
|
484
480
|
setSymbolPreset(value as "unicode" | "nerd" | "ascii").then(() => {
|
|
485
481
|
this.ctx.statusLine.invalidate();
|
|
486
|
-
this.ctx.
|
|
482
|
+
this.ctx.ui.requestRender();
|
|
487
483
|
this.ctx.ui.invalidate();
|
|
488
484
|
});
|
|
489
485
|
break;
|
|
@@ -557,7 +553,6 @@ export class SelectorController {
|
|
|
557
553
|
compactThinkingLevel: settings.get("statusLine.compactThinkingLevel"),
|
|
558
554
|
};
|
|
559
555
|
this.ctx.statusLine.updateSettings(statusLineSettings);
|
|
560
|
-
this.ctx.updateEditorTopBorder();
|
|
561
556
|
this.ctx.ui.requestRender();
|
|
562
557
|
break;
|
|
563
558
|
}
|
|
@@ -1046,7 +1041,7 @@ export class SelectorController {
|
|
|
1046
1041
|
this.ctx.clearTransientSessionUi();
|
|
1047
1042
|
this.ctx.statusLine.invalidate();
|
|
1048
1043
|
this.ctx.statusLine.resetActiveTime();
|
|
1049
|
-
this.ctx.
|
|
1044
|
+
this.ctx.ui.requestRender();
|
|
1050
1045
|
this.ctx.updateEditorBorderColor();
|
|
1051
1046
|
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
1052
1047
|
await this.ctx.reloadTodos();
|