@oh-my-pi/pi-coding-agent 16.4.6 → 16.5.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 +88 -0
- package/dist/cli.js +3312 -3254
- package/dist/types/cli/args.d.ts +5 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +15 -0
- package/dist/types/config/model-resolver.d.ts +4 -3
- package/dist/types/config/model-roles.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +46 -14
- package/dist/types/extensibility/extensions/types.d.ts +1 -1
- package/dist/types/launch/broker.d.ts +2 -0
- package/dist/types/launch/client.d.ts +22 -0
- package/dist/types/launch/paths.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +8 -0
- package/dist/types/launch/protocol.d.ts +170 -0
- package/dist/types/launch/terminal-output.d.ts +7 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -1
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-browser.d.ts +28 -3
- package/dist/types/modes/components/model-hub.d.ts +0 -10
- package/dist/types/modes/components/model-picker.d.ts +44 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +14 -8
- package/dist/types/modes/print-mode.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/sdk.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +42 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/thinking.d.ts +2 -2
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
- package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
- package/dist/types/tools/browser/tab-worker.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/launch.d.ts +121 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/terminal-output.d.ts +5 -0
- package/dist/types/vibe/runtime.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +0 -8
- package/package.json +20 -20
- package/src/cli/args.ts +11 -0
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gallery-fixtures/shell.ts +82 -1
- package/src/cli.ts +10 -0
- package/src/commands/launch.ts +17 -0
- package/src/config/model-resolver.ts +110 -31
- package/src/config/model-roles.ts +14 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +2 -2
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/completion-bridge.ts +4 -4
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/extensions/model-api.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/internal-urls/docs-index.ts +4 -4
- package/src/launch/broker.ts +1017 -0
- package/src/launch/client.ts +344 -0
- package/src/launch/paths.ts +17 -0
- package/src/launch/presence.ts +82 -0
- package/src/launch/protocol.ts +386 -0
- package/src/launch/terminal-output.ts +46 -0
- package/src/main.ts +50 -1
- package/src/modes/acp/acp-agent.ts +8 -1
- package/src/modes/components/agent-hub.ts +101 -31
- package/src/modes/components/compaction-summary-message.ts +8 -2
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/model-browser.ts +152 -49
- package/src/modes/components/model-hub.ts +55 -142
- package/src/modes/components/model-picker.ts +233 -0
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
- package/src/modes/components/status-line/component.test.ts +41 -2
- package/src/modes/components/status-line/component.ts +4 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +9 -1
- package/src/modes/controllers/event-controller.ts +31 -32
- package/src/modes/controllers/selector-controller.ts +96 -22
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +20 -3
- package/src/modes/print-mode.test.ts +71 -0
- package/src/modes/print-mode.ts +51 -2
- package/src/modes/theme/theme.ts +9 -0
- package/src/modes/utils/ui-helpers.ts +25 -4
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/scout.md +1 -1
- package/src/prompts/system/plan-yolo-handoff.md +5 -0
- package/src/prompts/system/prewalk-checklist.md +7 -0
- package/src/prompts/system/prewalk-continue.md +1 -0
- package/src/prompts/system/prewalk-plan.md +13 -0
- package/src/prompts/system/system-prompt.md +9 -7
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/browser.md +4 -4
- package/src/prompts/tools/launch.md +25 -0
- package/src/sdk.ts +8 -2
- package/src/session/agent-session.ts +550 -87
- package/src/session/session-context.test.ts +10 -5
- package/src/session/session-context.ts +25 -8
- package/src/session/session-entries.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +25 -0
- package/src/task/agents.ts +2 -2
- package/src/thinking.ts +10 -3
- package/src/tiny/models.ts +7 -7
- package/src/tools/bash-interactive.ts +5 -8
- package/src/tools/bash.ts +38 -24
- package/src/tools/browser/cmux/cmux-tab.ts +17 -2
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +66 -6
- package/src/tools/browser/tab-protocol.ts +6 -0
- package/src/tools/browser/tab-supervisor.ts +17 -1
- package/src/tools/browser/tab-worker.ts +140 -21
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/index.ts +5 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/launch.ts +643 -0
- package/src/tools/render-utils.ts +3 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/terminal-output.ts +141 -0
- package/src/tts/speech-enhancer.ts +2 -2
- package/src/utils/image-vision-fallback.ts +3 -3
- package/src/vibe/runtime.ts +2 -2
- package/src/web/search/provider.ts +0 -10
- package/src/web/search/providers/perplexity.ts +18 -2
- package/src/web/search/providers/public.ts +2 -4
- package/src/web/search/types.ts +0 -10
- package/dist/types/web/search/providers/bing.d.ts +0 -14
- package/dist/types/web/search/providers/yahoo.d.ts +0 -14
- package/src/web/search/providers/bing.ts +0 -197
- package/src/web/search/providers/yahoo.ts +0 -179
|
@@ -82,6 +82,8 @@ export interface PlanReviewOverlayCallbacks {
|
|
|
82
82
|
onPick: (label: string) => void;
|
|
83
83
|
/** Invoked on Esc / cancel. */
|
|
84
84
|
onCancel: () => void;
|
|
85
|
+
/** Invoked with the current full plan text when the copy hotkey is pressed. */
|
|
86
|
+
onCopyPlan?: (content: string) => void | Promise<void>;
|
|
85
87
|
/** Invoked when the external-editor key is pressed (overlay stays open). */
|
|
86
88
|
onExternalEditor?: () => void;
|
|
87
89
|
/** Invoked when the external-editor key edits the active annotation draft. */
|
|
@@ -302,6 +304,10 @@ export class PlanReviewOverlay implements Component {
|
|
|
302
304
|
this.callbacks.onExternalEditor();
|
|
303
305
|
return;
|
|
304
306
|
}
|
|
307
|
+
if (this.callbacks.onCopyPlan && keyData === "c") {
|
|
308
|
+
void this.callbacks.onCopyPlan(joinPlanSections(this.#sections));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
305
311
|
if (matchesKey(keyData, "tab") || keyData === "\t") {
|
|
306
312
|
this.#cycleRegion(1);
|
|
307
313
|
return;
|
|
@@ -677,6 +683,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
677
683
|
parts.push("↑↓ scroll", "⇧ faster", "pgup/pgdn", "g/G ends");
|
|
678
684
|
break;
|
|
679
685
|
}
|
|
686
|
+
if (this.callbacks.onCopyPlan) parts.push("c copy");
|
|
680
687
|
parts.push("tab regions");
|
|
681
688
|
if (this.#externalEditorLabel && this.#focus !== "toc") parts.push(`${this.#externalEditorLabel} editor`);
|
|
682
689
|
parts.push(this.#helpSuffix);
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
Fix the settings overlay crash. Wheeling past the last row throws.
|
|
1
|
+
¶user:Fix the settings overlay crash. Wheeling past the last row throws.
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
//Reading the select-list hit test
|
|
6
|
-
read(path="src/select-list.ts:140-180")
|
|
3
|
+
¶call:read(path="src/select-list.ts:140-180")//Reading the select-list hit test
|
|
7
4
|
<out>
|
|
8
5
|
162: const index = Math.floor(line / rowHeight); index is never checked against bounds.
|
|
9
6
|
</out>
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
Found it. The hit test indexes past the filtered list; clamping to the last row fixes the crash.
|
|
8
|
+
¶ai:Found it. The hit test indexes past the filtered list; clamping to the last row fixes the crash.
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
Does the fix survive filtering?
|
|
10
|
+
¶user:Does the fix survive filtering?
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
¶think:Check whether the clamp runs before or after filtering.
|
|
13
|
+
|
|
14
|
+
¶ai:Yes. The clamp applies after the filter pass, so a narrowed list keeps the hit map in sync. Added a regression test that wheels past the last row with a filter active and asserts no throw.
|
|
@@ -4,15 +4,43 @@ import type { AgentSession } from "../../../session/agent-session";
|
|
|
4
4
|
import { getThemeByName, setThemeInstance } from "../../theme/theme";
|
|
5
5
|
import { StatusLineComponent } from "./component";
|
|
6
6
|
|
|
7
|
-
function makeSessionWithLastMessage(lastMessage: unknown) {
|
|
7
|
+
function makeSessionWithLastMessage(lastMessage: unknown, prewalkArmed: boolean = false) {
|
|
8
8
|
return {
|
|
9
|
-
messages: [lastMessage],
|
|
9
|
+
messages: lastMessage ? [lastMessage] : [],
|
|
10
10
|
model: { contextWindow: 128000 },
|
|
11
11
|
contextUsageRevision: 0,
|
|
12
12
|
systemPrompt: [],
|
|
13
13
|
agent: { state: { tools: [] } },
|
|
14
14
|
skills: [],
|
|
15
15
|
getContextUsage: () => ({ tokens: 42, contextWindow: 128000 }),
|
|
16
|
+
state: {
|
|
17
|
+
messages: lastMessage ? [lastMessage] : [],
|
|
18
|
+
model: { contextWindow: 128000 },
|
|
19
|
+
},
|
|
20
|
+
sessionManager: {
|
|
21
|
+
getUsageStatistics: () => ({
|
|
22
|
+
input: 0,
|
|
23
|
+
output: 0,
|
|
24
|
+
cacheRead: 0,
|
|
25
|
+
cacheWrite: 0,
|
|
26
|
+
totalTokens: 0,
|
|
27
|
+
orchestrationInput: 0,
|
|
28
|
+
orchestrationOutput: 0,
|
|
29
|
+
orchestrationCacheRead: 0,
|
|
30
|
+
premiumRequests: 0,
|
|
31
|
+
cost: 0,
|
|
32
|
+
tokensPerSecond: null,
|
|
33
|
+
}),
|
|
34
|
+
getSessionName: () => "test-session",
|
|
35
|
+
},
|
|
36
|
+
getPrewalkState: () => (prewalkArmed ? { target: { id: "cheap-model", provider: "openai" } } : undefined),
|
|
37
|
+
getAsyncJobSnapshot: () => undefined,
|
|
38
|
+
isAdvisorActive: () => false,
|
|
39
|
+
isFastModeActive: () => false,
|
|
40
|
+
configuredThinkingLevel: () => undefined,
|
|
41
|
+
modelRegistry: {
|
|
42
|
+
isUsingOAuth: () => false,
|
|
43
|
+
},
|
|
16
44
|
};
|
|
17
45
|
}
|
|
18
46
|
|
|
@@ -41,4 +69,15 @@ describe("StatusLineComponent", () => {
|
|
|
41
69
|
|
|
42
70
|
expect(statusLine.getCachedContextBreakdown()).toEqual({ usedTokens: 42, contextWindow: 128000 });
|
|
43
71
|
});
|
|
72
|
+
|
|
73
|
+
it("renders Prewalk annotation when prewalk is armed", () => {
|
|
74
|
+
const statusLine = new StatusLineComponent(makeSessionWithLastMessage(null, true) as unknown as AgentSession);
|
|
75
|
+
|
|
76
|
+
// By default preset, 'mode' segment is included in left/right segments.
|
|
77
|
+
// Let's get the border and see if Prewalk is rendered.
|
|
78
|
+
const border = statusLine.getTopBorder(100);
|
|
79
|
+
// SGR codes might be included, so we check if the stripped content contains "Prewalk"
|
|
80
|
+
const stripped = border.content.replace(/\x1b\[[0-9;]*m/g, "");
|
|
81
|
+
expect(stripped).toContain("Prewalk");
|
|
82
|
+
});
|
|
44
83
|
});
|
|
@@ -1051,6 +1051,10 @@ export class StatusLineComponent implements Component {
|
|
|
1051
1051
|
compactThinkingLevel: this.#resolveSettings().compactThinkingLevel ?? false,
|
|
1052
1052
|
planMode: this.#planModeStatus,
|
|
1053
1053
|
loopMode: this.#loopModeStatus,
|
|
1054
|
+
prewalk:
|
|
1055
|
+
typeof this.session.getPrewalkState === "function" && this.session.getPrewalkState()
|
|
1056
|
+
? { enabled: true }
|
|
1057
|
+
: null,
|
|
1054
1058
|
goalMode: this.#goalModeStatus,
|
|
1055
1059
|
vibeMode: this.#vibeModeStatus,
|
|
1056
1060
|
collab: this.#collabStatus,
|
|
@@ -208,6 +208,12 @@ const modeSegment: StatusLineSegment = {
|
|
|
208
208
|
return { content: theme.fg(color, content), visible: true };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
const prewalk = ctx.prewalk;
|
|
212
|
+
if (prewalk?.enabled) {
|
|
213
|
+
const content = withIcon(theme.icon.prewalk, "Prewalk");
|
|
214
|
+
return { content: theme.fg("accent", content), visible: true };
|
|
215
|
+
}
|
|
216
|
+
|
|
211
217
|
const goal = ctx.goalMode;
|
|
212
218
|
if (goal && (goal.enabled || goal.paused)) {
|
|
213
219
|
return renderGoalMode(ctx, goal);
|
|
@@ -21,4 +21,5 @@ Pair up live: `/collab` shares your session through an end-to-end encrypted rela
|
|
|
21
21
|
Press ← ← to drill into a running or finished agent and inspect its tool calls and transcript
|
|
22
22
|
Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
|
|
23
23
|
No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
|
|
24
|
-
Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice
|
|
24
|
+
Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice
|
|
25
|
+
Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
|
|
@@ -44,20 +44,19 @@ const NEW_GLOW_PERIOD_MS = 1500;
|
|
|
44
44
|
* affordance surfaces this many times as often. */
|
|
45
45
|
const NEW_TIP_WEIGHT = 4;
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (r < 0) return TIPS[i] ?? "";
|
|
47
|
+
/** Pick a tip from `tips`, biased toward "[NEW]" tips by {@link NEW_TIP_WEIGHT};
|
|
48
|
+
* `r` is a uniform sample in [0, 1). Returns "" when `tips` is empty.
|
|
49
|
+
* Exported for tests. */
|
|
50
|
+
export function pickWeightedTip(tips: readonly string[], r: number): string {
|
|
51
|
+
if (tips.length === 0) return "";
|
|
52
|
+
const weights = tips.map(tip => (NEW_TIP_MARKER.test(tip) ? NEW_TIP_WEIGHT : 1));
|
|
53
|
+
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
54
|
+
let acc = r * total;
|
|
55
|
+
for (let i = 0; i < tips.length; i++) {
|
|
56
|
+
acc -= weights[i] ?? 1;
|
|
57
|
+
if (acc < 0) return tips[i] ?? "";
|
|
59
58
|
}
|
|
60
|
-
return
|
|
59
|
+
return tips[tips.length - 1] ?? "";
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
type ColorEncoding = "ansi-16m" | "ansi-256";
|
|
@@ -161,7 +160,7 @@ export class WelcomeComponent implements Component {
|
|
|
161
160
|
if (theme.getSymbolPreset() === "unicode" && Math.random() < 0.1) {
|
|
162
161
|
this.#selectedTip = "Please use nerdfont 😭.";
|
|
163
162
|
} else {
|
|
164
|
-
this.#selectedTip = pickWeightedTip();
|
|
163
|
+
this.#selectedTip = pickWeightedTip(TIPS, Math.random());
|
|
165
164
|
}
|
|
166
165
|
}
|
|
167
166
|
return this.#selectedTip || undefined;
|
|
@@ -1200,7 +1200,15 @@ export class CommandController {
|
|
|
1200
1200
|
this.ctx.rebuildChatFromMessages();
|
|
1201
1201
|
|
|
1202
1202
|
this.ctx.statusLine.invalidate();
|
|
1203
|
-
|
|
1203
|
+
// Same as the auto-compaction rebuild: a collapsed transcript is an
|
|
1204
|
+
// intentional replacement, so drop the stale pre-compaction scrollback
|
|
1205
|
+
// instead of repainting the shrunken frame below it. With collapse
|
|
1206
|
+
// disabled the full history stays inline and scrollback is kept.
|
|
1207
|
+
if (this.ctx.settings.get("display.collapseCompacted")) {
|
|
1208
|
+
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
1209
|
+
} else {
|
|
1210
|
+
this.ctx.ui.requestRender();
|
|
1211
|
+
}
|
|
1204
1212
|
} catch (error) {
|
|
1205
1213
|
if (error instanceof CompactionCancelledError) {
|
|
1206
1214
|
outcome = "cancelled";
|
|
@@ -76,7 +76,7 @@ export class EventController {
|
|
|
76
76
|
#lastVisibleBlockCount = 0;
|
|
77
77
|
#renderedCustomMessages = new Set<string>();
|
|
78
78
|
#lastIntent: string | undefined = undefined;
|
|
79
|
-
#
|
|
79
|
+
#backgroundTaskCallIds = new Set<string>();
|
|
80
80
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
81
81
|
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
82
82
|
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
|
@@ -281,7 +281,7 @@ export class EventController {
|
|
|
281
281
|
this.#lastVisibleBlockCount = 0;
|
|
282
282
|
this.#renderedCustomMessages.clear();
|
|
283
283
|
this.#lastIntent = undefined;
|
|
284
|
-
this.#
|
|
284
|
+
this.#backgroundTaskCallIds.clear();
|
|
285
285
|
this.#readToolCallArgs.clear();
|
|
286
286
|
this.#readToolCallAssistantComponents.clear();
|
|
287
287
|
this.#lastAssistantComponent = undefined;
|
|
@@ -828,9 +828,9 @@ export class EventController {
|
|
|
828
828
|
// The turn ended without running these calls (abort/error/TTSR rewind),
|
|
829
829
|
// so they will never produce a result. Seal them so they stop animating
|
|
830
830
|
// and freeze instead of pinning the transcript live region while a retry
|
|
831
|
-
// streams fresh blocks below them. Background
|
|
831
|
+
// streams fresh blocks below them. Background task calls keep updating.
|
|
832
832
|
for (const [toolCallId, component] of this.ctx.pendingTools.entries()) {
|
|
833
|
-
if (!this.#
|
|
833
|
+
if (!this.#backgroundTaskCallIds.has(toolCallId) && component instanceof ToolExecutionComponent) {
|
|
834
834
|
component.seal();
|
|
835
835
|
}
|
|
836
836
|
}
|
|
@@ -948,7 +948,7 @@ export class EventController {
|
|
|
948
948
|
// While the call is still executing — a mixed blocking+async task
|
|
949
949
|
// call whose jobs settle before its blocking subset — treat it as a
|
|
950
950
|
// partial frame: `tool_execution_end` still owns the terminal result.
|
|
951
|
-
const isTerminal = isFinalAsyncState && this.#
|
|
951
|
+
const isTerminal = isFinalAsyncState && this.#backgroundTaskCallIds.has(event.toolCallId);
|
|
952
952
|
component.updateResult(
|
|
953
953
|
{ ...event.partialResult, isError: asyncState === "failed" },
|
|
954
954
|
!isTerminal,
|
|
@@ -956,7 +956,7 @@ export class EventController {
|
|
|
956
956
|
);
|
|
957
957
|
if (isTerminal) {
|
|
958
958
|
this.ctx.pendingTools.delete(event.toolCallId);
|
|
959
|
-
this.#
|
|
959
|
+
this.#backgroundTaskCallIds.delete(event.toolCallId);
|
|
960
960
|
}
|
|
961
961
|
this.ctx.ui.requestRender();
|
|
962
962
|
}
|
|
@@ -977,13 +977,7 @@ export class EventController {
|
|
|
977
977
|
component.updateResult({ ...event.result, isError: event.isError }, false, event.toolCallId);
|
|
978
978
|
this.ctx.pendingTools.delete(event.toolCallId);
|
|
979
979
|
}
|
|
980
|
-
|
|
981
|
-
if (asyncState === "running") {
|
|
982
|
-
this.#backgroundToolCallIds.add(event.toolCallId);
|
|
983
|
-
} else {
|
|
984
|
-
this.#backgroundToolCallIds.delete(event.toolCallId);
|
|
985
|
-
this.#clearReadToolCall(event.toolCallId);
|
|
986
|
-
}
|
|
980
|
+
this.#clearReadToolCall(event.toolCallId);
|
|
987
981
|
this.ctx.ui.requestRender();
|
|
988
982
|
} else {
|
|
989
983
|
let component = this.ctx.pendingTools.get(event.toolCallId);
|
|
@@ -996,29 +990,22 @@ export class EventController {
|
|
|
996
990
|
component = group;
|
|
997
991
|
this.ctx.pendingTools.set(event.toolCallId, group);
|
|
998
992
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
if (isBackgroundRunning) {
|
|
1003
|
-
this.#backgroundToolCallIds.add(event.toolCallId);
|
|
1004
|
-
} else {
|
|
1005
|
-
this.ctx.pendingTools.delete(event.toolCallId);
|
|
1006
|
-
this.#backgroundToolCallIds.delete(event.toolCallId);
|
|
1007
|
-
this.#clearReadToolCall(event.toolCallId);
|
|
1008
|
-
}
|
|
993
|
+
component.updateResult({ ...event.result, isError: event.isError }, false, event.toolCallId);
|
|
994
|
+
this.ctx.pendingTools.delete(event.toolCallId);
|
|
995
|
+
this.#clearReadToolCall(event.toolCallId);
|
|
1009
996
|
this.ctx.ui.requestRender();
|
|
1010
997
|
}
|
|
1011
998
|
} else {
|
|
1012
999
|
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
1013
1000
|
if (component) {
|
|
1014
1001
|
const asyncState = (event.result.details as { async?: { state?: string } } | undefined)?.async?.state;
|
|
1015
|
-
const
|
|
1016
|
-
component.updateResult({ ...event.result, isError: event.isError },
|
|
1017
|
-
if (
|
|
1018
|
-
this.#
|
|
1002
|
+
const isBackgroundTask = event.toolName === "task" && asyncState === "running";
|
|
1003
|
+
component.updateResult({ ...event.result, isError: event.isError }, isBackgroundTask, event.toolCallId);
|
|
1004
|
+
if (isBackgroundTask) {
|
|
1005
|
+
this.#backgroundTaskCallIds.add(event.toolCallId);
|
|
1019
1006
|
} else {
|
|
1020
1007
|
this.ctx.pendingTools.delete(event.toolCallId);
|
|
1021
|
-
this.#
|
|
1008
|
+
this.#backgroundTaskCallIds.delete(event.toolCallId);
|
|
1022
1009
|
}
|
|
1023
1010
|
if (component instanceof ToolExecutionComponent && component.isDisplaceableBlock()) {
|
|
1024
1011
|
if (event.toolName === "job" && component.canBeDisplacedBy("job")) {
|
|
@@ -1098,7 +1085,7 @@ export class EventController {
|
|
|
1098
1085
|
}
|
|
1099
1086
|
await this.ctx.flushPendingModelSwitch();
|
|
1100
1087
|
for (const toolCallId of Array.from(this.ctx.pendingTools.keys())) {
|
|
1101
|
-
if (!this.#
|
|
1088
|
+
if (!this.#backgroundTaskCallIds.has(toolCallId)) {
|
|
1102
1089
|
// A foreground tool still pending at turn end never delivered a result;
|
|
1103
1090
|
// seal it so it freezes (and stops animating) rather than lingering in
|
|
1104
1091
|
// the transcript live region as a streaming preview until the next thaw.
|
|
@@ -1112,8 +1099,8 @@ export class EventController {
|
|
|
1112
1099
|
this.ctx.pendingTools.delete(toolCallId);
|
|
1113
1100
|
}
|
|
1114
1101
|
}
|
|
1115
|
-
this.#
|
|
1116
|
-
Array.from(this.#
|
|
1102
|
+
this.#backgroundTaskCallIds = new Set(
|
|
1103
|
+
Array.from(this.#backgroundTaskCallIds).filter(toolCallId => this.ctx.pendingTools.has(toolCallId)),
|
|
1117
1104
|
);
|
|
1118
1105
|
this.#readToolCallArgs.clear();
|
|
1119
1106
|
this.#readToolCallAssistantComponents.clear();
|
|
@@ -1247,7 +1234,19 @@ export class EventController {
|
|
|
1247
1234
|
this.ctx.lastAssistantUsage = undefined;
|
|
1248
1235
|
this.ctx.rebuildChatFromMessages();
|
|
1249
1236
|
this.ctx.statusLine.invalidate();
|
|
1250
|
-
|
|
1237
|
+
// When history collapses behind the summary divider, the frame
|
|
1238
|
+
// shrinks far below the committed row count; without clearing, the
|
|
1239
|
+
// differential renderer's "duplication, never loss" resync repaints
|
|
1240
|
+
// the whole collapsed transcript (welcome box included) BELOW the
|
|
1241
|
+
// stale pre-compaction scrollback. Compaction is an intentional
|
|
1242
|
+
// transcript replacement then — same as auto-handoff below. With
|
|
1243
|
+
// collapse disabled the rebuilt transcript keeps the full history,
|
|
1244
|
+
// so the resync handles it and scrollback stays.
|
|
1245
|
+
if (settings.get("display.collapseCompacted")) {
|
|
1246
|
+
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
1247
|
+
} else {
|
|
1248
|
+
this.ctx.ui.requestRender();
|
|
1249
|
+
}
|
|
1251
1250
|
} else if (event.errorMessage) {
|
|
1252
1251
|
this.ctx.showWarning(event.errorMessage);
|
|
1253
1252
|
} else if (isHandoffAction) {
|
|
@@ -67,10 +67,12 @@ import { ExtensionDashboard } from "../components/extensions";
|
|
|
67
67
|
import { HistorySearchComponent } from "../components/history-search";
|
|
68
68
|
import { LoginDialogComponent } from "../components/login-dialog";
|
|
69
69
|
import { LogoutAccountSelectorComponent } from "../components/logout-account-selector";
|
|
70
|
-
import { ModelHubComponent
|
|
70
|
+
import { ModelHubComponent } from "../components/model-hub";
|
|
71
|
+
import { ModelPickerComponent } from "../components/model-picker";
|
|
71
72
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
72
73
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
73
74
|
import { ResetUsageSelectorComponent } from "../components/reset-usage-selector";
|
|
75
|
+
import { renderSegmentTrack } from "../components/segment-track";
|
|
74
76
|
import { SessionSelectorComponent } from "../components/session-selector";
|
|
75
77
|
import { SettingsSelectorComponent } from "../components/settings-selector";
|
|
76
78
|
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
@@ -451,12 +453,23 @@ export class SelectorController {
|
|
|
451
453
|
this.ctx.rebuildChatFromMessages();
|
|
452
454
|
this.ctx.ui.resetDisplay();
|
|
453
455
|
break;
|
|
456
|
+
case "display.collapseCompacted":
|
|
457
|
+
// Rebuild swaps between the collapsed tail and the full inline
|
|
458
|
+
// history; full reset retires blocks already committed to native
|
|
459
|
+
// scrollback (mirrors cacheMissMarker).
|
|
460
|
+
this.ctx.rebuildChatFromMessages();
|
|
461
|
+
this.ctx.ui.resetDisplay();
|
|
462
|
+
break;
|
|
454
463
|
case "tui.tight":
|
|
455
464
|
setTuiTight(value as boolean);
|
|
456
465
|
this.ctx.ui.invalidate();
|
|
457
466
|
this.ctx.ui.requestRender();
|
|
458
467
|
break;
|
|
459
468
|
|
|
469
|
+
case "tui.scrollbackRebuild":
|
|
470
|
+
this.ctx.ui.setScrollbackRebuild(value as boolean);
|
|
471
|
+
break;
|
|
472
|
+
|
|
460
473
|
case "tui.renderMermaid":
|
|
461
474
|
setMarkdownMermaidRendering(value as boolean);
|
|
462
475
|
this.ctx.session.refreshBaseSystemPrompt().catch(err => {
|
|
@@ -586,7 +599,85 @@ export class SelectorController {
|
|
|
586
599
|
}
|
|
587
600
|
|
|
588
601
|
showModelSelector(options?: { temporaryOnly?: boolean }): void {
|
|
589
|
-
|
|
602
|
+
if (options?.temporaryOnly) {
|
|
603
|
+
this.#showModelPicker();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
this.#showModelHub({});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Compact session-only model picker (alt+p / `/switch`): a floating
|
|
611
|
+
* bottom-anchored overlay over the transcript. The current model is
|
|
612
|
+
* highlighted and preselected; a leading `@` searches ctrl+p quick roles.
|
|
613
|
+
*/
|
|
614
|
+
#showModelPicker(): void {
|
|
615
|
+
const currentContextTokens = this.ctx.session.getContextUsage()?.tokens ?? 0;
|
|
616
|
+
const current = this.ctx.session.model;
|
|
617
|
+
const quickRoleOrder = this.ctx.settings.get("cycleOrder");
|
|
618
|
+
const quickRoleCycle = this.ctx.session.getRoleModelCycle(quickRoleOrder);
|
|
619
|
+
let overlayHandle: OverlayHandle | undefined;
|
|
620
|
+
let closed = false;
|
|
621
|
+
const done = () => {
|
|
622
|
+
if (closed) return;
|
|
623
|
+
closed = true;
|
|
624
|
+
overlayHandle?.hide();
|
|
625
|
+
this.focusActiveEditorArea();
|
|
626
|
+
this.ctx.ui.requestRender();
|
|
627
|
+
};
|
|
628
|
+
const picker = new ModelPickerComponent(
|
|
629
|
+
this.ctx.ui,
|
|
630
|
+
this.ctx.settings,
|
|
631
|
+
this.ctx.session.modelRegistry,
|
|
632
|
+
this.ctx.session.scopedModels,
|
|
633
|
+
{
|
|
634
|
+
onPick: async (model, selector) => {
|
|
635
|
+
try {
|
|
636
|
+
// Session-only: update agent state but don't persist the model to settings.
|
|
637
|
+
await this.ctx.session.setModelTemporary(model);
|
|
638
|
+
this.ctx.statusLine.invalidate();
|
|
639
|
+
this.ctx.updateEditorBorderColor();
|
|
640
|
+
const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
|
|
641
|
+
this.ctx.showStatus(`Session-only model: ${selector}. Use ${roleSelectorHint} or /model for roles.`);
|
|
642
|
+
done();
|
|
643
|
+
} catch (error) {
|
|
644
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
onPickRole: async entry => {
|
|
648
|
+
try {
|
|
649
|
+
await this.ctx.session.applyRoleModel(entry);
|
|
650
|
+
this.ctx.statusLine.invalidate();
|
|
651
|
+
this.ctx.updateEditorBorderColor();
|
|
652
|
+
this.ctx.showModelCycleTrack(
|
|
653
|
+
renderSegmentTrack(
|
|
654
|
+
quickRoleOrder.map(role => ({ label: role })),
|
|
655
|
+
quickRoleOrder.indexOf(entry.role),
|
|
656
|
+
),
|
|
657
|
+
);
|
|
658
|
+
done();
|
|
659
|
+
} catch (error) {
|
|
660
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
onCancel: done,
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
currentContextTokens,
|
|
667
|
+
currentSelector: current ? `${current.provider}/${current.id}` : undefined,
|
|
668
|
+
quickRoles: quickRoleCycle?.models,
|
|
669
|
+
quickRoleOrder,
|
|
670
|
+
currentQuickRole: quickRoleCycle?.models[quickRoleCycle.currentIndex]?.role,
|
|
671
|
+
},
|
|
672
|
+
);
|
|
673
|
+
overlayHandle = this.ctx.ui.showOverlay(picker, {
|
|
674
|
+
anchor: "bottom-center",
|
|
675
|
+
width: "100%",
|
|
676
|
+
maxHeight: "100%",
|
|
677
|
+
margin: 0,
|
|
678
|
+
});
|
|
679
|
+
this.ctx.ui.setFocus(picker);
|
|
680
|
+
this.ctx.ui.requestRender();
|
|
590
681
|
}
|
|
591
682
|
|
|
592
683
|
/**
|
|
@@ -595,13 +686,13 @@ export class SelectorController {
|
|
|
595
686
|
* untouched underneath. `initialProviderId` preselects a provider's sidebar
|
|
596
687
|
* entry — used when reopening the hub after a /login round-trip.
|
|
597
688
|
*/
|
|
598
|
-
#showModelHub(hubOptions: {
|
|
689
|
+
#showModelHub(hubOptions: { initialProviderId?: string }): void {
|
|
599
690
|
const currentContextTokens = this.ctx.session.getContextUsage()?.tokens ?? 0;
|
|
600
691
|
let overlayHandle: OverlayHandle | undefined;
|
|
601
692
|
let hub: ModelHubComponent | undefined;
|
|
602
693
|
let closed = false;
|
|
603
694
|
const done = () => {
|
|
604
|
-
// Re-entrant guard: cancel paths (Esc,
|
|
695
|
+
// Re-entrant guard: cancel paths (Esc, login forward) may race;
|
|
605
696
|
// the overlay must hide exactly once.
|
|
606
697
|
if (closed) return;
|
|
607
698
|
closed = true;
|
|
@@ -686,21 +777,6 @@ export class SelectorController {
|
|
|
686
777
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
687
778
|
}
|
|
688
779
|
},
|
|
689
|
-
onPick: async (model, selector) => {
|
|
690
|
-
try {
|
|
691
|
-
// Session-only: update agent state but don't persist the model to settings.
|
|
692
|
-
await this.ctx.session.setModelTemporary(model);
|
|
693
|
-
this.ctx.statusLine.invalidate();
|
|
694
|
-
this.ctx.updateEditorBorderColor();
|
|
695
|
-
const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
|
|
696
|
-
this.ctx.showStatus(
|
|
697
|
-
`Session-only model: ${selector ?? model.id}. Use ${roleSelectorHint} or /model for roles.`,
|
|
698
|
-
);
|
|
699
|
-
done();
|
|
700
|
-
} catch (error) {
|
|
701
|
-
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
702
|
-
}
|
|
703
|
-
},
|
|
704
780
|
onLoginRequest: providerId => {
|
|
705
781
|
done();
|
|
706
782
|
void this.#loginThenReopenModelHub(providerId);
|
|
@@ -718,8 +794,6 @@ export class SelectorController {
|
|
|
718
794
|
onCancel: () => done(),
|
|
719
795
|
},
|
|
720
796
|
{
|
|
721
|
-
mode: hubOptions.mode,
|
|
722
|
-
currentContextTokens,
|
|
723
797
|
initialProviderId: hubOptions.initialProviderId,
|
|
724
798
|
},
|
|
725
799
|
);
|
|
@@ -738,7 +812,7 @@ export class SelectorController {
|
|
|
738
812
|
async #loginThenReopenModelHub(providerId: string): Promise<void> {
|
|
739
813
|
const succeeded = await this.#handleOAuthLogin(providerId);
|
|
740
814
|
if (succeeded) {
|
|
741
|
-
this.#showModelHub({
|
|
815
|
+
this.#showModelHub({ initialProviderId: providerId });
|
|
742
816
|
}
|
|
743
817
|
}
|
|
744
818
|
|
|
@@ -3,12 +3,14 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import backgroundTanDispatchPrompt from "../../prompts/system/background-tan-dispatch.md" with { type: "text" };
|
|
6
|
+
import tanContextSwitchPrompt from "../../prompts/system/tan-context-switch.md" with { type: "text" };
|
|
6
7
|
import { AgentRegistry, MAIN_AGENT_ID } from "../../registry/agent-registry";
|
|
7
8
|
import * as sdk from "../../sdk";
|
|
8
9
|
import type { AgentSession } from "../../session/agent-session";
|
|
9
10
|
import { BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE } from "../../session/messages";
|
|
10
11
|
import { SessionManager } from "../../session/session-manager";
|
|
11
12
|
import { createMCPProxyTools, createSubagentSettings } from "../../task/executor";
|
|
13
|
+
import { USER_TODO_EDIT_CUSTOM_TYPE } from "../../tools/todo";
|
|
12
14
|
import type { InteractiveModeContext } from "../types";
|
|
13
15
|
|
|
14
16
|
const TAN_LABEL_PREVIEW_LENGTH = 80;
|
|
@@ -66,6 +68,11 @@ export class TanCommandController {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
const parentSessionId = session.sessionId;
|
|
71
|
+
// Providers route on `promptCacheKey ?? sessionId`, so the parent's live
|
|
72
|
+
// requests may cache under a pinned key that differs from its session id
|
|
73
|
+
// (the parent being itself a fork/tan). Mirror exactly what the parent
|
|
74
|
+
// populated the cache under — same rule as advisor and handoff calls.
|
|
75
|
+
const parentPromptCacheKey = session.agent.promptCacheKey ?? parentSessionId;
|
|
69
76
|
const thinkingLevel = session.configuredThinkingLevel();
|
|
70
77
|
const systemPrompt = [...session.systemPrompt];
|
|
71
78
|
const toolNames = session.getActiveToolNames();
|
|
@@ -111,7 +118,7 @@ export class TanCommandController {
|
|
|
111
118
|
systemPrompt,
|
|
112
119
|
toolNames,
|
|
113
120
|
providerSessionId: `${parentSessionId}:tan:${Snowflake.next()}`,
|
|
114
|
-
providerPromptCacheKey:
|
|
121
|
+
providerPromptCacheKey: parentPromptCacheKey,
|
|
115
122
|
modelRegistry,
|
|
116
123
|
authStorage: modelRegistry.authStorage,
|
|
117
124
|
settings,
|
|
@@ -127,19 +134,51 @@ export class TanCommandController {
|
|
|
127
134
|
disableExtensionDiscovery: true,
|
|
128
135
|
});
|
|
129
136
|
clone = created.session;
|
|
137
|
+
clone.sessionManager?.appendSessionInit?.({
|
|
138
|
+
systemPrompt: clone.systemPrompt ? clone.systemPrompt.join("\n\n") : systemPrompt.join("\n\n"),
|
|
139
|
+
task: trimmedWork,
|
|
140
|
+
tools: clone.getActiveToolNames ? clone.getActiveToolNames() : toolNames,
|
|
141
|
+
});
|
|
130
142
|
const abortClone = () => {
|
|
131
143
|
void clone?.abort();
|
|
132
144
|
};
|
|
133
145
|
signal.addEventListener("abort", abortClone, { once: true });
|
|
146
|
+
// The fork inherits the parent's todo list via session entries;
|
|
147
|
+
// its reminders would drag the tan back onto the parent's task.
|
|
148
|
+
// Clear runtime state and persist an empty edit so reloads agree.
|
|
149
|
+
clone.setTodoPhases([]);
|
|
150
|
+
cloneManager.appendCustomEntry(USER_TODO_EDIT_CUSTOM_TYPE, { phases: [] });
|
|
151
|
+
const injectContextSwitch = () => {
|
|
152
|
+
clone?.agent.appendMessage({
|
|
153
|
+
role: "developer",
|
|
154
|
+
content: tanContextSwitchPrompt,
|
|
155
|
+
attribution: "agent",
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
// Compaction summarizes the fork notice away with the rest of the
|
|
160
|
+
// history, after which the clone re-adopts the parent's task as its
|
|
161
|
+
// own (the summary blends both). Re-inject after every successful
|
|
162
|
+
// compaction so the fork boundary survives summarization.
|
|
163
|
+
const unsubscribeCompaction = clone.subscribe(event => {
|
|
164
|
+
if (event.type === "auto_compaction_end" && event.result && !event.aborted) {
|
|
165
|
+
injectContextSwitch();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
134
168
|
try {
|
|
135
169
|
if (signal.aborted) {
|
|
136
170
|
abortClone();
|
|
137
171
|
throw new Error("Aborted before execution");
|
|
138
172
|
}
|
|
173
|
+
// Inject a context-switch developer message so the clone knows
|
|
174
|
+
// it is a tangential fork — its parent owns the prior conversation;
|
|
175
|
+
// this agent must focus exclusively on the user's request.
|
|
176
|
+
injectContextSwitch();
|
|
139
177
|
await clone.prompt(trimmedWork, { attribution: "user" });
|
|
140
178
|
await clone.waitForIdle();
|
|
141
179
|
return extractAssistantText(clone.getLastAssistantMessage()) || "(no output)";
|
|
142
180
|
} finally {
|
|
181
|
+
unsubscribeCompaction();
|
|
143
182
|
signal.removeEventListener("abort", abortClone);
|
|
144
183
|
}
|
|
145
184
|
} finally {
|