@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3

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 (187) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/cli.js +3621 -3579
  3. package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +8 -4
  5. package/dist/types/advisor/config.d.ts +88 -0
  6. package/dist/types/advisor/index.d.ts +1 -0
  7. package/dist/types/advisor/runtime.d.ts +15 -1
  8. package/dist/types/advisor/transcript-recorder.d.ts +13 -2
  9. package/dist/types/advisor/watchdog.d.ts +20 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/config/model-roles.d.ts +1 -1
  12. package/dist/types/config/settings-schema.d.ts +113 -12
  13. package/dist/types/debug/log-viewer.d.ts +1 -0
  14. package/dist/types/debug/raw-sse.d.ts +1 -0
  15. package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/edit/index.d.ts +18 -0
  18. package/dist/types/edit/streaming.d.ts +30 -0
  19. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  20. package/dist/types/extensibility/shared-events.d.ts +1 -0
  21. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  22. package/dist/types/extensibility/utils.d.ts +12 -0
  23. package/dist/types/mcp/oauth-discovery.d.ts +0 -11
  24. package/dist/types/mcp/transports/index.d.ts +1 -0
  25. package/dist/types/mcp/transports/sse.d.ts +20 -0
  26. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  27. package/dist/types/modes/components/index.d.ts +1 -0
  28. package/dist/types/modes/components/model-selector.d.ts +9 -1
  29. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  30. package/dist/types/modes/components/status-line/component.d.ts +30 -3
  31. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  32. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  33. package/dist/types/modes/interactive-mode.d.ts +10 -4
  34. package/dist/types/modes/skill-command.d.ts +32 -0
  35. package/dist/types/modes/types.d.ts +7 -2
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +84 -12
  38. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  39. package/dist/types/session/messages.d.ts +32 -7
  40. package/dist/types/session/messages.test.d.ts +1 -0
  41. package/dist/types/session/session-entries.d.ts +31 -3
  42. package/dist/types/session/session-history-format.d.ts +6 -0
  43. package/dist/types/session/session-loader.d.ts +9 -1
  44. package/dist/types/session/session-manager.d.ts +6 -4
  45. package/dist/types/session/session-storage.d.ts +11 -0
  46. package/dist/types/session/session-title-slot.d.ts +19 -0
  47. package/dist/types/session/settings-stream-fn.d.ts +21 -0
  48. package/dist/types/session/turn-persistence.d.ts +88 -0
  49. package/dist/types/ssh/connection-manager.d.ts +47 -0
  50. package/dist/types/ssh/utils.d.ts +16 -0
  51. package/dist/types/task/executor.d.ts +3 -16
  52. package/dist/types/task/render.d.ts +0 -5
  53. package/dist/types/task/renderer.d.ts +13 -0
  54. package/dist/types/task/types.d.ts +16 -0
  55. package/dist/types/task/yield-assembly.d.ts +28 -0
  56. package/dist/types/tiny/text.d.ts +8 -0
  57. package/dist/types/tools/render-utils.d.ts +2 -0
  58. package/dist/types/tools/review.d.ts +6 -4
  59. package/dist/types/tools/ssh.d.ts +1 -1
  60. package/dist/types/tools/todo.d.ts +6 -0
  61. package/dist/types/tools/yield.d.ts +8 -3
  62. package/dist/types/utils/thinking-display.d.ts +4 -0
  63. package/package.json +12 -12
  64. package/src/advisor/__tests__/advisor.test.ts +438 -10
  65. package/src/advisor/__tests__/config.test.ts +173 -0
  66. package/src/advisor/advise-tool.ts +11 -6
  67. package/src/advisor/config.ts +256 -0
  68. package/src/advisor/index.ts +1 -0
  69. package/src/advisor/runtime.ts +77 -4
  70. package/src/advisor/transcript-recorder.ts +25 -2
  71. package/src/advisor/watchdog.ts +57 -31
  72. package/src/auto-thinking/classifier.ts +2 -2
  73. package/src/autoresearch/index.ts +7 -2
  74. package/src/cli/gc-cli.ts +17 -10
  75. package/src/collab/guest.ts +43 -7
  76. package/src/config/model-registry.ts +80 -18
  77. package/src/config/model-resolver.ts +5 -1
  78. package/src/config/model-roles.ts +3 -3
  79. package/src/config/settings-schema.ts +107 -8
  80. package/src/debug/index.ts +32 -7
  81. package/src/debug/log-viewer.ts +111 -53
  82. package/src/debug/raw-sse.ts +68 -48
  83. package/src/discovery/codex.ts +13 -5
  84. package/src/discovery/omp-extension-roots.ts +38 -13
  85. package/src/edit/hashline/diff.ts +57 -4
  86. package/src/edit/index.ts +21 -0
  87. package/src/edit/streaming.ts +170 -0
  88. package/src/eval/js/shared/local-module-loader.ts +23 -1
  89. package/src/export/html/template.js +13 -7
  90. package/src/extensibility/custom-tools/types.ts +1 -0
  91. package/src/extensibility/extensions/loader.ts +5 -3
  92. package/src/extensibility/extensions/wrapper.ts +9 -3
  93. package/src/extensibility/hooks/loader.ts +3 -3
  94. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  95. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
  96. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
  97. package/src/extensibility/plugins/manager.ts +76 -5
  98. package/src/extensibility/shared-events.ts +1 -0
  99. package/src/extensibility/tool-event-input.ts +23 -0
  100. package/src/extensibility/utils.ts +74 -0
  101. package/src/internal-urls/docs-index.generated.txt +1 -1
  102. package/src/mcp/client.ts +3 -1
  103. package/src/mcp/manager.ts +12 -5
  104. package/src/mcp/oauth-discovery.ts +5 -29
  105. package/src/mcp/transports/http.ts +3 -1
  106. package/src/mcp/transports/index.ts +1 -0
  107. package/src/mcp/transports/sse.ts +377 -0
  108. package/src/memories/index.ts +15 -6
  109. package/src/mnemopi/backend.ts +2 -2
  110. package/src/modes/acp/acp-agent.ts +1 -1
  111. package/src/modes/components/advisor-config.ts +555 -0
  112. package/src/modes/components/advisor-message.ts +9 -2
  113. package/src/modes/components/agent-hub.ts +9 -4
  114. package/src/modes/components/assistant-message.ts +5 -5
  115. package/src/modes/components/index.ts +2 -0
  116. package/src/modes/components/model-selector.ts +79 -48
  117. package/src/modes/components/settings-selector.ts +1 -0
  118. package/src/modes/components/status-line/component.ts +145 -14
  119. package/src/modes/components/status-line/segments.ts +47 -22
  120. package/src/modes/components/status-line/types.ts +13 -1
  121. package/src/modes/components/tool-execution.ts +47 -6
  122. package/src/modes/controllers/command-controller.ts +23 -2
  123. package/src/modes/controllers/event-controller.ts +114 -11
  124. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  125. package/src/modes/controllers/input-controller.ts +61 -61
  126. package/src/modes/controllers/selector-controller.ts +100 -9
  127. package/src/modes/interactive-mode.ts +65 -10
  128. package/src/modes/print-mode.ts +1 -1
  129. package/src/modes/skill-command.ts +116 -0
  130. package/src/modes/types.ts +7 -2
  131. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  132. package/src/modes/utils/ui-helpers.ts +46 -27
  133. package/src/prompts/agents/reviewer.md +11 -10
  134. package/src/prompts/review-custom-request.md +1 -2
  135. package/src/prompts/review-request.md +1 -2
  136. package/src/prompts/system/interrupted-thinking.md +7 -0
  137. package/src/prompts/system/recap-user.md +9 -0
  138. package/src/prompts/system/subagent-system-prompt.md +8 -5
  139. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  140. package/src/prompts/system/system-prompt.md +0 -1
  141. package/src/prompts/tools/irc.md +2 -2
  142. package/src/prompts/tools/read.md +2 -2
  143. package/src/sdk.ts +40 -50
  144. package/src/session/agent-session.ts +1139 -600
  145. package/src/session/indexed-session-storage.ts +86 -13
  146. package/src/session/messages.test.ts +125 -0
  147. package/src/session/messages.ts +192 -21
  148. package/src/session/redis-session-storage.ts +49 -2
  149. package/src/session/session-entries.ts +39 -2
  150. package/src/session/session-history-format.ts +29 -2
  151. package/src/session/session-listing.ts +54 -24
  152. package/src/session/session-loader.ts +66 -3
  153. package/src/session/session-manager.ts +113 -19
  154. package/src/session/session-persistence.ts +96 -3
  155. package/src/session/session-storage.ts +36 -0
  156. package/src/session/session-title-slot.ts +141 -0
  157. package/src/session/settings-stream-fn.ts +49 -0
  158. package/src/session/sql-session-storage.ts +71 -11
  159. package/src/session/turn-persistence.ts +142 -0
  160. package/src/session/unexpected-stop-classifier.ts +2 -2
  161. package/src/slash-commands/builtin-registry.ts +16 -3
  162. package/src/slash-commands/helpers/mcp.ts +2 -1
  163. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  164. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  165. package/src/ssh/connection-manager.ts +139 -12
  166. package/src/ssh/file-transfer.ts +23 -18
  167. package/src/ssh/ssh-executor.ts +2 -13
  168. package/src/ssh/utils.ts +19 -0
  169. package/src/task/executor.ts +21 -23
  170. package/src/task/render.ts +162 -20
  171. package/src/task/renderer.ts +14 -0
  172. package/src/task/types.ts +17 -0
  173. package/src/task/yield-assembly.ts +207 -0
  174. package/src/tiny/models.ts +8 -6
  175. package/src/tiny/text.ts +37 -7
  176. package/src/tools/ask.ts +55 -4
  177. package/src/tools/image-gen.ts +2 -1
  178. package/src/tools/render-utils.ts +2 -0
  179. package/src/tools/renderers.ts +8 -2
  180. package/src/tools/review.ts +17 -7
  181. package/src/tools/ssh.ts +8 -4
  182. package/src/tools/todo.ts +17 -1
  183. package/src/tools/tts.ts +2 -1
  184. package/src/tools/yield.ts +140 -31
  185. package/src/utils/thinking-display.ts +15 -0
  186. package/src/utils/title-generator.ts +1 -1
  187. package/src/web/search/providers/tavily.ts +36 -19
@@ -0,0 +1,555 @@
1
+ /**
2
+ * Fullscreen `/advisor configure` overlay: a mouse- and keyboard-driven editor
3
+ * for the `WATCHDOG.yml` advisor roster at project or user level.
4
+ *
5
+ * It paints the entire alternate screen from row 0 (so SGR mouse rows index
6
+ * directly into the rendered frame) using the shared {@link ./overlay-box} chrome.
7
+ * The list screen is a two-pane split (the `/extensions` idiom): a clickable
8
+ * advisor/action sidebar on the left, and a scrollable preview of the highlighted
9
+ * advisor's model / tools / instructions on the right, filling the free space.
10
+ *
11
+ * Each screen is backed by a proven primitive — {@link SelectList} (list / detail
12
+ * / tools / thinking), {@link Input} (name), {@link ModelSelectorComponent} (the
13
+ * same rich `/model` picker, in direct-select mode), and {@link HookEditorComponent}
14
+ * (multiline instructions; Ctrl+G opens `$EDITOR`). The overlay edits an in-memory
15
+ * {@link WatchdogConfigDoc} and only touches disk + the live advisors via the host
16
+ * `save` callback.
17
+ */
18
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
19
+ import type { Model } from "@oh-my-pi/pi-ai";
20
+ import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
21
+ import {
22
+ type Component,
23
+ Input,
24
+ type MouseRoutable,
25
+ routeSgrMouseInput,
26
+ type SelectItem,
27
+ SelectList,
28
+ type SgrMouseEvent,
29
+ type TUI,
30
+ truncateToWidth,
31
+ } from "@oh-my-pi/pi-tui";
32
+ import {
33
+ ADVISOR_DEFAULT_TOOL_NAMES,
34
+ type AdvisorConfig,
35
+ type AdvisorConfigScope,
36
+ type WatchdogConfigDoc,
37
+ } from "../../advisor";
38
+ import type { ModelRegistry } from "../../config/model-registry";
39
+ import { formatModelSelectorValue } from "../../config/model-resolver";
40
+ import type { Settings } from "../../config/settings";
41
+ import { getSelectListTheme, theme } from "../theme/theme";
42
+ import { HookEditorComponent } from "./hook-editor";
43
+ import { ModelSelectorComponent } from "./model-selector";
44
+ import {
45
+ bottomBorder,
46
+ divider,
47
+ dividerSplit,
48
+ row,
49
+ splitBodyWidth,
50
+ splitRow,
51
+ topBorder,
52
+ topBorderSplit,
53
+ } from "./overlay-box";
54
+
55
+ /** Host callbacks: all disk + live-runtime effects flow through these. */
56
+ export interface AdvisorConfigCallbacks {
57
+ /** Load a scope's `WATCHDOG.yml` into an editable doc (empty when absent). */
58
+ loadDoc: (scope: AdvisorConfigScope) => Promise<WatchdogConfigDoc>;
59
+ /** Persist the doc to the scope's file and rebuild the live advisors. */
60
+ save: (scope: AdvisorConfigScope, doc: WatchdogConfigDoc) => Promise<void>;
61
+ /** Tear down the overlay and restore the editor. */
62
+ close: () => void;
63
+ requestRender: () => void;
64
+ /** Surface a transient status/warning line to the user. */
65
+ notify: (message: string) => void;
66
+ }
67
+
68
+ export interface AdvisorConfigDeps {
69
+ modelRegistry: ModelRegistry;
70
+ settings: Settings;
71
+ scopedModels: ReadonlyArray<{ model: Model; thinkingLevel?: ThinkingLevel }>;
72
+ availableToolNames: string[];
73
+ /** Formatted advisor-role model shown on the seeded default row (e.g. "anthropic/claude-..."). */
74
+ defaultModelLabel?: string;
75
+ }
76
+
77
+ const PREVIEW_WIDTH = 60;
78
+
79
+ function previewLine(text: string | undefined): string {
80
+ if (!text?.trim()) return "(none)";
81
+ const first = text.trim().split("\n", 1)[0] ?? "";
82
+ return first.length > PREVIEW_WIDTH ? `${first.slice(0, PREVIEW_WIDTH - 1)}…` : first;
83
+ }
84
+
85
+ /** Default when the set is empty or exactly read/grep/glob; else the available-ordered subset. */
86
+ function commitTools(selected: ReadonlySet<string>, all: readonly string[]): string[] | undefined {
87
+ if (selected.size === 0) return undefined;
88
+ if (selected.size === ADVISOR_DEFAULT_TOOL_NAMES.size) {
89
+ let matchesDefault = true;
90
+ for (const name of ADVISOR_DEFAULT_TOOL_NAMES) {
91
+ if (!selected.has(name)) {
92
+ matchesDefault = false;
93
+ break;
94
+ }
95
+ }
96
+ if (matchesDefault) return undefined;
97
+ }
98
+ return all.filter(name => selected.has(name));
99
+ }
100
+
101
+ /** Soft-wrap plain text to `width`, returning at least one (possibly empty) line. */
102
+ function wrap(text: string, width: number): string[] {
103
+ if (!text) return [""];
104
+ return Bun.wrapAnsi(text, Math.max(1, width), { trim: false }).split("\n");
105
+ }
106
+
107
+ type Screen = "list" | "detail" | "name" | "model" | "tools" | "thinking" | "instructions";
108
+
109
+ /**
110
+ * Fullscreen advisor-configuration overlay. Implements {@link Component} directly
111
+ * (rather than extending Container) so it owns the whole frame and the mouse
112
+ * geometry needed to make every row clickable.
113
+ */
114
+ export class AdvisorConfigOverlayComponent implements Component {
115
+ #tui: TUI;
116
+ #modelRegistry: ModelRegistry;
117
+ #settings: Settings;
118
+ #scopedModels: ReadonlyArray<{ model: Model; thinkingLevel?: ThinkingLevel }>;
119
+ #availableToolNames: readonly string[];
120
+ #defaultModelLabel: string | undefined;
121
+ #cb: AdvisorConfigCallbacks;
122
+ #scope: AdvisorConfigScope;
123
+ #doc: WatchdogConfigDoc;
124
+ #dirty = false;
125
+
126
+ #screen: Screen = "list";
127
+ /** The interactive element for the current screen. */
128
+ #active: Component = new SelectList([], 1, getSelectListTheme());
129
+ #footerHint = "";
130
+ #previewScroll = 0;
131
+
132
+ // Frame geometry from the last render (the frame paints from screen row 0,
133
+ // so SGR `event.row`/`event.col` — already 0-based — index it directly).
134
+ #bodyRowStart = 0;
135
+ #dividerCol = 0;
136
+
137
+ constructor(
138
+ tui: TUI,
139
+ deps: AdvisorConfigDeps,
140
+ scope: AdvisorConfigScope,
141
+ doc: WatchdogConfigDoc,
142
+ callbacks: AdvisorConfigCallbacks,
143
+ ) {
144
+ this.#tui = tui;
145
+ this.#modelRegistry = deps.modelRegistry;
146
+ this.#settings = deps.settings;
147
+ this.#scopedModels = deps.scopedModels;
148
+ this.#availableToolNames = deps.availableToolNames;
149
+ this.#defaultModelLabel = deps.defaultModelLabel;
150
+ this.#cb = callbacks;
151
+ this.#scope = scope;
152
+ this.#doc = doc;
153
+ this.#ensureRosterVisible();
154
+ this.#showList();
155
+ }
156
+
157
+ // ───────────────────────────── render ─────────────────────────────
158
+
159
+ render(width: number): readonly string[] {
160
+ const height = Math.max(14, process.stdout.rows || 40);
161
+ const bodyRows = Math.max(3, height - 4);
162
+ const title = `Advisor configuration · ${this.#scope}${this.#dirty ? " ● unsaved" : ""}`;
163
+ const out: string[] = [];
164
+
165
+ if (this.#screen === "list") {
166
+ const sidebarWidth = Math.max(22, Math.min(42, Math.floor(width * 0.34)));
167
+ this.#dividerCol = sidebarWidth + 3;
168
+ const bodyWidth = splitBodyWidth(width, sidebarWidth);
169
+ const sidebar = this.#active.render(sidebarWidth);
170
+ const preview = this.#previewWindow(bodyWidth, bodyRows);
171
+ out.push(topBorderSplit(width, title, sidebarWidth));
172
+ this.#bodyRowStart = out.length;
173
+ for (let i = 0; i < bodyRows; i++) {
174
+ out.push(splitRow(sidebar[i] ?? "", preview[i] ?? "", width, sidebarWidth));
175
+ }
176
+ out.push(dividerSplit(width, sidebarWidth));
177
+ } else {
178
+ out.push(topBorder(width, title));
179
+ this.#bodyRowStart = out.length;
180
+ const lines = this.#active.render(Math.max(1, width - 4));
181
+ for (let i = 0; i < bodyRows; i++) out.push(row(lines[i] ?? "", width));
182
+ out.push(divider(width));
183
+ }
184
+
185
+ out.push(row(theme.fg("dim", this.#footerHint), width));
186
+ out.push(bottomBorder(width));
187
+ return out;
188
+ }
189
+
190
+ // ───────────────────────────── input ─────────────────────────────
191
+
192
+ handleInput(data: string): void {
193
+ if (data.startsWith("\x1b[<")) {
194
+ routeSgrMouseInput(data, event => this.#routeMouseEvent(event));
195
+ return;
196
+ }
197
+ this.#active.handleInput?.(data);
198
+ }
199
+
200
+ /** Forward enhanced-paste transports into a multiline instructions editor. */
201
+ pasteText(text: string): void {
202
+ if (this.#active instanceof HookEditorComponent) this.#active.pasteText(text);
203
+ }
204
+
205
+ #routeMouseEvent(event: SgrMouseEvent): boolean {
206
+ // Right pane of the split (the preview) only scrolls; everything left of the
207
+ // divider routes into the active list/component at frame-local coordinates.
208
+ if (this.#screen === "list" && event.col >= this.#dividerCol) {
209
+ if (event.wheel !== null) {
210
+ this.#previewScroll = Math.max(0, this.#previewScroll + event.wheel);
211
+ this.#cb.requestRender();
212
+ }
213
+ return true;
214
+ }
215
+ const el = this.#active as Partial<MouseRoutable>;
216
+ if (typeof el.routeMouse === "function") {
217
+ el.routeMouse(event, event.row - this.#bodyRowStart, event.col);
218
+ return true;
219
+ }
220
+ return false;
221
+ }
222
+
223
+ // ───────────────────────────── preview ───────────────────────────
224
+
225
+ #previewWindow(bodyWidth: number, rows: number): string[] {
226
+ const lines = this.#previewContent(bodyWidth);
227
+ const maxScroll = Math.max(0, lines.length - rows);
228
+ const start = Math.min(this.#previewScroll, maxScroll);
229
+ const window = lines.slice(start, start + rows);
230
+ if (lines.length > rows) {
231
+ const marker =
232
+ start + rows < lines.length
233
+ ? theme.fg("dim", ` ↓ ${lines.length - rows - start} more`)
234
+ : theme.fg("dim", " (end)");
235
+ window[rows - 1] = marker;
236
+ }
237
+ return window;
238
+ }
239
+
240
+ #previewContent(bodyWidth: number): string[] {
241
+ const list = this.#active;
242
+ const value = list instanceof SelectList ? (list.getSelectedItem()?.value ?? "") : "";
243
+ const match = /^advisor:(\d+)$/.exec(value);
244
+ if (match) {
245
+ const advisor = this.#doc.advisors[Number(match[1])];
246
+ if (advisor) return this.#advisorPreview(advisor, bodyWidth);
247
+ }
248
+ if (value === "shared") {
249
+ const lines = [theme.bold("Shared instructions"), ""];
250
+ const text = this.#doc.instructions?.trim();
251
+ lines.push(...(text ? wrap(text, bodyWidth) : [theme.fg("muted", "(none)")]));
252
+ return lines.map(line => truncateToWidth(line, bodyWidth));
253
+ }
254
+ const help =
255
+ value === "add"
256
+ ? "Create a new advisor entry, then edit its model, tools, and instructions."
257
+ : value === "scope"
258
+ ? `Switch between the project and user WATCHDOG.yml. Currently editing the ${this.#scope}-level file.`
259
+ : value === "save"
260
+ ? "Write this scope's WATCHDOG.yml and reload the live advisors without a restart."
261
+ : value === "close"
262
+ ? "Close the editor. Unsaved changes are discarded."
263
+ : "";
264
+ return wrap(help, bodyWidth).map(line => truncateToWidth(theme.fg("muted", line), bodyWidth));
265
+ }
266
+
267
+ #advisorPreview(advisor: AdvisorConfig, bodyWidth: number): string[] {
268
+ const model = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
269
+ const tools = advisor.tools?.length ? advisor.tools.join(", ") : "read, grep, glob (default)";
270
+ const lines = [
271
+ theme.bold(advisor.name || "(unnamed)"),
272
+ "",
273
+ `${theme.fg("dim", "Model:")} ${model}`,
274
+ `${theme.fg("dim", "Tools:")} ${tools}`,
275
+ "",
276
+ theme.fg("dim", "Instructions:"),
277
+ ];
278
+ const instr = advisor.instructions?.trim();
279
+ lines.push(...(instr ? wrap(instr, bodyWidth) : [theme.fg("muted", "(none)")]));
280
+ return lines.map(line => truncateToWidth(line, bodyWidth));
281
+ }
282
+
283
+ // ───────────────────────────── screens ───────────────────────────
284
+
285
+ #setScreen(screen: Screen, active: Component, footerHint: string): void {
286
+ this.#screen = screen;
287
+ this.#active = active;
288
+ this.#footerHint = footerHint;
289
+ this.#previewScroll = 0;
290
+ this.#cb.requestRender();
291
+ }
292
+
293
+ #otherScope(): AdvisorConfigScope {
294
+ return this.#scope === "project" ? "user" : "project";
295
+ }
296
+
297
+ #ensureRosterVisible(): void {
298
+ if (this.#doc.advisors.length === 0) this.#doc.advisors.push({ name: "default" });
299
+ }
300
+
301
+ #isBareDefaultDoc(doc: WatchdogConfigDoc): boolean {
302
+ if (doc.advisors.length !== 1 || doc.instructions?.trim()) return false;
303
+ const advisor = doc.advisors[0];
304
+ if (!advisor) return false;
305
+ return (
306
+ advisor.name === "default" && !advisor.model?.trim() && !advisor.tools?.length && !advisor.instructions?.trim()
307
+ );
308
+ }
309
+
310
+ #advisorSummary(advisor: AdvisorConfig): string {
311
+ const model = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
312
+ const tools = advisor.tools?.length ? advisor.tools.join(", ") : "(default: read/grep/glob)";
313
+ return `${model} · ${tools}`;
314
+ }
315
+
316
+ #showList(): void {
317
+ this.#ensureRosterVisible();
318
+ const items: SelectItem[] = this.#doc.advisors.map((advisor, index) => ({
319
+ value: `advisor:${index}`,
320
+ label: advisor.name || "(unnamed)",
321
+ description: this.#advisorSummary(advisor),
322
+ }));
323
+ items.push({ value: "add", label: "+ Add advisor" });
324
+ items.push({ value: "shared", label: "Shared instructions", description: previewLine(this.#doc.instructions) });
325
+ items.push({ value: "scope", label: `Scope: ${this.#scope}`, description: `→ ${this.#otherScope()}` });
326
+ items.push({ value: "save", label: "Save & apply" });
327
+ items.push({ value: "close", label: "Close" });
328
+
329
+ // Show every row (no internal overflow-search); the split frame supplies height.
330
+ const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
331
+ list.onSelectionChange = () => {
332
+ this.#previewScroll = 0;
333
+ this.#cb.requestRender();
334
+ };
335
+ list.onSelect = item =>
336
+ void this.#onListSelect(item.value).catch(err => {
337
+ this.#cb.notify(`Advisor config: ${err instanceof Error ? err.message : String(err)}`);
338
+ });
339
+ list.onCancel = () => this.#cb.close();
340
+ this.#setScreen("list", list, "↑↓ move · Enter / click select · scroll preview on the right · Esc close");
341
+ }
342
+
343
+ async #onListSelect(value: string): Promise<void> {
344
+ if (value === "add") {
345
+ this.#doc.advisors.push({ name: `Advisor ${this.#doc.advisors.length + 1}` });
346
+ this.#dirty = true;
347
+ this.#showDetail(this.#doc.advisors.length - 1);
348
+ return;
349
+ }
350
+ if (value === "shared") {
351
+ this.#showInstructionsEditor(-1);
352
+ return;
353
+ }
354
+ if (value === "scope") {
355
+ if (this.#dirty) {
356
+ this.#cb.notify('Unsaved changes — "Save & apply" or Close before switching scope.');
357
+ return;
358
+ }
359
+ const next = this.#otherScope();
360
+ this.#doc = await this.#cb.loadDoc(next);
361
+ this.#ensureRosterVisible();
362
+ this.#scope = next;
363
+ this.#showList();
364
+ return;
365
+ }
366
+ if (value === "save") {
367
+ await this.#cb.save(this.#scope, this.#isBareDefaultDoc(this.#doc) ? { advisors: [] } : this.#doc);
368
+ this.#dirty = false;
369
+ this.#showList();
370
+ return;
371
+ }
372
+ if (value === "close") {
373
+ this.#cb.close();
374
+ return;
375
+ }
376
+ const match = /^advisor:(\d+)$/.exec(value);
377
+ if (match) this.#showDetail(Number(match[1]));
378
+ }
379
+
380
+ #showDetail(index: number): void {
381
+ const advisor = this.#doc.advisors[index];
382
+ if (!advisor) {
383
+ this.#showList();
384
+ return;
385
+ }
386
+ const modelDescription = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
387
+ const toolsDescription = advisor.tools?.length ? advisor.tools.join(", ") : "(default: read/grep/glob)";
388
+ const items: SelectItem[] = [
389
+ { value: "name", label: "Name", description: advisor.name },
390
+ { value: "model", label: "Model", description: modelDescription },
391
+ ];
392
+ if (advisor.model?.trim()) {
393
+ items.push({ value: "resetModel", label: "Reset model to advisor-role default" });
394
+ }
395
+ items.push(
396
+ { value: "tools", label: "Tools", description: toolsDescription },
397
+ { value: "instructions", label: "Instructions", description: previewLine(advisor.instructions) },
398
+ { value: "delete", label: "Delete this advisor" },
399
+ { value: "back", label: "Back" },
400
+ );
401
+ const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
402
+ list.onSelect = item => this.#onDetailSelect(index, item.value);
403
+ list.onCancel = () => this.#showList();
404
+ this.#setScreen("detail", list, `Editing "${advisor.name}" · Enter / click edit field · Esc back`);
405
+ }
406
+
407
+ #onDetailSelect(index: number, field: string): void {
408
+ switch (field) {
409
+ case "name":
410
+ this.#showNameEditor(index);
411
+ return;
412
+ case "model":
413
+ this.#showModelPicker(index);
414
+ return;
415
+ case "tools":
416
+ this.#showToolsEditor(
417
+ index,
418
+ new Set(this.#doc.advisors[index].tools ?? [...ADVISOR_DEFAULT_TOOL_NAMES]),
419
+ 0,
420
+ );
421
+ return;
422
+ case "resetModel":
423
+ this.#doc.advisors[index].model = undefined;
424
+ this.#dirty = true;
425
+ this.#showDetail(index);
426
+ return;
427
+ case "instructions":
428
+ this.#showInstructionsEditor(index);
429
+ return;
430
+ case "delete":
431
+ this.#doc.advisors.splice(index, 1);
432
+ this.#dirty = true;
433
+ this.#showList();
434
+ return;
435
+ default:
436
+ this.#showList();
437
+ }
438
+ }
439
+
440
+ #showNameEditor(index: number): void {
441
+ const input = new Input();
442
+ input.setValue(this.#doc.advisors[index].name);
443
+ input.onSubmit = value => {
444
+ const name = value.trim();
445
+ if (name) {
446
+ this.#doc.advisors[index].name = name;
447
+ this.#dirty = true;
448
+ }
449
+ this.#showDetail(index);
450
+ };
451
+ input.onEscape = () => this.#showDetail(index);
452
+ this.#setScreen("name", input, "Type a name · Enter save · Esc cancel");
453
+ }
454
+
455
+ #showModelPicker(index: number): void {
456
+ const picker = new ModelSelectorComponent(
457
+ this.#tui,
458
+ undefined,
459
+ this.#settings,
460
+ this.#modelRegistry,
461
+ this.#scopedModels,
462
+ (model, _role, _thinking, selector) => {
463
+ const base = selector ?? `${model.provider}/${model.id}`;
464
+ const efforts = getSupportedEfforts(model);
465
+ if (efforts.length === 0) {
466
+ this.#doc.advisors[index].model = base;
467
+ this.#dirty = true;
468
+ this.#showDetail(index);
469
+ } else {
470
+ this.#showThinkingPicker(index, base, efforts);
471
+ }
472
+ },
473
+ () => this.#showDetail(index),
474
+ { directSelect: true, pickerHint: "Pick this advisor's model · Enter / click select · Esc back" },
475
+ );
476
+ this.#setScreen("model", picker, "Type to search · Enter / click pick model · Esc back");
477
+ }
478
+
479
+ #showThinkingPicker(index: number, selector: string, efforts: readonly string[]): void {
480
+ const items: SelectItem[] = [{ value: "", label: "(model default thinking)" }];
481
+ for (const effort of efforts) items.push({ value: effort, label: effort });
482
+ const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
483
+ list.onSelect = item => {
484
+ // `item.value` is one of the model's own supported efforts (or "" for the
485
+ // model default); `formatModelSelectorValue` spells the `:level` suffix.
486
+ const level = item.value ? (item.value as ThinkingLevel) : undefined;
487
+ this.#doc.advisors[index].model = formatModelSelectorValue(selector, level);
488
+ this.#dirty = true;
489
+ this.#showDetail(index);
490
+ };
491
+ list.onCancel = () => this.#showModelPicker(index);
492
+ this.#setScreen("thinking", list, `Thinking effort for ${selector} · Enter / click pick · Esc back`);
493
+ }
494
+
495
+ #showToolsEditor(index: number, selected: Set<string>, cursor: number): void {
496
+ const all = this.#availableToolNames;
497
+ const items: SelectItem[] = all.map(name => ({
498
+ value: name,
499
+ label: `${selected.has(name) ? "[x]" : "[ ]"} ${name}`,
500
+ }));
501
+ items.push({ value: "__done", label: "Done" });
502
+ const list = new SelectList(items, Math.max(1, items.length), getSelectListTheme());
503
+ list.setSelectedIndex(cursor);
504
+ let cursorIndex = cursor;
505
+ list.onSelectionChange = item => {
506
+ cursorIndex = items.findIndex(i => i.value === item.value);
507
+ };
508
+ list.onSelect = item => {
509
+ if (item.value === "__done") {
510
+ this.#doc.advisors[index].tools = commitTools(selected, all);
511
+ this.#dirty = true;
512
+ this.#showDetail(index);
513
+ return;
514
+ }
515
+ if (selected.has(item.value)) selected.delete(item.value);
516
+ else selected.add(item.value);
517
+ this.#showToolsEditor(index, selected, cursorIndex);
518
+ };
519
+ list.onCancel = () => {
520
+ this.#doc.advisors[index].tools = commitTools(selected, all);
521
+ this.#dirty = true;
522
+ this.#showDetail(index);
523
+ };
524
+ this.#setScreen(
525
+ "tools",
526
+ list,
527
+ "Enter / click toggle · select Done or Esc to apply (empty or read/grep/glob = default)",
528
+ );
529
+ }
530
+
531
+ /** `index === -1` edits the shared top-level instructions; otherwise advisor[index]. */
532
+ #showInstructionsEditor(index: number): void {
533
+ const shared = index < 0;
534
+ const current = shared ? this.#doc.instructions : this.#doc.advisors[index].instructions;
535
+ const title = shared ? "Shared advisor instructions" : `Instructions — ${this.#doc.advisors[index].name}`;
536
+ const editor = new HookEditorComponent(
537
+ this.#tui,
538
+ title,
539
+ current,
540
+ value => {
541
+ const text = value.trim() ? value : undefined;
542
+ if (shared) this.#doc.instructions = text;
543
+ else this.#doc.advisors[index].instructions = text;
544
+ this.#dirty = true;
545
+ if (shared) this.#showList();
546
+ else this.#showDetail(index);
547
+ },
548
+ () => {
549
+ if (shared) this.#showList();
550
+ else this.#showDetail(index);
551
+ },
552
+ );
553
+ this.#setScreen("instructions", editor, "");
554
+ }
555
+ }
@@ -66,10 +66,17 @@ export function createAdvisorMessageCard(
66
66
  const badge = entry.severity
67
67
  ? `${formatBadge(entry.severity, severityColor(entry.severity), uiTheme)} `
68
68
  : "";
69
+ // Multi-advisor: attribute the note to its source. The implicit
70
+ // single ("default") advisor renders unlabeled, as before.
71
+ const who =
72
+ entry.advisor && entry.advisor !== "default"
73
+ ? `${uiTheme.fg("dim", `[${replaceTabs(entry.advisor)}]`)} `
74
+ : "";
69
75
  const quotePrefix = ` ${quote} `;
70
76
  const quoteWidth = visibleWidth(quotePrefix);
71
77
  const badgeWidth = visibleWidth(badge);
72
- const w1 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth - badgeWidth);
78
+ const whoWidth = visibleWidth(who);
79
+ const w1 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth - badgeWidth - whoWidth);
73
80
  const w2 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth);
74
81
 
75
82
  const paragraphs = entry.note.split("\n").filter(p => p.trim());
@@ -84,7 +91,7 @@ export function createAdvisorMessageCard(
84
91
  }
85
92
 
86
93
  bodyLines.forEach((line, index) => {
87
- const prefix = index === 0 ? badge : "";
94
+ const prefix = index === 0 ? `${badge}${who}` : "";
88
95
  lines.push(` ${quote} ${prefix}${uiTheme.fg("toolOutput", replaceTabs(line))}`);
89
96
  });
90
97
  }
@@ -18,7 +18,7 @@ import * as path from "node:path";
18
18
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
19
19
  import { Container, Ellipsis, matchesKey, type OverlayHandle, type TUI } from "@oh-my-pi/pi-tui";
20
20
  import { formatAge, getProjectDir, logger } from "@oh-my-pi/pi-utils";
21
- import { ADVISOR_TRANSCRIPT_FILENAME } from "../../advisor";
21
+ import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../../advisor";
22
22
  import type { KeyId } from "../../config/keybindings";
23
23
  import type { MessageRenderer } from "../../extensibility/extensions/types";
24
24
  import { IrcBus } from "../../irc/bus";
@@ -87,9 +87,14 @@ function registerPersistedSubagentsFromDir(registry: AgentRegistry, dir: string,
87
87
  // The advisor transcript is observability-only: register it as a non-peer
88
88
  // `advisor` kind under its owning session so the Hub can show its read-only
89
89
  // transcript, but it never joins agent-facing rosters and is not revivable.
90
- if (entry.name === ADVISOR_TRANSCRIPT_FILENAME) {
90
+ if (isAdvisorTranscriptName(entry.name)) {
91
91
  const owner = parentId ?? MAIN_AGENT_ID;
92
- const advisorId = `${owner}/advisor`;
92
+ // `__advisor.jsonl` the default advisor (no slug); `__advisor.<slug>.jsonl`
93
+ // → a named advisor, keyed and labeled by its slug.
94
+ const slug =
95
+ entry.name === ADVISOR_TRANSCRIPT_FILENAME ? "" : entry.name.slice("__advisor.".length, -".jsonl".length);
96
+ const advisorId = slug ? `${owner}/advisor:${slug}` : `${owner}/advisor`;
97
+ const displayName = slug ? `advisor:${slug}` : "advisor";
93
98
  const existing = registry.get(advisorId);
94
99
  // Never clobber a non-advisor ref that happens to share this id (a freak
95
100
  // user task literally named `<owner>/advisor`): leave it, skip the advisor.
@@ -99,7 +104,7 @@ function registerPersistedSubagentsFromDir(registry: AgentRegistry, dir: string,
99
104
  if (existing) registry.unregister(advisorId);
100
105
  registry.register({
101
106
  id: advisorId,
102
- displayName: "advisor",
107
+ displayName,
103
108
  kind: "advisor",
104
109
  parentId: owner,
105
110
  session: null,
@@ -581,11 +581,11 @@ export class AssistantMessageComponent extends Container {
581
581
  if (content.type === "toolCall") return false;
582
582
  }
583
583
  if (this.#toolImagesByCallId.size > 0) return false;
584
- if (message.stopReason === "aborted" && shouldRenderAbortReason(message.errorMessage)) return false;
584
+ if (message.stopReason === "aborted" && shouldRenderAbortReason(message)) return false;
585
585
  if (message.stopReason === "error" && !this.#errorPinned) return false;
586
586
  if (
587
587
  message.errorMessage &&
588
- shouldRenderAbortReason(message.errorMessage) &&
588
+ shouldRenderAbortReason(message) &&
589
589
  message.stopReason !== "aborted" &&
590
590
  message.stopReason !== "error"
591
591
  )
@@ -779,8 +779,8 @@ export class AssistantMessageComponent extends Container {
779
779
  // But only if there are no tool calls (tool execution components will show the error)
780
780
  const hasToolCalls = message.content.some(c => c.type === "toolCall");
781
781
  if (!hasToolCalls) {
782
- if (message.stopReason === "aborted" && shouldRenderAbortReason(message.errorMessage)) {
783
- const abortMessage = resolveAbortLabel(message.errorMessage);
782
+ if (message.stopReason === "aborted" && shouldRenderAbortReason(message)) {
783
+ const abortMessage = resolveAbortLabel(message);
784
784
  if (hasVisibleContent) {
785
785
  this.#contentContainer.addChild(new Spacer(1));
786
786
  } else {
@@ -793,7 +793,7 @@ export class AssistantMessageComponent extends Container {
793
793
  }
794
794
  if (
795
795
  message.errorMessage &&
796
- shouldRenderAbortReason(message.errorMessage) &&
796
+ shouldRenderAbortReason(message) &&
797
797
  message.stopReason !== "aborted" &&
798
798
  message.stopReason !== "error"
799
799
  ) {
@@ -1,4 +1,6 @@
1
1
  // UI Components barrel export
2
+
3
+ export * from "./advisor-config";
2
4
  export * from "./assistant-message";
3
5
  export * from "./bash-execution";
4
6
  export * from "./bordered-loader";