@oh-my-pi/pi-coding-agent 15.8.2 → 15.9.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 (116) hide show
  1. package/CHANGELOG.md +82 -29
  2. package/dist/types/capability/skill.d.ts +7 -0
  3. package/dist/types/config/settings-schema.d.ts +36 -22
  4. package/dist/types/debug/protocol-probe.d.ts +38 -0
  5. package/dist/types/debug/terminal-info.d.ts +34 -0
  6. package/dist/types/export/html/template.generated.d.ts +1 -1
  7. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  8. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -1
  10. package/dist/types/index.d.ts +1 -0
  11. package/dist/types/mcp/transports/stdio.d.ts +16 -7
  12. package/dist/types/modes/components/assistant-message.d.ts +3 -2
  13. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  14. package/dist/types/modes/components/todo-reminder.d.ts +1 -1
  15. package/dist/types/modes/components/transcript-container.d.ts +36 -0
  16. package/dist/types/modes/interactive-mode.d.ts +2 -1
  17. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  18. package/dist/types/modes/theme/theme.d.ts +20 -1
  19. package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
  20. package/dist/types/plan-mode/plan-protection.d.ts +12 -0
  21. package/dist/types/session/agent-session.d.ts +1 -1
  22. package/dist/types/session/indexed-session-storage.d.ts +59 -0
  23. package/dist/types/session/redis-session-storage.d.ts +12 -85
  24. package/dist/types/session/session-manager.d.ts +20 -0
  25. package/dist/types/session/session-storage.d.ts +5 -7
  26. package/dist/types/session/sql-session-storage.d.ts +16 -85
  27. package/dist/types/task/executor.d.ts +9 -0
  28. package/dist/types/task/index.d.ts +3 -1
  29. package/dist/types/telemetry-export.d.ts +19 -0
  30. package/dist/types/tiny/compiled-runtime.d.ts +35 -0
  31. package/dist/types/tools/ask.d.ts +1 -0
  32. package/dist/types/tools/index.d.ts +4 -2
  33. package/dist/types/tools/path-utils.d.ts +1 -1
  34. package/dist/types/tools/search.d.ts +2 -2
  35. package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
  36. package/dist/types/utils/jj.d.ts +1 -1
  37. package/dist/types/utils/session-color.d.ts +7 -2
  38. package/dist/types/web/search/index.d.ts +1 -1
  39. package/dist/types/web/search/provider.d.ts +2 -2
  40. package/dist/types/web/search/types.d.ts +65 -1
  41. package/package.json +15 -9
  42. package/scripts/build-binary.ts +12 -0
  43. package/src/capability/skill.ts +7 -0
  44. package/src/cli/args.ts +1 -1
  45. package/src/config/settings-schema.ts +22 -55
  46. package/src/debug/index.ts +67 -1
  47. package/src/debug/protocol-probe.ts +267 -0
  48. package/src/debug/terminal-info.ts +127 -0
  49. package/src/export/html/template.generated.ts +1 -1
  50. package/src/export/html/template.js +3 -3
  51. package/src/extensibility/custom-tools/types.ts +1 -1
  52. package/src/extensibility/extensions/types.ts +11 -0
  53. package/src/extensibility/shared-events.ts +1 -1
  54. package/src/extensibility/skills.ts +3 -3
  55. package/src/index.ts +1 -0
  56. package/src/internal-urls/docs-index.generated.ts +7 -7
  57. package/src/main.ts +12 -0
  58. package/src/mcp/transports/stdio.ts +37 -12
  59. package/src/modes/acp/acp-event-mapper.ts +7 -7
  60. package/src/modes/components/assistant-message.ts +3 -2
  61. package/src/modes/components/hook-selector.ts +149 -14
  62. package/src/modes/components/session-selector.ts +36 -5
  63. package/src/modes/components/status-line/segments.ts +2 -1
  64. package/src/modes/components/status-line.ts +1 -1
  65. package/src/modes/components/tips.txt +1 -1
  66. package/src/modes/components/todo-reminder.ts +1 -1
  67. package/src/modes/components/tool-execution.ts +9 -4
  68. package/src/modes/components/transcript-container.ts +91 -0
  69. package/src/modes/controllers/event-controller.ts +12 -7
  70. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  71. package/src/modes/controllers/todo-command-controller.ts +1 -1
  72. package/src/modes/interactive-mode.ts +18 -6
  73. package/src/modes/print-mode.ts +5 -0
  74. package/src/modes/rpc/rpc-types.ts +1 -1
  75. package/src/modes/theme/theme.ts +48 -8
  76. package/src/modes/utils/ui-helpers.ts +1 -0
  77. package/src/plan-mode/plan-handoff.ts +37 -0
  78. package/src/plan-mode/plan-protection.ts +31 -0
  79. package/src/priority.json +4 -0
  80. package/src/prompts/goals/goal-continuation.md +1 -1
  81. package/src/prompts/system/eager-todo.md +3 -3
  82. package/src/prompts/system/orchestrate-notice.md +5 -5
  83. package/src/prompts/system/plan-mode-approved.md +14 -17
  84. package/src/prompts/system/plan-mode-reference.md +3 -6
  85. package/src/prompts/system/subagent-system-prompt.md +11 -0
  86. package/src/prompts/system/workflow-notice.md +1 -1
  87. package/src/prompts/tools/browser.md +5 -2
  88. package/src/prompts/tools/search.md +1 -1
  89. package/src/prompts/tools/task.md +3 -1
  90. package/src/prompts/tools/{todo-write.md → todo.md} +8 -0
  91. package/src/sdk.ts +1 -0
  92. package/src/session/agent-session.ts +29 -16
  93. package/src/session/indexed-session-storage.ts +430 -0
  94. package/src/session/redis-session-storage.ts +66 -377
  95. package/src/session/session-manager.ts +102 -22
  96. package/src/session/session-storage.ts +148 -68
  97. package/src/session/sql-session-storage.ts +131 -382
  98. package/src/slash-commands/helpers/todo.ts +2 -2
  99. package/src/task/executor.ts +9 -1
  100. package/src/task/index.ts +51 -1
  101. package/src/telemetry-export.ts +126 -0
  102. package/src/tiny/compiled-runtime.ts +179 -0
  103. package/src/tiny/worker.ts +24 -2
  104. package/src/tools/ask.ts +133 -87
  105. package/src/tools/fetch.ts +17 -4
  106. package/src/tools/find.ts +2 -2
  107. package/src/tools/index.ts +6 -4
  108. package/src/tools/path-utils.ts +16 -7
  109. package/src/tools/renderers.ts +2 -2
  110. package/src/tools/search.ts +6 -3
  111. package/src/tools/{todo-write.ts → todo.ts} +32 -35
  112. package/src/utils/jj.ts +28 -5
  113. package/src/utils/session-color.ts +39 -14
  114. package/src/web/search/index.ts +1 -1
  115. package/src/web/search/provider.ts +18 -34
  116. package/src/web/search/types.ts +73 -32
@@ -0,0 +1,91 @@
1
+ import { type Component, Container, TERMINAL } from "@oh-my-pi/pi-tui";
2
+
3
+ const kSnapshot = Symbol("transcript.frozenRender");
4
+
5
+ interface FrozenRender {
6
+ width: number;
7
+ lines: string[];
8
+ generation: number;
9
+ }
10
+
11
+ interface SnapshotCarrier {
12
+ [kSnapshot]?: FrozenRender;
13
+ }
14
+
15
+ /**
16
+ * Transcript container that freezes the rendered output of every block except
17
+ * the bottom-most (live) one on terminals where committed native scrollback is
18
+ * immutable.
19
+ *
20
+ * On ED3-risk terminals with an unobservable viewport (ghostty/kitty/iTerm2/…)
21
+ * the renderer cannot clear saved lines (`\x1b[3J` may yank a reader) or query
22
+ * whether the user has scrolled, so any block that re-lays-out *after* it has
23
+ * scrolled past the viewport leaves a stale duplicate above the live region
24
+ * (a finalized assistant message re-wrapping, a tool preview collapsing to its
25
+ * compact result, a late async tool completion). The renderer's only safe move
26
+ * for such an offscreen edit is to not repaint — which is correct only if the
27
+ * committed region never changes underneath it.
28
+ *
29
+ * This container provides that guarantee: a block's render is snapshotted while
30
+ * it is the live (bottom-most) block, and once a newer block is appended it
31
+ * replays the snapshot instead of recomputing. Mutations after a block leaves
32
+ * live are intentionally deferred until the next checkpoint {@link thaw} (prompt
33
+ * submit → native-scrollback rebuild), where the whole transcript is replayed
34
+ * and any drift reconciles safely. On terminals that can rebuild history this
35
+ * freezing is unnecessary, so it renders every block live for full fidelity.
36
+ */
37
+ export class TranscriptContainer extends Container {
38
+ // Bumped to invalidate every block's snapshot at once; a snapshot is only
39
+ // honored when its stored generation still matches.
40
+ #generation = 0;
41
+
42
+ override invalidate(): void {
43
+ // A theme/global invalidation forces a full recompute on the rebuild that
44
+ // follows; retire every snapshot.
45
+ this.#generation++;
46
+ super.invalidate();
47
+ }
48
+
49
+ override clear(): void {
50
+ this.#generation++;
51
+ super.clear();
52
+ }
53
+
54
+ /**
55
+ * Retire all frozen snapshots so the next render reflects each block's current
56
+ * state. Call at reconciliation checkpoints (prompt submit) where the whole
57
+ * transcript is replayed into native scrollback and any drift a frozen block
58
+ * accumulated is reconciled.
59
+ */
60
+ thaw(): void {
61
+ this.#generation++;
62
+ }
63
+
64
+ override render(width: number): string[] {
65
+ width = Math.max(1, width);
66
+ if (!TERMINAL.eagerEraseScrollbackRisk) return super.render(width);
67
+
68
+ const lines: string[] = [];
69
+ const liveIndex = this.children.length - 1;
70
+ for (let i = 0; i < this.children.length; i++) {
71
+ const child = this.children[i]! as Component & SnapshotCarrier;
72
+ if (i !== liveIndex) {
73
+ const snapshot = child[kSnapshot];
74
+ // Replay the block's last render from while it was live. A stale
75
+ // generation (post-thaw) or width mismatch (resize in flight, an
76
+ // explicit rebuild that reconciles history anyway) recomputes instead.
77
+ if (snapshot && snapshot.generation === this.#generation && snapshot.width === width) {
78
+ lines.push(...snapshot.lines);
79
+ continue;
80
+ }
81
+ }
82
+ const rendered = child.render(width);
83
+ // Cache every block's latest render. While a block is live this keeps its
84
+ // snapshot current; the frame it stops being live the cache already holds
85
+ // its final live render, so nothing recomputes underneath it.
86
+ child[kSnapshot] = { width, lines: rendered, generation: this.#generation };
87
+ lines.push(...rendered);
88
+ }
89
+ return lines;
90
+ }
91
+ }
@@ -282,6 +282,7 @@ export class EventController {
282
282
  this.ctx.hideThinkingBlock,
283
283
  () => this.ctx.ui.requestRender(),
284
284
  this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
285
+ this.ctx.ui.imageBudget,
285
286
  );
286
287
  this.ctx.streamingMessage = event.message;
287
288
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
@@ -586,18 +587,18 @@ export class EventController {
586
587
  this.ctx.ui.requestRender();
587
588
  }
588
589
  }
589
- // Update todo display when todo_write tool completes
590
- if (event.toolName === "todo_write" && !event.isError) {
590
+ // Update todo display when todo tool completes
591
+ if (event.toolName === "todo" && !event.isError) {
591
592
  const details = event.result.details as { phases?: TodoPhase[] } | undefined;
592
593
  if (details?.phases) {
593
594
  this.ctx.setTodos(details.phases);
594
595
  }
595
- } else if (event.toolName === "todo_write" && event.isError) {
596
+ } else if (event.toolName === "todo" && event.isError) {
596
597
  const textContent = event.result.content.find(
597
598
  (content: { type: string; text?: string }) => content.type === "text",
598
599
  )?.text;
599
600
  this.ctx.showWarning(
600
- `Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until todo_write succeeds."}`,
601
+ `Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until todo succeeds."}`,
601
602
  );
602
603
  }
603
604
  if (event.toolName === "resolve" && !event.isError) {
@@ -847,9 +848,13 @@ export class EventController {
847
848
  const last = this.ctx.session.getLastAssistantMessage?.();
848
849
  if (last?.stopReason === "aborted" || last?.stopReason === "error") return;
849
850
 
850
- const title = this.ctx.sessionManager.getSessionName();
851
- const message = title ? `${title}: Complete` : "Complete";
852
- TERMINAL.sendNotification(message);
851
+ const sessionName = this.ctx.sessionManager.getSessionName();
852
+ TERMINAL.sendNotification({
853
+ title: sessionName || "Oh My Pi",
854
+ body: "Complete",
855
+ type: "completion",
856
+ actions: "focus",
857
+ });
853
858
  }
854
859
 
855
860
  async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
@@ -625,6 +625,9 @@ export class ExtensionUiController {
625
625
  tui: this.ctx.ui,
626
626
  outline: dialogOptions?.outline,
627
627
  disabledIndices: dialogOptions?.disabledIndices,
628
+ selectionMarker: dialogOptions?.selectionMarker,
629
+ checkedIndices: dialogOptions?.checkedIndices,
630
+ markableCount: dialogOptions?.markableCount,
628
631
  maxVisible,
629
632
  slider: extra?.slider,
630
633
  },
@@ -8,7 +8,7 @@ import {
8
8
  type TodoItem,
9
9
  type TodoPhase,
10
10
  USER_TODO_EDIT_CUSTOM_TYPE,
11
- } from "../../tools/todo-write";
11
+ } from "../../tools/todo";
12
12
  import { copyToClipboard } from "../../utils/clipboard";
13
13
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
14
14
  import type { InteractiveModeContext } from "../types";
@@ -28,6 +28,8 @@ import {
28
28
  Markdown,
29
29
  ProcessTerminal,
30
30
  Spacer,
31
+ setTerminalTextSizing,
32
+ TERMINAL,
31
33
  Text,
32
34
  TUI,
33
35
  visibleWidth,
@@ -84,7 +86,7 @@ import type { LspStartupServerInfo } from "../tools";
84
86
  import { normalizeLocalScheme } from "../tools/path-utils";
85
87
  import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
86
88
  import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
87
- import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo-write";
89
+ import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo";
88
90
  import { ToolError } from "../tools/tool-errors";
89
91
  import type { EventBus } from "../utils/event-bus";
90
92
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
@@ -100,6 +102,7 @@ import type { HookInputComponent } from "./components/hook-input";
100
102
  import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
101
103
  import { StatusLineComponent } from "./components/status-line";
102
104
  import type { ToolExecutionHandle } from "./components/tool-execution";
105
+ import { TranscriptContainer } from "./components/transcript-container";
103
106
  import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
104
107
  import { BtwController } from "./controllers/btw-controller";
105
108
  import { CommandController } from "./controllers/command-controller";
@@ -255,7 +258,7 @@ export class InteractiveMode implements InteractiveModeContext {
255
258
  historyStorage?: HistoryStorage;
256
259
 
257
260
  ui: TUI;
258
- chatContainer: Container;
261
+ chatContainer: TranscriptContainer;
259
262
  pendingMessagesContainer: Container;
260
263
  statusContainer: Container;
261
264
  todoContainer: Container;
@@ -390,7 +393,12 @@ export class InteractiveMode implements InteractiveModeContext {
390
393
 
391
394
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
392
395
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
393
- this.chatContainer = new Container();
396
+ this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
397
+ // OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
398
+ // capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off
399
+ // unless the user opts in, and never emits raw escapes on other terminals.
400
+ setTerminalTextSizing(settings.get("tui.textSizing") && TERMINAL.textSizing);
401
+ this.chatContainer = new TranscriptContainer();
394
402
  this.pendingMessagesContainer = new Container();
395
403
  this.statusContainer = new Container();
396
404
  this.todoContainer = new Container();
@@ -915,6 +923,10 @@ export class InteractiveMode implements InteractiveModeContext {
915
923
  this.#pendingSubmissionDispose = undefined;
916
924
  }
917
925
  this.editor.setText("");
926
+ // Reconciliation checkpoint: the rebuild below replays the whole transcript
927
+ // into native scrollback, so retire frozen block snapshots and let every
928
+ // block render its current state.
929
+ this.chatContainer.thaw();
918
930
  this.ui.refreshNativeScrollbackIfDirty({ allowUnknownViewport: true });
919
931
  this.ensureLoadingAnimation();
920
932
  this.ui.requestRender();
@@ -1001,7 +1013,7 @@ export class InteractiveMode implements InteractiveModeContext {
1001
1013
  } else {
1002
1014
  const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
1003
1015
  const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
1004
- const hex = sessionName ? getSessionAccentHex(sessionName) : undefined;
1016
+ const hex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
1005
1017
  const ansi = getSessionAccentAnsi(hex);
1006
1018
  if (ansi) {
1007
1019
  this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
@@ -1060,7 +1072,7 @@ export class InteractiveMode implements InteractiveModeContext {
1060
1072
  * Auto-complete any pending/in_progress todo whose content matches a
1061
1073
  * subagent that has finished successfully. Fires on every observer
1062
1074
  * `onChange` so the visual state stays in sync with subagent lifecycle
1063
- * without requiring the agent to issue a follow-up `todo_write`. Failed
1075
+ * without requiring the agent to issue a follow-up `todo`. Failed
1064
1076
  * and aborted subagents are intentionally NOT auto-completed — those
1065
1077
  * stay open so the user (or the next agent turn) can decide what to do.
1066
1078
  *
@@ -2495,7 +2507,7 @@ export class InteractiveMode implements InteractiveModeContext {
2495
2507
  const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
2496
2508
  const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
2497
2509
  if (!sessionName) return undefined;
2498
- const hex = getSessionAccentHex(sessionName);
2510
+ const hex = getSessionAccentHex(sessionName, theme.accentSurfaceLuminance);
2499
2511
  const main = getSessionAccentAnsi(hex);
2500
2512
  const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
2501
2513
  return main && dim ? { main, dim } : undefined;
@@ -9,6 +9,7 @@ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
9
9
  import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
10
10
  import type { AgentSession } from "../session/agent-session";
11
11
  import { isSilentAbort } from "../session/messages";
12
+ import { flushTelemetryExport } from "../telemetry-export";
12
13
  import { initializeExtensions } from "./runtime-init";
13
14
 
14
15
  /**
@@ -83,6 +84,10 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
83
84
  !isSilentAbort(assistantMsg.errorMessage)
84
85
  ) {
85
86
  const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
87
+ // Flush before this hard exit — it bypasses the awaited postmortem.quit()
88
+ // in main(), and the postmortem `exit` handler can't await, so the error
89
+ // spans would otherwise stay buffered in the batch processor and drop.
90
+ await flushTelemetryExport();
86
91
  const flushed = process.stderr.write(`${errorLine}\n`);
87
92
  if (flushed) {
88
93
  process.exit(1);
@@ -10,7 +10,7 @@ import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
10
10
  import type { BashResult } from "../../exec/bash-executor";
11
11
  import type { ContextUsage } from "../../extensibility/extensions/types";
12
12
  import type { SessionStats } from "../../session/agent-session";
13
- import type { TodoPhase } from "../../tools/todo-write";
13
+ import type { TodoPhase } from "../../tools/todo";
14
14
 
15
15
  // ============================================================================
16
16
  // RPC Commands (stdin)
@@ -10,7 +10,7 @@ import {
10
10
  supportsLanguage as nativeSupportsLanguage,
11
11
  } from "@oh-my-pi/pi-natives";
12
12
  import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
13
- import { adjustHsv, getCustomThemesDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
13
+ import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
14
14
  import chalk from "chalk";
15
15
  import * as z from "zod/v4";
16
16
  // Embed theme JSON files at build time
@@ -136,6 +136,9 @@ export type SymbolKey =
136
136
  // Checkboxes
137
137
  | "checkbox.checked"
138
138
  | "checkbox.unchecked"
139
+ // Radio (single-choice)
140
+ | "radio.selected"
141
+ | "radio.unselected"
139
142
  // Text Formatting
140
143
  | "format.bullet"
141
144
  | "format.dash"
@@ -302,6 +305,9 @@ const UNICODE_SYMBOLS: SymbolMap = {
302
305
  // Checkboxes
303
306
  "checkbox.checked": "☑",
304
307
  "checkbox.unchecked": "☐",
308
+ // Radio (single-choice)
309
+ "radio.selected": "◉",
310
+ "radio.unselected": "○",
305
311
  // Formatting
306
312
  "format.bullet": "•",
307
313
  "format.dash": "—",
@@ -559,6 +565,11 @@ const NERD_SYMBOLS: SymbolMap = {
559
565
  "checkbox.checked": "\uf14a",
560
566
  // pick:  | alt: 
561
567
  "checkbox.unchecked": "\uf096",
568
+ // Radio (single-choice)
569
+ // pick: (fa-dot-circle-o) | alt: ◉
570
+ "radio.selected": "\uf192",
571
+ // pick: (fa-circle-o) | alt: ○
572
+ "radio.unselected": "\uf10c",
562
573
  // pick:  | alt:   •
563
574
  "format.bullet": "\uf111",
564
575
  // pick: – | alt: — ― -
@@ -731,6 +742,8 @@ const ASCII_SYMBOLS: SymbolMap = {
731
742
  // Checkboxes
732
743
  "checkbox.checked": "[x]",
733
744
  "checkbox.unchecked": "[ ]",
745
+ "radio.selected": "(o)",
746
+ "radio.unselected": "( )",
734
747
  "format.bullet": "*",
735
748
  "format.dash": "-",
736
749
  "format.bracketLeft": "[",
@@ -1277,6 +1290,16 @@ export class Theme {
1277
1290
  #bgColors: Record<ThemeBg, string>;
1278
1291
  #symbols: SymbolMap;
1279
1292
  #spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>>;
1293
+ /**
1294
+ * Perceptual luma (0..1) of the status-line background — used to classify the
1295
+ * theme light/dark. Undefined when it can't be resolved. Classified against the
1296
+ * status line (the surface session accents render on) rather than the chat bubble
1297
+ * (`userMessageBg`), which some themes (e.g. `porcelain`) style dark on an
1298
+ * otherwise-light theme.
1299
+ */
1300
+ readonly statusLineLuminance: number | undefined;
1301
+ /** WCAG relative luminance of the status-line background — basis for accent contrast. */
1302
+ readonly #statusLineContrastLuminance: number | undefined;
1280
1303
 
1281
1304
  constructor(
1282
1305
  fgColors: Record<ThemeColor, string | number>,
@@ -1286,6 +1309,8 @@ export class Theme {
1286
1309
  symbolOverrides: Partial<Record<SymbolKey, string>>,
1287
1310
  spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>> = {},
1288
1311
  ) {
1312
+ this.statusLineLuminance = colorLuma(bgColors.statusLineBg);
1313
+ this.#statusLineContrastLuminance = relativeLuminance(bgColors.statusLineBg);
1289
1314
  this.#fgColors = {} as Record<ThemeColor, string>;
1290
1315
  for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
1291
1316
  this.#fgColors[key] = fgAnsi(value, mode);
@@ -1307,6 +1332,19 @@ export class Theme {
1307
1332
  this.#spinnerFramesOverrides = spinnerFramesOverrides;
1308
1333
  }
1309
1334
 
1335
+ /** True when the active theme has a light status-line background. */
1336
+ get isLight(): boolean {
1337
+ return this.statusLineLuminance !== undefined && this.statusLineLuminance > 0.5;
1338
+ }
1339
+
1340
+ /**
1341
+ * Surface luminance to size session accents against on light themes; undefined on
1342
+ * dark themes so accents stay vivid. Pass straight to `getSessionAccentHex`.
1343
+ */
1344
+ get accentSurfaceLuminance(): number | undefined {
1345
+ return this.isLight ? this.#statusLineContrastLuminance : undefined;
1346
+ }
1347
+
1310
1348
  fg(color: ThemeColor, text: string): string {
1311
1349
  const ansi = this.#fgColors[color];
1312
1350
  if (!ansi) throw new Error(`Unknown theme color: ${color}`);
@@ -1570,6 +1608,13 @@ export class Theme {
1570
1608
  };
1571
1609
  }
1572
1610
 
1611
+ get radio() {
1612
+ return {
1613
+ selected: this.#symbols["radio.selected"],
1614
+ unselected: this.#symbols["radio.unselected"],
1615
+ };
1616
+ }
1617
+
1573
1618
  get format() {
1574
1619
  return {
1575
1620
  bullet: this.#symbols["format.bullet"],
@@ -2312,13 +2357,8 @@ export function isLightTheme(themeName?: string): boolean {
2312
2357
  }
2313
2358
  try {
2314
2359
  const resolved = resolveVarRefs(themeJson.colors.userMessageBg, themeJson.vars ?? {});
2315
- if (typeof resolved !== "string" || !resolved.startsWith("#") || resolved.length !== 7) return false;
2316
- const r = parseInt(resolved.slice(1, 3), 16) / 255;
2317
- const g = parseInt(resolved.slice(3, 5), 16) / 255;
2318
- const b = parseInt(resolved.slice(5, 7), 16) / 255;
2319
- // Relative luminance (ITU-R BT.709)
2320
- const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
2321
- return luminance > 0.5;
2360
+ const luminance = colorLuma(resolved);
2361
+ return luminance !== undefined && luminance > 0.5;
2322
2362
  } catch {
2323
2363
  return false;
2324
2364
  }
@@ -267,6 +267,7 @@ export class UiHelpers {
267
267
  this.ctx.hideThinkingBlock,
268
268
  () => this.ctx.ui.requestRender(),
269
269
  this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
270
+ this.ctx.ui.imageBudget,
270
271
  );
271
272
  this.ctx.chatContainer.addChild(assistantComponent);
272
273
  break;
@@ -0,0 +1,37 @@
1
+ import { isEnoent } from "@oh-my-pi/pi-utils";
2
+ import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
3
+
4
+ /** The session's active plan, resolved for handoff into a subagent's context. */
5
+ export interface OverallPlanReference {
6
+ /** The `local://` reference path (e.g. `local://my-feature.md`), kept for display. */
7
+ path: string;
8
+ /** The full plan markdown, as written to disk. */
9
+ content: string;
10
+ }
11
+
12
+ /**
13
+ * Load the session's active overall plan for subagent handoff.
14
+ *
15
+ * Returns the plan referenced by `planReferencePath` when it exists on disk with
16
+ * non-empty content, or `undefined` when there is no plan (the file is absent or
17
+ * empty). This mirrors `AgentSession.#buildPlanReferenceMessage`'s gating so a
18
+ * subagent sees exactly the plan the main agent treats as its active reference.
19
+ *
20
+ * Callers MUST skip this during plan mode itself — read-only plan exploration
21
+ * uses a different prompt and a draft plan should not be handed off as approved.
22
+ */
23
+ export async function loadOverallPlanReference(
24
+ planReferencePath: string,
25
+ localProtocolOptions: LocalProtocolOptions,
26
+ ): Promise<OverallPlanReference | undefined> {
27
+ const resolved = resolveLocalUrlToPath(planReferencePath, localProtocolOptions);
28
+ let content: string;
29
+ try {
30
+ content = await Bun.file(resolved).text();
31
+ } catch (error) {
32
+ if (isEnoent(error)) return undefined;
33
+ throw error;
34
+ }
35
+ if (!content.trim()) return undefined;
36
+ return { path: planReferencePath, content };
37
+ }
@@ -0,0 +1,31 @@
1
+ import { getReadToolPath, type ProtectedToolContext } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
2
+ import { normalizeLocalScheme } from "../tools/path-utils";
3
+
4
+ /** Canonical plan alias every session's `local://` root resolves. */
5
+ const LOCAL_PLAN_ALIAS = "local://PLAN.md";
6
+
7
+ /** True when `readPath` targets `planTarget`, ignoring `local:/` vs `local://`
8
+ * scheme spelling and any trailing read selector (`:1-50`, `:raw`, …). */
9
+ function readTargetsPlan(readPath: string, planTarget: string): boolean {
10
+ const read = normalizeLocalScheme(readPath);
11
+ const target = normalizeLocalScheme(planTarget);
12
+ return read === target || read.startsWith(`${target}:`);
13
+ }
14
+
15
+ /**
16
+ * Build a compaction protection matcher that keeps `read` results for the active
17
+ * plan file intact through prune/shake — the plan analog of skill-read
18
+ * protection. Matches both the canonical `local://PLAN.md` alias and the
19
+ * session's current plan reference path (e.g. a titled `local://<title>.md`), so
20
+ * the plan survives compaction whether the agent reads it by alias or by title.
21
+ *
22
+ * `getPlanReferencePath` is evaluated at match time so a mid-session retitle
23
+ * (plan approval renames `PLAN.md` → `<title>.md`) is honored immediately.
24
+ */
25
+ export function createPlanReadMatcher(getPlanReferencePath: () => string): (context: ProtectedToolContext) => boolean {
26
+ return (context: ProtectedToolContext) => {
27
+ const path = getReadToolPath(context);
28
+ if (path === undefined) return false;
29
+ return readTargetsPlan(path, LOCAL_PLAN_ALIAS) || readTargetsPlan(path, getPlanReferencePath());
30
+ };
31
+ }
package/src/priority.json CHANGED
@@ -18,6 +18,10 @@
18
18
  "gpt-5.1-codex",
19
19
  "gpt-5.1",
20
20
  "codex",
21
+ "opus-4.8",
22
+ "opus-4-8",
23
+ "opus-4.7",
24
+ "opus-4-7",
21
25
  "opus-4.6",
22
26
  "opus-4-6",
23
27
  "opus-4.5",
@@ -16,7 +16,7 @@ This is an autonomous continuation. The objective persists across turns; do not
16
16
 
17
17
  Before calling `goal({op:"complete"})`, you MUST perform a completion audit against the current repo state:
18
18
 
19
- 1. **Restate the objective as concrete deliverables.** What files, behaviors, tests, gates, or artifacts must exist for the objective to be true? Write them down (todo_write, or in your reasoning).
19
+ 1. **Restate the objective as concrete deliverables.** What files, behaviors, tests, gates, or artifacts must exist for the objective to be true? Write them down (todo, or in your reasoning).
20
20
  2. **Map each deliverable to evidence.** For every requirement, identify the authoritative source that would prove it: a file's contents, a command's output, a test's pass status, a PR/issue state.
21
21
  3. **Inspect the actual current state.** Read the files. Run the commands. Check the tests. Do not rely on memory of earlier work in this session — the repo may have changed.
22
22
  4. **Match verification scope to claim scope.** A narrow check (one file passes its unit test) does not prove a broad claim (the feature works end-to-end).
@@ -1,13 +1,13 @@
1
1
  <system-reminder>
2
2
  Before substantive work, create a phased todo.
3
3
 
4
- You MUST call `todo_write` first in this turn.
4
+ You MUST call `todo` first in this turn.
5
5
  You MUST initialize the todo list with a single `init` op.
6
6
  You MUST cover the entire request from investigation through implementation and verification — not just the next immediate step.
7
7
  Task descriptions MUST be specific. A future turn MUST execute them without re-planning.
8
8
  You MUST keep task `content` to a short label (5-10 words). Put file paths, implementation steps, and specifics in `details`.
9
9
  You MUST keep exactly one task `in_progress` and all later tasks `pending`.
10
10
 
11
- After `todo_write` succeeds, continue the request in the same turn.
12
- Do not call `todo_write` again unless task state materially changed.
11
+ After `todo` succeeds, continue the request in the same turn.
12
+ Do not call `todo` again unless task state materially changed.
13
13
  </system-reminder>
@@ -2,12 +2,12 @@
2
2
  The user's message above is an **orchestration request**. Execute it as the orchestrator under the contract below. This contract overrides any default tendency to yield early, narrate, or do the work yourself.
3
3
 
4
4
  <role>
5
- You decompose, dispatch, verify, and iterate. Substantial and parallelizable work goes through `task` subagents — that is the whole point of orchestrating. But you are not forbidden from touching the tree: a trivial, self-contained edit is yours to make directly when spawning a subagent for it would cost more than the edit itself. Your tool budget is: reading for planning, `task` for dispatch, `edit`/`write` for trivial inline fixes only, verification (`bun check`, `bun test`, `lsp diagnostics`), git via `bash`, and `todo_write` for tracking.
5
+ You decompose, dispatch, verify, and iterate. Substantial and parallelizable work goes through `task` subagents — that is the whole point of orchestrating. But you are not forbidden from touching the tree: a trivial, self-contained edit is yours to make directly when spawning a subagent for it would cost more than the edit itself. Your tool budget is: reading for planning, `task` for dispatch, `edit`/`write` for trivial inline fixes only, verification (`bun check`, `bun test`, `lsp diagnostics`), git via `bash`, and `todo` for tracking.
6
6
  </role>
7
7
 
8
8
  <rules>
9
9
  1. **Do not yield until everything is closed.** A phase finishing is *not* a yield point — launch the next phase in the same turn. Stop only when every requested item is verifiably done, or you hit a concrete [blocked] state that genuinely requires the user.
10
- 2. **Enumerate the full surface before dispatching.** If the request references audits, plans, checklists, phase lists, or file lists, expand them into a flat set of items in `todo_write`. "Most of them" or "the important ones" is failure. Re-read the source documents — do not work from memory.
10
+ 2. **Enumerate the full surface before dispatching.** If the request references audits, plans, checklists, phase lists, or file lists, expand them into a flat set of items in `todo`. "Most of them" or "the important ones" is failure. Re-read the source documents — do not work from memory.
11
11
  3. **Parallelize maximally; never launch a one-off task.** Every set of edits with disjoint file scope MUST ship as one `task` batch — fan the work as wide as it decomposes. A single-task batch for divisible work is a failure: split it. If you are about to dispatch exactly one subagent, stop — either there is more to run alongside it (find it and batch them) or the change is small enough to make inline yourself (do it). Serialize only when one subagent produces a contract (types, schema, shared module) the next consumes — and state the dependency when you do.
12
12
  4. **Each `task` assignment is self-contained.** Subagents have no shared context. Spell out: target files (≤3–5 explicit paths, no globs), the change with APIs and patterns, edge cases, and observable acceptance criteria. Do not assume they read the same plan you did.
13
13
  5. **Verify after every phase before launching the next.** Run the appropriate gate: `bun check` for types, package-scoped `bun test` for behavior, `lsp diagnostics` for changed files. If a phase introduced breakage, dispatch fix-up subagents *before* moving on. Never declare a phase done on a red tree.
@@ -20,12 +20,12 @@ You decompose, dispatch, verify, and iterate. Substantial and parallelizable wor
20
20
 
21
21
  <workflow>
22
22
  1. **Ingest.** Read every referenced file (audits, plans, prior agent output, current branch state). Run `git status` to see uncommitted changes.
23
- 2. **Plan.** Materialize the full work surface in `todo_write` as ordered phases. Within each phase, list the parallelizable units.
23
+ 2. **Plan.** Materialize the full work surface in `todo` as ordered phases. Within each phase, list the parallelizable units.
24
24
  3. **Dispatch phase.** Launch all parallel `task` subagents in one call. Wait for the batch.
25
25
  4. **Verify phase.** Run the gates. On failure, dispatch fix-up subagents and re-verify. Do not advance with a red gate.
26
26
  5. **Commit phase** (if applicable). Focused message naming the phase.
27
- 6. **Advance.** Mark the phase done in `todo_write`, immediately start the next phase. No summary message between phases — keep going.
28
- 7. **Final verification.** When the last phase is green, run the full gate set once more and confirm every `todo_write` item is closed. Then yield with a terse status, not a recap.
27
+ 6. **Advance.** Mark the phase done in `todo`, immediately start the next phase. No summary message between phases — keep going.
28
+ 7. **Final verification.** When the last phase is green, run the full gate set once more and confirm every `todo` item is closed. Then yield with a terse status, not a recap.
29
29
  </workflow>
30
30
 
31
31
  <anti-patterns>
@@ -1,28 +1,25 @@
1
- <critical>
2
- Plan approved. You MUST execute it now.
3
- </critical>
4
-
5
- Finalized plan artifact: `{{finalPlanFilePath}}`
1
+ Plan approved.
6
2
  {{#if contextPreserved}}
7
- Context preserved. Use conversation history when useful; the finalized plan is the source of truth if it conflicts with earlier exploration.
8
- {{else}}
9
- Execution may be in fresh context. Treat the finalized plan as the source of truth.
3
+ - Context preserved. Use conversation history when useful; this plan is the source of truth if it conflicts with earlier exploration.
10
4
  {{/if}}
11
5
 
12
- ## Plan
13
-
14
- {{planContent}}
15
-
16
6
  <instruction>
17
- You MUST execute this plan step by step from `{{finalPlanFilePath}}`. You have full tool access.
7
+ You MUST execute this plan step by step. You have full tool access.
18
8
  You MUST verify each step before proceeding to the next.
19
- {{#has tools "todo_write"}}
20
- Before execution, initialize todo tracking with `todo_write`.
21
- After each completed step, immediately update `todo_write`.
22
- If `todo_write` fails, fix the payload and retry before continuing.
9
+ {{#has tools "todo"}}
10
+ Before execution, initialize todo tracking with `todo`.
11
+ After each completed step, immediately update `todo`.
12
+ If `todo` fails, fix the payload and retry before continuing.
23
13
  {{/has}}
14
+ The plan path is for subagent handoff only. You already have the plan; NEVER read it.
24
15
  </instruction>
25
16
 
17
+ The full plan is injected below. You MUST execute it now:
18
+
19
+ <plan path="{{finalPlanFilePath}}">
20
+ {{planContent}}
21
+ </plan>
22
+
26
23
  <critical>
27
24
  You MUST keep going until complete. This matters.
28
25
  </critical>
@@ -1,14 +1,11 @@
1
1
  ## Existing Plan
2
2
 
3
- Plan file from previous session: `{{planFilePath}}`
4
-
5
- <details>
6
- <summary>Plan contents</summary>
7
-
3
+ <plan path="{{planFilePath}}">
8
4
  {{planContent}}
9
- </details>
5
+ </plan>
10
6
 
11
7
  <instruction>
12
8
  If this plan is relevant to current work and not complete, you MUST continue executing it.
13
9
  If the plan is stale or unrelated, you MUST ignore it.
10
+ The plan path is for subagent handoff only. You already have the plan; NEVER read it.
14
11
  </instruction>
@@ -10,6 +10,17 @@ CONTEXT
10
10
  {{context}}
11
11
  {{/if}}
12
12
 
13
+ {{#if planReference}}
14
+ PLAN
15
+ ===================================
16
+
17
+ This session is executing an approved plan. Your assignment above is one part of it — use the plan to understand how your piece fits the whole and to stay consistent with decisions already made. Where the plan and your specific assignment conflict, the assignment wins. The plan path is for reference; you already have its full contents below, so NEVER re-read it.
18
+
19
+ <plan path="{{planReferencePath}}">
20
+ {{planReference}}
21
+ </plan>
22
+ {{/if}}
23
+
13
24
  COOP
14
25
  ===================================
15
26
 
@@ -62,7 +62,7 @@ Scale to the ask: "find any bugs" → a few finders, single-vote verify. "thorou
62
62
  </patterns>
63
63
 
64
64
  <execution>
65
- - Decompose the surface first; capture it in `todo_write` when it spans phases.
65
+ - Decompose the surface first; capture it in `todo` when it spans phases.
66
66
  - Prefer `schema=` for any agent whose output you branch on.
67
67
  - After a fan-out returns, YOU own correctness: read the artifacts, run the gate, verify before acting. Subagents do the legwork; they don't get the last word.
68
68
  - Keep going until the task is closed — a returned fan-out is a step, not a stopping point.