@oh-my-pi/pi-coding-agent 16.0.11 → 16.1.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 (71) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/cli.js +3166 -3202
  3. package/dist/types/config/settings-schema.d.ts +40 -39
  4. package/dist/types/lsp/types.d.ts +5 -3
  5. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +1 -0
  6. package/dist/types/modes/components/assistant-message.d.ts +8 -0
  7. package/dist/types/modes/components/cache-invalidation-marker.d.ts +39 -0
  8. package/dist/types/modes/components/compaction-summary-message.d.ts +14 -1
  9. package/dist/types/modes/components/index.d.ts +0 -1
  10. package/dist/types/modes/components/message-frame.d.ts +6 -4
  11. package/dist/types/modes/interactive-mode.d.ts +2 -1
  12. package/dist/types/modes/theme/theme.d.ts +7 -1
  13. package/dist/types/modes/types.d.ts +7 -1
  14. package/dist/types/sdk.d.ts +1 -1
  15. package/dist/types/session/agent-session.d.ts +20 -1
  16. package/dist/types/session/session-context.d.ts +7 -0
  17. package/dist/types/session/session-dump-format.d.ts +1 -0
  18. package/dist/types/session/tool-choice-queue.d.ts +14 -0
  19. package/dist/types/system-prompt.d.ts +3 -3
  20. package/dist/types/tools/index.d.ts +4 -0
  21. package/dist/types/tools/resolve.d.ts +15 -5
  22. package/package.json +12 -12
  23. package/src/config/settings-schema.ts +48 -39
  24. package/src/config/settings.ts +40 -0
  25. package/src/debug/log-viewer.ts +4 -4
  26. package/src/debug/raw-sse.ts +4 -4
  27. package/src/edit/renderer.ts +2 -2
  28. package/src/internal-urls/docs-index.generated.txt +1 -1
  29. package/src/lsp/client.ts +9 -9
  30. package/src/lsp/render.ts +7 -7
  31. package/src/lsp/types.ts +6 -3
  32. package/src/modes/components/__tests__/skill-message.test.ts +92 -0
  33. package/src/modes/components/agent-dashboard.ts +1 -1
  34. package/src/modes/components/assistant-message.ts +21 -0
  35. package/src/modes/components/cache-invalidation-marker.ts +94 -0
  36. package/src/modes/components/chat-transcript-builder.ts +16 -2
  37. package/src/modes/components/compaction-summary-message.ts +29 -1
  38. package/src/modes/components/custom-message.ts +4 -1
  39. package/src/modes/components/dynamic-border.ts +1 -1
  40. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  41. package/src/modes/components/extensions/inspector-panel.ts +5 -5
  42. package/src/modes/components/hook-selector.ts +2 -2
  43. package/src/modes/components/index.ts +0 -1
  44. package/src/modes/components/message-frame.ts +10 -6
  45. package/src/modes/components/model-selector.ts +2 -2
  46. package/src/modes/components/overlay-box.ts +10 -9
  47. package/src/modes/components/settings-defs.ts +7 -0
  48. package/src/modes/components/skill-message.ts +39 -19
  49. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  50. package/src/modes/components/welcome.ts +1 -1
  51. package/src/modes/controllers/event-controller.ts +14 -0
  52. package/src/modes/controllers/selector-controller.ts +7 -0
  53. package/src/modes/interactive-mode.ts +9 -1
  54. package/src/modes/theme/theme.ts +14 -0
  55. package/src/modes/types.ts +7 -1
  56. package/src/modes/utils/ui-helpers.ts +20 -2
  57. package/src/prompts/steering/user-interjection.md +3 -4
  58. package/src/sdk.ts +8 -6
  59. package/src/session/agent-session.ts +96 -23
  60. package/src/session/messages.ts +7 -9
  61. package/src/session/session-context.ts +54 -7
  62. package/src/session/session-dump-format.ts +3 -1
  63. package/src/session/snapcompact-inline.ts +2 -2
  64. package/src/session/tool-choice-queue.ts +59 -0
  65. package/src/system-prompt.ts +10 -9
  66. package/src/tools/bash-interactive.ts +4 -4
  67. package/src/tools/index.ts +4 -0
  68. package/src/tools/resolve.ts +66 -41
  69. package/src/tui/output-block.ts +9 -9
  70. package/dist/types/modes/components/branch-summary-message.d.ts +0 -13
  71. package/src/modes/components/branch-summary-message.ts +0 -46
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Shared box-drawing chrome for fullscreen overlays (the `/copy` picker, the
3
- * plan-review overlay, …). Every helper paints with `theme.boxSharp` glyphs and
4
- * the `border`/`accent` theme colors so all outlined overlays read identically.
3
+ * plan-review overlay, …). Every helper paints with `theme.boxRound` glyphs
4
+ * (rounded corners, sharp tee/cross junctions) and the `border`/`accent` theme
5
+ * colors so all outlined overlays read identically.
5
6
  */
6
7
  import { padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
7
8
  import { theme } from "../theme/theme";
@@ -23,7 +24,7 @@ function paint(s: string): string {
23
24
 
24
25
  /** Top border with an optional accent-colored title inset into the rule. */
25
26
  export function topBorder(width: number, title: string): string {
26
- const box = theme.boxSharp;
27
+ const box = theme.boxRound;
27
28
  const inner = Math.max(0, width - 2);
28
29
  if (!title) return paint(box.topLeft + box.horizontal.repeat(inner) + box.topRight);
29
30
  const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
@@ -37,18 +38,18 @@ export function topBorder(width: number, title: string): string {
37
38
 
38
39
  /** A horizontal rule with left/right tees, splitting overlay sections. */
39
40
  export function divider(width: number): string {
40
- const box = theme.boxSharp;
41
+ const box = theme.boxRound;
41
42
  return paint(box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
42
43
  }
43
44
 
44
45
  export function bottomBorder(width: number): string {
45
- const box = theme.boxSharp;
46
+ const box = theme.boxRound;
46
47
  return paint(box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
47
48
  }
48
49
 
49
50
  /** Wrap pre-styled content in vertical borders with single-column insets. */
50
51
  export function row(content: string, width: number): string {
51
- const box = theme.boxSharp;
52
+ const box = theme.boxRound;
52
53
  return `${paint(box.vertical)} ${fit(content, Math.max(0, width - 4))} ${paint(box.vertical)}`;
53
54
  }
54
55
 
@@ -70,7 +71,7 @@ export function splitBodyWidth(width: number, sidebarWidth: number): number {
70
71
 
71
72
  /** Top border carrying the title, split by a `┬` over the column divider. */
72
73
  export function topBorderSplit(width: number, title: string, sidebarWidth: number): string {
73
- const box = theme.boxSharp;
74
+ const box = theme.boxRound;
74
75
  const dividerCol = splitDividerCol(sidebarWidth);
75
76
  const leftLen = Math.max(0, dividerCol - 1);
76
77
  const rightLen = Math.max(0, width - 2 - dividerCol);
@@ -90,7 +91,7 @@ export function topBorderSplit(width: number, title: string, sidebarWidth: numbe
90
91
 
91
92
  /** Section rule that closes the sidebar column with a `┴` over the divider. */
92
93
  export function dividerSplit(width: number, sidebarWidth: number): string {
93
- const box = theme.boxSharp;
94
+ const box = theme.boxRound;
94
95
  const dividerCol = splitDividerCol(sidebarWidth);
95
96
  const leftLen = Math.max(0, dividerCol - 1);
96
97
  const rightLen = Math.max(0, width - 2 - dividerCol);
@@ -101,7 +102,7 @@ export function dividerSplit(width: number, sidebarWidth: number): string {
101
102
 
102
103
  /** A two-column content row: `│ sidebar │ body │`, each inset by one column. */
103
104
  export function splitRow(sidebar: string, body: string, width: number, sidebarWidth: number): string {
104
- const box = theme.boxSharp;
105
+ const box = theme.boxRound;
105
106
  const bodyWidth = splitBodyWidth(width, sidebarWidth);
106
107
  const bar = paint(box.vertical);
107
108
  return `${bar} ${fit(sidebar, sidebarWidth)} ${bar} ${fit(body, bodyWidth)} ${bar}`;
@@ -76,6 +76,13 @@ export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef
76
76
 
77
77
  const CONDITIONS: Record<string, () => boolean> = {
78
78
  hasImageProtocol: () => !!TERMINAL.imageProtocol,
79
+ advisorEnabled: () => {
80
+ try {
81
+ return Settings.instance.get("advisor.enabled") === true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ },
79
86
  hindsightActive: () => {
80
87
  try {
81
88
  return Settings.instance.get("memory.backend") === "hindsight";
@@ -3,6 +3,8 @@ import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
4
4
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
5
5
  import type { CustomMessage, SkillPromptDetails } from "../../session/messages";
6
+ import { shortenPath } from "../../tools/render-utils";
7
+ import { fileHyperlink } from "../../tui";
6
8
 
7
9
  export class SkillMessageComponent extends Container {
8
10
  #box: Box;
@@ -38,25 +40,26 @@ export class SkillMessageComponent extends Container {
38
40
  this.removeChild(this.#box);
39
41
  this.addChild(this.#box);
40
42
  this.#box.clear();
41
-
42
- const label = theme.fg("customMessageLabel", theme.bold("[skill]"));
43
- this.#box.addChild(new Text(label, 0, 0));
44
- this.#box.addChild(new Spacer(1));
43
+ // Re-read symbols every rebuild so a runtime theme/preset switch refreshes the outline.
44
+ this.#box.setBorder({ chars: theme.boxRound, color: t => theme.fg("borderMuted", t) });
45
45
 
46
46
  const details = this.message.details;
47
- const args = details?.args?.trim();
48
- const infoLines = [
49
- `Skill: ${details?.name ?? "unknown"}`,
50
- args ? `Args: ${args}` : undefined,
51
- details?.path ? `Path: ${details.path}` : undefined,
52
- typeof details?.lineCount === "number" ? `Prompt: ${details.lineCount} lines` : undefined,
53
- ].filter((line): line is string => Boolean(line));
54
-
55
- this.#box.addChild(
56
- new Markdown(infoLines.join("\n"), 0, 0, getMarkdownTheme(), {
57
- color: (value: string) => theme.fg("customMessageText", value),
58
- }),
59
- );
47
+ const name = details?.name?.trim() || "unknown";
48
+ // Collapse args to one line: a stray newline/tab in user-supplied args would split the header.
49
+ const args = details?.args?.replace(/\s+/g, " ").trim() ?? "";
50
+
51
+ // Header: icon-tag + skill name, with the invocation args trailing dimmed.
52
+ const tag = theme.fg("customMessageLabel", theme.bold(`${theme.icon.extensionSkill} skill`));
53
+ let header = `${tag} ${theme.fg("customMessageText", theme.bold(name))}`;
54
+ if (args) {
55
+ header += ` ${theme.fg("dim", args)}`;
56
+ }
57
+ this.#box.addChild(new Text(header, 0, 0));
58
+
59
+ const meta = this.#metaLine(details);
60
+ if (meta) {
61
+ this.#box.addChild(new Text(meta, 0, 0));
62
+ }
60
63
 
61
64
  if (!this.#expanded) {
62
65
  return;
@@ -68,8 +71,7 @@ export class SkillMessageComponent extends Container {
68
71
  }
69
72
 
70
73
  this.#box.addChild(new Spacer(1));
71
- const promptHeader = theme.fg("customMessageLabel", theme.bold("Prompt"));
72
- this.#box.addChild(new Text(promptHeader, 0, 0));
74
+ this.#box.addChild(new Text(theme.fg("muted", "prompt"), 0, 0));
73
75
  this.#box.addChild(new Spacer(1));
74
76
 
75
77
  this.#contentComponent = new Markdown(text, 0, 0, getMarkdownTheme(), {
@@ -78,6 +80,24 @@ export class SkillMessageComponent extends Container {
78
80
  this.#box.addChild(this.#contentComponent);
79
81
  }
80
82
 
83
+ /** Sub-line under the header: home-shortened (clickable) accent path · muted prompt size. */
84
+ #metaLine(details: SkillPromptDetails | undefined): string | undefined {
85
+ const parts: string[] = [];
86
+
87
+ const filePath = details?.path;
88
+ if (filePath) {
89
+ parts.push(fileHyperlink(filePath, theme.fg("accent", shortenPath(filePath)), { line: 1 }));
90
+ }
91
+ if (typeof details?.lineCount === "number") {
92
+ parts.push(theme.fg("muted", `${details.lineCount} ${details.lineCount === 1 ? "line" : "lines"}`));
93
+ }
94
+
95
+ if (parts.length === 0) {
96
+ return undefined;
97
+ }
98
+ return ` ${parts.join(theme.fg("muted", theme.sep.dot))}`;
99
+ }
100
+
81
101
  #extractText(): string {
82
102
  if (typeof this.message.content === "string") {
83
103
  return this.message.content;
@@ -74,7 +74,7 @@ export class TinyTitleDownloadProgressComponent implements Component {
74
74
  render(width: number): readonly string[] {
75
75
  width = Math.max(1, width);
76
76
  const spec = getTinyTitleModelSpec(this.#modelKey);
77
- const border = theme.fg("border", theme.boxSharp.horizontal.repeat(width));
77
+ const border = theme.fg("border", theme.boxRound.horizontal.repeat(width));
78
78
  const status = statusLabel(this.#event);
79
79
  const file = currentFile(this.#event);
80
80
  const pct =
@@ -308,7 +308,7 @@ export class WelcomeComponent implements Component {
308
308
  }
309
309
  // Bottom border
310
310
  if (showRightColumn) {
311
- lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
311
+ lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxRound.teeUp) + h.repeat(rightCol) + br);
312
312
  } else {
313
313
  lines.push(bl + h.repeat(leftCol) + br);
314
314
  }
@@ -5,6 +5,7 @@ import { extractTextContent } from "../../commit/utils";
5
5
  import { settings } from "../../config/settings";
6
6
  import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
7
7
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
8
+ import { detectCacheInvalidation } from "../../modes/components/cache-invalidation-marker";
8
9
  import {
9
10
  ReadToolGroupComponent,
10
11
  readArgsHaveTarget,
@@ -659,6 +660,16 @@ export class EventController {
659
660
  // waiting poll cannot be displaced anymore — freeze it in place.
660
661
  this.#resolveDisplaceablePoll();
661
662
  }
663
+ // Surface a prompt-cache invalidation: if the previous turn cached a
664
+ // meaningful prefix and this request read none of it back, flag the turn.
665
+ const usage = event.message.usage;
666
+ if (usage.cacheRead + usage.cacheWrite + usage.input > 0) {
667
+ if (settings.get("display.cacheMissMarker")) {
668
+ const invalidation = detectCacheInvalidation(this.ctx.lastAssistantUsage, usage);
669
+ if (invalidation) this.ctx.streamingComponent.setCacheInvalidation(invalidation);
670
+ }
671
+ this.ctx.lastAssistantUsage = usage;
672
+ }
662
673
  this.#lastAssistantComponent = this.ctx.streamingComponent;
663
674
  this.#lastAssistantComponent.markTranscriptBlockFinalized();
664
675
  if (settings.get("display.showTokenUsage")) {
@@ -969,12 +980,14 @@ export class EventController {
969
980
  }
970
981
  this.ctx.showWarning(event.errorMessage);
971
982
  } else if (!event.skipped) {
983
+ this.ctx.lastAssistantUsage = undefined;
972
984
  this.ctx.rebuildChatFromMessages();
973
985
  this.ctx.statusLine.invalidate();
974
986
  this.ctx.updateEditorTopBorder();
975
987
  this.ctx.showStatus("Auto-shake completed");
976
988
  }
977
989
  } else if (event.result) {
990
+ this.ctx.lastAssistantUsage = undefined;
978
991
  this.ctx.rebuildChatFromMessages();
979
992
  this.ctx.statusLine.invalidate();
980
993
  this.ctx.updateEditorTopBorder();
@@ -982,6 +995,7 @@ export class EventController {
982
995
  this.ctx.showWarning(event.errorMessage);
983
996
  } else if (isHandoffAction) {
984
997
  this.ctx.chatContainer.clear();
998
+ this.ctx.lastAssistantUsage = undefined;
985
999
  this.ctx.rebuildChatFromMessages();
986
1000
  this.ctx.statusLine.invalidate();
987
1001
  this.ctx.updateEditorTopBorder();
@@ -327,6 +327,13 @@ export class SelectorController {
327
327
  // InputController.toggleThinkingBlockVisibility).
328
328
  this.ctx.ui.resetDisplay();
329
329
  break;
330
+ case "display.cacheMissMarker":
331
+ // Rebuild re-runs the usage-based detection under the new setting so
332
+ // markers appear/disappear; full reset retires any already committed
333
+ // to native scrollback (mirrors hideThinking).
334
+ this.ctx.rebuildChatFromMessages();
335
+ this.ctx.ui.resetDisplay();
336
+ break;
330
337
  case "tui.tight":
331
338
  setTuiTight(value as boolean);
332
339
  this.ctx.ui.invalidate();
@@ -12,7 +12,7 @@ import {
12
12
  ThinkingLevel,
13
13
  } from "@oh-my-pi/pi-agent-core";
14
14
  import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
15
- import type { AssistantMessage, ImageContent, Message, Model, UsageReport } from "@oh-my-pi/pi-ai";
15
+ import type { AssistantMessage, ImageContent, Message, Model, Usage, UsageReport } from "@oh-my-pi/pi-ai";
16
16
  import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
17
17
  import type {
18
18
  Component,
@@ -412,6 +412,7 @@ export class InteractiveMode implements InteractiveModeContext {
412
412
  isPythonMode = false;
413
413
  streamingComponent: AssistantMessageComponent | undefined = undefined;
414
414
  streamingMessage: AssistantMessage | undefined = undefined;
415
+ lastAssistantUsage: Usage | undefined = undefined;
415
416
  loadingAnimation: Loader | undefined = undefined;
416
417
  autoCompactionLoader: Loader | undefined = undefined;
417
418
  retryLoader: Loader | undefined = undefined;
@@ -512,6 +513,7 @@ export class InteractiveMode implements InteractiveModeContext {
512
513
  this.compactionQueuedMessages = [];
513
514
  this.streamingComponent = undefined;
514
515
  this.streamingMessage = undefined;
516
+ this.lastAssistantUsage = undefined;
515
517
  this.pendingTools.clear();
516
518
  }
517
519
  readonly #uiHelpers: UiHelpers;
@@ -1858,6 +1860,9 @@ export class InteractiveMode implements InteractiveModeContext {
1858
1860
  this.#planModePreviousTools = previousTools;
1859
1861
  this.planModePlanFilePath = planFilePath;
1860
1862
  this.planModeEnabled = true;
1863
+ // Suppress cache-miss marker on the next turn: plan mode changes the system
1864
+ // prompt, which predictably invalidates the cache.
1865
+ this.lastAssistantUsage = undefined;
1861
1866
 
1862
1867
  await this.session.setActiveToolsByName(uniquePlanTools);
1863
1868
  this.session.setPlanModeState({
@@ -1975,6 +1980,9 @@ export class InteractiveMode implements InteractiveModeContext {
1975
1980
  this.session.setStandingResolveHandler?.(null);
1976
1981
  this.session.setPlanModeState(undefined);
1977
1982
  this.planModeEnabled = false;
1983
+ // Suppress cache-miss marker on the next turn: plan exit changes the system
1984
+ // prompt, which predictably invalidates the cache.
1985
+ this.lastAssistantUsage = undefined;
1978
1986
  this.planModePaused = options?.paused ?? false;
1979
1987
  this.planModePlanFilePath = undefined;
1980
1988
  this.#planModePreviousTools = undefined;
@@ -111,6 +111,7 @@ export type SymbolKey =
111
111
  | "icon.agents"
112
112
  | "icon.job"
113
113
  | "icon.cache"
114
+ | "icon.cacheMiss"
114
115
  | "icon.input"
115
116
  | "icon.output"
116
117
  | "icon.host"
@@ -310,6 +311,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
310
311
  "icon.agents": "👥",
311
312
  "icon.job": "⚙",
312
313
  "icon.cache": "💾",
314
+ "icon.cacheMiss": "⊘",
313
315
  "icon.input": "⤵",
314
316
  "icon.output": "⤴",
315
317
  "icon.host": "🖥",
@@ -579,6 +581,8 @@ const NERD_SYMBOLS: SymbolMap = {
579
581
  "icon.job": "\uf013",
580
582
  // pick:  | alt:  
581
583
  "icon.cache": "\uf1c0",
584
+ // pick: (fa-ban) | alt: ⊘
585
+ "icon.cacheMiss": "\uf05e",
582
586
  // pick:  | alt:  →
583
587
  "icon.input": "\uf090",
584
588
  // pick:  | alt:  →
@@ -810,6 +814,7 @@ const ASCII_SYMBOLS: SymbolMap = {
810
814
  "icon.agents": "AG",
811
815
  "icon.job": "bg",
812
816
  "icon.cache": "cache",
817
+ "icon.cacheMiss": "!",
813
818
  "icon.input": "in:",
814
819
  "icon.output": "out:",
815
820
  "icon.host": "host",
@@ -1711,6 +1716,14 @@ export class Theme {
1711
1716
  bottomRight: this.#symbols["boxRound.bottomRight"],
1712
1717
  horizontal: this.#symbols["boxRound.horizontal"],
1713
1718
  vertical: this.#symbols["boxRound.vertical"],
1719
+ // Junctions have no rounded Unicode variant, so a rounded box reuses the
1720
+ // sharp tee/cross glyphs. Sourcing them from the boxSharp.* tokens keeps a
1721
+ // theme's `boxSharp.tee*` overrides effective for rounded-box dividers.
1722
+ cross: this.#symbols["boxSharp.cross"],
1723
+ teeDown: this.#symbols["boxSharp.teeDown"],
1724
+ teeUp: this.#symbols["boxSharp.teeUp"],
1725
+ teeRight: this.#symbols["boxSharp.teeRight"],
1726
+ teeLeft: this.#symbols["boxSharp.teeLeft"],
1714
1727
  };
1715
1728
  }
1716
1729
 
@@ -1770,6 +1783,7 @@ export class Theme {
1770
1783
  agents: this.#symbols["icon.agents"],
1771
1784
  job: this.#symbols["icon.job"],
1772
1785
  cache: this.#symbols["icon.cache"],
1786
+ cacheMiss: this.#symbols["icon.cacheMiss"],
1773
1787
  input: this.#symbols["icon.input"],
1774
1788
  output: this.#symbols["icon.output"],
1775
1789
  host: this.#symbols["icon.host"],
@@ -1,6 +1,6 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
3
- import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
3
+ import type { AssistantMessage, ImageContent, Message, Usage, UsageReport } from "@oh-my-pi/pi-ai";
4
4
  import type { Component, Container, EditorTheme, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
5
5
  import type { CollabGuestLink } from "../collab/guest";
6
6
  import type { CollabHost } from "../collab/host";
@@ -159,6 +159,12 @@ export interface InteractiveModeContext {
159
159
  isPythonMode: boolean;
160
160
  streamingComponent: AssistantMessageComponent | undefined;
161
161
  streamingMessage: AssistantMessage | undefined;
162
+ /**
163
+ * Usage of the most recently rendered assistant turn, used to detect a
164
+ * prompt-cache invalidation on the next turn (cache footprint collapse).
165
+ * Reseeded by `renderSessionContext` on every rebuild/session switch.
166
+ */
167
+ lastAssistantUsage: Usage | undefined;
162
168
  loadingAnimation: Loader | undefined;
163
169
  autoCompactionLoader: Loader | undefined;
164
170
  retryLoader: Loader | undefined;
@@ -9,9 +9,10 @@ import { createAdvisorMessageCard } from "../../modes/components/advisor-message
9
9
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
10
10
  import { createBackgroundTanDispatchBlock } from "../../modes/components/background-tan-message";
11
11
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
12
- import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
12
+ import { detectCacheInvalidation } from "../../modes/components/cache-invalidation-marker";
13
13
  import { CollabPromptMessageComponent } from "../../modes/components/collab-prompt-message";
14
14
  import {
15
+ BranchSummaryMessageComponent,
15
16
  CompactionSummaryMessageComponent,
16
17
  createHandoffSummaryMessageComponent,
17
18
  } from "../../modes/components/compaction-summary-message";
@@ -358,6 +359,9 @@ export class UiHelpers {
358
359
  ): void {
359
360
  // Preserved: message_start handler owns this lifecycle (see #783)
360
361
  this.ctx.pendingTools.clear();
362
+ // Reseed the cache-invalidation baseline: this rebuild re-derives every
363
+ // turn's marker from usage, and the last turn becomes the live baseline.
364
+ this.ctx.lastAssistantUsage = undefined;
361
365
 
362
366
  if (options.updateFooter) {
363
367
  this.ctx.statusLine.invalidate();
@@ -399,13 +403,27 @@ export class UiHelpers {
399
403
  // updateResult armed.
400
404
  previous.seal();
401
405
  };
402
- for (const message of sessionContext.messages) {
406
+ const messages = sessionContext.messages;
407
+ const count = messages.length;
408
+ for (let i = 0; i < count; i++) {
409
+ const message = messages[i]!;
403
410
  if (message.role !== "toolResult") flushPendingUsage();
404
411
  // Assistant messages need special handling for tool calls
405
412
  if (message.role === "assistant") {
406
413
  this.ctx.addMessageToChat(message);
407
414
  const lastChild = this.ctx.chatContainer.children[this.ctx.chatContainer.children.length - 1];
408
415
  const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
416
+ if (assistantComponent) {
417
+ const usage = message.usage;
418
+ const explained = sessionContext.cacheMissExplainedAt?.[i] ?? false;
419
+ if (this.ctx.settings.get("display.cacheMissMarker") && !explained) {
420
+ const invalidation = detectCacheInvalidation(this.ctx.lastAssistantUsage, usage);
421
+ if (invalidation) assistantComponent.setCacheInvalidation(invalidation);
422
+ }
423
+ if (usage.cacheRead + usage.cacheWrite + usage.input > 0) {
424
+ this.ctx.lastAssistantUsage = usage;
425
+ }
426
+ }
409
427
  const hasVisibleAssistantContent = message.content.some(
410
428
  content =>
411
429
  (content.type === "text" && canonicalizeMessage(content.text)) ||
@@ -1,8 +1,7 @@
1
1
  <user_interjection>
2
- The user sent this message while you were working on the current task. It takes
3
- priority and supersedes your earlier plan wherever they conflict. Stop work that no
4
- longer matches their intent, re-read the request below, and adjust what you are doing
5
- now.
2
+ The user sent this message as an interjection while you were working. It takes
3
+ priority and supersedes earlier instructions wherever they conflict re-read it
4
+ and make sure your current work reflects their intent.
6
5
 
7
6
  <message>
8
7
  {{message}}
package/src/sdk.ts CHANGED
@@ -838,7 +838,7 @@ export interface BuildSystemPromptOptions {
838
838
  contextFiles?: Array<{ path: string; content: string }>;
839
839
  cwd?: string;
840
840
  appendPrompt?: string;
841
- repeatToolDescriptions?: boolean;
841
+ inlineToolDescriptors?: boolean;
842
842
  }
843
843
 
844
844
  /**
@@ -853,7 +853,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
853
853
  skills: options.skills,
854
854
  contextFiles: options.contextFiles,
855
855
  appendSystemPrompt: options.appendPrompt,
856
- repeatToolDescriptions: options.repeatToolDescriptions,
856
+ inlineToolDescriptors: options.inlineToolDescriptors,
857
857
  });
858
858
  }
859
859
 
@@ -2130,7 +2130,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2130
2130
  emitEvent: event => cursorEventEmitter?.(event),
2131
2131
  });
2132
2132
 
2133
- const repeatToolDescriptions = settings.get("repeatToolDescriptions");
2133
+ const inlineToolDescriptors = settings.get("inlineToolDescriptors");
2134
2134
  const eagerTasks = settings.get("task.eager") !== "default";
2135
2135
  const eagerTasksAlways = settings.get("task.eager") === "always";
2136
2136
  const intentField = $flag("PI_INTENT_TRACING", settings.get("tools.intentTracing")) ? INTENT_FIELD : undefined;
@@ -2198,7 +2198,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2198
2198
  }
2199
2199
  appendPrompt = parts.join("\n\n");
2200
2200
  }
2201
- // Owned/in-band tool dialect (non-native) repeats the catalog as `# Tool:`
2201
+ // Owned/in-band tool dialects (non-native) require the catalog as `# Tool:`
2202
2202
  // sections; native tool calling lets the compact name list suffice.
2203
2203
  const nativeTools = resolveDialect(settings.get("tools.format"), agent?.state.model ?? model) === undefined;
2204
2204
  const defaultPrompt = await buildSystemPromptInternal({
@@ -2211,7 +2211,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2211
2211
  alwaysApplyRules,
2212
2212
  skillsSettings: settings.getGroup("skills"),
2213
2213
  appendSystemPrompt: appendPrompt,
2214
- repeatToolDescriptions,
2214
+ inlineToolDescriptors,
2215
2215
  nativeTools,
2216
2216
  intentField,
2217
2217
  mcpDiscoveryMode: hasDiscoverableTools,
@@ -2536,9 +2536,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2536
2536
  return result;
2537
2537
  },
2538
2538
  intentTracing: !!intentField,
2539
+ pruneToolDescriptions: inlineToolDescriptors,
2539
2540
  dialect: resolveDialect(settings.get("tools.format"), model),
2540
2541
  abortOnFabricatedToolResult: settings.get("tools.abortOnFabricatedResult"),
2541
- getToolChoice: () => session?.nextToolChoice(),
2542
+ getToolChoice: () => session?.nextToolChoiceDirective(),
2542
2543
  telemetry: options.telemetry,
2543
2544
  appendOnlyContext: model
2544
2545
  ? shouldEnableAppendOnlyContext(settings.get("provider.appendOnlyContext"), model)
@@ -2606,6 +2607,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2606
2607
  session = new AgentSession({
2607
2608
  advisorWatchdogPrompt,
2608
2609
  agent,
2610
+ pruneToolDescriptions: inlineToolDescriptors,
2609
2611
  thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
2610
2612
  sessionManager,
2611
2613
  settings,