@oh-my-pi/pi-coding-agent 15.13.3 → 16.0.0

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 (50) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/cli.js +506 -443
  3. package/dist/types/advisor/__tests__/advisor.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +58 -0
  5. package/dist/types/advisor/index.d.ts +3 -0
  6. package/dist/types/advisor/runtime.d.ts +52 -0
  7. package/dist/types/advisor/watchdog.d.ts +5 -0
  8. package/dist/types/config/model-roles.d.ts +1 -1
  9. package/dist/types/config/settings-schema.d.ts +44 -5
  10. package/dist/types/modes/components/advisor-message.d.ts +9 -0
  11. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  12. package/dist/types/modes/controllers/command-controller.d.ts +3 -1
  13. package/dist/types/modes/interactive-mode.d.ts +3 -1
  14. package/dist/types/modes/types.d.ts +3 -1
  15. package/dist/types/sdk.d.ts +3 -3
  16. package/dist/types/session/agent-session.d.ts +71 -2
  17. package/dist/types/session/session-history-format.d.ts +4 -0
  18. package/dist/types/session/yield-queue.d.ts +2 -0
  19. package/dist/types/tools/path-utils.d.ts +1 -0
  20. package/dist/types/tools/report-tool-issue.d.ts +0 -1
  21. package/package.json +13 -13
  22. package/src/advisor/__tests__/advisor.test.ts +586 -0
  23. package/src/advisor/advise-tool.ts +87 -0
  24. package/src/advisor/index.ts +3 -0
  25. package/src/advisor/runtime.ts +248 -0
  26. package/src/advisor/watchdog.ts +83 -0
  27. package/src/config/model-roles.ts +13 -1
  28. package/src/config/settings-schema.ts +42 -5
  29. package/src/internal-urls/docs-index.generated.ts +6 -5
  30. package/src/main.ts +4 -0
  31. package/src/modes/components/advisor-message.ts +99 -0
  32. package/src/modes/components/agent-hub.ts +7 -0
  33. package/src/modes/components/assistant-message.ts +86 -0
  34. package/src/modes/components/status-line/segments.ts +20 -7
  35. package/src/modes/controllers/command-controller.ts +69 -2
  36. package/src/modes/interactive-mode.ts +12 -2
  37. package/src/modes/types.ts +3 -1
  38. package/src/modes/utils/ui-helpers.ts +9 -0
  39. package/src/prompts/advisor/advise-tool.md +1 -0
  40. package/src/prompts/advisor/system.md +31 -0
  41. package/src/sdk.ts +52 -13
  42. package/src/session/agent-session.ts +560 -13
  43. package/src/session/session-dump-format.ts +15 -131
  44. package/src/session/session-history-format.ts +30 -11
  45. package/src/session/yield-queue.ts +5 -1
  46. package/src/slash-commands/builtin-registry.ts +102 -4
  47. package/src/system-prompt.ts +1 -1
  48. package/src/tools/path-utils.ts +33 -2
  49. package/src/tools/report-tool-issue.ts +2 -7
  50. package/src/web/scrapers/docs-rs.ts +2 -3
package/src/main.ts CHANGED
@@ -140,6 +140,10 @@ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
140
140
  // memory should opt in explicitly through their own settings layer.
141
141
  "memory.backend",
142
142
  "memories.enabled",
143
+ // Advisor is interactive-session assistance. Protocol hosts opt in explicitly
144
+ // instead of inheriting a user's globally-enabled local preference.
145
+ "advisor.enabled",
146
+ "advisor.subagents",
143
147
  ];
144
148
 
145
149
  const RPC_BACKGROUND_DEFAULTED_SETTING_PATHS: SettingPath[] = [
@@ -0,0 +1,99 @@
1
+ import { type Component, visibleWidth } from "@oh-my-pi/pi-tui";
2
+ import type { AdvisorMessageDetails, AdvisorSeverity } from "../../advisor";
3
+ import {
4
+ createCachedComponent,
5
+ formatBadge,
6
+ replaceTabs,
7
+ type ToolUIColor,
8
+ wrapTextWithAnsi,
9
+ } from "../../tools/render-utils";
10
+ import { Ellipsis, renderStatusLine, truncateToWidth } from "../../tui";
11
+ import type { Theme } from "../theme/theme";
12
+
13
+ const COLLAPSED_NOTES = 3;
14
+ const NOTE_LINE_WIDTH = 110;
15
+
16
+ function wrapVarying(text: string, w1: number, w2: number): string[] {
17
+ if (text.length === 0) return [];
18
+ const firstWrap = wrapTextWithAnsi(text, w1);
19
+ if (firstWrap.length <= 1) {
20
+ return firstWrap;
21
+ }
22
+ const firstLine = firstWrap[0];
23
+ const idx = text.indexOf(firstLine);
24
+ if (idx === -1) {
25
+ return wrapTextWithAnsi(text, w2);
26
+ }
27
+ const remainder = text.slice(idx + firstLine.length).trimStart();
28
+ const restWrap = wrapTextWithAnsi(remainder, w2);
29
+ return [firstLine, ...restWrap];
30
+ }
31
+
32
+ function severityColor(severity: AdvisorSeverity | undefined): ToolUIColor {
33
+ switch (severity) {
34
+ case "blocker":
35
+ return "error";
36
+ case "concern":
37
+ return "warning";
38
+ default:
39
+ return "muted";
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Display-only transcript card for advisor notes injected into the primary
45
+ * session. Mirrors the IRC card's glyph + quote-border conventions so passive
46
+ * advice reads as a distinct, non-interrupting aside rather than a user turn.
47
+ */
48
+ export function createAdvisorMessageCard(
49
+ details: AdvisorMessageDetails | undefined,
50
+ getExpanded: () => boolean,
51
+ uiTheme: Theme,
52
+ ): Component {
53
+ const notes = details?.notes ?? [];
54
+ const blockers = notes.filter(note => note.severity === "blocker").length;
55
+ const meta: string[] = [`${notes.length} ${notes.length === 1 ? "note" : "notes"}`];
56
+ if (blockers > 0) meta.push(uiTheme.fg("error", `${blockers} blocker${blockers === 1 ? "" : "s"}`));
57
+
58
+ return createCachedComponent(
59
+ getExpanded,
60
+ (width, expanded) => {
61
+ const glyph = uiTheme.styledSymbol("status.info", "accent");
62
+ const lines = [renderStatusLine({ iconOverride: glyph, title: "Advisor", meta }, uiTheme)];
63
+ const quote = uiTheme.fg("dim", uiTheme.md.quoteBorder);
64
+ const shown = expanded ? notes : notes.slice(0, COLLAPSED_NOTES);
65
+ for (const entry of shown) {
66
+ const badge = entry.severity
67
+ ? `${formatBadge(entry.severity, severityColor(entry.severity), uiTheme)} `
68
+ : "";
69
+ const quotePrefix = ` ${quote} `;
70
+ const quoteWidth = visibleWidth(quotePrefix);
71
+ const badgeWidth = visibleWidth(badge);
72
+ const w1 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth - badgeWidth);
73
+ const w2 = Math.max(10, Math.min(NOTE_LINE_WIDTH, width) - quoteWidth);
74
+
75
+ const paragraphs = entry.note.split("\n").filter(p => p.trim());
76
+ const bodyLines: string[] = [];
77
+ for (let i = 0; i < paragraphs.length; i++) {
78
+ const p = paragraphs[i];
79
+ if (i === 0) {
80
+ bodyLines.push(...wrapVarying(p, w1, w2));
81
+ } else {
82
+ bodyLines.push(...wrapTextWithAnsi(p, w2));
83
+ }
84
+ }
85
+
86
+ bodyLines.forEach((line, index) => {
87
+ const prefix = index === 0 ? badge : "";
88
+ lines.push(` ${quote} ${prefix}${uiTheme.fg("toolOutput", replaceTabs(line))}`);
89
+ });
90
+ }
91
+ const hidden = notes.length - shown.length;
92
+ if (hidden > 0) {
93
+ lines.push(` ${quote} ${uiTheme.fg("dim", `… +${hidden} more ${hidden === 1 ? "note" : "notes"}`)}`);
94
+ }
95
+ return lines.map(line => truncateToWidth(line, width, Ellipsis.Unicode));
96
+ },
97
+ { paddingX: 1 },
98
+ );
99
+ }
@@ -19,6 +19,7 @@ import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
19
19
  import type { Usage } from "@oh-my-pi/pi-ai";
20
20
  import { Container, Editor, matchesKey, ScrollView, Text, type TUI } from "@oh-my-pi/pi-tui";
21
21
  import { formatAge, formatBytes, formatDuration, formatNumber, getProjectDir, logger } from "@oh-my-pi/pi-utils";
22
+ import type { AdvisorMessageDetails } from "../../advisor";
22
23
  import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
23
24
  import type { KeyId } from "../../config/keybindings";
24
25
  import { settings } from "../../config/settings";
@@ -45,6 +46,7 @@ import { canonicalizeMessage } from "../../utils/thinking-display";
45
46
  import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
46
47
  import { getEditorTheme, theme } from "../theme/theme";
47
48
  import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
49
+ import { createAdvisorMessageCard } from "./advisor-message";
48
50
  import { AssistantMessageComponent } from "./assistant-message";
49
51
  import { createBackgroundTanDispatchBlock } from "./background-tan-message";
50
52
  import { BashExecutionComponent } from "./bash-execution";
@@ -1241,6 +1243,11 @@ export class AgentHubOverlayComponent extends Container {
1241
1243
  this.#chatLog.addChild(card);
1242
1244
  return;
1243
1245
  }
1246
+ if (message.customType === "advisor") {
1247
+ const details = (message as CustomMessage<AdvisorMessageDetails>).details;
1248
+ this.#chatLog.addChild(createAdvisorMessageCard(details, () => this.#chatExpanded, theme));
1249
+ return;
1250
+ }
1244
1251
  if (message.customType === BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE) {
1245
1252
  this.#chatLog.addChild(createBackgroundTanDispatchBlock(message as CustomMessage<unknown>));
1246
1253
  return;
@@ -15,6 +15,15 @@ import { canonicalizeMessage } from "../../utils/thinking-display";
15
15
  */
16
16
  const MAX_TRANSCRIPT_ERROR_LINES = 8;
17
17
 
18
+ /**
19
+ * Frames for the streaming "thinking" pulse rendered in place of a hidden
20
+ * thinking block while the model is still producing it. A single fixed-width
21
+ * glyph that rises ▁▃▄▃ so the indicator animates without shifting the line.
22
+ * Advanced every {@link THINKING_DOTS_FRAME_MS}.
23
+ */
24
+ const THINKING_DOTS_FRAMES = ["▁", "▃", "▄", "▃"] as const;
25
+ const THINKING_DOTS_FRAME_MS = 320;
26
+
18
27
  /**
19
28
  * Component that renders a complete assistant message
20
29
  */
@@ -50,6 +59,11 @@ export class AssistantMessageComponent extends Container {
50
59
  #fastPathItems:
51
60
  | Array<{ md: Markdown; contentIndex: number; blockType: "text" | "thinking"; lastText: string }>
52
61
  | undefined;
62
+ /** Live "thinking" pulse shown in place of a hidden thinking block while it
63
+ * streams; undefined when not animating. Driven by {@link #thinkingDotsTimer}. */
64
+ #thinkingDots: Text | undefined;
65
+ #thinkingDotsTimer: NodeJS.Timeout | undefined;
66
+ #thinkingDotsFrame = 0;
53
67
 
54
68
  constructor(
55
69
  message?: AssistantMessage,
@@ -87,6 +101,60 @@ export class AssistantMessageComponent extends Container {
87
101
  this.hideThinkingBlock = hide;
88
102
  }
89
103
 
104
+ override dispose(): void {
105
+ this.#stopThinkingAnimation();
106
+ super.dispose();
107
+ }
108
+
109
+ /**
110
+ * Whether to render the animated "thinking" pulse in place of the suppressed
111
+ * reasoning: only while this block is still streaming (not yet finalized — the
112
+ * in-flight message always carries `stopReason: "stop"`, so finalization is the
113
+ * only reliable live signal), thinking is hidden, no tool call has started, and
114
+ * the active tail block is a thinking block (the model is reasoning right now).
115
+ * Once text starts, a tool call streams, or the block is sealed, the pulse ends.
116
+ */
117
+ #shouldAnimateThinking(message: AssistantMessage): boolean {
118
+ if (!this.hideThinkingBlock || this.#transcriptBlockFinalized) return false;
119
+ let tail: "text" | "thinking" | undefined;
120
+ for (const content of message.content) {
121
+ if (content.type === "toolCall") return false;
122
+ if (content.type === "text" && canonicalizeMessage(content.text)) tail = "text";
123
+ else if (content.type === "thinking" && canonicalizeMessage(content.thinking)) tail = "thinking";
124
+ }
125
+ return tail === "thinking";
126
+ }
127
+
128
+ #thinkingDotsLabel(): string {
129
+ const glyph = THINKING_DOTS_FRAMES[this.#thinkingDotsFrame % THINKING_DOTS_FRAMES.length] ?? "…";
130
+ return theme.fg("thinkingText", glyph);
131
+ }
132
+
133
+ #startThinkingAnimation(): void {
134
+ if (this.#thinkingDotsTimer) return;
135
+ this.#thinkingDotsTimer = setInterval(() => this.#advanceThinkingDots(), THINKING_DOTS_FRAME_MS);
136
+ this.#thinkingDotsTimer.unref?.();
137
+ }
138
+
139
+ #advanceThinkingDots(): void {
140
+ if (!this.#thinkingDots) {
141
+ this.#stopThinkingAnimation();
142
+ return;
143
+ }
144
+ this.#thinkingDotsFrame = (this.#thinkingDotsFrame + 1) % THINKING_DOTS_FRAMES.length;
145
+ if (this.#thinkingDots.setText(this.#thinkingDotsLabel())) {
146
+ this.onImageUpdate?.();
147
+ }
148
+ }
149
+
150
+ #stopThinkingAnimation(): void {
151
+ if (this.#thinkingDotsTimer) {
152
+ clearInterval(this.#thinkingDotsTimer);
153
+ this.#thinkingDotsTimer = undefined;
154
+ }
155
+ this.#thinkingDotsFrame = 0;
156
+ }
157
+
90
158
  /**
91
159
  * Toggle suppression of the inline `Error: …` line while the same error is
92
160
  * pinned in the banner above the editor. Re-renders so the change is visible.
@@ -109,6 +177,14 @@ export class AssistantMessageComponent extends Container {
109
177
 
110
178
  markTranscriptBlockFinalized(): void {
111
179
  this.#transcriptBlockFinalized = true;
180
+ this.#stopThinkingAnimation();
181
+ // If the live pulse was on screen when the block sealed, drop the fast path
182
+ // and rebuild so the placeholder is removed — finalized blocks never animate.
183
+ if (this.#thinkingDots) {
184
+ this.#fastPathKey = undefined;
185
+ this.#fastPathItems = undefined;
186
+ if (this.#lastMessage) this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
187
+ }
112
188
  }
113
189
 
114
190
  /**
@@ -326,6 +402,7 @@ export class AssistantMessageComponent extends Container {
326
402
 
327
403
  // Clear content container
328
404
  this.#contentContainer.clear();
405
+ this.#thinkingDots = undefined;
329
406
 
330
407
  // Determine if we should capture Markdown instances for next fast path
331
408
  const shouldCapture = this.#canFastPath(message);
@@ -382,6 +459,15 @@ export class AssistantMessageComponent extends Container {
382
459
  }
383
460
  }
384
461
 
462
+ if (this.#shouldAnimateThinking(message)) {
463
+ if (hasVisibleContent) this.#contentContainer.addChild(new Spacer(1));
464
+ this.#thinkingDots = new Text(this.#thinkingDotsLabel(), 1, 0);
465
+ this.#contentContainer.addChild(this.#thinkingDots);
466
+ this.#startThinkingAnimation();
467
+ } else {
468
+ this.#stopThinkingAnimation();
469
+ }
470
+
385
471
  this.#renderToolImages();
386
472
  // Check if aborted - show after partial content
387
473
  // But only if there are no tool calls (tool execution components will show the error)
@@ -86,32 +86,45 @@ const modelSegment: StatusLineSegment = {
86
86
  modelName = modelName.slice(7);
87
87
  }
88
88
 
89
- let content = withIcon(theme.icon.model, modelName);
90
-
89
+ // Fast-mode icon and thinking-level suffix trail the model name and are
90
+ // colored together with it as `statusLineModel`. The advisor "++" badge
91
+ // sits between the name and that tail in `accent`, so it reads as a
92
+ // distinct marker. theme.fg resets only the fg, so the spans are
93
+ // concatenated (not nested) to keep each color intact.
94
+ let tail = "";
91
95
  if (ctx.session.isFastModeActive() && theme.icon.fast) {
92
- content += ` ${theme.icon.fast}`;
96
+ tail += ` ${theme.icon.fast}`;
93
97
  }
94
98
 
95
- // Add thinking level with dot separator
96
99
  if (opts.showThinkingLevel !== false && state.model?.thinking) {
97
100
  if (ctx.session.isAutoThinking) {
98
101
  // Pending (no turn classified yet / classifying) shows a symbol-theme
99
102
  // question-box marker; once resolved it shows `<level>`.
100
103
  const resolved = ctx.session.autoResolvedThinkingLevel();
101
104
  const resolvedText = resolved ? (theme.thinking[resolved as keyof typeof theme.thinking] ?? resolved) : "";
102
- content += `${theme.sep.dot}${resolved ? resolvedText : `${theme.thinking.autoPending} auto`}`;
105
+ tail += `${theme.sep.dot}${resolved ? resolvedText : `${theme.thinking.autoPending} auto`}`;
103
106
  } else {
104
107
  const level = state.thinkingLevel ?? ThinkingLevel.Off;
105
108
  if (level !== ThinkingLevel.Off) {
106
109
  const thinkingText = theme.thinking[level as keyof typeof theme.thinking];
107
110
  if (thinkingText) {
108
- content += `${theme.sep.dot}${thinkingText}`;
111
+ tail += `${theme.sep.dot}${thinkingText}`;
109
112
  }
110
113
  }
111
114
  }
112
115
  }
113
116
 
114
- return { content: theme.fg("statusLineModel", content), visible: true };
117
+ // `statusLineModel` is aliased to `accent` in many themes, so the badge
118
+ // uses `success` to stay visibly distinct from the model name color.
119
+ let content = theme.fg("statusLineModel", withIcon(theme.icon.model, modelName));
120
+ if (ctx.session.isAdvisorActive()) {
121
+ content += theme.fg("success", "++");
122
+ }
123
+ if (tail) {
124
+ content += theme.fg("statusLineModel", tail);
125
+ }
126
+
127
+ return { content, visible: true };
115
128
  },
116
129
  };
117
130
 
@@ -84,9 +84,9 @@ export class CommandController {
84
84
  }
85
85
  }
86
86
 
87
- handleDumpCommand() {
87
+ handleDumpCommand(isRaw = false) {
88
88
  try {
89
- const formatted = this.ctx.session.formatSessionAsText();
89
+ const formatted = this.ctx.session.formatSessionAsText({ compact: !isRaw });
90
90
  if (!formatted) {
91
91
  this.ctx.showError("No messages to dump yet.");
92
92
  return;
@@ -98,6 +98,26 @@ export class CommandController {
98
98
  }
99
99
  }
100
100
 
101
+ handleAdvisorDumpCommand(isRaw = false) {
102
+ try {
103
+ const advisorHistory = this.ctx.session.formatAdvisorHistoryAsText({ compact: !isRaw });
104
+ if (advisorHistory === null) {
105
+ this.ctx.showError("Advisor is not active for this session.");
106
+ return;
107
+ }
108
+ if (!advisorHistory) {
109
+ this.ctx.showError("Advisor has no history yet.");
110
+ return;
111
+ }
112
+ copyToClipboard(advisorHistory);
113
+ this.ctx.showStatus("Advisor history copied to clipboard");
114
+ } catch (error: unknown) {
115
+ this.ctx.showError(
116
+ `Failed to copy advisor history: ${error instanceof Error ? error.message : "Unknown error"}`,
117
+ );
118
+ }
119
+ }
120
+
101
121
  async handleDebugTranscriptCommand(): Promise<void> {
102
122
  try {
103
123
  const width = Math.max(1, this.ctx.ui.terminal.columns);
@@ -305,6 +325,53 @@ export class CommandController {
305
325
  this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
306
326
  }
307
327
 
328
+ async handleAdvisorStatusCommand(): Promise<void> {
329
+ const stats = this.ctx.session.getAdvisorStats();
330
+ if (!stats.active) {
331
+ this.ctx.present([
332
+ new Spacer(1),
333
+ new Text(
334
+ stats.configured
335
+ ? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
336
+ : "Advisor is disabled.",
337
+ 1,
338
+ 0,
339
+ ),
340
+ ]);
341
+ return;
342
+ }
343
+ const model = stats.model!;
344
+ let info = `${theme.bold("Advisor Status")}\n\n`;
345
+ info += `${theme.bold("Provider")}\n`;
346
+ info += `${theme.fg("dim", "Model:")} ${model.provider}/${model.id}\n`;
347
+ info += `\n${theme.bold("Messages")}\n`;
348
+ info += `${theme.fg("dim", "User:")} ${stats.messages.user.toLocaleString()}\n`;
349
+ info += `${theme.fg("dim", "Assistant:")} ${stats.messages.assistant.toLocaleString()}\n`;
350
+ info += `${theme.fg("dim", "Total:")} ${stats.messages.total.toLocaleString()}\n`;
351
+ info += `\n${theme.bold("Context")}\n`;
352
+ if (stats.contextWindow > 0) {
353
+ const percent = Math.round((stats.contextTokens / stats.contextWindow) * 100);
354
+ info += `${theme.fg("dim", "Tokens:")} ${stats.contextTokens.toLocaleString()} / ${stats.contextWindow.toLocaleString()} (${percent}%)\n`;
355
+ } else {
356
+ info += `${theme.fg("dim", "Tokens:")} ${stats.contextTokens.toLocaleString()}\n`;
357
+ }
358
+ info += `\n${theme.bold("Spend")}\n`;
359
+ info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
360
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
361
+ if (stats.tokens.cacheRead > 0) {
362
+ info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
363
+ }
364
+ if (stats.tokens.cacheWrite > 0) {
365
+ info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
366
+ }
367
+ info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
368
+ if (stats.cost > 0) {
369
+ info += `\n${theme.bold("Cost")}\n`;
370
+ info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(4)}\n`;
371
+ }
372
+ this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
373
+ }
374
+
308
375
  async handleJobsCommand(): Promise<void> {
309
376
  const snapshot = this.ctx.session.getAsyncJobSnapshot({ recentLimit: 5 });
310
377
  if (!snapshot) {
@@ -542,6 +542,7 @@ export class InteractiveMode implements InteractiveModeContext {
542
542
  if (eventBus) {
543
543
  this.#eventBusUnsubscribers.push(
544
544
  eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
545
+ if (this.settings.get("startup.quiet")) return;
545
546
  this.#handleLspStartupEvent(data as LspStartupEvent);
546
547
  }),
547
548
  );
@@ -551,6 +552,7 @@ export class InteractiveMode implements InteractiveModeContext {
551
552
  logger.warn("Ignoring malformed mcp:connecting event", { data });
552
553
  return;
553
554
  }
555
+ if (this.settings.get("startup.quiet")) return;
554
556
  this.showStatus(formatMCPConnectingMessage(data.serverNames));
555
557
  }),
556
558
  );
@@ -3333,8 +3335,12 @@ export class InteractiveMode implements InteractiveModeContext {
3333
3335
  return this.#commandController.handleExportCommand(text);
3334
3336
  }
3335
3337
 
3336
- handleDumpCommand() {
3337
- return this.#commandController.handleDumpCommand();
3338
+ handleDumpCommand(isRaw?: boolean) {
3339
+ return this.#commandController.handleDumpCommand(isRaw);
3340
+ }
3341
+
3342
+ handleAdvisorDumpCommand(isRaw?: boolean) {
3343
+ return this.#commandController.handleAdvisorDumpCommand(isRaw);
3338
3344
  }
3339
3345
 
3340
3346
  handleDebugTranscriptCommand(): Promise<void> {
@@ -3353,6 +3359,10 @@ export class InteractiveMode implements InteractiveModeContext {
3353
3359
  return this.#commandController.handleSessionCommand();
3354
3360
  }
3355
3361
 
3362
+ handleAdvisorStatusCommand(): Promise<void> {
3363
+ return this.#commandController.handleAdvisorStatusCommand();
3364
+ }
3365
+
3356
3366
  handleJobsCommand(): Promise<void> {
3357
3367
  return this.#commandController.handleJobsCommand();
3358
3368
  }
@@ -270,13 +270,15 @@ export interface InteractiveModeContext {
270
270
  handleShareCommand(): Promise<void>;
271
271
  handleTodoCommand(args: string): Promise<void>;
272
272
  handleSessionCommand(): Promise<void>;
273
+ handleAdvisorStatusCommand(): Promise<void>;
273
274
  handleJobsCommand(): Promise<void>;
274
275
  handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
275
276
  handleChangelogCommand(showFull?: boolean): Promise<void>;
276
277
  handleHotkeysCommand(): void;
277
278
  handleToolsCommand(): void;
278
279
  handleContextCommand(): void;
279
- handleDumpCommand(): void;
280
+ handleDumpCommand(isRaw?: boolean): void;
281
+ handleAdvisorDumpCommand(isRaw?: boolean): void;
280
282
  handleDebugTranscriptCommand(): Promise<void>;
281
283
  handleClearCommand(): Promise<void>;
282
284
  handleFreshCommand(): Promise<void>;
@@ -1,9 +1,11 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { AssistantMessage, ImageContent, Message, Usage } from "@oh-my-pi/pi-ai";
3
3
  import { type Component, Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
4
+ import type { AdvisorMessageDetails } from "../../advisor";
4
5
  import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
5
6
  import { settings } from "../../config/settings";
6
7
  import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
8
+ import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
7
9
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
8
10
  import { createBackgroundTanDispatchBlock } from "../../modes/components/background-tan-message";
9
11
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
@@ -240,6 +242,13 @@ export class UiHelpers {
240
242
  this.ctx.chatContainer.addChild(card);
241
243
  return [card];
242
244
  }
245
+ if (message.customType === "advisor") {
246
+ const details = (message as CustomMessage<AdvisorMessageDetails>).details;
247
+ this.ctx.chatContainer.addChild(
248
+ createAdvisorMessageCard(details, () => this.ctx.toolOutputExpanded, theme),
249
+ );
250
+ break;
251
+ }
243
252
  if (message.customType === BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE) {
244
253
  this.ctx.chatContainer.addChild(createBackgroundTanDispatchBlock(message as CustomMessage<unknown>));
245
254
  break;
@@ -0,0 +1 @@
1
+ Send one concrete, terse piece of advice to the agent you are watching. Use sparingly; stay silent when nothing matters.
@@ -0,0 +1,31 @@
1
+ <system-conventions>
2
+ RFC 2119 applies to MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` and `AVOID` are aliases for `MUST NOT` and `SHOULD NOT`.
3
+ You can explore the workspace; budget is 2–3 tool calls per advise (exception: critical bugs warrant deeper verification before raising a blocker).
4
+ </system-conventions>
5
+
6
+ You bring a different angle.
7
+ The agent might not have thought about an edge case, spotted a hallucinated API, or realized a simpler approach exists.
8
+ Your job is to offer that view before they sink work into the wrong direction.
9
+
10
+ <workflow>
11
+ You receive the agent's transcript incrementally, including private thinking.
12
+ You have read-only access through `read`, `search`, `find` to verify your suspicions.
13
+ Keep exploration lean — 2–3 calls per advise unless you've spotted a critical bug and need to be absolutely certain before raising a blocker.
14
+ </workflow>
15
+
16
+ <communication>
17
+ At most one `advise` per update. Prefer silence when the agent is on track. Address the agent directly. Offer alternatives, not lectures. Never restate what they know; never explain how to use the advisor.
18
+ </communication>
19
+
20
+ <critical>
21
+ You SHOULD call `advise` when: agent might be heading the wrong way, missed an edge case, about to call a hallucinated API, going in circles, picking brittle approach over better one. Low confidence bar — "this might be wrong" is worth noting if they didn't think about it.
22
+ NEVER advise just to second-guess decisions the agent understands and is committed to, if you are not certain.
23
+ </critical>
24
+
25
+ <completeness>
26
+ **`nit`** — Non-urgent cleanup, refactor, style, missed opportunity. Folded at next step boundary; agent keeps working. Examples: edge cases that don't break correctness, simplifications, better approach the agent can consider.
27
+ **`concern`** — Agent might be heading wrong or missed something material. Offers your view; agent decides. Use when: exploring wrong code path, picking fragile approach when better exists, missing constraint, hallucinated API, going in circles, edge case about to be baked in.
28
+ **`blocker`** — Stop and reconsider. Use ONLY when: continuing will clearly waste the turn, produce broken output, or the path is fundamentally unsound. Verify thoroughly before raising.
29
+ </completeness>
30
+
31
+ You MAY suggest an approach or fix if you've explored enough to be confident. Your job is pair programming, not just bugs — offer the better designs, not just the warning.