@oh-my-pi/pi-coding-agent 16.4.4 → 16.4.6
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 +62 -0
- package/dist/cli.js +3799 -3729
- package/dist/types/async/job-manager.d.ts +8 -0
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +19 -9
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +100 -0
- package/dist/types/modes/components/model-hub.d.ts +52 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/async/job-manager.ts +9 -0
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +18 -7
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +32 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +769 -0
- package/src/modes/components/model-hub.ts +2002 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +160 -97
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +8 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +50 -24
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +186 -49
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
|
@@ -3,7 +3,7 @@ import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
4
4
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
|
|
5
5
|
import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import {
|
|
6
|
+
import { Loader, Spacer, setTuiTight, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { getAgentDbPath, getAgentDir, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import {
|
|
9
9
|
type AdvisorConfigScope,
|
|
@@ -65,8 +65,9 @@ import { AssistantMessageComponent } from "../components/assistant-message";
|
|
|
65
65
|
import { CopySelectorComponent } from "../components/copy-selector";
|
|
66
66
|
import { ExtensionDashboard } from "../components/extensions";
|
|
67
67
|
import { HistorySearchComponent } from "../components/history-search";
|
|
68
|
+
import { LoginDialogComponent } from "../components/login-dialog";
|
|
68
69
|
import { LogoutAccountSelectorComponent } from "../components/logout-account-selector";
|
|
69
|
-
import {
|
|
70
|
+
import { ModelHubComponent, type ModelHubMode } from "../components/model-hub";
|
|
70
71
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
71
72
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
72
73
|
import { ResetUsageSelectorComponent } from "../components/reset-usage-selector";
|
|
@@ -585,50 +586,45 @@ export class SelectorController {
|
|
|
585
586
|
}
|
|
586
587
|
|
|
587
588
|
showModelSelector(options?: { temporaryOnly?: boolean }): void {
|
|
589
|
+
this.#showModelHub({ mode: options?.temporaryOnly ? "pick" : "roles" });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Fullscreen model hub on the alternate screen (the /settings idiom): the
|
|
594
|
+
* overlay enables mouse tracking for its lifetime and the transcript stays
|
|
595
|
+
* untouched underneath. `initialProviderId` preselects a provider's sidebar
|
|
596
|
+
* entry — used when reopening the hub after a /login round-trip.
|
|
597
|
+
*/
|
|
598
|
+
#showModelHub(hubOptions: { mode: ModelHubMode; initialProviderId?: string }): void {
|
|
588
599
|
const currentContextTokens = this.ctx.session.getContextUsage()?.tokens ?? 0;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
600
|
+
let overlayHandle: OverlayHandle | undefined;
|
|
601
|
+
let hub: ModelHubComponent | undefined;
|
|
602
|
+
let closed = false;
|
|
603
|
+
const done = () => {
|
|
604
|
+
// Re-entrant guard: cancel paths (Esc, pick, login forward) may race;
|
|
605
|
+
// the overlay must hide exactly once.
|
|
606
|
+
if (closed) return;
|
|
607
|
+
closed = true;
|
|
608
|
+
hub?.dispose();
|
|
609
|
+
overlayHandle?.hide();
|
|
610
|
+
this.focusActiveEditorArea();
|
|
611
|
+
this.ctx.ui.requestRender();
|
|
612
|
+
};
|
|
613
|
+
hub = new ModelHubComponent(
|
|
614
|
+
this.ctx.ui,
|
|
615
|
+
this.ctx.settings,
|
|
616
|
+
this.ctx.session.modelRegistry,
|
|
617
|
+
this.ctx.session.scopedModels,
|
|
618
|
+
{
|
|
619
|
+
onAssign: async (model, role, thinkingLevel, selector) => {
|
|
597
620
|
// `auto` is session-global: never baked into a per-role model value
|
|
598
621
|
// (it can't round-trip through `model:<level>`). Apply it to the session
|
|
599
622
|
// separately and persist via `defaultThinkingLevel`.
|
|
600
623
|
const isAuto = thinkingLevel === AUTO_THINKING;
|
|
601
|
-
const concreteThinking = isAuto ? undefined : thinkingLevel;
|
|
624
|
+
const concreteThinking = isAuto || thinkingLevel === undefined ? undefined : thinkingLevel;
|
|
602
625
|
const selectorValue = selector ?? `${model.provider}/${model.id}`;
|
|
603
626
|
try {
|
|
604
|
-
if (
|
|
605
|
-
const fallbackSelector = formatModelSelectorValue(selectorValue, concreteThinking);
|
|
606
|
-
const fallbackChains = this.ctx.settings.get("retry.fallbackChains");
|
|
607
|
-
const chain = Array.isArray(fallbackChains[role]) ? fallbackChains[role] : [];
|
|
608
|
-
this.ctx.settings.set("retry.fallbackChains", {
|
|
609
|
-
...fallbackChains,
|
|
610
|
-
[role]: [fallbackSelector, ...chain.filter(existing => existing !== fallbackSelector)],
|
|
611
|
-
});
|
|
612
|
-
const roleInfo = getRoleInfo(role, settings);
|
|
613
|
-
const roleLabel = roleInfo?.name ?? role;
|
|
614
|
-
this.ctx.showStatus(`${roleLabel} fallback model: ${fallbackSelector}`);
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (role === null) {
|
|
618
|
-
// Temporary: update agent state but don't persist the model to settings
|
|
619
|
-
await this.ctx.session.setModelTemporary(model);
|
|
620
|
-
if (isAuto) {
|
|
621
|
-
this.ctx.session.setThinkingLevel(AUTO_THINKING, true);
|
|
622
|
-
}
|
|
623
|
-
this.ctx.statusLine.invalidate();
|
|
624
|
-
this.ctx.updateEditorBorderColor();
|
|
625
|
-
const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
|
|
626
|
-
this.ctx.showStatus(
|
|
627
|
-
`Session-only model: ${selector ?? model.id}. Use ${roleSelectorHint} or /model for roles.`,
|
|
628
|
-
);
|
|
629
|
-
done();
|
|
630
|
-
this.ctx.ui.requestRender();
|
|
631
|
-
} else if (role === "default") {
|
|
627
|
+
if (role === "default") {
|
|
632
628
|
const { switched } = await this.ctx.session.setModel(model, role, {
|
|
633
629
|
selector,
|
|
634
630
|
thinkingLevel: concreteThinking,
|
|
@@ -649,33 +645,101 @@ export class SelectorController {
|
|
|
649
645
|
this.ctx.updateEditorBorderColor();
|
|
650
646
|
}
|
|
651
647
|
this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
|
|
652
|
-
// Don't call done() - selector stays open for role assignment
|
|
653
648
|
} else {
|
|
654
|
-
// Other roles (smol, slow):
|
|
655
|
-
this.ctx.settings.setModelRole(
|
|
656
|
-
role,
|
|
657
|
-
formatModelSelectorValue(selector ?? `${model.provider}/${model.id}`, concreteThinking),
|
|
658
|
-
);
|
|
649
|
+
// Other roles (smol, slow, custom): update settings, not the current model.
|
|
650
|
+
this.ctx.settings.setModelRole(role, formatModelSelectorValue(selectorValue, concreteThinking));
|
|
659
651
|
if (isAuto) {
|
|
660
652
|
this.ctx.session.setThinkingLevel(AUTO_THINKING, true);
|
|
661
653
|
}
|
|
662
654
|
const roleInfo = getRoleInfo(role, settings);
|
|
663
|
-
|
|
664
|
-
this.ctx.showStatus(`${roleLabel} model: ${selector ?? model.id}`);
|
|
665
|
-
// Don't call done() - selector stays open
|
|
655
|
+
this.ctx.showStatus(`${roleInfo?.name ?? role} model: ${selector ?? model.id}`);
|
|
666
656
|
}
|
|
667
657
|
} catch (error) {
|
|
668
658
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
669
659
|
}
|
|
670
660
|
},
|
|
671
|
-
|
|
661
|
+
onUnassign: role => {
|
|
662
|
+
try {
|
|
663
|
+
this.ctx.settings.setModelRole(role, undefined);
|
|
664
|
+
const roleInfo = getRoleInfo(role, settings);
|
|
665
|
+
this.ctx.showStatus(`${roleInfo?.name ?? role} role cleared — auto-selection applies`);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
onFallbackChainChange: (role, chain) => {
|
|
671
|
+
try {
|
|
672
|
+
const chains = { ...this.ctx.settings.get("retry.fallbackChains") };
|
|
673
|
+
if (chain.length === 0) {
|
|
674
|
+
delete chains[role];
|
|
675
|
+
} else {
|
|
676
|
+
chains[role] = chain;
|
|
677
|
+
}
|
|
678
|
+
this.ctx.settings.set("retry.fallbackChains", chains);
|
|
679
|
+
const roleInfo = getRoleInfo(role, settings);
|
|
680
|
+
this.ctx.showStatus(
|
|
681
|
+
chain.length > 0
|
|
682
|
+
? `${roleInfo?.name ?? role} fallbacks: ${chain.join(" → ")}`
|
|
683
|
+
: `${roleInfo?.name ?? role} fallbacks cleared`,
|
|
684
|
+
);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
687
|
+
}
|
|
688
|
+
},
|
|
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
|
+
onLoginRequest: providerId => {
|
|
672
705
|
done();
|
|
673
|
-
this
|
|
706
|
+
void this.#loginThenReopenModelHub(providerId);
|
|
674
707
|
},
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
708
|
+
onCycleOrderChange: order => {
|
|
709
|
+
try {
|
|
710
|
+
this.ctx.settings.set("cycleOrder", order);
|
|
711
|
+
this.ctx.showStatus(
|
|
712
|
+
order.length > 0 ? `Quick-switch cycle: ${order.join(" → ")}` : "Quick-switch cycle cleared",
|
|
713
|
+
);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
onCancel: () => done(),
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
mode: hubOptions.mode,
|
|
722
|
+
currentContextTokens,
|
|
723
|
+
initialProviderId: hubOptions.initialProviderId,
|
|
724
|
+
},
|
|
725
|
+
);
|
|
726
|
+
overlayHandle = this.ctx.ui.showOverlay(hub, {
|
|
727
|
+
anchor: "bottom-center",
|
|
728
|
+
width: "100%",
|
|
729
|
+
maxHeight: "100%",
|
|
730
|
+
margin: 0,
|
|
731
|
+
fullscreen: true,
|
|
678
732
|
});
|
|
733
|
+
this.ctx.ui.setFocus(hub);
|
|
734
|
+
this.ctx.ui.requestRender();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** /login round-trip for a locked provider; reopen the hub on that provider only after a successful login. */
|
|
738
|
+
async #loginThenReopenModelHub(providerId: string): Promise<void> {
|
|
739
|
+
const succeeded = await this.#handleOAuthLogin(providerId);
|
|
740
|
+
if (succeeded) {
|
|
741
|
+
this.#showModelHub({ mode: "roles", initialProviderId: providerId });
|
|
742
|
+
}
|
|
679
743
|
}
|
|
680
744
|
|
|
681
745
|
async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
|
|
@@ -1122,60 +1186,51 @@ export class SelectorController {
|
|
|
1122
1186
|
await this.showSessionSelector();
|
|
1123
1187
|
}
|
|
1124
1188
|
|
|
1125
|
-
|
|
1189
|
+
/**
|
|
1190
|
+
* Run the OAuth login flow for `providerId` inside a cancellable
|
|
1191
|
+
* {@link LoginDialogComponent} that replaces the editor slot. Esc aborts:
|
|
1192
|
+
* the dialog's abort signal reaches the provider flow, any pending prompt
|
|
1193
|
+
* rejects, and the editor is restored immediately. Returns true when
|
|
1194
|
+
* credentials were stored.
|
|
1195
|
+
*/
|
|
1196
|
+
async #handleOAuthLogin(providerId: string): Promise<boolean> {
|
|
1126
1197
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
1127
1198
|
const manualInput = this.ctx.oauthManualInput;
|
|
1128
1199
|
const useManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(providerId);
|
|
1200
|
+
let restored = false;
|
|
1201
|
+
const restoreEditor = () => {
|
|
1202
|
+
if (restored) return;
|
|
1203
|
+
restored = true;
|
|
1204
|
+
this.ctx.editorContainer.clear();
|
|
1205
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
1206
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
1207
|
+
this.ctx.ui.requestRender();
|
|
1208
|
+
};
|
|
1209
|
+
const dialog = new LoginDialogComponent(this.ctx.ui, providerId, (_success, message) => {
|
|
1210
|
+
// Fires on Esc: unblock the editor immediately; the aborted flow's
|
|
1211
|
+
// rejection settles the awaited login below.
|
|
1212
|
+
restoreEditor();
|
|
1213
|
+
if (message) this.ctx.showStatus(message);
|
|
1214
|
+
});
|
|
1215
|
+
this.ctx.editorContainer.clear();
|
|
1216
|
+
this.ctx.editorContainer.addChild(dialog);
|
|
1217
|
+
this.ctx.ui.setFocus(dialog);
|
|
1218
|
+
this.ctx.ui.requestRender();
|
|
1129
1219
|
try {
|
|
1130
1220
|
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
1221
|
+
signal: dialog.signal,
|
|
1131
1222
|
onAuth: (info: { url: string; launchUrl?: string; instructions?: string }) => {
|
|
1132
|
-
|
|
1133
|
-
//
|
|
1134
|
-
|
|
1135
|
-
// user's local browser and fail.
|
|
1136
|
-
block.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
1137
|
-
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
1138
|
-
block.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
1139
|
-
if (info.launchUrl && info.launchUrl !== info.url) {
|
|
1140
|
-
block.addChild(
|
|
1141
|
-
new Text(theme.fg("dim", `Local shortcut (this machine only): ${info.launchUrl}`), 1, 0),
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
if (info.instructions) {
|
|
1145
|
-
block.addChild(new Spacer(1));
|
|
1146
|
-
block.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
1147
|
-
}
|
|
1223
|
+
// The dialog renders the full URL (SSH-safe copy target) and
|
|
1224
|
+
// opens the browser best-effort.
|
|
1225
|
+
dialog.showAuth(info.url, info.instructions, info.launchUrl);
|
|
1148
1226
|
if (useManualInput) {
|
|
1149
|
-
|
|
1150
|
-
block.addChild(new Text(theme.fg("dim", MANUAL_LOGIN_TIP), 1, 0));
|
|
1151
|
-
}
|
|
1152
|
-
this.ctx.present(block);
|
|
1153
|
-
this.ctx.openInBrowser(info.url);
|
|
1154
|
-
},
|
|
1155
|
-
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
1156
|
-
const promptBlock = new TranscriptBlock();
|
|
1157
|
-
promptBlock.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
|
|
1158
|
-
if (prompt.placeholder) {
|
|
1159
|
-
promptBlock.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
|
|
1227
|
+
dialog.showProgress(MANUAL_LOGIN_TIP);
|
|
1160
1228
|
}
|
|
1161
|
-
this.ctx.present(promptBlock);
|
|
1162
|
-
const { promise, resolve } = Promise.withResolvers<string>();
|
|
1163
|
-
const codeInput = new Input();
|
|
1164
|
-
codeInput.onSubmit = () => {
|
|
1165
|
-
const code = codeInput.getValue();
|
|
1166
|
-
this.ctx.editorContainer.clear();
|
|
1167
|
-
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
1168
|
-
this.ctx.ui.setFocus(this.ctx.editor);
|
|
1169
|
-
resolve(code);
|
|
1170
|
-
};
|
|
1171
|
-
this.ctx.editorContainer.clear();
|
|
1172
|
-
this.ctx.editorContainer.addChild(codeInput);
|
|
1173
|
-
this.ctx.ui.setFocus(codeInput);
|
|
1174
|
-
this.ctx.ui.requestRender();
|
|
1175
|
-
return promise;
|
|
1176
1229
|
},
|
|
1230
|
+
onPrompt: (prompt: { message: string; placeholder?: string }) =>
|
|
1231
|
+
dialog.showPrompt(prompt.message, prompt.placeholder),
|
|
1177
1232
|
onProgress: (message: string) => {
|
|
1178
|
-
|
|
1233
|
+
dialog.showProgress(message);
|
|
1179
1234
|
},
|
|
1180
1235
|
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
|
|
1181
1236
|
});
|
|
@@ -1186,12 +1241,20 @@ export class SelectorController {
|
|
|
1186
1241
|
);
|
|
1187
1242
|
block.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
|
|
1188
1243
|
this.ctx.present(block);
|
|
1244
|
+
return true;
|
|
1189
1245
|
} catch (error: unknown) {
|
|
1246
|
+
if (dialog.signal.aborted) {
|
|
1247
|
+
// User-cancelled: the dialog already restored the editor and
|
|
1248
|
+
// surfaced "Login cancelled".
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1190
1251
|
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1252
|
+
return false;
|
|
1191
1253
|
} finally {
|
|
1192
1254
|
if (useManualInput) {
|
|
1193
1255
|
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
|
|
1194
1256
|
}
|
|
1257
|
+
restoreEditor();
|
|
1195
1258
|
}
|
|
1196
1259
|
}
|
|
1197
1260
|
|
|
@@ -133,8 +133,7 @@ export class TodoCommandController {
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* True latest todo state for the user-facing /todo verbs. Reads from session
|
|
136
|
-
* entries
|
|
137
|
-
* (where `session.getTodoPhases()` would have stripped them).
|
|
136
|
+
* entries or falls back to the active session state.
|
|
138
137
|
*/
|
|
139
138
|
#currentPhases(): TodoPhase[] {
|
|
140
139
|
const fromEntries = getLatestTodoPhasesFromEntries(this.ctx.sessionManager.getBranch());
|
|
@@ -566,6 +566,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
566
566
|
get focusedAgentId(): string | undefined {
|
|
567
567
|
return this.#focusController.focusedAgentId;
|
|
568
568
|
}
|
|
569
|
+
get sessionName(): string | undefined {
|
|
570
|
+
return this.session.sessionName;
|
|
571
|
+
}
|
|
569
572
|
focusAgentSession(id: string): Promise<void> {
|
|
570
573
|
return this.#focusController.focusAgent(id);
|
|
571
574
|
}
|
|
@@ -4284,6 +4287,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4284
4287
|
return this.#inputController.handleImagePaste();
|
|
4285
4288
|
}
|
|
4286
4289
|
|
|
4290
|
+
/** Queue slash-command input behind the active turn. */
|
|
4291
|
+
handleQueueCommand(message: string): Promise<void> {
|
|
4292
|
+
return this.#inputController.handleQueueCommand(message);
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4287
4295
|
handleBtwCommand(question: string): Promise<void> {
|
|
4288
4296
|
return this.#btwController.start(question);
|
|
4289
4297
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const QUEUE_PREFIXES: readonly string[] = ["->", "=>"];
|
|
2
|
+
/** Prefix matcher shared by queue-list parsing and editor highlighting. */
|
|
3
|
+
export const QUEUE_LIST_MARKER_RE = /^([\t ]*)(\d+|[A-Za-z]+)([.)])(?=[\t ]|$)/;
|
|
4
|
+
const CANONICAL_ROMAN_RE = /^(?=[MDCLXVI])M{0,3}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})$/i;
|
|
5
|
+
|
|
6
|
+
interface EnumeratedItem {
|
|
7
|
+
line: number;
|
|
8
|
+
indent: string;
|
|
9
|
+
marker: string;
|
|
10
|
+
punctuation: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface EnumeratedList {
|
|
15
|
+
source: string;
|
|
16
|
+
lines: string[];
|
|
17
|
+
items: EnumeratedItem[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Extract the message body from the `->` / `=>` yield-queue shorthand. */
|
|
21
|
+
export function parseQueueShorthand(text: string): string | undefined {
|
|
22
|
+
const prefix = QUEUE_PREFIXES.find(candidate => text.startsWith(candidate));
|
|
23
|
+
return prefix ? text.slice(prefix.length).trim() : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseEnumeratedItem(line: string, lineIndex: number): EnumeratedItem | undefined {
|
|
27
|
+
const match = QUEUE_LIST_MARKER_RE.exec(line);
|
|
28
|
+
if (!match) return undefined;
|
|
29
|
+
const [matched, indent, marker, punctuation] = match;
|
|
30
|
+
if (indent === undefined || marker === undefined || punctuation === undefined) return undefined;
|
|
31
|
+
return { line: lineIndex, indent, marker, punctuation, content: line.slice(matched.length).trimStart() };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodeDecimal(marker: string): number | undefined {
|
|
35
|
+
if (!/^\d+$/.test(marker)) return undefined;
|
|
36
|
+
const value = Number(marker);
|
|
37
|
+
return Number.isSafeInteger(value) ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function decodeRoman(marker: string): number | undefined {
|
|
41
|
+
if (!CANONICAL_ROMAN_RE.test(marker)) return undefined;
|
|
42
|
+
const values: Readonly<Record<string, number>> = {
|
|
43
|
+
I: 1,
|
|
44
|
+
V: 5,
|
|
45
|
+
X: 10,
|
|
46
|
+
L: 50,
|
|
47
|
+
C: 100,
|
|
48
|
+
D: 500,
|
|
49
|
+
M: 1000,
|
|
50
|
+
};
|
|
51
|
+
const upper = marker.toUpperCase();
|
|
52
|
+
let value = 0;
|
|
53
|
+
for (let index = 0; index < upper.length; index++) {
|
|
54
|
+
const current = values[upper[index] ?? ""];
|
|
55
|
+
if (current === undefined) return undefined;
|
|
56
|
+
const next = values[upper[index + 1] ?? ""] ?? 0;
|
|
57
|
+
value += current < next ? -current : current;
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decodeAlpha(marker: string): number | undefined {
|
|
63
|
+
if (!/^[A-Za-z]+$/.test(marker)) return undefined;
|
|
64
|
+
let value = 0;
|
|
65
|
+
for (const char of marker.toUpperCase()) {
|
|
66
|
+
value = value * 26 + char.charCodeAt(0) - 64;
|
|
67
|
+
if (!Number.isSafeInteger(value)) return undefined;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isSequential(markers: readonly string[], decode: (marker: string) => number | undefined): boolean {
|
|
73
|
+
let previous = decode(markers[0] ?? "");
|
|
74
|
+
if (previous === undefined) return false;
|
|
75
|
+
for (let index = 1; index < markers.length; index++) {
|
|
76
|
+
const current = decode(markers[index] ?? "");
|
|
77
|
+
if (current === undefined || current !== previous + 1) return false;
|
|
78
|
+
previous = current;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isEnumeratedSequence(items: readonly EnumeratedItem[]): boolean {
|
|
84
|
+
const markers = items.map(item => item.marker);
|
|
85
|
+
if (markers.every(marker => /^\d+$/.test(marker))) return isSequential(markers, decodeDecimal);
|
|
86
|
+
if (
|
|
87
|
+
!markers.every(marker => marker === marker.toUpperCase()) &&
|
|
88
|
+
!markers.every(marker => marker === marker.toLowerCase())
|
|
89
|
+
) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return isSequential(markers, decodeRoman) || isSequential(markers, decodeAlpha);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseEnumeratedList(text: string): EnumeratedList | undefined {
|
|
96
|
+
const source = text.trim();
|
|
97
|
+
if (!source) return undefined;
|
|
98
|
+
const lines = source.split(/\r?\n/);
|
|
99
|
+
const first = parseEnumeratedItem(lines[0] ?? "", 0);
|
|
100
|
+
if (!first) return undefined;
|
|
101
|
+
|
|
102
|
+
const items = [first];
|
|
103
|
+
for (let lineIndex = 1; lineIndex < lines.length; lineIndex++) {
|
|
104
|
+
const item = parseEnumeratedItem(lines[lineIndex] ?? "", lineIndex);
|
|
105
|
+
if (item?.indent === first.indent) items.push(item);
|
|
106
|
+
}
|
|
107
|
+
if (items.length < 2 || items.some(item => item.punctuation !== first.punctuation) || !isEnumeratedSequence(items)) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return { source, lines, items };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Whether text currently forms a sequential queue list, including an unfinished trailing item. */
|
|
114
|
+
export function isQueuedMessageList(text: string): boolean {
|
|
115
|
+
return parseEnumeratedList(text) !== undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Split a sequential numeric, Roman-numeral, or alphabetic list into queue entries. */
|
|
119
|
+
export function splitQueuedMessages(text: string): string[] {
|
|
120
|
+
const list = parseEnumeratedList(text);
|
|
121
|
+
if (!list) {
|
|
122
|
+
const source = text.trim();
|
|
123
|
+
return source ? [source] : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const messages = list.items.map((item, index) => {
|
|
127
|
+
const nextLine = list.items[index + 1]?.line ?? list.lines.length;
|
|
128
|
+
return [item.content, ...list.lines.slice(item.line + 1, nextLine)].join("\n").trim();
|
|
129
|
+
});
|
|
130
|
+
while (messages.at(-1) === "") messages.pop();
|
|
131
|
+
return messages.length > 0 && messages.every(Boolean) ? messages : [list.source];
|
|
132
|
+
}
|
package/src/modes/shared.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function sanitizeStatusText(text: string): string {
|
|
|
18
18
|
// Tab Bar Theme
|
|
19
19
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
20
|
|
|
21
|
-
/** Shared tab bar theme used by
|
|
21
|
+
/** Shared tab bar theme used by fullscreen overlays (settings, agent hub). */
|
|
22
22
|
export function getTabBarTheme(): TabBarTheme {
|
|
23
23
|
return {
|
|
24
24
|
label: (text: string) => theme.bold(theme.fg("accent", text)),
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -249,7 +249,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
249
249
|
"status.disabled": "⦸",
|
|
250
250
|
"status.enabled": "●",
|
|
251
251
|
"status.running": "⟳",
|
|
252
|
-
"status.shadowed": "
|
|
252
|
+
"status.shadowed": "○",
|
|
253
253
|
"status.aborted": "⏹",
|
|
254
254
|
"status.done": "•",
|
|
255
255
|
// Navigation
|
|
@@ -464,8 +464,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
464
464
|
"status.enabled": "\uf111",
|
|
465
465
|
// pick: | alt:
|
|
466
466
|
"status.running": "\uf110",
|
|
467
|
-
// pick:
|
|
468
|
-
"status.shadowed": "
|
|
467
|
+
// pick: (nf-fa-circle_o, pairs with status.enabled's nf-fa-circle) | alt: ◐ ◑
|
|
468
|
+
"status.shadowed": "\uf10c",
|
|
469
469
|
// pick: | alt:
|
|
470
470
|
"status.aborted": "\uf04d",
|
|
471
471
|
// pick: • | alt: ● ·
|
package/src/modes/types.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface InteractiveModeContext {
|
|
|
113
113
|
// Session access
|
|
114
114
|
session: AgentSession;
|
|
115
115
|
sessionManager: SessionManager;
|
|
116
|
+
/** The current session display name / title. */
|
|
117
|
+
readonly sessionName: string | undefined;
|
|
116
118
|
/** Session the transcript/editor/status are attached to: the focused agent's, else `session`. */
|
|
117
119
|
readonly viewSession: AgentSession;
|
|
118
120
|
/** Id of the focused agent, undefined when the main session is attached. */
|
|
@@ -375,6 +377,8 @@ export interface InteractiveModeContext {
|
|
|
375
377
|
handleCtrlZ(): void;
|
|
376
378
|
handleDequeue(): void;
|
|
377
379
|
handleImagePaste(): Promise<boolean>;
|
|
380
|
+
/** Queue a message for delivery only after the active agent turn would stop. */
|
|
381
|
+
handleQueueCommand(message: string): Promise<void>;
|
|
378
382
|
handleBtwCommand(question: string): Promise<void>;
|
|
379
383
|
handleTanCommand(work: string): Promise<void>;
|
|
380
384
|
hasActiveBtw(): boolean;
|