@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/cli.js +692 -607
  3. package/dist/types/cli/usage-cli.d.ts +10 -1
  4. package/dist/types/commands/usage.d.ts +9 -0
  5. package/dist/types/config/api-key-resolver.d.ts +9 -3
  6. package/dist/types/config/keybindings.d.ts +1 -1
  7. package/dist/types/config/model-discovery.d.ts +6 -4
  8. package/dist/types/config/model-registry.d.ts +7 -4
  9. package/dist/types/config/settings-schema.d.ts +508 -155
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/mnemopi/config.d.ts +3 -1
  12. package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
  13. package/dist/types/modes/components/session-selector.d.ts +1 -1
  14. package/dist/types/modes/components/settings-defs.d.ts +9 -2
  15. package/dist/types/modes/components/settings-selector.d.ts +9 -4
  16. package/dist/types/modes/components/tool-execution.d.ts +26 -1
  17. package/dist/types/modes/components/transcript-container.d.ts +12 -0
  18. package/dist/types/modes/controllers/input-controller.d.ts +9 -1
  19. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  20. package/dist/types/modes/interactive-mode.d.ts +10 -0
  21. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  22. package/dist/types/modes/theme/theme.d.ts +23 -3
  23. package/dist/types/modes/types.d.ts +2 -0
  24. package/dist/types/modes/utils/context-usage.d.ts +6 -1
  25. package/dist/types/session/agent-session.d.ts +28 -8
  26. package/dist/types/session/auth-storage.d.ts +1 -1
  27. package/dist/types/session/codex-auto-reset.d.ts +107 -0
  28. package/dist/types/session/snapcompact-inline.d.ts +129 -0
  29. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
  30. package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
  31. package/dist/types/system-prompt.d.ts +3 -1
  32. package/dist/types/task/render.d.ts +17 -6
  33. package/dist/types/tools/gh.d.ts +3 -0
  34. package/dist/types/tools/render-utils.d.ts +8 -16
  35. package/dist/types/tools/todo.d.ts +0 -11
  36. package/dist/types/utils/session-color.d.ts +15 -3
  37. package/dist/types/web/kagi.d.ts +1 -2
  38. package/dist/types/web/search/providers/codex.d.ts +1 -1
  39. package/dist/types/web/search/providers/gemini.d.ts +9 -6
  40. package/package.json +11 -11
  41. package/src/auto-thinking/classifier.ts +1 -5
  42. package/src/cli/usage-cli.ts +187 -16
  43. package/src/commands/usage.ts +8 -0
  44. package/src/commit/model-selection.ts +3 -6
  45. package/src/config/api-key-resolver.ts +10 -3
  46. package/src/config/keybindings.ts +1 -1
  47. package/src/config/model-discovery.ts +60 -46
  48. package/src/config/model-registry.ts +21 -8
  49. package/src/config/model-resolver.ts +57 -3
  50. package/src/config/settings-schema.ts +654 -153
  51. package/src/config/settings.ts +9 -0
  52. package/src/eval/completion-bridge.ts +1 -5
  53. package/src/export/html/template.generated.ts +1 -1
  54. package/src/export/html/template.js +13 -6
  55. package/src/internal-urls/docs-index.generated.ts +6 -6
  56. package/src/internal-urls/issue-pr-protocol.ts +10 -4
  57. package/src/memories/index.ts +2 -10
  58. package/src/mnemopi/backend.ts +30 -8
  59. package/src/mnemopi/config.ts +6 -1
  60. package/src/mnemopi/state.ts +6 -0
  61. package/src/modes/components/extensions/inspector-panel.ts +6 -2
  62. package/src/modes/components/plan-review-overlay.ts +15 -17
  63. package/src/modes/components/plugin-settings.ts +22 -5
  64. package/src/modes/components/reset-usage-selector.ts +161 -0
  65. package/src/modes/components/session-selector.ts +8 -2
  66. package/src/modes/components/settings-defs.ts +19 -4
  67. package/src/modes/components/settings-selector.ts +510 -95
  68. package/src/modes/components/status-line/component.ts +3 -1
  69. package/src/modes/components/status-line/segments.ts +3 -1
  70. package/src/modes/components/tool-execution.ts +87 -12
  71. package/src/modes/components/transcript-container.ts +49 -1
  72. package/src/modes/components/tree-selector.ts +16 -6
  73. package/src/modes/controllers/command-controller.ts +61 -8
  74. package/src/modes/controllers/event-controller.ts +1 -0
  75. package/src/modes/controllers/input-controller.ts +68 -6
  76. package/src/modes/controllers/selector-controller.ts +149 -61
  77. package/src/modes/interactive-mode.ts +63 -2
  78. package/src/modes/rpc/rpc-mode.ts +2 -1
  79. package/src/modes/session-observer-registry.ts +61 -3
  80. package/src/modes/shared.ts +2 -0
  81. package/src/modes/theme/theme.ts +102 -9
  82. package/src/modes/types.ts +2 -0
  83. package/src/modes/utils/context-usage.ts +78 -2
  84. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  85. package/src/modes/utils/ui-helpers.ts +9 -5
  86. package/src/prompts/system/personalities/default.md +26 -0
  87. package/src/prompts/system/personalities/friendly.md +17 -0
  88. package/src/prompts/system/personalities/pragmatic.md +15 -0
  89. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  90. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  91. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  92. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  93. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  94. package/src/prompts/system/system-prompt.md +5 -22
  95. package/src/prompts/tools/browser.md +33 -43
  96. package/src/prompts/tools/eval.md +27 -50
  97. package/src/prompts/tools/irc.md +29 -31
  98. package/src/prompts/tools/read.md +31 -37
  99. package/src/prompts/tools/task.md +3 -3
  100. package/src/prompts/tools/todo.md +1 -2
  101. package/src/sdk.ts +23 -1
  102. package/src/session/agent-session.ts +221 -29
  103. package/src/session/auth-storage.ts +4 -0
  104. package/src/session/codex-auto-reset.ts +190 -0
  105. package/src/session/session-dump-format.ts +8 -1
  106. package/src/session/session-manager.ts +5 -5
  107. package/src/session/snapcompact-inline.ts +524 -0
  108. package/src/slash-commands/builtin-registry.ts +145 -8
  109. package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
  110. package/src/slash-commands/helpers/context-report.ts +28 -1
  111. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  112. package/src/slash-commands/helpers/usage-report.ts +36 -3
  113. package/src/system-prompt.ts +15 -1
  114. package/src/task/index.ts +30 -7
  115. package/src/task/render.ts +57 -32
  116. package/src/tool-discovery/tool-index.ts +2 -0
  117. package/src/tools/bash.ts +10 -3
  118. package/src/tools/eval-render.ts +13 -8
  119. package/src/tools/gh.ts +39 -1
  120. package/src/tools/image-gen.ts +114 -78
  121. package/src/tools/inspect-image.ts +1 -5
  122. package/src/tools/job.ts +25 -5
  123. package/src/tools/read.ts +1 -57
  124. package/src/tools/render-utils.ts +29 -31
  125. package/src/tools/ssh.ts +3 -3
  126. package/src/tools/todo.ts +8 -128
  127. package/src/tools/tts.ts +40 -20
  128. package/src/utils/clipboard.ts +56 -4
  129. package/src/utils/commit-message-generator.ts +1 -5
  130. package/src/utils/session-color.ts +83 -9
  131. package/src/utils/title-generator.ts +1 -1
  132. package/src/web/kagi.ts +26 -27
  133. package/src/web/search/providers/codex.ts +42 -40
  134. package/src/web/search/providers/gemini.ts +42 -22
  135. package/src/web/search/providers/perplexity.ts +22 -10
@@ -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";
@@ -92,71 +99,86 @@ export class SelectorController {
92
99
 
93
100
  showSettingsSelector(): void {
94
101
  getAvailableThemes().then(availableThemes => {
95
- this.showSelector(done => {
96
- const selector = new SettingsSelectorComponent(
97
- {
98
- availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
99
- thinkingLevel: this.ctx.session.thinkingLevel,
100
- availableThemes,
101
- cwd: getProjectDir(),
102
- },
103
- {
104
- onChange: (id, value) => this.handleSettingChange(id, value),
105
- onThemePreview: async themeName => {
106
- const result = await previewTheme(themeName);
107
- if (result.success) {
108
- this.ctx.statusLine.invalidate();
109
- this.ctx.updateEditorTopBorder();
110
- this.ctx.ui.invalidate();
111
- this.ctx.ui.requestRender();
112
- }
113
- },
114
- onStatusLinePreview: previewSettings => {
115
- // Update status line with preview settings
116
- this.ctx.statusLine.updateSettings({
117
- preset: settings.get("statusLine.preset"),
118
- leftSegments: settings.get("statusLine.leftSegments"),
119
- rightSegments: settings.get("statusLine.rightSegments"),
120
- separator: settings.get("statusLine.separator"),
121
- showHookStatus: settings.get("statusLine.showHookStatus"),
122
- sessionAccent: settings.get("statusLine.sessionAccent"),
123
- transparent: settings.get("statusLine.transparent"),
124
- ...previewSettings,
125
- });
126
- this.ctx.updateEditorTopBorder();
127
- this.ctx.ui.requestRender();
128
- },
129
- getStatusLinePreview: () => {
130
- // Return the rendered status line for inline preview
131
- const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
132
- return this.ctx.statusLine.getTopBorder(availableWidth).content;
133
- },
134
- onPluginsChanged: async () => {
135
- const projectPath = await resolveActiveProjectRegistryPath(this.ctx.sessionManager.getCwd());
136
- clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
137
- await this.ctx.refreshSlashCommandState();
138
- await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
139
- this.ctx.ui.requestRender();
140
- },
141
- onCancel: () => {
142
- done();
143
- // Restore status line to saved settings
144
- this.ctx.statusLine.updateSettings({
145
- preset: settings.get("statusLine.preset"),
146
- leftSegments: settings.get("statusLine.leftSegments"),
147
- rightSegments: settings.get("statusLine.rightSegments"),
148
- separator: settings.get("statusLine.separator"),
149
- showHookStatus: settings.get("statusLine.showHookStatus"),
150
- sessionAccent: settings.get("statusLine.sessionAccent"),
151
- transparent: settings.get("statusLine.transparent"),
152
- });
102
+ // Fullscreen settings editor on the alternate screen: the overlay
103
+ // enables mouse tracking (click/hover/wheel) for its lifetime and
104
+ // the transcript stays untouched underneath.
105
+ let overlayHandle: OverlayHandle | undefined;
106
+ const done = () => {
107
+ overlayHandle?.hide();
108
+ this.ctx.ui.setFocus(this.ctx.editor);
109
+ this.ctx.ui.requestRender();
110
+ };
111
+ const selector = new SettingsSelectorComponent(
112
+ {
113
+ availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
114
+ thinkingLevel: this.ctx.session.thinkingLevel,
115
+ availableThemes,
116
+ cwd: getProjectDir(),
117
+ },
118
+ {
119
+ onChange: (id, value) => this.handleSettingChange(id, value),
120
+ onThemePreview: async themeName => {
121
+ const result = await previewTheme(themeName);
122
+ if (result.success) {
123
+ this.ctx.statusLine.invalidate();
153
124
  this.ctx.updateEditorTopBorder();
125
+ this.ctx.ui.invalidate();
154
126
  this.ctx.ui.requestRender();
155
- },
127
+ }
156
128
  },
157
- );
158
- return { component: selector, focus: selector };
129
+ onStatusLinePreview: previewSettings => {
130
+ // Update status line with preview settings
131
+ this.ctx.statusLine.updateSettings({
132
+ preset: settings.get("statusLine.preset"),
133
+ leftSegments: settings.get("statusLine.leftSegments"),
134
+ rightSegments: settings.get("statusLine.rightSegments"),
135
+ separator: settings.get("statusLine.separator"),
136
+ showHookStatus: settings.get("statusLine.showHookStatus"),
137
+ sessionAccent: settings.get("statusLine.sessionAccent"),
138
+ transparent: settings.get("statusLine.transparent"),
139
+ ...previewSettings,
140
+ });
141
+ this.ctx.updateEditorTopBorder();
142
+ this.ctx.ui.requestRender();
143
+ },
144
+ getStatusLinePreview: () => {
145
+ // Return the rendered status line for inline preview
146
+ const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
147
+ return this.ctx.statusLine.getTopBorder(availableWidth).content;
148
+ },
149
+ onPluginsChanged: async () => {
150
+ const projectPath = await resolveActiveProjectRegistryPath(this.ctx.sessionManager.getCwd());
151
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
152
+ await this.ctx.refreshSlashCommandState();
153
+ await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
154
+ this.ctx.ui.requestRender();
155
+ },
156
+ onCancel: () => {
157
+ done();
158
+ // Restore status line to saved settings
159
+ this.ctx.statusLine.updateSettings({
160
+ preset: settings.get("statusLine.preset"),
161
+ leftSegments: settings.get("statusLine.leftSegments"),
162
+ rightSegments: settings.get("statusLine.rightSegments"),
163
+ separator: settings.get("statusLine.separator"),
164
+ showHookStatus: settings.get("statusLine.showHookStatus"),
165
+ sessionAccent: settings.get("statusLine.sessionAccent"),
166
+ transparent: settings.get("statusLine.transparent"),
167
+ });
168
+ this.ctx.updateEditorTopBorder();
169
+ this.ctx.ui.requestRender();
170
+ },
171
+ },
172
+ );
173
+ overlayHandle = this.ctx.ui.showOverlay(selector, {
174
+ anchor: "bottom-center",
175
+ width: "100%",
176
+ maxHeight: "100%",
177
+ margin: 0,
178
+ fullscreen: true,
159
179
  });
180
+ this.ctx.ui.setFocus(selector);
181
+ this.ctx.ui.requestRender();
160
182
  });
161
183
  }
162
184
 
@@ -267,6 +289,11 @@ export class SelectorController {
267
289
  this.ctx.statusLine.invalidate();
268
290
  this.ctx.updateEditorBorderColor();
269
291
  break;
292
+ case "personality":
293
+ void this.ctx.session.refreshBaseSystemPrompt().catch(err => {
294
+ this.ctx.showError(`Failed to apply personality: ${err}`);
295
+ });
296
+ break;
270
297
 
271
298
  case "autocompleteMaxVisible":
272
299
  this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
@@ -1071,6 +1098,67 @@ export class SelectorController {
1071
1098
  });
1072
1099
  }
1073
1100
 
1101
+ async showResetUsageSelector(): Promise<void> {
1102
+ const session = this.ctx.session;
1103
+ this.ctx.showStatus("Checking saved rate-limit resets…", { dim: true });
1104
+ let statuses: Awaited<ReturnType<typeof session.listResetCredits>>;
1105
+ try {
1106
+ statuses = await session.listResetCredits();
1107
+ } catch (error) {
1108
+ this.ctx.showError(`Could not load saved resets: ${error instanceof Error ? error.message : String(error)}`);
1109
+ return;
1110
+ }
1111
+ const accounts = toResetUsageAccounts(statuses);
1112
+ if (accounts.length === 0) {
1113
+ this.ctx.showStatus("No Codex accounts found. Use /login to add one.");
1114
+ return;
1115
+ }
1116
+ if (!accounts.some(account => account.availableCount > 0)) {
1117
+ this.ctx.showStatus(
1118
+ accounts.some(account => account.error)
1119
+ ? "No saved resets available — some accounts couldn't be reached (try /login)."
1120
+ : "No saved rate-limit resets available to spend right now.",
1121
+ );
1122
+ return;
1123
+ }
1124
+ this.showSelector(done => {
1125
+ const selector = new ResetUsageSelectorComponent(
1126
+ accounts,
1127
+ account => {
1128
+ done();
1129
+ void this.#redeemReset(account);
1130
+ },
1131
+ () => {
1132
+ done();
1133
+ this.ctx.ui.requestRender();
1134
+ },
1135
+ );
1136
+ return { component: selector, focus: selector };
1137
+ });
1138
+ }
1139
+
1140
+ async #redeemReset(account: ResetUsageAccount): Promise<void> {
1141
+ this.ctx.showStatus(`Spending 1 saved reset for ${account.label}…`, { dim: true });
1142
+ let outcome: ResetCreditRedeemOutcome;
1143
+ try {
1144
+ outcome = await this.ctx.session.redeemResetCredit(account.target);
1145
+ } catch (error) {
1146
+ this.ctx.showError(
1147
+ `Reset failed for ${account.label}: ${error instanceof Error ? error.message : String(error)}`,
1148
+ );
1149
+ return;
1150
+ }
1151
+ const message = describeRedeemOutcome(outcome, account.label);
1152
+ if (outcome.ok) {
1153
+ this.ctx.showStatus(message);
1154
+ // Refresh the status-line usage so the freshly-reset window shows.
1155
+ this.ctx.statusLine.invalidate();
1156
+ this.ctx.ui.requestRender();
1157
+ } else {
1158
+ this.ctx.showWarning(message);
1159
+ }
1160
+ }
1161
+
1074
1162
  async showDebugSelector(): Promise<void> {
1075
1163
  const { DebugSelectorComponent } = await import("../../debug");
1076
1164
  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
 
@@ -1093,7 +1135,9 @@ export class InteractiveMode implements InteractiveModeContext {
1093
1135
  } else {
1094
1136
  const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
1095
1137
  const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
1096
- const hex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
1138
+ const hex = sessionName
1139
+ ? getSessionAccentHex(sessionName, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance)
1140
+ : undefined;
1097
1141
  const ansi = getSessionAccentAnsi(hex);
1098
1142
  if (ansi) {
1099
1143
  this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
@@ -1280,6 +1324,19 @@ export class InteractiveMode implements InteractiveModeContext {
1280
1324
  this.todoContainer.addChild(new Text(lines.join("\n"), 1, 0));
1281
1325
  }
1282
1326
 
1327
+ /**
1328
+ * Anchored HUD of in-flight subagents, mirroring the Todos block above the
1329
+ * editor. Driven entirely by observer-registry change events, so rows appear
1330
+ * on spawn and the whole block clears itself once the last subagent leaves
1331
+ * the "active" state.
1332
+ */
1333
+ #renderSubagentList(): void {
1334
+ this.subagentContainer.clear();
1335
+ const lines = renderSubagentHudLines(this.#observerRegistry.getSessions(), this.ui.terminal.columns);
1336
+ if (lines.length === 0) return;
1337
+ this.subagentContainer.addChild(new Text(lines.join("\n"), 1, 0));
1338
+ }
1339
+
1283
1340
  async #loadTodoList(): Promise<void> {
1284
1341
  this.todoPhases = this.session.getTodoPhases();
1285
1342
  this.#syncTodoAutoClearTimer();
@@ -2823,7 +2880,7 @@ export class InteractiveMode implements InteractiveModeContext {
2823
2880
  if (!key.sessionAccentEnabled || !key.sessionName) {
2824
2881
  return this.#cacheWorkingMessageAccent(key, undefined);
2825
2882
  }
2826
- const hex = getSessionAccentHex(key.sessionName, key.accentSurfaceLuminance);
2883
+ const hex = getSessionAccentHex(key.sessionName, theme.getMajorThemeColorHexes(), key.accentSurfaceLuminance);
2827
2884
  const main = getSessionAccentAnsi(hex);
2828
2885
  const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
2829
2886
  return this.#cacheWorkingMessageAccent(key, main && dim ? { main, dim } : undefined);
@@ -3233,6 +3290,10 @@ export class InteractiveMode implements InteractiveModeContext {
3233
3290
  return this.#selectorController.showOAuthSelector(mode, providerId);
3234
3291
  }
3235
3292
 
3293
+ showResetUsageSelector(): Promise<void> {
3294
+ return this.#selectorController.showResetUsageSelector();
3295
+ }
3296
+
3236
3297
  showProviderSetup(): Promise<void> {
3237
3298
  return runProviderSetupWizard(this);
3238
3299
  }
@@ -11,6 +11,7 @@
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
14
+ import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
14
15
  import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
15
16
  import { reset as resetCapabilities } from "../../capability";
16
17
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -662,7 +663,7 @@ export async function runRpcMode(
662
663
  dumpTools: session.agent.state.tools.map(tool => ({
663
664
  name: tool.name,
664
665
  description: tool.description,
665
- parameters: tool.parameters,
666
+ parameters: isZodSchema(tool.parameters) ? zodToWireSchema(tool.parameters) : tool.parameters,
666
667
  })),
667
668
  contextUsage: session.getContextUsage(),
668
669
  };
@@ -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
  });
@@ -24,6 +24,8 @@ export function getTabBarTheme(): TabBarTheme {
24
24
  label: (text: string) => theme.bold(theme.fg("accent", text)),
25
25
  activeTab: (text: string) => theme.bold(theme.bg("selectedBg", theme.fg("text", text))),
26
26
  inactiveTab: (text: string) => theme.fg("muted", text),
27
+ mutedTab: (text: string) => theme.fg("dim", text),
28
+ hoverTab: (text: string) => theme.bg("selectedBg", theme.fg("text", text)),
27
29
  hint: (text: string) => theme.fg("dim", text),
28
30
  };
29
31
  }