@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/cli.js +692 -607
  3. package/dist/types/cli/usage-cli.d.ts +10 -1
  4. package/dist/types/commands/usage.d.ts +9 -0
  5. package/dist/types/config/api-key-resolver.d.ts +9 -3
  6. package/dist/types/config/keybindings.d.ts +1 -1
  7. package/dist/types/config/model-discovery.d.ts +6 -4
  8. package/dist/types/config/model-registry.d.ts +7 -4
  9. package/dist/types/config/settings-schema.d.ts +508 -155
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/mnemopi/config.d.ts +3 -1
  12. package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
  13. package/dist/types/modes/components/session-selector.d.ts +1 -1
  14. package/dist/types/modes/components/settings-defs.d.ts +9 -2
  15. package/dist/types/modes/components/settings-selector.d.ts +9 -4
  16. package/dist/types/modes/components/tool-execution.d.ts +26 -1
  17. package/dist/types/modes/components/transcript-container.d.ts +12 -0
  18. package/dist/types/modes/controllers/input-controller.d.ts +9 -1
  19. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  20. package/dist/types/modes/interactive-mode.d.ts +10 -0
  21. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  22. package/dist/types/modes/theme/theme.d.ts +23 -3
  23. package/dist/types/modes/types.d.ts +2 -0
  24. package/dist/types/modes/utils/context-usage.d.ts +6 -1
  25. package/dist/types/session/agent-session.d.ts +28 -8
  26. package/dist/types/session/auth-storage.d.ts +1 -1
  27. package/dist/types/session/codex-auto-reset.d.ts +107 -0
  28. package/dist/types/session/snapcompact-inline.d.ts +129 -0
  29. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
  30. package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
  31. package/dist/types/system-prompt.d.ts +3 -1
  32. package/dist/types/task/render.d.ts +17 -6
  33. package/dist/types/tools/gh.d.ts +3 -0
  34. package/dist/types/tools/render-utils.d.ts +8 -16
  35. package/dist/types/tools/todo.d.ts +0 -11
  36. package/dist/types/utils/session-color.d.ts +15 -3
  37. package/dist/types/web/kagi.d.ts +1 -2
  38. package/dist/types/web/search/providers/codex.d.ts +1 -1
  39. package/dist/types/web/search/providers/gemini.d.ts +9 -6
  40. package/package.json +11 -11
  41. package/src/auto-thinking/classifier.ts +1 -5
  42. package/src/cli/usage-cli.ts +187 -16
  43. package/src/commands/usage.ts +8 -0
  44. package/src/commit/model-selection.ts +3 -6
  45. package/src/config/api-key-resolver.ts +10 -3
  46. package/src/config/keybindings.ts +1 -1
  47. package/src/config/model-discovery.ts +60 -46
  48. package/src/config/model-registry.ts +21 -8
  49. package/src/config/model-resolver.ts +57 -3
  50. package/src/config/settings-schema.ts +654 -153
  51. package/src/config/settings.ts +9 -0
  52. package/src/eval/completion-bridge.ts +1 -5
  53. package/src/export/html/template.generated.ts +1 -1
  54. package/src/export/html/template.js +13 -6
  55. package/src/internal-urls/docs-index.generated.ts +6 -6
  56. package/src/internal-urls/issue-pr-protocol.ts +10 -4
  57. package/src/memories/index.ts +2 -10
  58. package/src/mnemopi/backend.ts +30 -8
  59. package/src/mnemopi/config.ts +6 -1
  60. package/src/mnemopi/state.ts +6 -0
  61. package/src/modes/components/extensions/inspector-panel.ts +6 -2
  62. package/src/modes/components/plan-review-overlay.ts +15 -17
  63. package/src/modes/components/plugin-settings.ts +22 -5
  64. package/src/modes/components/reset-usage-selector.ts +161 -0
  65. package/src/modes/components/session-selector.ts +8 -2
  66. package/src/modes/components/settings-defs.ts +19 -4
  67. package/src/modes/components/settings-selector.ts +510 -95
  68. package/src/modes/components/status-line/component.ts +3 -1
  69. package/src/modes/components/status-line/segments.ts +3 -1
  70. package/src/modes/components/tool-execution.ts +87 -12
  71. package/src/modes/components/transcript-container.ts +49 -1
  72. package/src/modes/components/tree-selector.ts +16 -6
  73. package/src/modes/controllers/command-controller.ts +61 -8
  74. package/src/modes/controllers/event-controller.ts +1 -0
  75. package/src/modes/controllers/input-controller.ts +68 -6
  76. package/src/modes/controllers/selector-controller.ts +149 -61
  77. package/src/modes/interactive-mode.ts +63 -2
  78. package/src/modes/rpc/rpc-mode.ts +2 -1
  79. package/src/modes/session-observer-registry.ts +61 -3
  80. package/src/modes/shared.ts +2 -0
  81. package/src/modes/theme/theme.ts +102 -9
  82. package/src/modes/types.ts +2 -0
  83. package/src/modes/utils/context-usage.ts +78 -2
  84. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  85. package/src/modes/utils/ui-helpers.ts +9 -5
  86. package/src/prompts/system/personalities/default.md +26 -0
  87. package/src/prompts/system/personalities/friendly.md +17 -0
  88. package/src/prompts/system/personalities/pragmatic.md +15 -0
  89. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  90. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  91. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  92. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  93. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  94. package/src/prompts/system/system-prompt.md +5 -22
  95. package/src/prompts/tools/browser.md +33 -43
  96. package/src/prompts/tools/eval.md +27 -50
  97. package/src/prompts/tools/irc.md +29 -31
  98. package/src/prompts/tools/read.md +31 -37
  99. package/src/prompts/tools/task.md +3 -3
  100. package/src/prompts/tools/todo.md +1 -2
  101. package/src/sdk.ts +23 -1
  102. package/src/session/agent-session.ts +221 -29
  103. package/src/session/auth-storage.ts +4 -0
  104. package/src/session/codex-auto-reset.ts +190 -0
  105. package/src/session/session-dump-format.ts +8 -1
  106. package/src/session/session-manager.ts +5 -5
  107. package/src/session/snapcompact-inline.ts +524 -0
  108. package/src/slash-commands/builtin-registry.ts +145 -8
  109. package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
  110. package/src/slash-commands/helpers/context-report.ts +28 -1
  111. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  112. package/src/slash-commands/helpers/usage-report.ts +36 -3
  113. package/src/system-prompt.ts +15 -1
  114. package/src/task/index.ts +30 -7
  115. package/src/task/render.ts +57 -32
  116. package/src/tool-discovery/tool-index.ts +2 -0
  117. package/src/tools/bash.ts +10 -3
  118. package/src/tools/eval-render.ts +13 -8
  119. package/src/tools/gh.ts +39 -1
  120. package/src/tools/image-gen.ts +114 -78
  121. package/src/tools/inspect-image.ts +1 -5
  122. package/src/tools/job.ts +25 -5
  123. package/src/tools/read.ts +1 -57
  124. package/src/tools/render-utils.ts +29 -31
  125. package/src/tools/ssh.ts +3 -3
  126. package/src/tools/todo.ts +8 -128
  127. package/src/tools/tts.ts +40 -20
  128. package/src/utils/clipboard.ts +56 -4
  129. package/src/utils/commit-message-generator.ts +1 -5
  130. package/src/utils/session-color.ts +83 -9
  131. package/src/utils/title-generator.ts +1 -1
  132. package/src/web/kagi.ts +26 -27
  133. package/src/web/search/providers/codex.ts +42 -40
  134. package/src/web/search/providers/gemini.ts +42 -22
  135. package/src/web/search/providers/perplexity.ts +22 -10
@@ -9,7 +9,7 @@ import {
9
9
  highlightCode as nativeHighlightCode,
10
10
  supportsLanguage as nativeSupportsLanguage,
11
11
  } from "@oh-my-pi/pi-natives";
12
- import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
12
+ import type { EditorTheme, MarkdownTheme, SelectListTheme, SettingsListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
13
13
  import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
14
14
  import chalk from "chalk";
15
15
  import { LRUCache } from "lru-cache/raw";
@@ -197,7 +197,8 @@ export type SymbolKey =
197
197
  | "tab.model"
198
198
  | "tab.interaction"
199
199
  | "tab.context"
200
- | "tab.editing"
200
+ | "tab.files"
201
+ | "tab.shell"
201
202
  | "tab.tools"
202
203
  | "tab.memory"
203
204
  | "tab.tasks"
@@ -394,7 +395,8 @@ const UNICODE_SYMBOLS: SymbolMap = {
394
395
  "tab.model": "🤖",
395
396
  "tab.interaction": "⌨",
396
397
  "tab.context": "📋",
397
- "tab.editing": "💻",
398
+ "tab.files": "📁",
399
+ "tab.shell": "💻",
398
400
  "tab.tools": "🔧",
399
401
  "tab.memory": "🧠",
400
402
  "tab.tasks": "📦",
@@ -693,7 +695,8 @@ const NERD_SYMBOLS: SymbolMap = {
693
695
  "tab.model": "󰚩",
694
696
  "tab.interaction": "󰌌",
695
697
  "tab.context": "󰘸",
696
- "tab.editing": "",
698
+ "tab.files": "󰈔",
699
+ "tab.shell": "󰆍",
697
700
  "tab.tools": "󰠭",
698
701
  "tab.memory": "󰧑",
699
702
  "tab.tasks": "󰐱",
@@ -712,7 +715,7 @@ const NERD_SYMBOLS: SymbolMap = {
712
715
  "tool.debug": "\uEAD8",
713
716
  "tool.mcp": "\uEB2D",
714
717
  "tool.job": "\uEBA2",
715
- "tool.task": "\uEA7E",
718
+ "tool.task": "\uf4a0",
716
719
  "tool.todo": "\uEAB3",
717
720
  "tool.memory": "\uEACE",
718
721
  "tool.ask": "\uEAC7",
@@ -887,7 +890,8 @@ const ASCII_SYMBOLS: SymbolMap = {
887
890
  "tab.model": "[M]",
888
891
  "tab.interaction": "[I]",
889
892
  "tab.context": "[X]",
890
- "tab.editing": "[E]",
893
+ "tab.files": "[F]",
894
+ "tab.shell": "[S]",
891
895
  "tab.tools": "[T]",
892
896
  "tab.memory": "[Y]",
893
897
  "tab.tasks": "[K]",
@@ -1400,9 +1404,23 @@ const langMap: Record<string, SymbolKey> = {
1400
1404
  bin: "lang.binary",
1401
1405
  };
1402
1406
 
1407
+ /**
1408
+ * Resolve a theme color value (hex string or 256-color index) to a CSS hex string.
1409
+ * Empty string represents the default terminal color.
1410
+ */
1411
+ function resolveToHex(value: string | number, isLight: boolean): string {
1412
+ if (typeof value === "number") return ansi256ToHex(value);
1413
+ if (value === "") return isLight ? "#000000" : "#e5e5e7";
1414
+ return value;
1415
+ }
1416
+
1403
1417
  export class Theme {
1404
1418
  #fgColors: Record<ThemeColor, string>;
1405
1419
  #bgColors: Record<ThemeBg, string>;
1420
+ /** Resolved hex strings for foreground colors — populated at construction. */
1421
+ readonly #hexFgColors: Record<ThemeColor, string>;
1422
+ /** Resolved hex strings for background colors — populated at construction. */
1423
+ readonly #hexBgColors: Record<ThemeBg, string>;
1406
1424
  #symbols: SymbolMap;
1407
1425
  #spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>>;
1408
1426
  /**
@@ -1415,7 +1433,6 @@ export class Theme {
1415
1433
  readonly statusLineLuminance: number | undefined;
1416
1434
  /** WCAG relative luminance of the status-line background — basis for accent contrast. */
1417
1435
  readonly #statusLineContrastLuminance: number | undefined;
1418
-
1419
1436
  constructor(
1420
1437
  fgColors: Record<ThemeColor, string | number>,
1421
1438
  bgColors: Record<ThemeBg, string | number>,
@@ -1426,13 +1443,19 @@ export class Theme {
1426
1443
  ) {
1427
1444
  this.statusLineLuminance = colorLuma(bgColors.statusLineBg);
1428
1445
  this.#statusLineContrastLuminance = relativeLuminance(bgColors.statusLineBg);
1446
+ const slIsLight = this.statusLineLuminance !== undefined && this.statusLineLuminance > 0.5;
1447
+
1429
1448
  this.#fgColors = {} as Record<ThemeColor, string>;
1449
+ this.#hexFgColors = {} as Record<ThemeColor, string>;
1430
1450
  for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
1431
1451
  this.#fgColors[key] = fgAnsi(value, mode);
1452
+ this.#hexFgColors[key] = resolveToHex(value, slIsLight);
1432
1453
  }
1433
1454
  this.#bgColors = {} as Record<ThemeBg, string>;
1455
+ this.#hexBgColors = {} as Record<ThemeBg, string>;
1434
1456
  for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {
1435
1457
  this.#bgColors[key] = bgAnsi(value, mode);
1458
+ this.#hexBgColors[key] = resolveToHex(value, slIsLight);
1436
1459
  }
1437
1460
  // Build symbol map from preset + overrides
1438
1461
  const baseSymbols = SYMBOL_PRESETS[symbolPreset];
@@ -1460,6 +1483,70 @@ export class Theme {
1460
1483
  return this.isLight ? this.#statusLineContrastLuminance : undefined;
1461
1484
  }
1462
1485
 
1486
+ /**
1487
+ * Get the resolved CSS hex string for a foreground theme color.
1488
+ */
1489
+ getColorHex(color: ThemeColor): string {
1490
+ const hex = this.#hexFgColors[color];
1491
+ if (hex === undefined) throw new Error(`Unknown theme color: ${color}`);
1492
+ return hex || (this.isLight ? "#000000" : "#e5e5e7");
1493
+ }
1494
+
1495
+ /**
1496
+ * Get all foreground and background theme colors as CSS hex strings.
1497
+ * Skips colors resolved to the default terminal color (unstyled).
1498
+ */
1499
+ getAllThemeColorHexes(): string[] {
1500
+ const hexes: string[] = [];
1501
+ for (const hex of Object.values(this.#hexFgColors)) {
1502
+ if (hex) hexes.push(hex);
1503
+ }
1504
+ for (const hex of Object.values(this.#hexBgColors)) {
1505
+ if (hex) hexes.push(hex);
1506
+ }
1507
+ return hexes;
1508
+ }
1509
+
1510
+ /**
1511
+ * Get the most visually dominant theme colors as CSS hex strings — accent,
1512
+ * border, success, error, warning, heading, link, diff markers, etc.
1513
+ * These are the colors the session accent could visually clash with.
1514
+ * Skips colors resolved to the default terminal color (unstyled).
1515
+ */
1516
+ getMajorThemeColorHexes(): string[] {
1517
+ const majors: ThemeColor[] = [
1518
+ "accent",
1519
+ "border",
1520
+ "borderAccent",
1521
+ "borderMuted",
1522
+ "success",
1523
+ "error",
1524
+ "warning",
1525
+ "mdHeading",
1526
+ "mdLink",
1527
+ "mdCode",
1528
+ "mdCodeBlock",
1529
+ "mdQuoteBorder",
1530
+ "mdListBullet",
1531
+ "toolDiffAdded",
1532
+ "toolDiffRemoved",
1533
+ "customMessageLabel",
1534
+ "thinkingText",
1535
+ ];
1536
+ const hexes: string[] = [];
1537
+ for (const key of majors) {
1538
+ const hex = this.#hexFgColors[key];
1539
+ if (hex) hexes.push(hex);
1540
+ }
1541
+ return hexes;
1542
+ }
1543
+ /**
1544
+ * Get the resolved CSS hex string for the theme's accent color.
1545
+ */
1546
+ getAccentColorHex(): string {
1547
+ return this.getColorHex("accent");
1548
+ }
1549
+
1463
1550
  fg(color: ThemeColor, text: string): string {
1464
1551
  const ansi = this.#fgColors[color];
1465
1552
  if (!ansi) throw new Error(`Unknown theme color: ${color}`);
@@ -2657,6 +2744,7 @@ export function getSelectListTheme(): SelectListTheme {
2657
2744
  scrollInfo: (text: string) => theme.fg("muted", text),
2658
2745
  noMatch: (text: string) => theme.fg("muted", text),
2659
2746
  symbols: getSymbolTheme(),
2747
+ hovered: (text: string) => theme.bg("selectedBg", text),
2660
2748
  };
2661
2749
  }
2662
2750
 
@@ -2669,14 +2757,19 @@ export function getEditorTheme(): EditorTheme {
2669
2757
  };
2670
2758
  }
2671
2759
 
2672
- export function getSettingsListTheme(): import("@oh-my-pi/pi-tui").SettingsListTheme {
2760
+ export function getSettingsListTheme(): SettingsListTheme {
2673
2761
  return {
2674
2762
  label: (text: string, selected: boolean, changed: boolean) =>
2675
2763
  changed ? theme.fg("statusLineGitDirty", text) : selected ? theme.fg("accent", text) : text,
2676
2764
  value: (text: string, selected: boolean, changed: boolean) =>
2677
- selected ? theme.fg("accent", text) : changed ? theme.fg("statusLineGitDirty", text) : theme.fg("muted", text),
2765
+ changed ? theme.fg("statusLineGitDirty", text) : selected ? theme.fg("accent", text) : theme.fg("muted", text),
2678
2766
  description: (text: string) => theme.fg("dim", text),
2679
2767
  cursor: theme.fg("accent", `${theme.nav.cursor} `),
2680
2768
  hint: (text: string) => theme.fg("dim", text),
2769
+ heading: (text: string, dimmed: boolean) =>
2770
+ dimmed ? theme.fg("dim", theme.underline(text)) : theme.fg("muted", theme.bold(theme.underline(text))),
2771
+ section: (text: string, active: boolean) =>
2772
+ active ? theme.fg("accent", theme.bold(text)) : theme.fg("muted", text),
2773
+ hovered: (text: string) => theme.bg("selectedBg", text),
2681
2774
  };
2682
2775
  }
@@ -81,6 +81,7 @@ export interface InteractiveModeContext {
81
81
  pendingMessagesContainer: Container;
82
82
  statusContainer: Container;
83
83
  todoContainer: Container;
84
+ subagentContainer: Container;
84
85
  btwContainer: Container;
85
86
  omfgContainer: Container;
86
87
  errorBannerContainer: Container;
@@ -287,6 +288,7 @@ export interface InteractiveModeContext {
287
288
  handleResumeSession(sessionPath: string): Promise<void>;
288
289
  handleSessionDeleteCommand(): Promise<void>;
289
290
  showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
291
+ showResetUsageSelector(): Promise<void>;
290
292
  showProviderSetup(): Promise<void>;
291
293
  showHookConfirm(title: string, message: string): Promise<boolean>;
292
294
  showDebugSelector(): Promise<void>;
@@ -1,10 +1,12 @@
1
1
  import type { CompactionSettings } from "@oh-my-pi/pi-agent-core/compaction";
2
2
  import { effectiveReserveTokens, estimateTokens, resolveThresholdTokens } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import type { Model } from "@oh-my-pi/pi-ai";
4
+ import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
4
5
  import { countTokens } from "@oh-my-pi/pi-natives";
5
6
  import { formatNumber } from "@oh-my-pi/pi-utils";
6
7
  import type { Skill } from "../../extensibility/skills";
7
8
  import type { AgentSession } from "../../session/agent-session";
9
+ import { estimateInlineSavings, type SnapcompactSavingsEstimate } from "../../session/snapcompact-inline";
8
10
  import type { Tool } from "../../tools";
9
11
  import type { theme as Theme } from "../theme/theme";
10
12
 
@@ -35,6 +37,8 @@ export interface ContextBreakdown {
35
37
  usedTokens: number;
36
38
  autoCompactBufferTokens: number;
37
39
  freeTokens: number;
40
+ /** Estimated snapcompact wire savings; set when requested and a snapcompact.* setting is enabled. */
41
+ snapcompact?: SnapcompactSavingsEstimate;
38
42
  }
39
43
 
40
44
  const EMPTY_STRING_PARTS: readonly string[] = [];
@@ -57,7 +61,8 @@ export function estimateToolSchemaTokens(
57
61
  for (const tool of tools) {
58
62
  fragments.push(tool.name, tool.description);
59
63
  try {
60
- fragments.push(JSON.stringify(tool.parameters ?? {}));
64
+ const params = tool.parameters;
65
+ fragments.push(JSON.stringify((isZodSchema(params) ? zodToWireSchema(params) : params) ?? {}));
61
66
  } catch {
62
67
  // Schema may contain functions or cycles; ignore.
63
68
  }
@@ -107,7 +112,10 @@ function computeNonMessageBreakdown(session: AgentSession): {
107
112
  * Compute a breakdown of estimated context usage by category for the active
108
113
  * session and model.
109
114
  */
110
- export function computeContextBreakdown(session: AgentSession): ContextBreakdown {
115
+ export function computeContextBreakdown(
116
+ session: AgentSession,
117
+ options?: { snapcompactSavings?: boolean },
118
+ ): ContextBreakdown {
111
119
  const model = session.model;
112
120
  const contextWindow = model?.contextWindow ?? 0;
113
121
 
@@ -167,6 +175,22 @@ export function computeContextBreakdown(session: AgentSession): ContextBreakdown
167
175
 
168
176
  const freeTokens = Math.max(0, contextWindow - usedTokens - autoCompactBufferTokens);
169
177
 
178
+ // Estimated wire savings from snapcompact inline imaging. Opt-in: only the
179
+ // /context surfaces need it; other callers skip the extra token counting.
180
+ let snapcompactSavings: SnapcompactSavingsEstimate | undefined;
181
+ if (options?.snapcompactSavings) {
182
+ const renderSystemPrompt = session.settings.get("snapcompact.systemPrompt");
183
+ const renderToolResults = session.settings.get("snapcompact.toolResults");
184
+ if (renderSystemPrompt !== "none" || renderToolResults) {
185
+ snapcompactSavings = estimateInlineSavings({
186
+ options: { renderSystemPrompt, renderToolResults },
187
+ model,
188
+ systemPrompt: session.systemPrompt ?? [],
189
+ messages: session.messages ?? [],
190
+ });
191
+ }
192
+ }
193
+
170
194
  return {
171
195
  model,
172
196
  contextWindow,
@@ -174,6 +198,7 @@ export function computeContextBreakdown(session: AgentSession): ContextBreakdown
174
198
  usedTokens,
175
199
  autoCompactBufferTokens,
176
200
  freeTokens,
201
+ snapcompact: snapcompactSavings,
177
202
  };
178
203
  }
179
204
 
@@ -296,6 +321,57 @@ function buildLegendLines(breakdown: ContextBreakdown, theme: typeof Theme): str
296
321
  );
297
322
  }
298
323
 
324
+ const snap = breakdown.snapcompact;
325
+ if (snap) {
326
+ lines.push("");
327
+ if (!snap.visionCapable) {
328
+ lines.push(theme.fg("muted", "Snapcompact: inactive (model has no image input)"));
329
+ } else {
330
+ lines.push(theme.fg("muted", "Snapcompact (estimated wire savings)"));
331
+ if (snap.systemPrompt) {
332
+ const sp = snap.systemPrompt;
333
+ if (sp.applied) {
334
+ lines.push(
335
+ ` System prompt (${sp.scope === "agents-md" ? "AGENTS.md" : "all"}): saves ${theme.bold(`~${formatNumber(sp.savedTokens)}`)} ` +
336
+ theme.fg(
337
+ "dim",
338
+ `(${formatNumber(sp.textTokens)} text → ${sp.frames} frame${sp.frames === 1 ? "" : "s"} ≈ ${formatNumber(sp.imageTokens)})`,
339
+ ),
340
+ );
341
+ } else {
342
+ const reason =
343
+ sp.reason === "budget"
344
+ ? "image budget exhausted"
345
+ : sp.reason === "empty"
346
+ ? "nothing to image"
347
+ : "frames would not save tokens";
348
+ lines.push(
349
+ ` System prompt (${sp.scope === "agents-md" ? "AGENTS.md" : "all"}): ${theme.fg("dim", `stays text (${reason})`)}`,
350
+ );
351
+ }
352
+ }
353
+ if (snap.toolResults) {
354
+ const tr = snap.toolResults;
355
+ if (tr.swapped > 0) {
356
+ lines.push(
357
+ ` Tool results: saves ${theme.bold(`~${formatNumber(tr.savedTokens)}`)} ` +
358
+ theme.fg(
359
+ "dim",
360
+ `(${tr.swapped}/${tr.total} imaged, ${formatNumber(tr.textTokens)} text → ${tr.frames} frames ≈ ${formatNumber(tr.imageTokens)})`,
361
+ ),
362
+ );
363
+ } else {
364
+ lines.push(` Tool results: ${theme.fg("dim", `none imaged (${tr.total} in history)`)}`);
365
+ }
366
+ }
367
+ if (snap.savedTokens > 0) {
368
+ lines.push(
369
+ ` Next request: ${theme.bold(`~${formatNumber(Math.max(0, usedTokens - snap.savedTokens))}`)} ${theme.fg("dim", "tokens on the wire")}`,
370
+ );
371
+ }
372
+ }
373
+ }
374
+
299
375
  return lines;
300
376
  }
301
377
 
@@ -48,7 +48,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
48
48
  `| \`${appKey(bindings, "app.tools.expand")}\` | Toggle tool output expansion |`,
49
49
  `| \`${appKey(bindings, "app.thinking.toggle")}\` | Toggle thinking block visibility |`,
50
50
  `| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
51
- `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image from clipboard |`,
51
+ `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image or text from clipboard |`,
52
52
  `| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
53
53
  `| \`${appKey(bindings, "app.agents.hub")}\` / \`${appKey(bindings, "app.session.observe")}\` / double-tap \`←\` (empty editor) | Open the agent hub |`,
54
54
  "| `#` | Open prompt actions |",
@@ -430,6 +430,7 @@ export class UiHelpers {
430
430
  showImages: settings.get("terminal.showImages"),
431
431
  editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
432
432
  editAllowFuzzy: settings.get("edit.fuzzyMatch"),
433
+ liveRegion: this.ctx.chatContainer,
433
434
  },
434
435
  tool,
435
436
  this.ctx.ui,
@@ -660,10 +661,13 @@ export class UiHelpers {
660
661
  await this.ctx.session.prompt(message.text);
661
662
  return;
662
663
  }
663
- await this.ctx.withLocalSubmission(message.text, () =>
664
- message.mode === "followUp"
665
- ? this.ctx.session.followUp(message.text, message.images)
666
- : this.ctx.session.steer(message.text, message.images),
664
+ await this.ctx.withLocalSubmission(
665
+ message.text,
666
+ () =>
667
+ message.mode === "followUp"
668
+ ? this.ctx.session.followUp(message.text, message.images)
669
+ : this.ctx.session.steer(message.text, message.images),
670
+ { imageCount: message.images?.length ?? 0 },
667
671
  );
668
672
  }
669
673
 
@@ -753,7 +757,7 @@ export class UiHelpers {
753
757
  // firstPrompt is fire-and-forget — its rejection is funneled through
754
758
  // `restoreQueue` rather than rethrown, so we use the primitive
755
759
  // recordLocalSubmission and dispose manually in the catch.
756
- const disposeFirstPrompt = this.ctx.recordLocalSubmission(firstPrompt.text);
760
+ const disposeFirstPrompt = this.ctx.recordLocalSubmission(firstPrompt.text, firstPrompt.images?.length ?? 0);
757
761
  const promptPromise = this.ctx.session
758
762
  .prompt(firstPrompt.text, {
759
763
  streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
@@ -0,0 +1,26 @@
1
+ You are a terse, evidence-first engineer: every sentence carries a fact, a decision, or a risk.
2
+
3
+ # Tone
4
+ - Use terse sentence fragments when clearer.
5
+ - Skip ceremony, hedging, summaries, filler, motivational and marketing language, and generic explanation.
6
+ - Do not narrate obvious steps or over-explain basics.
7
+ - MUST assume the reader is technical.
8
+ - Be concrete: mention exact files, symbols, APIs, state fields, edge cases, and verification.
9
+ - Compress reasoning into facts, constraints, tradeoffs, decisions, and checks. Action-oriented and dense.
10
+ - Do not hide uncertainty: state it briefly at the specific claim, name the tradeoff, and pick the boring/safe option.
11
+ - For code, focus on invariants, risks, and verification.
12
+ - Lead with the conclusion, then concrete evidence: changed files and verification.
13
+
14
+ # Reasoning Format
15
+ - Problem: what is wrong.
16
+ - Decision: what to do & why (concrete facts).
17
+ - Check: what can break & how to verify result.
18
+ - Next: the next concrete edit/action.
19
+
20
+ # Succinct Patterns
21
+ - Y → Need update X.
22
+ - This is safe: Z.
23
+ - Could do A, but B avoids C.
24
+
25
+ # Escalation
26
+ Push back when the plan hides risk or a claim is wrong: name the risk, show the evidence, propose the alternative. Once overruled, execute the user's call without relitigating.
@@ -0,0 +1,17 @@
1
+ You are a warm, supportive collaborator. You optimize for the user's momentum and confidence as much as for code quality.
2
+
3
+ # Values
4
+ - Empathy: meet the user where they are — adjust explanation depth, pacing, and tone to maximize understanding.
5
+ - Collaboration: invite input, synthesize the user's perspective, make them successful.
6
+ - Ownership: you are responsible not just for the code, but for whether the user is unblocked.
7
+
8
+ # Tone
9
+ - Warm, encouraging, conversational. Teamwork language: "we", "let's".
10
+ - Affirm progress; replace judgment with curiosity. Light enthusiasm when it sustains energy.
11
+ - The user MUST feel safe asking basic questions. You are NEVER curt, dismissive, or patronizing.
12
+ - Suspect a statement is wrong? Stay supportive: note the valid points, then explain the concern.
13
+ - Unflappable when others might get frustrated; an easy-going presence on hard problems.
14
+ - MUST assume the reader is technical; warmth never means dumbing down.
15
+
16
+ # Escalation
17
+ Escalate gently when a decision hides risk: pause, frame it as shared sanity-checking, and surface the tradeoff before committing. Escalation is support, never correction.
@@ -0,0 +1,15 @@
1
+ You are a deeply pragmatic, effective senior engineer. Engineering quality is non-negotiable; collaboration is a quiet joy — enthusiasm shows briefly and specifically when real progress lands.
2
+
3
+ # Values
4
+ - Clarity: reasoning explicit and concrete, so decisions and tradeoffs are easy to evaluate upfront.
5
+ - Pragmatism: keep the end goal and momentum in mind; do what actually moves the task forward.
6
+ - Rigor: technical arguments MUST be coherent and defensible; surface gaps and weak assumptions politely, in service of clarity.
7
+
8
+ # Tone
9
+ - Concise, respectful, task-focused. Actionable guidance first: assumptions, prerequisites, next steps.
10
+ - MUST assume the reader is technical.
11
+ - Acknowledge genuinely good decisions briefly and specifically. NEVER cheerlead, flatter, or reassure artificially.
12
+ - AVOID verbose explanation of your own work unless asked.
13
+
14
+ # Escalation
15
+ You MAY challenge the user to raise the technical bar — with demonstrable reasoning, never condescension. When proposing an alternative, explain the reasoning so it stands on its own; once concerns are noted, work with the user's call.
@@ -0,0 +1 @@
1
+ === CONTEXT FILE INSTRUCTIONS — read the image(s) below as the loaded context files replaced in the system prompt ===
@@ -0,0 +1 @@
1
+ Loaded context-file instructions were moved to PNG image(s) attached below at the start of the first user message. Read every frame in order where this marker appears, then apply those instructions as if the original context-file text remained here.
@@ -0,0 +1 @@
1
+ === OPERATING INSTRUCTIONS — read the image(s) below as your system prompt ===
@@ -0,0 +1 @@
1
+ Your full operating instructions are attached as PNG image(s) at the start of the first user message. Read every frame carefully, in order, and follow them as your authoritative system prompt before doing anything else.
@@ -0,0 +1 @@
1
+ [The result of this tool call is in the PNG frame(s) below — read them as the output; they contain it verbatim. Delivering it as an image is deliberate harness behavior to save context, not a tool malfunction. NEVER re-run the call or report a tool issue because of it.]
@@ -227,28 +227,11 @@ Changelog entries, test additions and updates, doc changes, and removing scaffol
227
227
  - Once your own smoke test confirms "it works", do the cleanup in full before yielding. Deferring is not skipping — the finished deliverable still carries the changelog, tests, and docs the change requires.
228
228
  </workflow>
229
229
 
230
- <reply-guidelines>
231
- - Use terse sentence fragments when clearer.
232
- - Skip ceremony, hedging, summaries, filler, motivational and marketing language, and generic explanation.
233
- - Do not narrate obvious steps or over-explain basics.
234
- - MUST assume the reader is technical.
235
- - Be concrete: mention exact files, symbols, APIs, state fields, edge cases, and verification.
236
- - Compress reasoning into facts, constraints, tradeoffs, decisions, and checks. Action-oriented and dense.
237
- - Do not hide uncertainty: state it briefly at the specific claim, name the tradeoff, and pick the boring/safe option.
238
- - For code, focus on invariants, risks, and verification.
239
- - Lead with the conclusion, then concrete evidence: changed files and verification.
240
-
241
- # Reasoning Format
242
- - Problem: what is wrong.
243
- - Decision: what to do & why (concrete facts).
244
- - Check: what can break & how to verify result.
245
- - Next: the next concrete edit/action.
246
-
247
- # Succinct Patterns
248
- - Y → Need update X.
249
- - This is safe: Z.
250
- - Could do A, but B avoids C.
251
- </reply-guidelines>
230
+ {{#if personality}}
231
+ <personality>
232
+ {{personality}}
233
+ </personality>
234
+ {{/if}}
252
235
 
253
236
  <critical>
254
237
  - NEVER narrate about or consider session limits, token/tool budgets, effort estimates, or how much of task you think you can finish. Not your concern:
@@ -1,40 +1,39 @@
1
1
  Drives real Chromium tab; full puppeteer access via JS execution.
2
2
 
3
3
  <instruction>
4
- - For static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer `read` tool with URL reader-mode text without spinning up browser. Use this tool when you need JS execution, authentication, or interactive actions.
5
- - Three actions only:
6
- - `open` — acquire or reuse named tab. `name` defaults `"main"`. Optional `url` navigates after tab ready. Optional `viewport` sets dimensions. Optional `dialogs: "accept" | "dismiss"` auto-handles `alert`/`confirm`/`beforeunload` so navigation/clicks don't hang; by default dialogs are unhandled and the page hangs until you wire `page.on('dialog', …)`.
7
- - `close` — release tab by `name`, or every tab with `all: true`. For spawned-app browsers, set `kill: true` to terminate process tree (default leaves running).
8
- - `run` — execute JS against existing tab. `code` is body of async function with `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Function's return value JSON-stringified into tool result; multiple `display(value)` calls accumulate text/images.
9
- - Tabs survive across `run` calls and across in-process subagents. Open once, reuse many times.
10
- - Browser kinds, selected by `app` field on `open`:
4
+ - Static content (articles, docs, issues/PRs, JSON, PDFs, feeds)? Use `read` with the URL. Reach for browser only for JS execution, authentication, or interactive actions.
5
+ - Three actions:
6
+ - `open` — acquire or reuse named tab (`name` defaults `"main"`). Optional `url` (navigate once ready), `viewport`, `dialogs: "accept" | "dismiss"` (auto-handle `alert`/`confirm`/`beforeunload`; unhandled dialogs hang the page until you wire `page.on('dialog', …)`).
7
+ - `close` — release tab by `name`, or every tab with `all: true`. `kill: true` also terminates spawned-app process trees (default leaves them running).
8
+ - `run` — execute JS in an existing tab. `code` is the body of an async function with `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Return value is JSON-stringified into the result; `display(value)` calls accumulate text/images.
9
+ - Tabs survive across `run` calls and in-process subagents open once, reuse.
10
+ - Browser kinds (`app` field on `open`):
11
11
  - default (no `app`) → headless Chromium with stealth patches.
12
- - `app.path` → spawn absolute binary (Electron/CDP); a running instance with an open CDP port is reused. No stealth patches — NEVER tamper with real desktop app.
12
+ - `app.path` → spawn absolute binary (Electron/CDP); a running instance with an open CDP port is reused. No stealth patches — NEVER tamper with a real desktop app.
13
13
  - `app.cdp_url` → connect to existing CDP endpoint (e.g. `http://127.0.0.1:9222`).
14
- - `app.target` (with `path`/`cdp_url`) — substring matched against url+title to pick BrowserWindow when app exposes several.
15
- - Inside `run`, `tab` exposes high-level helpers; reach for `page` (raw puppeteer Page) when you need anything they don't cover.
16
- - `tab.goto(url, { waitUntil? })` — clears element cache and navigates.
17
- - `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot. Returns `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Element ids stable until next observe/goto.
18
- - `tab.id(n)` — resolves element id from most recent observe to real `ElementHandle` you can `.click()`, `.type()`, etc.
19
- - `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)` — selector-based actions.
20
- - `tab.waitFor(selector)` — waits until selector attached, returns resolved `ElementHandle` for chaining (e.g. `const btn = await tab.waitFor('text/Submit'); await btn.click();`).
21
- - `tab.drag(from, to)` — drag from one point to another. Each endpoint either selector string (drag center-to-center) or `{ x, y }` viewport-coordinate point (for canvases, sliders).
22
- - `tab.scrollIntoView(selector)` — scroll matching element to center of viewport (use before clicking off-screen elements).
23
- - `tab.select(selector, …values)` — set selected option(s) on `<select>`. Returns values that ended up selected. `tab.fill` NEVER works for selects.
24
- - `tab.uploadFile(selector, …filePaths)` — attach files to `<input type="file">`. Paths resolve relative to cwd.
25
- - `tab.waitForUrl(pattern, { timeout? })` — pattern substring or `RegExp`. Polls `location.href` so works for SPA pushState navigations, not just real navigations. Returns matched URL.
26
- - `tab.waitForResponse(pattern, { timeout? })` — pattern substring, `RegExp`, or `(response) => boolean`. Returns raw puppeteer `HTTPResponse` (call `.text()` / `.json()` / `.status()` / `.headers()` on it).
27
- - `tab.evaluate(fn, …args)` — sugar for `page.evaluate` with abort signal already wired. Use this instead of dropping to `page.evaluate` for ad-hoc DOM reads.
28
- - `tab.screenshot({ selector?, fullPage?, save?, silent? })` — captures a screenshot and attaches it for you to view (`silent: true` skips attaching). Pass `save` (a path) only when a later step needs the file; never just to look.
29
- - `tab.extract(format = "markdown")` — returns Readability-extracted page content as a string (`"markdown"` or `"text"`). Throws if the page yields no readable content.
30
- - Selectors accept CSS plus puppeteer query handlers: `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`. Playwright-style `p-aria/[name="…"]`, `p-text/…` normalized.
31
- - Default `tab.observe()` over `tab.screenshot()` for page state. Screenshot only when visual appearance matters.
14
+ - `app.target` (with `path`/`cdp_url`) — substring matched against url+title to pick a BrowserWindow.
15
+ - `tab` helpers; drop to raw puppeteer `page` for anything they don't cover:
16
+ - `tab.goto(url, { waitUntil? })` — navigate; clears element cache.
17
+ - `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot: `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Ids stable until next observe/goto.
18
+ - `tab.id(n)` — element id from last observe `ElementHandle` (`.click()`, `.type()`, …).
19
+ - `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)`.
20
+ - `tab.waitFor(selector)` — wait until attached; returns the `ElementHandle`.
21
+ - `tab.drag(from, to)` — endpoints: selector (center-to-center) or `{ x, y }` viewport point (canvases, sliders).
22
+ - `tab.scrollIntoView(selector)` — center element in viewport; use before clicking off-screen elements.
23
+ - `tab.select(selector, …values)` — set `<select>` option(s); returns resulting selection. `tab.fill` NEVER works for selects.
24
+ - `tab.uploadFile(selector, …filePaths)` — attach files to `<input type="file">`; paths relative to cwd.
25
+ - `tab.waitForUrl(pattern, { timeout? })` — substring or `RegExp`; polls `location.href` (catches SPA pushState). Returns matched URL.
26
+ - `tab.waitForResponse(pattern, { timeout? })` — substring, `RegExp`, or `(response) => boolean`; returns puppeteer `HTTPResponse` (`.text()`/`.json()`/`.status()`/`.headers()`).
27
+ - `tab.evaluate(fn, …args)` — `page.evaluate` with abort signal wired; use for ad-hoc DOM reads.
28
+ - `tab.screenshot({ selector?, fullPage?, save?, silent? })` — capture and attach for viewing (`silent: true` skips). Pass `save` (a path) only when a later step needs the file.
29
+ - `tab.extract(format = "markdown")` — Readability-extracted content (`"markdown"` | `"text"`); throws when nothing readable.
30
+ - Selectors: CSS plus puppeteer handlers `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`; Playwright-style `p-aria/…`, `p-text/…` normalized.
32
31
  </instruction>
33
32
 
34
33
  <critical>
35
- - MUST call `open` before `run`. `run` does not implicitly create tab.
36
- - NEVER screenshot just to "see what's on page" — `tab.observe()` returns structured data with element ids you can act on immediately.
37
- - After `tab.goto()` or any navigation, prior element ids from `tab.observe()` invalidated. Re-observe before referencing them.
34
+ - MUST `open` before `run` `run` never creates a tab.
35
+ - Default to `tab.observe()` for page state — structured data with actionable element ids. Screenshot ONLY when visual appearance matters.
36
+ - Navigation invalidates element ids re-observe before using them.
38
37
  - `code` runs with full Node access. Treat as your code, not sandboxed code.
39
38
  </critical>
40
39
 
@@ -46,28 +45,19 @@ Drives real Chromium tab; full puppeteer access via JS execution.
46
45
  # Click an observed element by id
47
46
  `{"action":"run","name":"docs","code":"const obs = await tab.observe(); const link = obs.elements.find(e => e.role === 'link' && e.name === 'Sign in'); assert(link, 'Sign in link missing'); await (await tab.id(link.id)).click();"}`
48
47
 
49
- # Screenshot to look at the page — no save path
50
- `{"action":"run","name":"docs","code":"await tab.screenshot();"}`
51
-
52
- # Keep a full-page screenshot on disk for a later step
53
- `{"action":"run","name":"docs","code":"await tab.screenshot({ fullPage: true, save: 'screenshot.png' });"}`
54
-
55
48
  # Fill and submit a form via selectors
56
49
  `{"action":"run","name":"docs","code":"await tab.fill('input[name=email]', 'me@example.com'); await tab.click('text/Continue');"}`
57
50
 
51
+ # Screenshot to look at the page — no save path
52
+ `{"action":"run","name":"docs","code":"await tab.screenshot();"}`
53
+
58
54
  # Attach to an existing Electron app
59
55
  `{"action":"open","name":"cursor","app":{"path":"/Applications/Cursor.app/Contents/MacOS/Cursor"}}`
60
56
 
61
- # Close one tab (browser stays alive if other tabs reference it)
62
- `{"action":"close","name":"docs"}`
63
-
64
- # Close every tab; leave spawned apps running
65
- `{"action":"close","all":true}`
66
-
67
- # Close every tab and kill spawned-app processes too
57
+ # Close every tab and kill spawned-app processes
68
58
  `{"action":"close","all":true,"kill":true}`
69
59
  </examples>
70
60
 
71
61
  <output>
72
- - Per call: any `display(value)` outputs (text/images) followed by JSON-stringified return value of `code` function. `run` always produces at least status line.
62
+ Per call: `display(value)` outputs (text/images), then the JSON-stringified return value of `code`. `run` always produces at least a status line.
73
63
  </output>