@oh-my-pi/pi-coding-agent 15.11.4 → 15.11.7
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 +82 -1
- package/dist/cli.js +520 -451
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/cli/usage-cli.d.ts +10 -1
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/usage.d.ts +9 -0
- package/dist/types/config/model-resolver.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +125 -3
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/settings-selector.d.ts +8 -1
- package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +14 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/codex-auto-reset.d.ts +107 -0
- package/dist/types/session/snapcompact-inline.d.ts +107 -4
- package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
- package/dist/types/task/render.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/renderers.d.ts +13 -0
- package/dist/types/tools/ssh.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +0 -11
- package/package.json +11 -11
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli/usage-cli.ts +187 -16
- package/src/cli-commands.ts +1 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/usage.ts +8 -0
- package/src/config/model-registry.ts +52 -5
- package/src/config/model-resolver.ts +36 -5
- package/src/config/settings-schema.ts +148 -3
- package/src/config/settings.ts +9 -0
- package/src/edit/renderer.ts +5 -0
- package/src/hindsight/client.ts +26 -1
- package/src/hindsight/state.ts +6 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/mcp/transports/stdio.ts +81 -7
- package/src/modes/components/oauth-selector.ts +67 -7
- package/src/modes/components/reset-usage-selector.ts +161 -0
- package/src/modes/components/session-selector.ts +8 -2
- package/src/modes/components/settings-selector.ts +89 -47
- package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
- package/src/modes/components/snapcompact-shape-preview.ts +192 -0
- package/src/modes/components/tool-execution.ts +26 -0
- package/src/modes/components/transcript-container.ts +23 -1
- package/src/modes/controllers/command-controller.ts +24 -1
- package/src/modes/controllers/input-controller.ts +8 -6
- package/src/modes/controllers/selector-controller.ts +72 -2
- package/src/modes/interactive-mode.ts +83 -0
- package/src/modes/session-observer-registry.ts +61 -3
- package/src/modes/setup-wizard/index.ts +1 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
- package/src/modes/setup-wizard/scenes/providers.ts +36 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
- package/src/modes/setup-wizard/scenes/theme.ts +28 -1
- package/src/modes/setup-wizard/scenes/types.ts +10 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
- package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/context-usage.ts +75 -1
- package/src/prompts/bench.md +7 -0
- package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
- package/src/prompts/tools/browser.md +33 -43
- package/src/prompts/tools/eval.md +27 -50
- package/src/prompts/tools/irc.md +29 -31
- package/src/prompts/tools/read.md +31 -37
- package/src/prompts/tools/todo.md +1 -2
- package/src/sdk.ts +4 -2
- package/src/session/agent-session.ts +136 -6
- package/src/session/auth-storage.ts +3 -0
- package/src/session/codex-auto-reset.ts +190 -0
- package/src/session/snapcompact-inline.ts +404 -75
- package/src/slash-commands/builtin-registry.ts +145 -8
- package/src/slash-commands/helpers/context-report.ts +28 -1
- package/src/slash-commands/helpers/reset-usage.ts +66 -0
- package/src/slash-commands/helpers/usage-report.ts +12 -0
- package/src/task/index.ts +30 -7
- package/src/task/render.ts +34 -19
- package/src/tools/bash.ts +3 -0
- package/src/tools/eval-render.ts +4 -0
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +3 -0
- package/src/tools/todo.ts +8 -128
|
@@ -27,8 +27,14 @@ import {
|
|
|
27
27
|
theme,
|
|
28
28
|
} from "../../modes/theme/theme";
|
|
29
29
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
30
|
+
import type { ResetCreditRedeemOutcome } from "../../session/auth-storage";
|
|
30
31
|
import { type SessionInfo, SessionManager } from "../../session/session-manager";
|
|
31
32
|
import { FileSessionStorage } from "../../session/session-storage";
|
|
33
|
+
import {
|
|
34
|
+
describeRedeemOutcome,
|
|
35
|
+
type ResetUsageAccount,
|
|
36
|
+
toResetUsageAccounts,
|
|
37
|
+
} from "../../slash-commands/helpers/reset-usage";
|
|
32
38
|
import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
|
|
33
39
|
import {
|
|
34
40
|
isImageProviderPreference,
|
|
@@ -48,6 +54,7 @@ import { HistorySearchComponent } from "../components/history-search";
|
|
|
48
54
|
import { ModelSelectorComponent } from "../components/model-selector";
|
|
49
55
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
50
56
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
57
|
+
import { ResetUsageSelectorComponent } from "../components/reset-usage-selector";
|
|
51
58
|
import { SessionSelectorComponent } from "../components/session-selector";
|
|
52
59
|
import { SettingsSelectorComponent } from "../components/settings-selector";
|
|
53
60
|
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
@@ -107,6 +114,9 @@ export class SelectorController {
|
|
|
107
114
|
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
108
115
|
availableThemes,
|
|
109
116
|
cwd: getProjectDir(),
|
|
117
|
+
model: this.ctx.session.model,
|
|
118
|
+
imageBudget: this.ctx.ui.imageBudget,
|
|
119
|
+
requestRender: () => this.ctx.ui.requestRender(),
|
|
110
120
|
},
|
|
111
121
|
{
|
|
112
122
|
onChange: (id, value) => this.handleSettingChange(id, value),
|
|
@@ -306,10 +316,9 @@ export class SelectorController {
|
|
|
306
316
|
for (const child of this.ctx.chatContainer.children) {
|
|
307
317
|
if (child instanceof AssistantMessageComponent) {
|
|
308
318
|
child.setHideThinkingBlock(value as boolean);
|
|
319
|
+
child.invalidate();
|
|
309
320
|
}
|
|
310
321
|
}
|
|
311
|
-
this.ctx.chatContainer.clear();
|
|
312
|
-
this.ctx.rebuildChatFromMessages();
|
|
313
322
|
break;
|
|
314
323
|
case "theme": {
|
|
315
324
|
setTheme(value as string, true).then(result => {
|
|
@@ -1091,6 +1100,67 @@ export class SelectorController {
|
|
|
1091
1100
|
});
|
|
1092
1101
|
}
|
|
1093
1102
|
|
|
1103
|
+
async showResetUsageSelector(): Promise<void> {
|
|
1104
|
+
const session = this.ctx.session;
|
|
1105
|
+
this.ctx.showStatus("Checking saved rate-limit resets…", { dim: true });
|
|
1106
|
+
let statuses: Awaited<ReturnType<typeof session.listResetCredits>>;
|
|
1107
|
+
try {
|
|
1108
|
+
statuses = await session.listResetCredits();
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
this.ctx.showError(`Could not load saved resets: ${error instanceof Error ? error.message : String(error)}`);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const accounts = toResetUsageAccounts(statuses);
|
|
1114
|
+
if (accounts.length === 0) {
|
|
1115
|
+
this.ctx.showStatus("No Codex accounts found. Use /login to add one.");
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (!accounts.some(account => account.availableCount > 0)) {
|
|
1119
|
+
this.ctx.showStatus(
|
|
1120
|
+
accounts.some(account => account.error)
|
|
1121
|
+
? "No saved resets available — some accounts couldn't be reached (try /login)."
|
|
1122
|
+
: "No saved rate-limit resets available to spend right now.",
|
|
1123
|
+
);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
this.showSelector(done => {
|
|
1127
|
+
const selector = new ResetUsageSelectorComponent(
|
|
1128
|
+
accounts,
|
|
1129
|
+
account => {
|
|
1130
|
+
done();
|
|
1131
|
+
void this.#redeemReset(account);
|
|
1132
|
+
},
|
|
1133
|
+
() => {
|
|
1134
|
+
done();
|
|
1135
|
+
this.ctx.ui.requestRender();
|
|
1136
|
+
},
|
|
1137
|
+
);
|
|
1138
|
+
return { component: selector, focus: selector };
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async #redeemReset(account: ResetUsageAccount): Promise<void> {
|
|
1143
|
+
this.ctx.showStatus(`Spending 1 saved reset for ${account.label}…`, { dim: true });
|
|
1144
|
+
let outcome: ResetCreditRedeemOutcome;
|
|
1145
|
+
try {
|
|
1146
|
+
outcome = await this.ctx.session.redeemResetCredit(account.target);
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
this.ctx.showError(
|
|
1149
|
+
`Reset failed for ${account.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1150
|
+
);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const message = describeRedeemOutcome(outcome, account.label);
|
|
1154
|
+
if (outcome.ok) {
|
|
1155
|
+
this.ctx.showStatus(message);
|
|
1156
|
+
// Refresh the status-line usage so the freshly-reset window shows.
|
|
1157
|
+
this.ctx.statusLine.invalidate();
|
|
1158
|
+
this.ctx.ui.requestRender();
|
|
1159
|
+
} else {
|
|
1160
|
+
this.ctx.showWarning(message);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1094
1164
|
async showDebugSelector(): Promise<void> {
|
|
1095
1165
|
const { DebugSelectorComponent } = await import("../../debug");
|
|
1096
1166
|
this.showSelector(done => {
|
|
@@ -86,8 +86,10 @@ import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES } from "../slash-commands/builtin-
|
|
|
86
86
|
import { formatDuration } from "../slash-commands/helpers/format";
|
|
87
87
|
import { STTController, type SttState } from "../stt";
|
|
88
88
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
89
|
+
import { formatTaskId } from "../task/render";
|
|
89
90
|
import type { LspStartupServerInfo } from "../tools";
|
|
90
91
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
92
|
+
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
|
|
91
93
|
import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
|
|
92
94
|
import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
|
|
93
95
|
import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo";
|
|
@@ -132,6 +134,7 @@ import {
|
|
|
132
134
|
parseLoopLimitArgs,
|
|
133
135
|
} from "./loop-limit";
|
|
134
136
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
137
|
+
import type { ObservableSession } from "./session-observer-registry";
|
|
135
138
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
136
139
|
import { runProviderSetupWizard } from "./setup-wizard/lazy";
|
|
137
140
|
import { interruptHint } from "./shared";
|
|
@@ -277,6 +280,41 @@ class StatusContainer extends Container implements NativeScrollbackLiveRegion {
|
|
|
277
280
|
}
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
|
285
|
+
* one hooked row per running agent in the same `Id: description` shape the
|
|
286
|
+
* inline task rows use (muted task preview when no description was given).
|
|
287
|
+
* Returns an empty array when nothing is running so the container can clear.
|
|
288
|
+
*/
|
|
289
|
+
export function renderSubagentHudLines(sessions: ObservableSession[], columns: number): string[] {
|
|
290
|
+
const running = sessions.filter(session => session.kind === "subagent" && session.status === "active");
|
|
291
|
+
if (running.length === 0) return [];
|
|
292
|
+
|
|
293
|
+
const indent = " ";
|
|
294
|
+
const hook = theme.tree.hook;
|
|
295
|
+
const dot = theme.styledSymbol("status.done", "accent");
|
|
296
|
+
const lines = ["", indent + theme.bold(theme.fg("accent", "Subagents"))];
|
|
297
|
+
running.forEach((session, index) => {
|
|
298
|
+
const prefix = `${indent}${index === 0 ? hook : " "} `;
|
|
299
|
+
const displayId = formatTaskId(session.id);
|
|
300
|
+
let line = `${prefix}${dot} ${theme.fg("accent", theme.bold(displayId))}`;
|
|
301
|
+
const description = session.description?.trim() || session.progress?.description?.trim();
|
|
302
|
+
if (description) {
|
|
303
|
+
const budget = Math.max(TRUNCATE_LENGTHS.SHORT, columns - visibleWidth(prefix) - visibleWidth(displayId) - 6);
|
|
304
|
+
line += `${theme.fg("accent", ":")} ${theme.fg("accent", truncateToWidth(replaceTabs(description), budget))}`;
|
|
305
|
+
} else {
|
|
306
|
+
// No spawn description: fall back to a muted task preview, same as
|
|
307
|
+
// the inline task rows when a row has no label.
|
|
308
|
+
const taskPreview = session.progress?.task?.trim();
|
|
309
|
+
if (taskPreview) {
|
|
310
|
+
line += ` ${theme.fg("muted", truncateToWidth(replaceTabs(taskPreview), TRUNCATE_LENGTHS.SHORT))}`;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
lines.push(line);
|
|
314
|
+
});
|
|
315
|
+
return lines;
|
|
316
|
+
}
|
|
317
|
+
|
|
280
318
|
export class InteractiveMode implements InteractiveModeContext {
|
|
281
319
|
session: AgentSession;
|
|
282
320
|
sessionManager: SessionManager;
|
|
@@ -291,6 +329,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
291
329
|
pendingMessagesContainer: Container;
|
|
292
330
|
statusContainer: Container;
|
|
293
331
|
todoContainer: Container;
|
|
332
|
+
subagentContainer: Container;
|
|
294
333
|
btwContainer: Container;
|
|
295
334
|
omfgContainer: Container;
|
|
296
335
|
errorBannerContainer: Container;
|
|
@@ -440,6 +479,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
440
479
|
this.pendingMessagesContainer = new Container();
|
|
441
480
|
this.statusContainer = new StatusContainer();
|
|
442
481
|
this.todoContainer = new Container();
|
|
482
|
+
this.subagentContainer = new Container();
|
|
443
483
|
this.btwContainer = new Container();
|
|
444
484
|
this.omfgContainer = new Container();
|
|
445
485
|
this.errorBannerContainer = new Container();
|
|
@@ -606,6 +646,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
606
646
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
607
647
|
this.ui.addChild(this.statusContainer);
|
|
608
648
|
this.ui.addChild(this.todoContainer);
|
|
649
|
+
this.ui.addChild(this.subagentContainer);
|
|
609
650
|
this.ui.addChild(this.btwContainer);
|
|
610
651
|
this.ui.addChild(this.omfgContainer);
|
|
611
652
|
this.ui.addChild(this.errorBannerContainer);
|
|
@@ -632,6 +673,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
632
673
|
this.#reconcileTodosWithSubagents();
|
|
633
674
|
this.#syncTodoAutoClearTimer();
|
|
634
675
|
this.#renderTodoList();
|
|
676
|
+
this.#renderSubagentList();
|
|
635
677
|
this.ui.requestRender();
|
|
636
678
|
});
|
|
637
679
|
|
|
@@ -1120,6 +1162,30 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1120
1162
|
// of restarting the visible conversation (the LLM context still resets).
|
|
1121
1163
|
const context = this.session.buildTranscriptSessionContext();
|
|
1122
1164
|
this.renderSessionContext(context);
|
|
1165
|
+
// During the pre-streaming window — after `startPendingSubmission` has
|
|
1166
|
+
// optimistically rendered the user's message but before the user
|
|
1167
|
+
// `message_start` event lands it in `session` entries — any rebuild
|
|
1168
|
+
// (e.g. Ctrl+T toggleThinkingBlockVisibility, theme selector) would
|
|
1169
|
+
// otherwise erase the user's just-submitted message until the first
|
|
1170
|
+
// assistant token arrived (#2372). Once `message_start` fires the
|
|
1171
|
+
// signature is cleared by `EventController`, so this replay is a no-op
|
|
1172
|
+
// post-streaming and cannot duplicate.
|
|
1173
|
+
this.#replayOptimisticUserMessage();
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
#replayOptimisticUserMessage(): void {
|
|
1177
|
+
if (!this.optimisticUserMessageSignature) return;
|
|
1178
|
+
const submission = this.#pendingSubmittedInput;
|
|
1179
|
+
if (!submission || submission.cancelled || submission.customType) return;
|
|
1180
|
+
this.addMessageToChat(
|
|
1181
|
+
{
|
|
1182
|
+
role: "user",
|
|
1183
|
+
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
1184
|
+
attribution: "user",
|
|
1185
|
+
timestamp: Date.now(),
|
|
1186
|
+
},
|
|
1187
|
+
{ imageLinks: submission.imageLinks },
|
|
1188
|
+
);
|
|
1123
1189
|
}
|
|
1124
1190
|
|
|
1125
1191
|
#formatTodoLine(todo: TodoItem, prefix: string, matched: boolean): string {
|
|
@@ -1282,6 +1348,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1282
1348
|
this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
|
1283
1349
|
}
|
|
1284
1350
|
|
|
1351
|
+
/**
|
|
1352
|
+
* Anchored HUD of in-flight subagents, mirroring the Todos block above the
|
|
1353
|
+
* editor. Driven entirely by observer-registry change events, so rows appear
|
|
1354
|
+
* on spawn and the whole block clears itself once the last subagent leaves
|
|
1355
|
+
* the "active" state.
|
|
1356
|
+
*/
|
|
1357
|
+
#renderSubagentList(): void {
|
|
1358
|
+
this.subagentContainer.clear();
|
|
1359
|
+
const lines = renderSubagentHudLines(this.#observerRegistry.getSessions(), this.ui.terminal.columns);
|
|
1360
|
+
if (lines.length === 0) return;
|
|
1361
|
+
this.subagentContainer.addChild(new Text(lines.join("\n"), 1, 0));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1285
1364
|
async #loadTodoList(): Promise<void> {
|
|
1286
1365
|
this.todoPhases = this.session.getTodoPhases();
|
|
1287
1366
|
this.#syncTodoAutoClearTimer();
|
|
@@ -3235,6 +3314,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3235
3314
|
return this.#selectorController.showOAuthSelector(mode, providerId);
|
|
3236
3315
|
}
|
|
3237
3316
|
|
|
3317
|
+
showResetUsageSelector(): Promise<void> {
|
|
3318
|
+
return this.#selectorController.showResetUsageSelector();
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3238
3321
|
showProviderSetup(): Promise<void> {
|
|
3239
3322
|
return runProviderSetupWizard(this);
|
|
3240
3323
|
}
|
|
@@ -10,6 +10,8 @@ export interface ObservableSession {
|
|
|
10
10
|
description?: string;
|
|
11
11
|
status: "active" | "completed" | "failed" | "aborted";
|
|
12
12
|
sessionFile?: string;
|
|
13
|
+
parentToolCallId?: string;
|
|
14
|
+
index?: number;
|
|
13
15
|
lastUpdate: number;
|
|
14
16
|
/** Latest progress snapshot from the subagent executor */
|
|
15
17
|
progress?: AgentProgress;
|
|
@@ -26,6 +28,9 @@ export class SessionObserverRegistry {
|
|
|
26
28
|
#sessions = new Map<string, ObservableSession>();
|
|
27
29
|
#listeners = new Set<() => void>();
|
|
28
30
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
31
|
+
#sortOrderById = new Map<string, number>();
|
|
32
|
+
#parentSortOrderById = new Map<string, number>();
|
|
33
|
+
#nextSortOrder = 0;
|
|
29
34
|
|
|
30
35
|
/** Add a change listener. Returns unsubscribe function. */
|
|
31
36
|
onChange(cb: () => void): () => void {
|
|
@@ -37,8 +42,34 @@ export class SessionObserverRegistry {
|
|
|
37
42
|
for (const cb of this.#listeners) cb();
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
#ensureSortOrder(id: string): number {
|
|
46
|
+
const existing = this.#sortOrderById.get(id);
|
|
47
|
+
if (existing !== undefined) return existing;
|
|
48
|
+
const order = this.#nextSortOrder++;
|
|
49
|
+
this.#sortOrderById.set(id, order);
|
|
50
|
+
return order;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#ensureParentSortOrder(parentToolCallId: string | undefined, order: number): void {
|
|
54
|
+
if (!parentToolCallId) return;
|
|
55
|
+
if (this.#parentSortOrderById.has(parentToolCallId)) return;
|
|
56
|
+
this.#parentSortOrderById.set(parentToolCallId, order);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#getStableOrder(session: ObservableSession): number {
|
|
60
|
+
return this.#sortOrderById.get(session.id) ?? Number.MAX_SAFE_INTEGER;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#getGroupOrder(session: ObservableSession): number {
|
|
64
|
+
const parentOrder = session.parentToolCallId
|
|
65
|
+
? this.#parentSortOrderById.get(session.parentToolCallId)
|
|
66
|
+
: undefined;
|
|
67
|
+
return parentOrder ?? this.#getStableOrder(session);
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
setMainSession(sessionFile?: string): void {
|
|
41
71
|
const existing = this.#sessions.get("main");
|
|
72
|
+
this.#ensureSortOrder("main");
|
|
42
73
|
this.#sessions.set("main", {
|
|
43
74
|
id: "main",
|
|
44
75
|
kind: "main",
|
|
@@ -53,9 +84,18 @@ export class SessionObserverRegistry {
|
|
|
53
84
|
getSessions(): ObservableSession[] {
|
|
54
85
|
const sessions = [...this.#sessions.values()];
|
|
55
86
|
sessions.sort((a, b) => {
|
|
56
|
-
if (a.kind === "main") return -1;
|
|
57
|
-
if (b.kind === "main") return 1;
|
|
58
|
-
|
|
87
|
+
if (a.kind === "main" && b.kind !== "main") return -1;
|
|
88
|
+
if (b.kind === "main" && a.kind !== "main") return 1;
|
|
89
|
+
if (a.kind === "main" || b.kind === "main") return 0;
|
|
90
|
+
|
|
91
|
+
const groupDiff = this.#getGroupOrder(a) - this.#getGroupOrder(b);
|
|
92
|
+
if (groupDiff !== 0) return groupDiff;
|
|
93
|
+
|
|
94
|
+
const aIndex = a.index ?? Number.MAX_SAFE_INTEGER;
|
|
95
|
+
const bIndex = b.index ?? Number.MAX_SAFE_INTEGER;
|
|
96
|
+
if (aIndex !== bIndex) return aIndex - bIndex;
|
|
97
|
+
|
|
98
|
+
return this.#getStableOrder(a) - this.#getStableOrder(b);
|
|
59
99
|
});
|
|
60
100
|
return sessions;
|
|
61
101
|
}
|
|
@@ -71,6 +111,9 @@ export class SessionObserverRegistry {
|
|
|
71
111
|
/** Clear all tracked sessions (e.g. on session switch). Keeps EventBus subscriptions and listeners. */
|
|
72
112
|
resetSessions(): void {
|
|
73
113
|
this.#sessions.clear();
|
|
114
|
+
this.#sortOrderById.clear();
|
|
115
|
+
this.#parentSortOrderById.clear();
|
|
116
|
+
this.#nextSortOrder = 0;
|
|
74
117
|
this.#notifyListeners();
|
|
75
118
|
}
|
|
76
119
|
|
|
@@ -78,6 +121,9 @@ export class SessionObserverRegistry {
|
|
|
78
121
|
for (const unsub of this.#eventBusUnsubscribers) unsub();
|
|
79
122
|
this.#eventBusUnsubscribers = [];
|
|
80
123
|
this.#sessions.clear();
|
|
124
|
+
this.#sortOrderById.clear();
|
|
125
|
+
this.#parentSortOrderById.clear();
|
|
126
|
+
this.#nextSortOrder = 0;
|
|
81
127
|
this.#listeners.clear();
|
|
82
128
|
}
|
|
83
129
|
|
|
@@ -92,10 +138,14 @@ export class SessionObserverRegistry {
|
|
|
92
138
|
const status = STATUS_MAP[payload.status];
|
|
93
139
|
if (!status) return;
|
|
94
140
|
|
|
141
|
+
const sortOrder = this.#ensureSortOrder(payload.id);
|
|
142
|
+
this.#ensureParentSortOrder(payload.parentToolCallId, sortOrder);
|
|
95
143
|
const existing = this.#sessions.get(payload.id);
|
|
96
144
|
if (existing) {
|
|
97
145
|
existing.status = status;
|
|
98
146
|
existing.lastUpdate = Date.now();
|
|
147
|
+
existing.index = payload.index;
|
|
148
|
+
existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
|
|
99
149
|
if (payload.description) existing.description = payload.description;
|
|
100
150
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
101
151
|
} else {
|
|
@@ -107,6 +157,8 @@ export class SessionObserverRegistry {
|
|
|
107
157
|
description: payload.description,
|
|
108
158
|
status,
|
|
109
159
|
sessionFile: payload.sessionFile,
|
|
160
|
+
parentToolCallId: payload.parentToolCallId,
|
|
161
|
+
index: payload.index,
|
|
110
162
|
lastUpdate: Date.now(),
|
|
111
163
|
});
|
|
112
164
|
}
|
|
@@ -121,8 +173,12 @@ export class SessionObserverRegistry {
|
|
|
121
173
|
const id = progress.id;
|
|
122
174
|
const existing = this.#sessions.get(id);
|
|
123
175
|
|
|
176
|
+
const sortOrder = this.#ensureSortOrder(id);
|
|
177
|
+
this.#ensureParentSortOrder(payload.parentToolCallId, sortOrder);
|
|
124
178
|
if (existing) {
|
|
125
179
|
existing.lastUpdate = Date.now();
|
|
180
|
+
existing.index = payload.index;
|
|
181
|
+
existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
|
|
126
182
|
existing.progress = progress;
|
|
127
183
|
if (progress.description) existing.description = progress.description;
|
|
128
184
|
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
|
|
@@ -135,6 +191,8 @@ export class SessionObserverRegistry {
|
|
|
135
191
|
description: progress.description,
|
|
136
192
|
status: "active",
|
|
137
193
|
sessionFile: payload.sessionFile,
|
|
194
|
+
parentToolCallId: payload.parentToolCallId,
|
|
195
|
+
index: payload.index,
|
|
138
196
|
lastUpdate: Date.now(),
|
|
139
197
|
progress,
|
|
140
198
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { getSelectListTheme, type SymbolPreset, setSymbolPreset, theme } from "../../theme/theme";
|
|
3
3
|
import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
|
|
4
4
|
|
|
@@ -29,6 +29,8 @@ class GlyphSceneController implements SetupSceneController {
|
|
|
29
29
|
#selectList: SelectList;
|
|
30
30
|
#previewRequest = 0;
|
|
31
31
|
#committing = false;
|
|
32
|
+
/** Render line where the select list begins. */
|
|
33
|
+
#listRowStart = 0;
|
|
32
34
|
|
|
33
35
|
constructor(private readonly host: SetupSceneHost) {
|
|
34
36
|
this.#selectList = new SelectList(GLYPH_ITEMS, GLYPH_ITEMS.length, getSelectListTheme());
|
|
@@ -60,12 +62,28 @@ class GlyphSceneController implements SetupSceneController {
|
|
|
60
62
|
this.#selectList.handleInput(data);
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/** Wheel moves the highlight (live preview); hover lights the row under the pointer; click confirms it. */
|
|
66
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
67
|
+
if (this.#committing) return;
|
|
68
|
+
if (event.wheel !== null) {
|
|
69
|
+
this.#selectList.handleWheel(event.wheel);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const index = this.#selectList.hitTest(line - this.#listRowStart);
|
|
73
|
+
if (event.motion) {
|
|
74
|
+
this.#selectList.setHoverIndex(index ?? null);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (event.leftClick && index !== undefined) {
|
|
78
|
+
this.#selectList.clickItem(index);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
render(width: number): readonly string[] {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
];
|
|
83
|
+
const lines = [theme.fg("muted", "If a row shows boxes, tofu, or misaligned icons, pick another."), ""];
|
|
84
|
+
this.#listRowStart = lines.length;
|
|
85
|
+
lines.push(...this.#selectList.render(width));
|
|
86
|
+
return lines;
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
async #commit(preset: SymbolPreset): Promise<void> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TabBar } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type SgrMouseEvent, TabBar } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { getTabBarTheme } from "../../shared";
|
|
3
3
|
import { SignInTab } from "./sign-in";
|
|
4
4
|
import type { SetupScene, SetupSceneController, SetupSceneHost, SetupTab } from "./types";
|
|
@@ -16,6 +16,8 @@ class ProvidersSceneController implements SetupSceneController {
|
|
|
16
16
|
|
|
17
17
|
#tabs: SetupTab[];
|
|
18
18
|
#tabBar: TabBar;
|
|
19
|
+
/** Lines the tab bar occupied in the last render (body starts one blank line below). */
|
|
20
|
+
#tabRowCount = 1;
|
|
19
21
|
|
|
20
22
|
constructor(host: SetupSceneHost) {
|
|
21
23
|
this.#tabs = [new SignInTab(host), new WebSearchTab(host)];
|
|
@@ -52,8 +54,40 @@ class ProvidersSceneController implements SetupSceneController {
|
|
|
52
54
|
tab.handleInput(data);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Hit-test mouse reports against the last render: rows inside the tab bar
|
|
59
|
+
* hover/switch tabs (suppressed while the active panel is modal, matching
|
|
60
|
+
* keyboard tab cycling); everything else forwards to the active panel at
|
|
61
|
+
* panel-local coordinates. Wheel always goes to the panel so scrolling
|
|
62
|
+
* works regardless of pointer position.
|
|
63
|
+
*/
|
|
64
|
+
routeMouse(event: SgrMouseEvent, line: number, col: number): void {
|
|
65
|
+
const tab = this.#activeTab();
|
|
66
|
+
if (event.wheel === null && line >= 0 && line < this.#tabRowCount) {
|
|
67
|
+
if (tab.modal) return;
|
|
68
|
+
const hit = this.#tabBar.tabAt(line, col);
|
|
69
|
+
if (event.motion) {
|
|
70
|
+
this.#tabBar.setHoverTab(hit && !hit.muted ? hit.id : null);
|
|
71
|
+
} else if (event.leftClick && hit) {
|
|
72
|
+
this.#tabBar.selectTab(hit.id);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (event.motion) this.#tabBar.setHoverTab(null);
|
|
77
|
+
const bodyLine = line - this.#tabRowCount - 1;
|
|
78
|
+
if (tab.routeMouse) {
|
|
79
|
+
tab.routeMouse(event, bodyLine, col);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.wheel !== null && !tab.modal) {
|
|
83
|
+
tab.handleInput(event.wheel === -1 ? "\x1b[A" : "\x1b[B");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
55
87
|
render(width: number): readonly string[] {
|
|
56
|
-
|
|
88
|
+
const tabLines = this.#tabBar.render(width);
|
|
89
|
+
this.#tabRowCount = tabLines.length;
|
|
90
|
+
return [...tabLines, "", ...this.#activeTab().render(width)];
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
dispose(): void {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
|
|
4
|
-
import { Input, matchesKey, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { Input, matchesKey, type SgrMouseEvent, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { OAuthSelectorComponent } from "../../components/oauth-selector";
|
|
7
7
|
import { theme } from "../../theme/theme";
|
|
@@ -35,6 +35,8 @@ export class SignInTab implements SetupTab {
|
|
|
35
35
|
#loginAbort: AbortController | undefined;
|
|
36
36
|
#loggingInProvider: string | undefined;
|
|
37
37
|
#disposed = false;
|
|
38
|
+
/** Render line where the provider selector begins. */
|
|
39
|
+
#selectorRowStart = 2;
|
|
38
40
|
|
|
39
41
|
constructor(private readonly host: SetupSceneHost) {
|
|
40
42
|
this.#authStorage = host.ctx.session.modelRegistry.authStorage;
|
|
@@ -68,12 +70,19 @@ export class SignInTab implements SetupTab {
|
|
|
68
70
|
this.#selector.handleInput(data);
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/** Forward mouse to the provider selector; pointer is inert during an active login or code prompt. */
|
|
74
|
+
routeMouse(event: SgrMouseEvent, line: number, col: number): void {
|
|
75
|
+
if (this.#loggingInProvider || this.#prompt) return;
|
|
76
|
+
this.#selector.routeMouse(event, line - this.#selectorRowStart, col);
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
render(width: number): readonly string[] {
|
|
72
80
|
const lines: string[] = [];
|
|
73
81
|
if (this.#loggingInProvider) {
|
|
74
82
|
lines.push(theme.bold(`Signing in to ${this.#loggingInProvider}`));
|
|
75
83
|
} else {
|
|
76
84
|
lines.push(theme.fg("muted", "Pick a provider to sign in — you can connect more than one."), "");
|
|
85
|
+
this.#selectorRowStart = lines.length;
|
|
77
86
|
lines.push(...this.#selector.render(width));
|
|
78
87
|
}
|
|
79
88
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
padding,
|
|
3
|
+
type SelectItem,
|
|
4
|
+
SelectList,
|
|
5
|
+
type SgrMouseEvent,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
visibleWidth,
|
|
8
|
+
} from "@oh-my-pi/pi-tui";
|
|
2
9
|
import {
|
|
3
10
|
enableAutoTheme,
|
|
4
11
|
getAvailableThemes,
|
|
@@ -89,6 +96,8 @@ class ThemeSceneController implements SetupSceneController {
|
|
|
89
96
|
#message: string | undefined;
|
|
90
97
|
#previewRequest = 0;
|
|
91
98
|
#disposed = false;
|
|
99
|
+
/** Render line where the select list began, or -1 while it is not shown. */
|
|
100
|
+
#listRowStart = -1;
|
|
92
101
|
readonly #originalTheme = getCurrentThemeName();
|
|
93
102
|
readonly #originalSymbolPreset: SymbolPreset;
|
|
94
103
|
readonly #originalColorBlindMode: boolean;
|
|
@@ -117,6 +126,22 @@ class ThemeSceneController implements SetupSceneController {
|
|
|
117
126
|
this.#selectList.handleInput(data);
|
|
118
127
|
}
|
|
119
128
|
|
|
129
|
+
/** Wheel moves the highlight (live preview); hover lights the row under the pointer; click confirms it. */
|
|
130
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
131
|
+
if (event.wheel !== null) {
|
|
132
|
+
this.#selectList.handleWheel(event.wheel);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const index = this.#listRowStart >= 0 ? this.#selectList.hitTest(line - this.#listRowStart) : undefined;
|
|
136
|
+
if (event.motion) {
|
|
137
|
+
this.#selectList.setHoverIndex(index ?? null);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (event.leftClick && index !== undefined) {
|
|
141
|
+
this.#selectList.clickItem(index);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
render(width: number): readonly string[] {
|
|
121
146
|
const lines = [
|
|
122
147
|
theme.fg("muted", "Theme changes preview live. Nothing is saved until you press Enter."),
|
|
@@ -128,8 +153,10 @@ class ThemeSceneController implements SetupSceneController {
|
|
|
128
153
|
"",
|
|
129
154
|
];
|
|
130
155
|
if (this.#loadingAllThemes) {
|
|
156
|
+
this.#listRowStart = -1;
|
|
131
157
|
lines.push(theme.fg("dim", "Loading themes…"));
|
|
132
158
|
} else {
|
|
159
|
+
this.#listRowStart = lines.length;
|
|
133
160
|
lines.push(...this.#selectList.render(width));
|
|
134
161
|
}
|
|
135
162
|
if (this.#message) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import type { Component, SgrMouseEvent } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { InteractiveModeContext } from "../../types";
|
|
3
3
|
|
|
4
4
|
export type SetupSceneResult = "done" | "skipped";
|
|
@@ -17,6 +17,13 @@ export interface SetupSceneController extends Component {
|
|
|
17
17
|
onMount?(): void | Promise<void>;
|
|
18
18
|
onUnmount?(): void;
|
|
19
19
|
dispose?(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Route an SGR mouse report (tracking is on while the wizard holds the
|
|
22
|
+
* alternate screen). `line`/`col` are 0-based within this controller's
|
|
23
|
+
* last rendered output. When absent, the wizard falls back to synthesizing
|
|
24
|
+
* arrow keys from wheel notches.
|
|
25
|
+
*/
|
|
26
|
+
routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/**
|
|
@@ -36,6 +43,8 @@ export interface SetupTab {
|
|
|
36
43
|
invalidate(): void;
|
|
37
44
|
/** Called when the tab becomes active (including initial mount). */
|
|
38
45
|
onActivate?(): void;
|
|
46
|
+
/** Mouse routing at tab-local coordinates; see {@link SetupSceneController.routeMouse}. */
|
|
47
|
+
routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
|
|
39
48
|
dispose(): void;
|
|
40
49
|
}
|
|
41
50
|
|