@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/cli.js +520 -451
  3. package/dist/types/cli/bench-cli.d.ts +78 -0
  4. package/dist/types/cli/usage-cli.d.ts +10 -1
  5. package/dist/types/commands/bench.d.ts +29 -0
  6. package/dist/types/commands/usage.d.ts +9 -0
  7. package/dist/types/config/model-resolver.d.ts +3 -2
  8. package/dist/types/config/settings-schema.d.ts +125 -3
  9. package/dist/types/edit/renderer.d.ts +1 -0
  10. package/dist/types/modes/components/oauth-selector.d.ts +10 -1
  11. package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
  12. package/dist/types/modes/components/session-selector.d.ts +1 -1
  13. package/dist/types/modes/components/settings-selector.d.ts +8 -1
  14. package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
  15. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/interactive-mode.d.ts +10 -0
  18. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  19. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
  20. package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
  21. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
  22. package/dist/types/modes/types.d.ts +2 -0
  23. package/dist/types/modes/utils/context-usage.d.ts +6 -1
  24. package/dist/types/session/agent-session.d.ts +14 -1
  25. package/dist/types/session/auth-storage.d.ts +1 -1
  26. package/dist/types/session/codex-auto-reset.d.ts +107 -0
  27. package/dist/types/session/snapcompact-inline.d.ts +107 -4
  28. package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
  29. package/dist/types/task/render.d.ts +1 -0
  30. package/dist/types/tools/bash.d.ts +2 -0
  31. package/dist/types/tools/eval-render.d.ts +1 -0
  32. package/dist/types/tools/renderers.d.ts +13 -0
  33. package/dist/types/tools/ssh.d.ts +1 -0
  34. package/dist/types/tools/todo.d.ts +0 -11
  35. package/package.json +11 -11
  36. package/src/cli/bench-cli.ts +437 -0
  37. package/src/cli/usage-cli.ts +187 -16
  38. package/src/cli-commands.ts +1 -0
  39. package/src/commands/bench.ts +42 -0
  40. package/src/commands/usage.ts +8 -0
  41. package/src/config/model-registry.ts +52 -5
  42. package/src/config/model-resolver.ts +36 -5
  43. package/src/config/settings-schema.ts +148 -3
  44. package/src/config/settings.ts +9 -0
  45. package/src/edit/renderer.ts +5 -0
  46. package/src/hindsight/client.ts +26 -1
  47. package/src/hindsight/state.ts +6 -2
  48. package/src/internal-urls/docs-index.generated.ts +2 -2
  49. package/src/mcp/transports/stdio.ts +81 -7
  50. package/src/modes/components/oauth-selector.ts +67 -7
  51. package/src/modes/components/reset-usage-selector.ts +161 -0
  52. package/src/modes/components/session-selector.ts +8 -2
  53. package/src/modes/components/settings-selector.ts +89 -47
  54. package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
  55. package/src/modes/components/snapcompact-shape-preview.ts +192 -0
  56. package/src/modes/components/tool-execution.ts +26 -0
  57. package/src/modes/components/transcript-container.ts +23 -1
  58. package/src/modes/controllers/command-controller.ts +24 -1
  59. package/src/modes/controllers/input-controller.ts +8 -6
  60. package/src/modes/controllers/selector-controller.ts +72 -2
  61. package/src/modes/interactive-mode.ts +83 -0
  62. package/src/modes/session-observer-registry.ts +61 -3
  63. package/src/modes/setup-wizard/index.ts +1 -0
  64. package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
  65. package/src/modes/setup-wizard/scenes/providers.ts +36 -2
  66. package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
  67. package/src/modes/setup-wizard/scenes/theme.ts +28 -1
  68. package/src/modes/setup-wizard/scenes/types.ts +10 -1
  69. package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
  70. package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
  71. package/src/modes/theme/theme.ts +2 -2
  72. package/src/modes/types.ts +2 -0
  73. package/src/modes/utils/context-usage.ts +75 -1
  74. package/src/prompts/bench.md +7 -0
  75. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  76. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  77. package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
  78. package/src/prompts/tools/browser.md +33 -43
  79. package/src/prompts/tools/eval.md +27 -50
  80. package/src/prompts/tools/irc.md +29 -31
  81. package/src/prompts/tools/read.md +31 -37
  82. package/src/prompts/tools/todo.md +1 -2
  83. package/src/sdk.ts +4 -2
  84. package/src/session/agent-session.ts +136 -6
  85. package/src/session/auth-storage.ts +3 -0
  86. package/src/session/codex-auto-reset.ts +190 -0
  87. package/src/session/snapcompact-inline.ts +404 -75
  88. package/src/slash-commands/builtin-registry.ts +145 -8
  89. package/src/slash-commands/helpers/context-report.ts +28 -1
  90. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  91. package/src/slash-commands/helpers/usage-report.ts +12 -0
  92. package/src/task/index.ts +30 -7
  93. package/src/task/render.ts +34 -19
  94. package/src/tools/bash.ts +3 -0
  95. package/src/tools/eval-render.ts +4 -0
  96. package/src/tools/renderers.ts +13 -0
  97. package/src/tools/ssh.ts +3 -0
  98. 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
- return a.lastUpdate - b.lastUpdate;
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
  });
@@ -82,6 +82,7 @@ export async function runSetupWizard(
82
82
  maxHeight: "100%",
83
83
  anchor: "top-left",
84
84
  margin: 0,
85
+ fullscreen: true,
85
86
  });
86
87
  try {
87
88
  await component.run();
@@ -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
- return [
65
- theme.fg("muted", "If a row shows boxes, tofu, or misaligned icons, pick another."),
66
- "",
67
- ...this.#selectList.render(width),
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
- return [...this.#tabBar.render(width), "", ...this.#activeTab().render(width)];
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 { padding, type SelectItem, SelectList, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
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