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

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 (93) hide show
  1. package/CHANGELOG.md +155 -133
  2. package/dist/cli.js +621 -530
  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 +66 -5
  10. package/dist/types/discovery/helpers.d.ts +7 -0
  11. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
  12. package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
  13. package/dist/types/modes/components/advisor-message.d.ts +9 -0
  14. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  15. package/dist/types/modes/controllers/command-controller.d.ts +3 -1
  16. package/dist/types/modes/interactive-mode.d.ts +3 -1
  17. package/dist/types/modes/types.d.ts +8 -1
  18. package/dist/types/sdk.d.ts +3 -3
  19. package/dist/types/session/agent-session.d.ts +81 -2
  20. package/dist/types/session/session-history-format.d.ts +4 -0
  21. package/dist/types/session/session-manager.d.ts +4 -1
  22. package/dist/types/session/yield-queue.d.ts +2 -0
  23. package/dist/types/task/index.d.ts +21 -0
  24. package/dist/types/tools/github-cache.d.ts +5 -4
  25. package/dist/types/tools/job.d.ts +1 -0
  26. package/dist/types/tools/path-utils.d.ts +1 -0
  27. package/dist/types/tools/report-tool-issue.d.ts +0 -1
  28. package/dist/types/web/search/index.d.ts +2 -2
  29. package/dist/types/web/search/provider.d.ts +2 -0
  30. package/package.json +13 -13
  31. package/src/advisor/__tests__/advisor.test.ts +586 -0
  32. package/src/advisor/advise-tool.ts +87 -0
  33. package/src/advisor/index.ts +3 -0
  34. package/src/advisor/runtime.ts +248 -0
  35. package/src/advisor/watchdog.ts +83 -0
  36. package/src/cli/args.ts +1 -0
  37. package/src/collab/host.ts +1 -1
  38. package/src/config/model-roles.ts +13 -1
  39. package/src/config/settings-schema.ts +65 -6
  40. package/src/discovery/claude-plugins.ts +3 -42
  41. package/src/discovery/github.ts +101 -6
  42. package/src/discovery/helpers.ts +11 -0
  43. package/src/eval/__tests__/prelude-agent.test.ts +73 -0
  44. package/src/eval/js/shared/prelude.txt +12 -3
  45. package/src/eval/py/prelude.py +26 -2
  46. package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
  47. package/src/extensibility/plugins/loader.ts +3 -2
  48. package/src/extensibility/plugins/manager.ts +4 -3
  49. package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
  50. package/src/extensibility/plugins/runtime-config.ts +9 -0
  51. package/src/internal-urls/docs-index.generated.ts +10 -9
  52. package/src/internal-urls/issue-pr-protocol.ts +8 -4
  53. package/src/main.ts +9 -1
  54. package/src/modes/acp/acp-agent.ts +3 -3
  55. package/src/modes/components/advisor-message.ts +99 -0
  56. package/src/modes/components/agent-hub.ts +7 -0
  57. package/src/modes/components/assistant-message.ts +86 -0
  58. package/src/modes/components/settings-defs.ts +7 -0
  59. package/src/modes/components/status-line/segments.ts +20 -7
  60. package/src/modes/components/tips.txt +1 -1
  61. package/src/modes/controllers/command-controller.ts +69 -2
  62. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  63. package/src/modes/controllers/input-controller.ts +1 -0
  64. package/src/modes/controllers/selector-controller.ts +7 -0
  65. package/src/modes/interactive-mode.ts +59 -2
  66. package/src/modes/rpc/rpc-mode.ts +3 -3
  67. package/src/modes/runtime-init.ts +2 -1
  68. package/src/modes/types.ts +8 -1
  69. package/src/modes/utils/ui-helpers.ts +9 -0
  70. package/src/prompts/advisor/advise-tool.md +1 -0
  71. package/src/prompts/advisor/system.md +31 -0
  72. package/src/prompts/agents/designer.md +8 -0
  73. package/src/prompts/review-request.md +1 -1
  74. package/src/prompts/system/subagent-system-prompt.md +4 -1
  75. package/src/prompts/tools/eval.md +13 -3
  76. package/src/prompts/tools/irc.md +1 -1
  77. package/src/sdk.ts +61 -14
  78. package/src/session/agent-session.ts +667 -13
  79. package/src/session/session-dump-format.ts +15 -131
  80. package/src/session/session-history-format.ts +30 -11
  81. package/src/session/session-manager.ts +3 -1
  82. package/src/session/yield-queue.ts +5 -1
  83. package/src/slash-commands/builtin-registry.ts +105 -4
  84. package/src/system-prompt.ts +1 -1
  85. package/src/task/executor.ts +5 -4
  86. package/src/task/index.ts +70 -9
  87. package/src/tools/github-cache.ts +32 -7
  88. package/src/tools/job.ts +14 -1
  89. package/src/tools/path-utils.ts +33 -2
  90. package/src/tools/report-tool-issue.ts +2 -7
  91. package/src/web/scrapers/docs-rs.ts +2 -3
  92. package/src/web/search/index.ts +2 -2
  93. package/src/web/search/provider.ts +14 -2
@@ -28,7 +28,7 @@ import {
28
28
  parsePositiveDecimalInt,
29
29
  resolveDefaultRepoMemoized,
30
30
  } from "../tools/gh";
31
- import { formatFreshnessNote } from "../tools/github-cache";
31
+ import { type CacheStatus, formatFreshnessNote } from "../tools/github-cache";
32
32
  import * as git from "../utils/git";
33
33
  import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext } from "./types";
34
34
 
@@ -355,7 +355,7 @@ interface BuildSingleArgs {
355
355
  scheme: Scheme;
356
356
  parsed: ParsedSingle;
357
357
  rendered: string;
358
- status: "miss" | "fresh" | "stale" | "disabled";
358
+ status: CacheStatus;
359
359
  fetchedAt: number;
360
360
  /** Resolved repo (post short-form expansion) — used for the PR-only diff hint. */
361
361
  repo?: string;
@@ -377,11 +377,15 @@ function buildSingleResource({
377
377
  const diffUrl = repoSegment ? `pr://${repoSegment}/${parsed.number}/diff` : `pr://${parsed.number}/diff`;
378
378
  notes.push(`Diff: ${diffUrl}`);
379
379
  }
380
+ const content =
381
+ status === "stale"
382
+ ? `> WARNING: Live GitHub refresh failed; this ${scheme} content is cached and may be stale.\n\n${rendered}`
383
+ : rendered;
380
384
  return {
381
385
  url: url.href,
382
- content: rendered,
386
+ content,
383
387
  contentType: "text/markdown",
384
- size: Buffer.byteLength(rendered, "utf-8"),
388
+ size: Buffer.byteLength(content, "utf-8"),
385
389
  notes,
386
390
  };
387
391
  }
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[] = [
@@ -307,7 +311,11 @@ export async function submitInteractiveInput(
307
311
  // developer directive to a visible user message. A synthetic submit while
308
312
  // streaming keeps its prior behavior (rejected as busy) rather than changing
309
313
  // its role.
310
- await session.prompt(input.text, { synthetic: true, expandPromptTemplates: false });
314
+ await session.prompt(input.text, {
315
+ synthetic: true,
316
+ expandPromptTemplates: false,
317
+ userInitiated: input.userInitiated,
318
+ });
311
319
  } else {
312
320
  await session.prompt(input.text, { images: input.images, streamingBehavior });
313
321
  }
@@ -62,7 +62,7 @@ import { loadAllExtensions } from "../../modes/components/extensions/state-manag
62
62
  import { theme } from "../../modes/theme/theme";
63
63
  import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
64
64
  import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
65
- import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
65
+ import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
66
66
  import type { UsageStatistics } from "../../session/session-entries";
67
67
  import type { SessionInfo as StoredSessionInfo } from "../../session/session-listing";
68
68
  import { SessionManager } from "../../session/session-manager";
@@ -836,7 +836,7 @@ export class AcpAgent implements Agent {
836
836
  timer = setTimeout(() => reject(new Error("ACP cancel cleanup timed out")), this.#cancelCleanupTimeoutMs);
837
837
  });
838
838
  try {
839
- await Promise.race([record.session.abort(), timeout]);
839
+ await Promise.race([record.session.abort({ reason: USER_INTERRUPT_LABEL }), timeout]);
840
840
  } finally {
841
841
  if (timer) clearTimeout(timer);
842
842
  // Order matters: clear `cleanup` before evicting the slot so the slot-eviction
@@ -2098,7 +2098,7 @@ export class AcpAgent implements Agent {
2098
2098
  getModel: () => record.session.model,
2099
2099
  isIdle: () => !record.session.isStreaming,
2100
2100
  abort: () => {
2101
- void record.session.abort();
2101
+ void record.session.abort({ reason: USER_INTERRUPT_LABEL });
2102
2102
  },
2103
2103
  hasPendingMessages: () => record.session.queuedMessageCount > 0,
2104
2104
  shutdown: () => {},
@@ -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)
@@ -104,6 +104,13 @@ const CONDITIONS: Record<string, () => boolean> = {
104
104
  return false;
105
105
  }
106
106
  },
107
+ planModeEnabled: () => {
108
+ try {
109
+ return Settings.instance.get("plan.enabled");
110
+ } catch {
111
+ return false;
112
+ }
113
+ },
107
114
  };
108
115
 
109
116
  // ═══════════════════════════════════════════════════════════════════════════
@@ -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
 
@@ -6,7 +6,7 @@ Find out which model you emotionally abuse the most with `omp stats`
6
6
  Try task isolation to create CoW worktrees
7
7
  Need a cheap nested model call? Use `completion(x...)`. Have a big batch of tasks? Ask clanker to use it!
8
8
  Spaghetti code? Try complaining with /omfg
9
- Did you know? Each kitty/tmux/cmux split keeps its own session — `omp -c` resumes the right one
9
+ Did you know? Each kitty/tmux/cmux/zellij/wezterm split keeps its own session — `omp -c` resumes the right one
10
10
  Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
11
11
  Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
12
12
  Say `workflowz` in your message to drive the task with parallel subagents in eval — watch it glow as you type
@@ -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) {
@@ -23,6 +23,7 @@ import { HookInputComponent } from "../../modes/components/hook-input";
23
23
  import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
24
24
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
25
25
  import type { InteractiveModeContext, InteractiveSelectorDialogOptions } from "../../modes/types";
26
+ import { USER_INTERRUPT_LABEL } from "../../session/messages";
26
27
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
27
28
 
28
29
  const MAX_WIDGET_LINES = 10;
@@ -123,7 +124,7 @@ export class ExtensionUiController {
123
124
  const contextActions: ExtensionContextActions = {
124
125
  getModel: () => this.ctx.session.model,
125
126
  isIdle: () => !this.ctx.session.isStreaming,
126
- abort: () => this.ctx.session.abort(),
127
+ abort: () => this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL }),
127
128
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
128
129
  shutdown: () => {
129
130
  // Defer the actual teardown to the main loop, which calls
@@ -359,7 +360,7 @@ export class ExtensionUiController {
359
360
  const contextActions: ExtensionContextActions = {
360
361
  getModel: () => this.ctx.session.model,
361
362
  isIdle: () => !this.ctx.session.isStreaming,
362
- abort: () => this.ctx.session.abort(),
363
+ abort: () => this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL }),
363
364
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
364
365
  shutdown: () => {
365
366
  // Defer the actual teardown to the main loop, which calls
@@ -500,7 +501,7 @@ export class ExtensionUiController {
500
501
  isIdle: () => !this.ctx.session.isStreaming,
501
502
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
502
503
  abort: () => {
503
- this.ctx.session.abort();
504
+ this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
504
505
  },
505
506
  shutdown: () => {
506
507
  // Signal shutdown request
@@ -483,6 +483,7 @@ export class InputController {
483
483
  cancelled: false,
484
484
  started: true,
485
485
  synthetic: true,
486
+ userInitiated: true,
486
487
  });
487
488
  }
488
489
  return;
@@ -40,7 +40,9 @@ import {
40
40
  import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
41
41
  import {
42
42
  isImageProviderPreference,
43
+ isSearchProviderId,
43
44
  isSearchProviderPreference,
45
+ setExcludedSearchProviders,
44
46
  setPreferredImageProvider,
45
47
  setPreferredSearchProvider,
46
48
  } from "../../tools";
@@ -419,6 +421,11 @@ export class SelectorController {
419
421
  setPreferredSearchProvider(value);
420
422
  }
421
423
  break;
424
+ case "providers.webSearchExclude":
425
+ if (Array.isArray(value)) {
426
+ setExcludedSearchProviders(value.filter(isSearchProviderId));
427
+ }
428
+ break;
422
429
  case "providers.image":
423
430
  if (isImageProviderPreference(value)) {
424
431
  setPreferredImageProvider(value);