@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
@@ -15,7 +15,7 @@ import type { ExecOptions, ExecResult } from "../../exec/exec";
15
15
  import type { HookUIContext } from "../../extensibility/hooks/types";
16
16
  import type { Theme } from "../../modes/theme/theme";
17
17
  import type { ReadonlySessionManager } from "../../session/session-manager";
18
- import type { TodoItem } from "../../tools/todo-write";
18
+ import type { TodoItem } from "../../tools/todo";
19
19
  /** Alias for clarity */
20
20
  export type CustomToolUIContext = HookUIContext;
21
21
  export type { ExecOptions, ExecResult } from "../../exec/exec";
@@ -59,6 +59,17 @@ export interface ExtensionUIDialogOptions {
59
59
  onExternalEditor?: () => void;
60
60
  /** Optional footer hint text rendered by interactive selector */
61
61
  helpText?: string;
62
+ /** Render a leading radio/checkbox marker before each markable option in
63
+ * select dialogs (matches the ask transcript). "radio" fills the cursor row
64
+ * for single-choice; "checkbox" reflects `checkedIndices` per row for
65
+ * multi-select. Options beyond `markableCount` keep the plain cursor. */
66
+ selectionMarker?: "radio" | "checkbox";
67
+ /** For `selectionMarker: "checkbox"`: option indices currently checked. */
68
+ checkedIndices?: readonly number[];
69
+ /** Number of leading options that receive a selection marker; the remaining
70
+ * trailing options (e.g. "Other"/"Done" actions) keep the plain cursor.
71
+ * Defaults to all options when `selectionMarker` is set. */
72
+ markableCount?: number;
62
73
  }
63
74
  /** Raw terminal input listener for extensions. */
64
75
  export type TerminalInputHandler = (data: string) => {
@@ -18,7 +18,7 @@ import type { ImageContent, TextContent, ToolResultMessage } from "@oh-my-pi/pi-
18
18
  import type { Rule } from "../capability/rule";
19
19
  import type { Goal, GoalModeState } from "../goals/state";
20
20
  import type { BranchSummaryEntry, CompactionEntry, SessionEntry } from "../session/session-manager";
21
- import type { TodoItem } from "../tools/todo-write";
21
+ import type { TodoItem } from "../tools/todo";
22
22
  /** Fired on initial session load */
23
23
  export interface SessionStartEvent {
24
24
  type: "session_start";
@@ -22,6 +22,7 @@ export * from "./modes/theme/theme";
22
22
  export * from "./sdk";
23
23
  export * from "./session/agent-session";
24
24
  export * from "./session/auth-storage";
25
+ export * from "./session/indexed-session-storage";
25
26
  export * from "./session/messages";
26
27
  export * from "./session/redis-session-storage";
27
28
  export * from "./session/session-dump-format";
@@ -12,15 +12,24 @@ interface FrameSink {
12
12
  }
13
13
  /**
14
14
  * Write a newline-delimited JSON-RPC frame to the subprocess's stdin sink,
15
- * swallowing synchronous errors so the caller can decide how to react.
15
+ * swallowing both synchronous throws and asynchronous rejections so the caller
16
+ * can decide how to react.
16
17
  *
17
- * Bun's `FileSink` may throw synchronously (most reliably on Windows) when
18
- * the read end of the pipe has been closed by a subprocess that exited
19
- * between read-loop ticks. Letting that throw escape an `async` method
20
- * surfaces as an unhandled promise rejection at the call site.
18
+ * Bun's `FileSink.write()`/`flush()` can fail two ways once the read end of the
19
+ * pipe has been closed by a subprocess that exited between read-loop ticks:
20
+ * - a synchronous throw (most reliably observed on Windows), and
21
+ * - a *rejected Promise* returned from `write()`/`flush()`, i.e. the EPIPE is
22
+ * surfaced asynchronously (note the `processTicksAndRejections` frame in the
23
+ * stack traces on #1710 and the follow-up report).
21
24
  *
22
- * Returns `true` when the frame was accepted by the sink, `false` when the
23
- * sink threw callers signal transport closure on `false`.
25
+ * A sibling `async` method's `try/catch` only catches the synchronous case; an
26
+ * un-awaited rejected Promise escapes as a fatal unhandled rejection. So we both
27
+ * catch the throw and neutralize any returned promise's rejection.
28
+ *
29
+ * Returns `true` when the frame was accepted synchronously, `false` when the
30
+ * sink threw — callers signal transport closure on `false`. An asynchronous
31
+ * failure cannot be reflected in the return value; it is neutralized here and
32
+ * the dead transport is detected by the read loop / request timeout instead.
24
33
  */
25
34
  export declare function writeFrame(stdin: FrameSink, frame: string): boolean;
26
35
  /**
@@ -1,5 +1,5 @@
1
1
  import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai";
2
- import { Container } from "@oh-my-pi/pi-tui";
2
+ import { Container, type ImageBudget } from "@oh-my-pi/pi-tui";
3
3
  import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
4
4
  /**
5
5
  * Component that renders a complete assistant message
@@ -9,7 +9,8 @@ export declare class AssistantMessageComponent extends Container {
9
9
  private hideThinkingBlock;
10
10
  private readonly onImageUpdate?;
11
11
  private readonly thinkingRenderers;
12
- constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, onImageUpdate?: (() => void) | undefined, thinkingRenderers?: readonly AssistantThinkingRenderer[]);
12
+ private readonly imageBudget?;
13
+ constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, onImageUpdate?: (() => void) | undefined, thinkingRenderers?: readonly AssistantThinkingRenderer[], imageBudget?: ImageBudget | undefined);
13
14
  invalidate(): void;
14
15
  setHideThinkingBlock(hide: boolean): void;
15
16
  setToolResultImages(toolCallId: string, images: ImageContent[]): void;
@@ -44,6 +44,17 @@ export interface HookSelectorOptions {
44
44
  /** Indices into the original options that cannot be selected: they render
45
45
  * dimmed, are skipped during navigation, and reject enter/timeout. */
46
46
  disabledIndices?: readonly number[];
47
+ /** Render a leading radio/checkbox marker before each markable option,
48
+ * matching the ask transcript. "radio" fills the marker on the cursor row
49
+ * (single-choice); "checkbox" reflects {@link checkedIndices} per row
50
+ * (multi-select). Options at or beyond {@link markableCount} keep the plain
51
+ * cursor prefix — used for trailing control rows like "Other"/"Done". */
52
+ selectionMarker?: "radio" | "checkbox";
53
+ /** For `selectionMarker: "checkbox"`: original-indices currently checked. */
54
+ checkedIndices?: readonly number[];
55
+ /** Number of leading options (original order) that receive a selection
56
+ * marker. Defaults to every option when {@link selectionMarker} is set. */
57
+ markableCount?: number;
47
58
  }
48
59
  export interface HookSelectorOption {
49
60
  label: string;
@@ -1,5 +1,5 @@
1
1
  import { Container } from "@oh-my-pi/pi-tui";
2
- import type { TodoItem } from "../../tools/todo-write";
2
+ import type { TodoItem } from "../../tools/todo";
3
3
  /**
4
4
  * Component that renders a todo completion reminder notification.
5
5
  * Shows when the agent stops with incomplete todos.
@@ -0,0 +1,36 @@
1
+ import { Container } from "@oh-my-pi/pi-tui";
2
+ /**
3
+ * Transcript container that freezes the rendered output of every block except
4
+ * the bottom-most (live) one on terminals where committed native scrollback is
5
+ * immutable.
6
+ *
7
+ * On ED3-risk terminals with an unobservable viewport (ghostty/kitty/iTerm2/…)
8
+ * the renderer cannot clear saved lines (`\x1b[3J` may yank a reader) or query
9
+ * whether the user has scrolled, so any block that re-lays-out *after* it has
10
+ * scrolled past the viewport leaves a stale duplicate above the live region
11
+ * (a finalized assistant message re-wrapping, a tool preview collapsing to its
12
+ * compact result, a late async tool completion). The renderer's only safe move
13
+ * for such an offscreen edit is to not repaint — which is correct only if the
14
+ * committed region never changes underneath it.
15
+ *
16
+ * This container provides that guarantee: a block's render is snapshotted while
17
+ * it is the live (bottom-most) block, and once a newer block is appended it
18
+ * replays the snapshot instead of recomputing. Mutations after a block leaves
19
+ * live are intentionally deferred until the next checkpoint {@link thaw} (prompt
20
+ * submit → native-scrollback rebuild), where the whole transcript is replayed
21
+ * and any drift reconciles safely. On terminals that can rebuild history this
22
+ * freezing is unnecessary, so it renders every block live for full fidelity.
23
+ */
24
+ export declare class TranscriptContainer extends Container {
25
+ #private;
26
+ invalidate(): void;
27
+ clear(): void;
28
+ /**
29
+ * Retire all frozen snapshots so the next render reflects each block's current
30
+ * state. Call at reconciliation checkpoints (prompt submit) where the whole
31
+ * transcript is replayed into native scrollback and any drift a frozen block
32
+ * accumulated is reconciled.
33
+ */
34
+ thaw(): void;
35
+ render(width: number): string[];
36
+ }
@@ -23,6 +23,7 @@ import type { HookInputComponent } from "./components/hook-input";
23
23
  import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
24
24
  import { StatusLineComponent } from "./components/status-line";
25
25
  import type { ToolExecutionHandle } from "./components/tool-execution";
26
+ import { TranscriptContainer } from "./components/transcript-container";
26
27
  import { type LoopLimitRuntime } from "./loop-limit";
27
28
  import { OAuthManualInputManager } from "./oauth-manual-input";
28
29
  import type { Theme } from "./theme/theme";
@@ -49,7 +50,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
49
50
  agent: Agent;
50
51
  historyStorage?: HistoryStorage;
51
52
  ui: TUI;
52
- chatContainer: Container;
53
+ chatContainer: TranscriptContainer;
53
54
  pendingMessagesContainer: Container;
54
55
  statusContainer: Container;
55
56
  todoContainer: Container;
@@ -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
  export type RpcCommand = {
15
15
  id?: string;
16
16
  type: "prompt";
@@ -6,7 +6,7 @@ export type SymbolPreset = "unicode" | "nerd" | "ascii";
6
6
  /**
7
7
  * All available symbol keys organized by category.
8
8
  */
9
- export type SymbolKey = "status.success" | "status.error" | "status.warning" | "status.info" | "status.pending" | "status.disabled" | "status.enabled" | "status.running" | "status.shadowed" | "status.aborted" | "nav.cursor" | "nav.selected" | "nav.expand" | "nav.collapse" | "nav.back" | "tree.branch" | "tree.last" | "tree.vertical" | "tree.horizontal" | "tree.hook" | "boxRound.topLeft" | "boxRound.topRight" | "boxRound.bottomLeft" | "boxRound.bottomRight" | "boxRound.horizontal" | "boxRound.vertical" | "boxSharp.topLeft" | "boxSharp.topRight" | "boxSharp.bottomLeft" | "boxSharp.bottomRight" | "boxSharp.horizontal" | "boxSharp.vertical" | "boxSharp.cross" | "boxSharp.teeDown" | "boxSharp.teeUp" | "boxSharp.teeRight" | "boxSharp.teeLeft" | "sep.powerline" | "sep.powerlineThin" | "sep.powerlineLeft" | "sep.powerlineRight" | "sep.powerlineThinLeft" | "sep.powerlineThinRight" | "sep.block" | "sep.space" | "sep.asciiLeft" | "sep.asciiRight" | "sep.dot" | "sep.slash" | "sep.pipe" | "icon.model" | "icon.plan" | "icon.goal" | "icon.pause" | "icon.loop" | "icon.folder" | "icon.scratchFolder" | "icon.file" | "icon.git" | "icon.branch" | "icon.pr" | "icon.tokens" | "icon.context" | "icon.cost" | "icon.time" | "icon.pi" | "icon.agents" | "icon.cache" | "icon.input" | "icon.output" | "icon.host" | "icon.session" | "icon.package" | "icon.warning" | "icon.rewind" | "icon.auto" | "icon.fast" | "icon.extensionSkill" | "icon.extensionTool" | "icon.extensionSlashCommand" | "icon.extensionMcp" | "icon.extensionRule" | "icon.extensionHook" | "icon.extensionPrompt" | "icon.extensionContextFile" | "icon.extensionInstruction" | "icon.mic" | "thinking.minimal" | "thinking.low" | "thinking.medium" | "thinking.high" | "thinking.xhigh" | "thinking.autoPending" | "checkbox.checked" | "checkbox.unchecked" | "format.bullet" | "format.dash" | "format.bracketLeft" | "format.bracketRight" | "md.quoteBorder" | "md.hrChar" | "md.bullet" | "md.colorSwatch" | "lang.default" | "lang.typescript" | "lang.javascript" | "lang.python" | "lang.rust" | "lang.go" | "lang.java" | "lang.c" | "lang.cpp" | "lang.csharp" | "lang.ruby" | "lang.php" | "lang.swift" | "lang.kotlin" | "lang.shell" | "lang.html" | "lang.css" | "lang.json" | "lang.yaml" | "lang.markdown" | "lang.sql" | "lang.docker" | "lang.lua" | "lang.text" | "lang.env" | "lang.toml" | "lang.xml" | "lang.ini" | "lang.conf" | "lang.log" | "lang.csv" | "lang.tsv" | "lang.image" | "lang.pdf" | "lang.archive" | "lang.binary" | "tab.appearance" | "tab.model" | "tab.interaction" | "tab.context" | "tab.editing" | "tab.tools" | "tab.memory" | "tab.tasks" | "tab.providers";
9
+ export type SymbolKey = "status.success" | "status.error" | "status.warning" | "status.info" | "status.pending" | "status.disabled" | "status.enabled" | "status.running" | "status.shadowed" | "status.aborted" | "nav.cursor" | "nav.selected" | "nav.expand" | "nav.collapse" | "nav.back" | "tree.branch" | "tree.last" | "tree.vertical" | "tree.horizontal" | "tree.hook" | "boxRound.topLeft" | "boxRound.topRight" | "boxRound.bottomLeft" | "boxRound.bottomRight" | "boxRound.horizontal" | "boxRound.vertical" | "boxSharp.topLeft" | "boxSharp.topRight" | "boxSharp.bottomLeft" | "boxSharp.bottomRight" | "boxSharp.horizontal" | "boxSharp.vertical" | "boxSharp.cross" | "boxSharp.teeDown" | "boxSharp.teeUp" | "boxSharp.teeRight" | "boxSharp.teeLeft" | "sep.powerline" | "sep.powerlineThin" | "sep.powerlineLeft" | "sep.powerlineRight" | "sep.powerlineThinLeft" | "sep.powerlineThinRight" | "sep.block" | "sep.space" | "sep.asciiLeft" | "sep.asciiRight" | "sep.dot" | "sep.slash" | "sep.pipe" | "icon.model" | "icon.plan" | "icon.goal" | "icon.pause" | "icon.loop" | "icon.folder" | "icon.scratchFolder" | "icon.file" | "icon.git" | "icon.branch" | "icon.pr" | "icon.tokens" | "icon.context" | "icon.cost" | "icon.time" | "icon.pi" | "icon.agents" | "icon.cache" | "icon.input" | "icon.output" | "icon.host" | "icon.session" | "icon.package" | "icon.warning" | "icon.rewind" | "icon.auto" | "icon.fast" | "icon.extensionSkill" | "icon.extensionTool" | "icon.extensionSlashCommand" | "icon.extensionMcp" | "icon.extensionRule" | "icon.extensionHook" | "icon.extensionPrompt" | "icon.extensionContextFile" | "icon.extensionInstruction" | "icon.mic" | "thinking.minimal" | "thinking.low" | "thinking.medium" | "thinking.high" | "thinking.xhigh" | "thinking.autoPending" | "checkbox.checked" | "checkbox.unchecked" | "radio.selected" | "radio.unselected" | "format.bullet" | "format.dash" | "format.bracketLeft" | "format.bracketRight" | "md.quoteBorder" | "md.hrChar" | "md.bullet" | "md.colorSwatch" | "lang.default" | "lang.typescript" | "lang.javascript" | "lang.python" | "lang.rust" | "lang.go" | "lang.java" | "lang.c" | "lang.cpp" | "lang.csharp" | "lang.ruby" | "lang.php" | "lang.swift" | "lang.kotlin" | "lang.shell" | "lang.html" | "lang.css" | "lang.json" | "lang.yaml" | "lang.markdown" | "lang.sql" | "lang.docker" | "lang.lua" | "lang.text" | "lang.env" | "lang.toml" | "lang.xml" | "lang.ini" | "lang.conf" | "lang.log" | "lang.csv" | "lang.tsv" | "lang.image" | "lang.pdf" | "lang.archive" | "lang.binary" | "tab.appearance" | "tab.model" | "tab.interaction" | "tab.context" | "tab.editing" | "tab.tools" | "tab.memory" | "tab.tasks" | "tab.providers";
10
10
  export type SpinnerType = "status" | "activity";
11
11
  export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" | "mdLink" | "mdLinkUrl" | "mdCode" | "mdCodeBlock" | "mdCodeBlockBorder" | "mdQuote" | "mdQuoteBorder" | "mdHr" | "mdListBullet" | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext" | "syntaxComment" | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator" | "syntaxPunctuation" | "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "bashMode" | "pythonMode" | "statusLineSep" | "statusLineModel" | "statusLinePath" | "statusLineGitClean" | "statusLineGitDirty" | "statusLineContext" | "statusLineSpend" | "statusLineStaged" | "statusLineDirty" | "statusLineUntracked" | "statusLineOutput" | "statusLineCost" | "statusLineSubagents";
12
12
  /** Check if a string is a valid ThemeColor value */
@@ -17,7 +17,22 @@ export declare class Theme {
17
17
  #private;
18
18
  private readonly mode;
19
19
  private readonly symbolPreset;
20
+ /**
21
+ * Perceptual luma (0..1) of the status-line background — used to classify the
22
+ * theme light/dark. Undefined when it can't be resolved. Classified against the
23
+ * status line (the surface session accents render on) rather than the chat bubble
24
+ * (`userMessageBg`), which some themes (e.g. `porcelain`) style dark on an
25
+ * otherwise-light theme.
26
+ */
27
+ readonly statusLineLuminance: number | undefined;
20
28
  constructor(fgColors: Record<ThemeColor, string | number>, bgColors: Record<ThemeBg, string | number>, mode: ColorMode, symbolPreset: SymbolPreset, symbolOverrides: Partial<Record<SymbolKey, string>>, spinnerFramesOverrides?: Partial<Record<SpinnerType, string[]>>);
29
+ /** True when the active theme has a light status-line background. */
30
+ get isLight(): boolean;
31
+ /**
32
+ * Surface luminance to size session accents against on light themes; undefined on
33
+ * dark themes so accents stay vivid. Pass straight to `getSessionAccentHex`.
34
+ */
35
+ get accentSurfaceLuminance(): number | undefined;
21
36
  fg(color: ThemeColor, text: string): string;
22
37
  bg(color: ThemeBg, text: string): string;
23
38
  bold(text: string): string;
@@ -167,6 +182,10 @@ export declare class Theme {
167
182
  checked: string;
168
183
  unchecked: string;
169
184
  };
185
+ get radio(): {
186
+ selected: string;
187
+ unselected: string;
188
+ };
170
189
  get format(): {
171
190
  bullet: string;
172
191
  dash: string;
@@ -0,0 +1,20 @@
1
+ import { type LocalProtocolOptions } from "../internal-urls";
2
+ /** The session's active plan, resolved for handoff into a subagent's context. */
3
+ export interface OverallPlanReference {
4
+ /** The `local://` reference path (e.g. `local://my-feature.md`), kept for display. */
5
+ path: string;
6
+ /** The full plan markdown, as written to disk. */
7
+ content: string;
8
+ }
9
+ /**
10
+ * Load the session's active overall plan for subagent handoff.
11
+ *
12
+ * Returns the plan referenced by `planReferencePath` when it exists on disk with
13
+ * non-empty content, or `undefined` when there is no plan (the file is absent or
14
+ * empty). This mirrors `AgentSession.#buildPlanReferenceMessage`'s gating so a
15
+ * subagent sees exactly the plan the main agent treats as its active reference.
16
+ *
17
+ * Callers MUST skip this during plan mode itself — read-only plan exploration
18
+ * uses a different prompt and a draft plan should not be handed off as approved.
19
+ */
20
+ export declare function loadOverallPlanReference(planReferencePath: string, localProtocolOptions: LocalProtocolOptions): Promise<OverallPlanReference | undefined>;
@@ -0,0 +1,12 @@
1
+ import { type ProtectedToolContext } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
2
+ /**
3
+ * Build a compaction protection matcher that keeps `read` results for the active
4
+ * plan file intact through prune/shake — the plan analog of skill-read
5
+ * protection. Matches both the canonical `local://PLAN.md` alias and the
6
+ * session's current plan reference path (e.g. a titled `local://<title>.md`), so
7
+ * the plan survives compaction whether the agent reads it by alias or by title.
8
+ *
9
+ * `getPlanReferencePath` is evaluated at match time so a mid-session retitle
10
+ * (plan approval renames `PLAN.md` → `<title>.md`) is honored immediately.
11
+ */
12
+ export declare function createPlanReadMatcher(getPlanReferencePath: () => string): (context: ProtectedToolContext) => boolean;
@@ -43,7 +43,7 @@ import { type SecretObfuscator } from "../secrets/obfuscator";
43
43
  import { type ConfiguredThinkingLevel } from "../thinking";
44
44
  import { type DiscoverableTool, type DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
45
45
  import type { CheckpointState } from "../tools/checkpoint";
46
- import { type TodoItem, type TodoPhase } from "../tools/todo-write";
46
+ import { type TodoItem, type TodoPhase } from "../tools/todo";
47
47
  import type { ClientBridge } from "./client-bridge";
48
48
  import { type CustomMessage } from "./messages";
49
49
  import type { BranchSummaryEntry, NewSessionOptions, SessionContext, SessionManager } from "./session-manager";
@@ -0,0 +1,59 @@
1
+ import type { SessionStorage, SessionStorageStat, SessionStorageWriter } from "./session-storage";
2
+ export interface SessionStorageIndexEntry {
3
+ path: string;
4
+ size: number;
5
+ mtimeMs: number;
6
+ }
7
+ export interface SessionStorageBackend {
8
+ init(): Promise<void>;
9
+ loadIndex(): Promise<Iterable<SessionStorageIndexEntry>>;
10
+ readFull(path: string): Promise<string | null>;
11
+ readSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]>;
12
+ writeFull(path: string, content: string, mtimeMs: number): Promise<void>;
13
+ append(path: string, line: string, mtimeMs: number): Promise<void>;
14
+ truncate(path: string, mtimeMs: number): Promise<void>;
15
+ remove(paths: string[]): Promise<void>;
16
+ move(src: string, dst: string, mtimeMs: number): Promise<void>;
17
+ }
18
+ export declare class IndexedSessionStorage implements SessionStorage {
19
+ #private;
20
+ constructor(backend: SessionStorageBackend);
21
+ initialize(): Promise<void>;
22
+ refresh(): Promise<void>;
23
+ drain(): Promise<void>;
24
+ ensureDirSync(_dir: string): void;
25
+ existsSync(path: string): boolean;
26
+ writeTextSync(path: string, content: string): void;
27
+ statSync(path: string): SessionStorageStat;
28
+ listFilesSync(dir: string, pattern: string): string[];
29
+ exists(path: string): Promise<boolean>;
30
+ readText(path: string): Promise<string>;
31
+ readTextSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]>;
32
+ writeText(path: string, content: string): Promise<void>;
33
+ rename(src: string, dst: string): Promise<void>;
34
+ unlink(path: string): Promise<void>;
35
+ deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
36
+ openWriter(path: string, options?: {
37
+ flags?: "a" | "w";
38
+ onError?: (err: Error) => void;
39
+ }): SessionStorageWriter;
40
+ _writerClosed(writer: IndexedSessionStorageWriter): void;
41
+ _truncateForWriter(path: string): number;
42
+ _queueTruncate(path: string, mtimeMs: number, getError?: () => Error | undefined): Promise<void>;
43
+ _appendForWriter(path: string, line: string): number;
44
+ _queueAppend(path: string, line: string, mtimeMs: number, getError?: () => Error | undefined): Promise<void>;
45
+ }
46
+ declare class IndexedSessionStorageWriter implements SessionStorageWriter {
47
+ #private;
48
+ constructor(storage: IndexedSessionStorage, path: string, options?: {
49
+ flags?: "a" | "w";
50
+ onError?: (err: Error) => void;
51
+ });
52
+ writeLineSync(line: string): void;
53
+ writeLine(line: string): Promise<void>;
54
+ flush(): Promise<void>;
55
+ fsync(): Promise<void>;
56
+ close(): Promise<void>;
57
+ getError(): Error | undefined;
58
+ }
59
+ export {};
@@ -1,4 +1,4 @@
1
- import type { SessionStorage, SessionStorageStat, SessionStorageWriter } from "./session-storage";
1
+ import { IndexedSessionStorage } from "./indexed-session-storage";
2
2
  /**
3
3
  * Minimal subset of the `bun:redis` `RedisClient` surface used by
4
4
  * {@link RedisSessionStorage}. Keeping the contract narrow (and accepting any
@@ -7,6 +7,8 @@ import type { SessionStorage, SessionStorageStat, SessionStorageWriter } from ".
7
7
  */
8
8
  export interface RedisSessionStorageClient {
9
9
  get(key: string): Promise<string | null>;
10
+ getrange(key: string, start: number, end: number): Promise<string>;
11
+ strlen(key: string): Promise<number>;
10
12
  set(key: string, value: string): Promise<unknown>;
11
13
  append(key: string, value: string): Promise<number>;
12
14
  del(...keys: string[]): Promise<number>;
@@ -26,7 +28,7 @@ export interface RedisSessionStorageOptions {
26
28
  */
27
29
  prefix?: string;
28
30
  /**
29
- * Maximum number of keys returned per SCAN batch when warming the mirror.
31
+ * Maximum number of keys returned per SCAN batch when warming the metadata index.
30
32
  * Default 500.
31
33
  */
32
34
  scanCount?: number;
@@ -34,91 +36,16 @@ export interface RedisSessionStorageOptions {
34
36
  /**
35
37
  * Redis-backed implementation of {@link SessionStorage}. Each session JSONL
36
38
  * file maps to a Redis STRING key, with per-key metadata (mtime) tracked in a
37
- * single sibling HASH. An in-memory mirror is loaded on construction so the
38
- * interface's synchronous methods (`existsSync`, `statSync`, `listFilesSync`,
39
- * `readTextSync`, `writeTextSync`) keep their contracts Bun's Redis client
40
- * is async only, and the persist hot path (`writer.writeLineSync`) cannot
41
- * wait on a network round-trip.
42
- *
43
- * Trade-offs vs `FileSessionStorage`:
44
- * - Mirror state is process-local. Two processes writing the same session key
45
- * will diverge until one of them reloads via {@link refresh}. This matches
46
- * `FileSessionStorage`'s existing single-writer assumption.
47
- * - `writeLineSync` updates the mirror synchronously and queues an async
48
- * `APPEND`. The promise is awaited by `flush()` / `close()` / {@link drain}.
49
- * A SIGKILL landing between the sync mirror update and the network round
50
- * trip loses the last line; the file-backed implementation survives that
51
- * window because bytes are handed to the kernel page cache before
52
- * returning.
53
- * - Blobs (image data) and tool artifact files still live on disk via
54
- * `BlobStore` / `ArtifactManager`. Those are out of scope for this storage.
39
+ * single sibling HASH. This process keeps only a metadata index (`size`,
40
+ * `mtimeMs`) in memory so synchronous `existsSync` / `statSync` /
41
+ * `listFilesSync` calls remain available without mirroring full content.
55
42
  */
56
- export declare class RedisSessionStorage implements SessionStorage {
57
- #private;
58
- private constructor();
43
+ export declare class RedisSessionStorage extends IndexedSessionStorage {
59
44
  /**
60
- * Warm the in-memory mirror with every existing session key under the
61
- * configured prefix and return the ready-to-use storage. Must be awaited
62
- * before passing the storage into `SessionManager.create()` so synchronous
63
- * lookups (session resume, recent sessions, EPERM-backup recovery) see
64
- * the existing keyspace.
45
+ * Warm the metadata index with every existing session key under the configured
46
+ * prefix and return the ready-to-use storage. Must be awaited before passing
47
+ * the storage into `SessionManager.create()` so synchronous lookups (session
48
+ * resume, recent sessions, EPERM-backup recovery) see the existing keyspace.
65
49
  */
66
50
  static create(options: RedisSessionStorageOptions): Promise<RedisSessionStorage>;
67
- /**
68
- * Re-scan Redis and replace the mirror's contents. Call this from a
69
- * different process that took over a session keyspace, or after an
70
- * out-of-band write made by another agent.
71
- */
72
- refresh(): Promise<void>;
73
- /**
74
- * Resolve once every pending background write (issued via `writeTextSync`
75
- * or `writer.writeLineSync`) has been acknowledged by Redis. Throws if any
76
- * background write failed since the last drain.
77
- *
78
- * Call this on graceful shutdown to avoid losing the last unflushed line.
79
- * The session-manager's own `flush()` / `close()` already drain through
80
- * the writer chain — this method exists for callers (test harnesses,
81
- * subprocess-style consumers) that bypass the writer.
82
- */
83
- drain(): Promise<void>;
84
- ensureDirSync(_dir: string): void;
85
- existsSync(path: string): boolean;
86
- writeTextSync(path: string, content: string): void;
87
- readTextSync(path: string): string;
88
- statSync(path: string): SessionStorageStat;
89
- listFilesSync(dir: string, pattern: string): string[];
90
- exists(path: string): Promise<boolean>;
91
- readText(path: string): Promise<string>;
92
- readTextPrefix(path: string, maxBytes: number): Promise<string>;
93
- writeText(path: string, content: string): Promise<void>;
94
- rename(src: string, dst: string): Promise<void>;
95
- unlink(path: string): Promise<void>;
96
- deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
97
- openWriter(path: string, options?: {
98
- flags?: "a" | "w";
99
- onError?: (err: Error) => void;
100
- }): SessionStorageWriter;
101
- _writerClosed(writer: RedisSessionStorageWriter): void;
102
- /** Mirror-only mutation, no Redis call. Used by writers to update local state synchronously. */
103
- _mirrorAppend(path: string, line: string): void;
104
- /** Mirror-only mutation, no Redis call. Used by writers opened with `flags: "w"` to truncate. */
105
- _mirrorTruncate(path: string): void;
106
- _remoteTruncate(path: string): Promise<void>;
107
- _remoteAppend(path: string, line: string): Promise<void>;
108
- /** Record a writer's pending promise on the storage-level tail so `drain()` waits for it. */
109
- _attachPending(promise: Promise<void>): void;
110
- }
111
- declare class RedisSessionStorageWriter implements SessionStorageWriter {
112
- #private;
113
- constructor(storage: RedisSessionStorage, path: string, options?: {
114
- flags?: "a" | "w";
115
- onError?: (err: Error) => void;
116
- });
117
- writeLineSync(line: string): void;
118
- writeLine(line: string): Promise<void>;
119
- flush(): Promise<void>;
120
- fsync(): Promise<void>;
121
- close(): Promise<void>;
122
- getError(): Error | undefined;
123
51
  }
124
- export {};
@@ -171,6 +171,21 @@ export interface SessionContext {
171
171
  }
172
172
  /** Lists session model strings to try when restoring, in fallback order. */
173
173
  export declare function getRestorableSessionModels(models: Readonly<Record<string, string>>, lastModelChangeRole: string | undefined): string[];
174
+ /**
175
+ * Coarse lifecycle status of a session, derived from its last persisted message.
176
+ *
177
+ * - `complete` — the last assistant turn ended with no unanswered tool calls, i.e.
178
+ * the agent yielded control back to the user.
179
+ * - `interrupted` — work was cut off mid-flight: a trailing assistant turn with
180
+ * pending tool calls, a trailing tool result the agent never continued from, or
181
+ * a length-truncated turn.
182
+ * - `aborted` — the last assistant turn was cancelled by the user.
183
+ * - `error` — the last assistant turn ended in an error.
184
+ * - `pending` — a trailing user message with no assistant reply persisted after it.
185
+ * - `unknown` — status could not be determined (empty/header-only session, or the
186
+ * final message was larger than the tail window that was read).
187
+ */
188
+ export type SessionStatus = "complete" | "interrupted" | "aborted" | "error" | "pending" | "unknown";
174
189
  export interface SessionInfo {
175
190
  path: string;
176
191
  id: string;
@@ -186,6 +201,11 @@ export interface SessionInfo {
186
201
  size: number;
187
202
  firstMessage: string;
188
203
  allMessagesText: string;
204
+ /**
205
+ * Coarse lifecycle status from the session's last persisted message. Optional:
206
+ * synthesized {@link SessionInfo}s (cross-project stubs, tests) leave it unset.
207
+ */
208
+ status?: SessionStatus;
189
209
  }
190
210
  export type ReadonlySessionManager = Pick<SessionManager, "getCwd" | "getSessionDir" | "getSessionId" | "getSessionFile" | "getSessionName" | "getArtifactsDir" | "getArtifactManager" | "allocateArtifactPath" | "saveArtifact" | "getArtifactPath" | "getLeafId" | "getLeafEntry" | "getEntry" | "getLabel" | "getBranch" | "getHeader" | "getEntries" | "getTree" | "getUsageStatistics" | "putBlob">;
191
211
  /** Exported for testing */
@@ -22,12 +22,12 @@ export interface SessionStorage {
22
22
  ensureDirSync(dir: string): void;
23
23
  existsSync(path: string): boolean;
24
24
  writeTextSync(path: string, content: string): void;
25
- readTextSync(path: string): string;
26
25
  statSync(path: string): SessionStorageStat;
27
26
  listFilesSync(dir: string, pattern: string): string[];
28
27
  exists(path: string): Promise<boolean>;
29
28
  readText(path: string): Promise<string>;
30
- readTextPrefix(path: string, maxBytes: number): Promise<string>;
29
+ /** Read the requested UTF-8 byte windows from the head and tail of the file. */
30
+ readTextSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]>;
31
31
  writeText(path: string, content: string): Promise<void>;
32
32
  rename(path: string, nextPath: string): Promise<void>;
33
33
  unlink(path: string): Promise<void>;
@@ -41,12 +41,11 @@ export declare class FileSessionStorage implements SessionStorage {
41
41
  ensureDirSync(dir: string): void;
42
42
  existsSync(path: string): boolean;
43
43
  writeTextSync(fpath: string, content: string): void;
44
- readTextSync(fpath: string): string;
45
44
  statSync(path: string): SessionStorageStat;
46
45
  listFilesSync(dir: string, pattern: string): string[];
47
46
  exists(path: string): Promise<boolean>;
48
47
  readText(path: string): Promise<string>;
49
- readTextPrefix(path: string, maxBytes: number): Promise<string>;
48
+ readTextSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]>;
50
49
  writeText(path: string, content: string): Promise<void>;
51
50
  rename(path: string, nextPath: string): Promise<void>;
52
51
  unlink(path: string): Promise<void>;
@@ -70,13 +69,12 @@ export declare class MemorySessionStorage implements SessionStorage {
70
69
  * creates the entry. External callers should go through `openWriter()`
71
70
  * rather than touching the mirror directly.
72
71
  */
73
- appendChunkSync(path: string, chunk: string): void;
74
- readTextSync(path: string): string;
72
+ appendSync(path: string, chunk: string): void;
75
73
  statSync(path: string): SessionStorageStat;
76
74
  listFilesSync(dir: string, pattern: string): string[];
77
75
  exists(path: string): Promise<boolean>;
78
76
  readText(path: string): Promise<string>;
79
- readTextPrefix(path: string, maxBytes: number): Promise<string>;
77
+ readTextSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]>;
80
78
  writeText(path: string, content: string): Promise<void>;
81
79
  rename(path: string, nextPath: string): Promise<void>;
82
80
  unlink(path: string): Promise<void>;