@oh-my-pi/pi-coding-agent 16.4.8 → 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 +63 -0
- package/dist/cli.js +3502 -3444
- 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 +17 -2
- 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/status-line/types.d.ts +3 -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/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 +111 -21
- package/src/modes/components/model-hub.ts +23 -116
- package/src/modes/components/model-picker.ts +233 -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/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 +7 -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/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
|
@@ -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 {
|
|
@@ -665,6 +665,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
665
665
|
setMarkdownMermaidRendering(settings.get("tui.renderMermaid"));
|
|
666
666
|
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
|
|
667
667
|
this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
|
|
668
|
+
this.ui.setScrollbackRebuild(settings.get("tui.scrollbackRebuild"));
|
|
668
669
|
// OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
|
|
669
670
|
// capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off
|
|
670
671
|
// unless the user opts in, and never emits raw escapes on other terminals.
|
|
@@ -1614,9 +1615,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1614
1615
|
}
|
|
1615
1616
|
}
|
|
1616
1617
|
this.chatContainer.clear();
|
|
1617
|
-
// Live display
|
|
1618
|
-
//
|
|
1619
|
-
|
|
1618
|
+
// Live display collapses to the compacted transcript tail unless the
|
|
1619
|
+
// user opted into the full inline history; export/resume callers choose
|
|
1620
|
+
// their own mode.
|
|
1621
|
+
const context = this.viewSession.buildTranscriptSessionContext({
|
|
1622
|
+
collapseCompactedHistory: settings.get("display.collapseCompacted"),
|
|
1623
|
+
});
|
|
1620
1624
|
this.renderSessionContext(context);
|
|
1621
1625
|
for (const child of liveComponents) {
|
|
1622
1626
|
this.chatContainer.addChild(child);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract: `--mode json` output stays linear in conversation size. Streaming
|
|
3
|
+
* `message_update` events must not re-serialize the in-progress message, and
|
|
4
|
+
* printed messages must not carry provider-opaque replay payloads (encrypted
|
|
5
|
+
* reasoning history), which previously produced multi-GB transcripts.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from "bun:test";
|
|
8
|
+
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { AgentSessionEvent } from "../session/agent-session";
|
|
10
|
+
import { printableEvent } from "./print-mode";
|
|
11
|
+
|
|
12
|
+
const assistant: AssistantMessage = {
|
|
13
|
+
role: "assistant",
|
|
14
|
+
content: [{ type: "text", text: "hello" }],
|
|
15
|
+
api: "openai-responses",
|
|
16
|
+
provider: "openai",
|
|
17
|
+
model: "gpt-test",
|
|
18
|
+
usage: {} as AssistantMessage["usage"],
|
|
19
|
+
stopReason: "stop",
|
|
20
|
+
timestamp: 1,
|
|
21
|
+
providerPayload: { type: "openaiResponsesHistory", provider: "openai", dt: true, items: [{ big: "blob" }] },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("printableEvent", () => {
|
|
25
|
+
it("emits only the incremental delta for message_update", () => {
|
|
26
|
+
const event: AgentSessionEvent = {
|
|
27
|
+
type: "message_update",
|
|
28
|
+
message: assistant,
|
|
29
|
+
assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "hel", partial: assistant },
|
|
30
|
+
};
|
|
31
|
+
const printed = JSON.parse(JSON.stringify(printableEvent(event)));
|
|
32
|
+
expect(printed).toEqual({
|
|
33
|
+
type: "message_update",
|
|
34
|
+
assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: "hel" },
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("drops the done-variant message snapshot from message_update", () => {
|
|
39
|
+
const event: AgentSessionEvent = {
|
|
40
|
+
type: "message_update",
|
|
41
|
+
message: assistant,
|
|
42
|
+
assistantMessageEvent: { type: "done", reason: "stop", message: assistant },
|
|
43
|
+
};
|
|
44
|
+
const printed = JSON.parse(JSON.stringify(printableEvent(event)));
|
|
45
|
+
expect(printed).toEqual({ type: "message_update", assistantMessageEvent: { type: "done", reason: "stop" } });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("strips providerPayload from message_end but keeps the message content", () => {
|
|
49
|
+
const event: AgentSessionEvent = { type: "message_end", message: assistant };
|
|
50
|
+
const printed = JSON.parse(JSON.stringify(printableEvent(event))) as {
|
|
51
|
+
message: Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
expect(printed.message.providerPayload).toBeUndefined();
|
|
54
|
+
expect(printed.message.content).toEqual([{ type: "text", text: "hello" }]);
|
|
55
|
+
expect(printed.message.model).toBe("gpt-test");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("strips providerPayload from every message in agent_end", () => {
|
|
59
|
+
const event: AgentSessionEvent = { type: "agent_end", messages: [assistant, assistant] };
|
|
60
|
+
const printed = JSON.parse(JSON.stringify(printableEvent(event))) as {
|
|
61
|
+
messages: Array<Record<string, unknown>>;
|
|
62
|
+
};
|
|
63
|
+
expect(printed.messages).toHaveLength(2);
|
|
64
|
+
for (const message of printed.messages) expect(message.providerPayload).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("passes unrelated events through untouched", () => {
|
|
68
|
+
const event: AgentSessionEvent = { type: "notice", level: "info", message: "hi" };
|
|
69
|
+
expect(printableEvent(event)).toBe(event);
|
|
70
|
+
});
|
|
71
|
+
});
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* - `omp -p "prompt"` - text output
|
|
6
6
|
* - `omp --mode json "prompt"` - JSON event stream
|
|
7
7
|
*/
|
|
8
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
8
9
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
9
10
|
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
10
|
-
import type { AgentSession } from "../session/agent-session";
|
|
11
|
+
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
11
12
|
import { isSilentAbort } from "../session/messages";
|
|
12
13
|
import { flushTelemetryExport } from "../telemetry-export";
|
|
13
14
|
import { initializeExtensions } from "./runtime-init";
|
|
@@ -28,6 +29,54 @@ export interface PrintModeOptions {
|
|
|
28
29
|
printThoughts?: boolean;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** Drop the provider-opaque replay payload (e.g. encrypted reasoning items) before printing. */
|
|
33
|
+
function stripProviderPayload<T extends AgentMessage>(message: T): T {
|
|
34
|
+
if (!("providerPayload" in message) || message.providerPayload === undefined) return message;
|
|
35
|
+
const { providerPayload: _providerPayload, ...rest } = message;
|
|
36
|
+
return rest as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Shape an event for `--mode json` output.
|
|
41
|
+
*
|
|
42
|
+
* Removes two classes of bloat so transcripts grow linearly with conversation
|
|
43
|
+
* size instead of quadratically (a single long turn used to re-serialize its
|
|
44
|
+
* whole in-progress message on every streamed delta, producing multi-GB logs):
|
|
45
|
+
* - `message_update` snapshots (`message`, `assistantMessageEvent.partial`,
|
|
46
|
+
* and the `done`/`error` payloads) are dropped; only the incremental delta
|
|
47
|
+
* is printed. The authoritative message follows in `message_end`.
|
|
48
|
+
* - `providerPayload` is transport-native replay state, opaque and useless
|
|
49
|
+
* outside this process.
|
|
50
|
+
*/
|
|
51
|
+
export function printableEvent(event: AgentSessionEvent): unknown {
|
|
52
|
+
switch (event.type) {
|
|
53
|
+
case "message_update": {
|
|
54
|
+
const streamEvent = event.assistantMessageEvent;
|
|
55
|
+
if (streamEvent.type === "done" || streamEvent.type === "error") {
|
|
56
|
+
return {
|
|
57
|
+
type: "message_update",
|
|
58
|
+
assistantMessageEvent: { type: streamEvent.type, reason: streamEvent.reason },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const { partial: _partial, ...rest } = streamEvent;
|
|
62
|
+
return { type: "message_update", assistantMessageEvent: rest };
|
|
63
|
+
}
|
|
64
|
+
case "message_start":
|
|
65
|
+
case "message_end":
|
|
66
|
+
return { ...event, message: stripProviderPayload(event.message) };
|
|
67
|
+
case "turn_end":
|
|
68
|
+
return {
|
|
69
|
+
...event,
|
|
70
|
+
message: stripProviderPayload(event.message),
|
|
71
|
+
toolResults: event.toolResults.map(stripProviderPayload),
|
|
72
|
+
};
|
|
73
|
+
case "agent_end":
|
|
74
|
+
return { ...event, messages: event.messages.map(stripProviderPayload) };
|
|
75
|
+
default:
|
|
76
|
+
return event;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
31
80
|
/**
|
|
32
81
|
* Run in print (single-shot) mode.
|
|
33
82
|
* Sends prompts to the agent and outputs the result.
|
|
@@ -58,7 +107,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
58
107
|
session.subscribe(event => {
|
|
59
108
|
// In JSON mode, output all events
|
|
60
109
|
if (mode === "json") {
|
|
61
|
-
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
110
|
+
process.stdout.write(`${JSON.stringify(printableEvent(event))}\n`);
|
|
62
111
|
}
|
|
63
112
|
});
|
|
64
113
|
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -92,6 +92,7 @@ export type SymbolKey =
|
|
|
92
92
|
// Icons
|
|
93
93
|
| "icon.model"
|
|
94
94
|
| "icon.plan"
|
|
95
|
+
| "icon.prewalk"
|
|
95
96
|
| "icon.goal"
|
|
96
97
|
| "icon.pause"
|
|
97
98
|
| "icon.loop"
|
|
@@ -225,6 +226,7 @@ export type SymbolKey =
|
|
|
225
226
|
| "tool.debug"
|
|
226
227
|
| "tool.mcp"
|
|
227
228
|
| "tool.job"
|
|
229
|
+
| "tool.launch"
|
|
228
230
|
| "tool.task"
|
|
229
231
|
| "tool.todo"
|
|
230
232
|
| "tool.memory"
|
|
@@ -300,6 +302,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
300
302
|
// Icons
|
|
301
303
|
"icon.model": "⬢",
|
|
302
304
|
"icon.plan": "🗺",
|
|
305
|
+
"icon.prewalk": "🏃",
|
|
303
306
|
"icon.goal": "🎯",
|
|
304
307
|
"icon.pause": "⏸",
|
|
305
308
|
"icon.loop": "↻",
|
|
@@ -433,6 +436,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
433
436
|
"tool.debug": "🐞",
|
|
434
437
|
"tool.mcp": "🔌",
|
|
435
438
|
"tool.job": "⚙",
|
|
439
|
+
"tool.launch": "🚀",
|
|
436
440
|
"tool.task": "⇶",
|
|
437
441
|
"tool.todo": "☑",
|
|
438
442
|
"tool.memory": "🧠",
|
|
@@ -560,6 +564,7 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
560
564
|
"icon.model": "\uec19",
|
|
561
565
|
// pick: | alt:
|
|
562
566
|
"icon.plan": "\uf2d2",
|
|
567
|
+
"icon.prewalk": "\uf29d",
|
|
563
568
|
// pick: (nf-fa-bullseye) | alt: (nf-md-target) ◎ ⌖
|
|
564
569
|
"icon.goal": "\uf140",
|
|
565
570
|
// pick: (nf-fa-pause) | alt: ⏸ ||
|
|
@@ -742,6 +747,7 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
742
747
|
"tool.debug": "\uEAD8",
|
|
743
748
|
"tool.mcp": "\uEB2D",
|
|
744
749
|
"tool.job": "\uEBA2",
|
|
750
|
+
"tool.launch": "\uF135",
|
|
745
751
|
"tool.task": "\uf4a0",
|
|
746
752
|
"tool.todo": "\uEAB3",
|
|
747
753
|
"tool.memory": "\uEACE",
|
|
@@ -816,6 +822,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
816
822
|
// Icons
|
|
817
823
|
"icon.model": "[M]",
|
|
818
824
|
"icon.plan": "plan",
|
|
825
|
+
"icon.prewalk": "prewalk",
|
|
819
826
|
"icon.goal": "goal",
|
|
820
827
|
"icon.pause": "||",
|
|
821
828
|
"icon.loop": "loop",
|
|
@@ -946,6 +953,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
946
953
|
"tool.debug": "dbg",
|
|
947
954
|
"tool.mcp": "<>",
|
|
948
955
|
"tool.job": "job",
|
|
956
|
+
"tool.launch": "run",
|
|
949
957
|
"tool.task": ">>>",
|
|
950
958
|
"tool.todo": "[x]",
|
|
951
959
|
"tool.memory": "mem",
|
|
@@ -1815,6 +1823,7 @@ export class Theme {
|
|
|
1815
1823
|
return {
|
|
1816
1824
|
model: this.#symbols["icon.model"],
|
|
1817
1825
|
plan: this.#symbols["icon.plan"],
|
|
1826
|
+
prewalk: this.#symbols["icon.prewalk"],
|
|
1818
1827
|
goal: this.#symbols["icon.goal"],
|
|
1819
1828
|
pause: this.#symbols["icon.pause"],
|
|
1820
1829
|
loop: this.#symbols["icon.loop"],
|