@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.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 (176) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  4. package/dist/types/commit/analysis/summary.d.ts +2 -2
  5. package/dist/types/commit/changelog/generate.d.ts +2 -2
  6. package/dist/types/commit/changelog/index.d.ts +2 -2
  7. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  8. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  10. package/dist/types/commit/model-selection.d.ts +10 -4
  11. package/dist/types/config/api-key-resolver.d.ts +34 -0
  12. package/dist/types/config/model-registry.d.ts +17 -1
  13. package/dist/types/config/settings-schema.d.ts +9 -0
  14. package/dist/types/dap/config.d.ts +14 -1
  15. package/dist/types/dap/types.d.ts +10 -0
  16. package/dist/types/lsp/utils.d.ts +3 -2
  17. package/dist/types/modes/components/chat-block.d.ts +64 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  19. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  20. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  21. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  22. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  23. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  27. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  28. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  29. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  30. package/dist/types/modes/interactive-mode.d.ts +15 -5
  31. package/dist/types/modes/theme/theme.d.ts +1 -1
  32. package/dist/types/modes/types.d.ts +18 -5
  33. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  34. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  35. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  36. package/dist/types/sdk.d.ts +2 -0
  37. package/dist/types/session/agent-session.d.ts +21 -0
  38. package/dist/types/session/messages.d.ts +12 -0
  39. package/dist/types/session/session-manager.d.ts +3 -1
  40. package/dist/types/slash-commands/types.d.ts +4 -6
  41. package/dist/types/task/executor.d.ts +7 -0
  42. package/dist/types/task/index.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +3 -2
  44. package/dist/types/tools/archive-reader.d.ts +5 -0
  45. package/dist/types/tools/ast-edit.d.ts +3 -0
  46. package/dist/types/tools/ast-grep.d.ts +3 -0
  47. package/dist/types/tools/bash.d.ts +1 -0
  48. package/dist/types/tools/find.d.ts +8 -4
  49. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  50. package/dist/types/tools/memory-render.d.ts +4 -1
  51. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  52. package/dist/types/tools/render-utils.d.ts +5 -9
  53. package/dist/types/tools/search.d.ts +4 -0
  54. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  55. package/dist/types/tools/todo.d.ts +3 -2
  56. package/dist/types/tools/write.d.ts +3 -0
  57. package/dist/types/tui/output-block.d.ts +16 -4
  58. package/dist/types/tui/status-line.d.ts +3 -0
  59. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  60. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  61. package/package.json +9 -9
  62. package/src/auto-thinking/classifier.ts +5 -1
  63. package/src/cli/dry-balance-cli.ts +52 -17
  64. package/src/cli/gallery-cli.ts +4 -1
  65. package/src/cli/gallery-fixtures/misc.ts +29 -0
  66. package/src/commit/analysis/conventional.ts +2 -2
  67. package/src/commit/analysis/summary.ts +2 -2
  68. package/src/commit/changelog/generate.ts +2 -2
  69. package/src/commit/changelog/index.ts +2 -2
  70. package/src/commit/map-reduce/index.ts +3 -3
  71. package/src/commit/map-reduce/map-phase.ts +2 -2
  72. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  73. package/src/commit/model-selection.ts +33 -9
  74. package/src/commit/pipeline.ts +4 -4
  75. package/src/config/api-key-resolver.ts +58 -0
  76. package/src/config/model-registry.ts +25 -2
  77. package/src/config/settings-schema.ts +10 -0
  78. package/src/config/settings.ts +20 -2
  79. package/src/dap/config.ts +41 -2
  80. package/src/dap/defaults.json +1 -0
  81. package/src/dap/session.ts +1 -0
  82. package/src/dap/types.ts +10 -0
  83. package/src/debug/index.ts +40 -54
  84. package/src/edit/renderer.ts +82 -78
  85. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  86. package/src/eval/llm-bridge.ts +8 -3
  87. package/src/goals/tools/goal-tool.ts +36 -26
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/lsp/utils.ts +3 -2
  90. package/src/main.ts +9 -7
  91. package/src/memories/index.ts +12 -5
  92. package/src/mnemopi/backend.ts +5 -1
  93. package/src/modes/acp/acp-agent.ts +33 -26
  94. package/src/modes/components/assistant-message.ts +2 -9
  95. package/src/modes/components/chat-block.ts +111 -0
  96. package/src/modes/components/copy-selector.ts +1 -44
  97. package/src/modes/components/custom-editor.ts +23 -0
  98. package/src/modes/components/custom-message.ts +1 -3
  99. package/src/modes/components/execution-shared.ts +1 -2
  100. package/src/modes/components/hook-message.ts +1 -3
  101. package/src/modes/components/overlay-box.ts +108 -0
  102. package/src/modes/components/plan-review-overlay.ts +799 -0
  103. package/src/modes/components/plan-toc.ts +138 -0
  104. package/src/modes/components/read-tool-group.ts +20 -4
  105. package/src/modes/components/skill-message.ts +0 -1
  106. package/src/modes/components/tips.txt +1 -0
  107. package/src/modes/components/todo-reminder.ts +0 -2
  108. package/src/modes/components/tool-execution.ts +68 -88
  109. package/src/modes/components/transcript-container.ts +84 -24
  110. package/src/modes/components/user-message.ts +1 -2
  111. package/src/modes/controllers/command-controller-shared.ts +7 -6
  112. package/src/modes/controllers/command-controller.ts +57 -55
  113. package/src/modes/controllers/event-controller.ts +41 -40
  114. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  115. package/src/modes/controllers/input-controller.ts +124 -119
  116. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  117. package/src/modes/controllers/selector-controller.ts +23 -25
  118. package/src/modes/controllers/streaming-reveal.ts +212 -0
  119. package/src/modes/controllers/tan-command-controller.ts +173 -0
  120. package/src/modes/interactive-mode.ts +169 -94
  121. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  122. package/src/modes/theme/theme-schema.json +1 -1
  123. package/src/modes/theme/theme.ts +8 -4
  124. package/src/modes/types.ts +18 -7
  125. package/src/modes/utils/copy-targets.ts +133 -27
  126. package/src/modes/utils/ui-helpers.ts +44 -46
  127. package/src/plan-mode/approved-plan.ts +66 -43
  128. package/src/plan-mode/plan-protection.ts +4 -4
  129. package/src/prompts/system/background-tan-dispatch.md +8 -0
  130. package/src/prompts/system/plan-mode-active.md +67 -58
  131. package/src/prompts/system/plan-mode-approved.md +1 -1
  132. package/src/sdk.ts +11 -37
  133. package/src/session/agent-session.ts +82 -6
  134. package/src/session/messages.ts +26 -0
  135. package/src/session/session-manager.ts +13 -5
  136. package/src/slash-commands/builtin-registry.ts +36 -9
  137. package/src/slash-commands/types.ts +4 -6
  138. package/src/task/executor.ts +5 -2
  139. package/src/task/index.ts +4 -0
  140. package/src/task/render.ts +212 -147
  141. package/src/tools/archive-reader.ts +64 -0
  142. package/src/tools/ask.ts +119 -164
  143. package/src/tools/ast-edit.ts +98 -71
  144. package/src/tools/ast-grep.ts +37 -43
  145. package/src/tools/bash.ts +50 -6
  146. package/src/tools/debug.ts +20 -8
  147. package/src/tools/fetch.ts +297 -7
  148. package/src/tools/find.ts +44 -30
  149. package/src/tools/gh-renderer.ts +81 -42
  150. package/src/tools/grouped-file-output.ts +272 -48
  151. package/src/tools/image-gen.ts +150 -103
  152. package/src/tools/inspect-image-renderer.ts +63 -41
  153. package/src/tools/inspect-image.ts +8 -1
  154. package/src/tools/job.ts +3 -4
  155. package/src/tools/memory-render.ts +4 -1
  156. package/src/tools/plan-mode-guard.ts +21 -39
  157. package/src/tools/read.ts +23 -16
  158. package/src/tools/render-utils.ts +21 -37
  159. package/src/tools/resolve.ts +14 -0
  160. package/src/tools/search-tool-bm25.ts +36 -23
  161. package/src/tools/search.ts +80 -78
  162. package/src/tools/sqlite-reader.ts +9 -12
  163. package/src/tools/todo.ts +118 -52
  164. package/src/tools/write.ts +81 -62
  165. package/src/tui/output-block.ts +60 -13
  166. package/src/tui/status-line.ts +5 -1
  167. package/src/utils/commit-message-generator.ts +9 -1
  168. package/src/utils/enhanced-paste.ts +202 -0
  169. package/src/utils/title-generator.ts +2 -1
  170. package/src/web/search/providers/anthropic.ts +25 -19
  171. package/src/web/search/providers/exa.ts +11 -3
  172. package/src/web/search/providers/kimi.ts +28 -17
  173. package/src/web/search/providers/parallel.ts +35 -24
  174. package/src/web/search/providers/synthetic.ts +8 -6
  175. package/src/web/search/providers/tavily.ts +9 -8
  176. package/src/web/search/providers/zai.ts +8 -6
@@ -15,10 +15,14 @@ declare const findSchema: z.ZodObject<{
15
15
  }, z.core.$strict>;
16
16
  export type FindToolInput = z.infer<typeof findSchema>;
17
17
  /**
18
- * Group find matches by their directory so the model doesn't pay repeated
19
- * tokens for shared path prefixes. Preserves the input order: groups appear in
20
- * the order their first member was emitted (mtime-desc for native glob), and
21
- * within a group entries keep their relative order.
18
+ * Group find matches into a multi-level directory tree so the model doesn't pay
19
+ * repeated tokens for shared path prefixes. Single-child directory chains fold
20
+ * into one header (`# a/b/c/`), so a common prefix including an absolute root
21
+ * for out-of-cwd results — collapses to a single line. Each level adds one `#`;
22
+ * files are listed bare under the deepest directory header that owns them.
23
+ *
24
+ * Order follows the input (mtime-desc for native glob): a directory appears when
25
+ * its first member is emitted, and a node's own files precede its subdirectories.
22
26
  */
23
27
  export declare function formatFindGroupedOutput(paths: readonly string[]): string;
24
28
  export interface FindToolDetails {
@@ -1,7 +1,55 @@
1
+ interface PathTreeNode {
2
+ /** Direct file leaves, in first-seen order. */
3
+ files: Array<{
4
+ name: string;
5
+ key: string;
6
+ }>;
7
+ /** Dedup set for `files` (a glob can surface the same path twice on retry). */
8
+ fileNames: Set<string>;
9
+ /** Child directories, in first-seen order. */
10
+ subdirs: Array<{
11
+ name: string;
12
+ node: PathTreeNode;
13
+ }>;
14
+ /** Dedup index for `subdirs`. */
15
+ dirIndex: Map<string, PathTreeNode>;
16
+ }
17
+ export interface PathTreeInput {
18
+ /** Path string; absolute, cwd-relative, or url-like. Backslashes are normalized. */
19
+ path: string;
20
+ /** Whether the leaf itself is a directory (trailing-slash match from find). */
21
+ isDir: boolean;
22
+ /** Opaque key carried onto file events for section lookup. Defaults to `path`. */
23
+ key?: string;
24
+ }
25
+ /** One node emitted while walking the tree: a folded directory or a file leaf. */
26
+ export interface GroupedTreeEvent {
27
+ kind: "dir" | "file";
28
+ /** 0-based nesting depth (root children are depth 0). */
29
+ depth: number;
30
+ /** Folded chain for dirs (e.g. `a/b/c`, no trailing slash); basename for files. */
31
+ name: string;
32
+ /** File key for `kind === "file"`; empty string for directories. */
33
+ key: string;
34
+ }
35
+ /**
36
+ * Build a directory tree from a flat list of paths. URL-like entries are kept
37
+ * whole as root-level file leaves (they have no meaningful directory structure).
38
+ * Absolute paths carry a leading empty segment so they share a common `/` root
39
+ * and fold like any other prefix.
40
+ */
41
+ export declare function buildPathTree(entries: Iterable<PathTreeInput>): PathTreeNode;
42
+ /**
43
+ * Depth-first walk yielding directory and file events. Directories collapse their
44
+ * single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
45
+ * header. Each node's direct files are emitted before its subdirectories, keeping
46
+ * a file unambiguously attached to the header above it.
47
+ */
48
+ export declare function walkPathTree(node: PathTreeNode, depth?: number): Generator<GroupedTreeEvent>;
1
49
  /**
2
50
  * One file's contribution to a grouped file output. The header itself is generated
3
- * by `formatGroupedFiles` (single `#` for root files, `##` for files inside a dir);
4
- * use `headerSuffix` to tack on extras like ` (1 replacement)`.
51
+ * by `formatGroupedFiles` (one `#` per nesting level); use `headerSuffix` to tack
52
+ * on extras like ` (1 replacement)`.
5
53
  */
6
54
  export interface GroupedFileSection {
7
55
  /** Optional suffix appended to the file header. */
@@ -18,19 +66,54 @@ export interface GroupedFilesOutput {
18
66
  display: string[];
19
67
  }
20
68
  /**
21
- * Render a list of files as directory-grouped sections shared by grep, ast-grep,
22
- * ast-edit, and the LSP diagnostic formatter.
69
+ * Render a list of files as a multi-level, prefix-folded directory tree shared by
70
+ * grep, ast-grep, ast-edit, and the LSP diagnostic formatter.
23
71
  *
24
- * Layout:
25
- * # dir/
26
- * ## file.ts
72
+ * Layout (one `#` per level; the shared prefix folds into the top header):
73
+ * # packages/pkg/src/
74
+ * ## root.ts
27
75
  * …body…
28
- *
29
- * # otherdir/
30
- * ## other.ts
76
+ * ## nested/
77
+ * ### child.ts
31
78
  * …body…
32
79
  *
33
- * Files in the project root (directory `.`) become single-`#` headers without a
34
- * `## file` line, matching the existing convention.
80
+ * Files in the (folded) project root become single-`#` headers with no parent
81
+ * directory line. A blank line precedes every directory header and every
82
+ * root-level file so the renderers can split the output into collapsible groups.
35
83
  */
36
84
  export declare function formatGroupedFiles(files: string[], renderFile: (filePath: string) => GroupedFileSection): GroupedFilesOutput;
85
+ /** Per-line classification of grouped output, used by renderers for hyperlinks. */
86
+ export interface GroupedLineContext {
87
+ /** Directory header, file header, or any non-header body/content line. */
88
+ kind: "dir" | "file" | "content";
89
+ /** Number of leading `#` for headers; 0 for content lines. */
90
+ depth: number;
91
+ /** Resolved absolute path of the dir/file a header points at (when resolvable). */
92
+ headerPath?: string;
93
+ /** For content lines, the absolute path of the owning file (line hyperlinks). */
94
+ filePath?: string;
95
+ /** Header is an internal/url-like target the caller resolves itself. */
96
+ isUrl?: boolean;
97
+ }
98
+ /**
99
+ * Walk grouped output lines, tracking a directory stack keyed by header depth, so
100
+ * each header and body line can be linked back to its absolute filesystem path.
101
+ * Reconstruction is stack-based (not per-blank-group) so nested directory headers
102
+ * resolve correctly across the whole output.
103
+ *
104
+ * `headerBase` is the directory the displayed (folded) header paths are relative
105
+ * to — for grep/ast tools that is the session cwd, since display paths are
106
+ * formatted relative to cwd regardless of the (sub)directory the search was
107
+ * scoped to. `fileScope` is the initial owning file for body lines that appear
108
+ * before any header (single-file scopes have no `#` headers); it defaults to
109
+ * `headerBase` and should be passed the scoped file's absolute path.
110
+ */
111
+ export declare function classifyGroupedLines(lines: readonly string[], headerBase: string | undefined, fileScope?: string | undefined): GroupedLineContext[];
112
+ /**
113
+ * Split line indices into blank-line-separated groups, mirroring
114
+ * `splitGroupsByBlankLine`: when any blank line is present, break on runs of
115
+ * blanks; otherwise return a single group of the non-empty lines. Returning
116
+ * indices lets callers slice parallel arrays (raw lines, styled lines, contexts).
117
+ */
118
+ export declare function groupLineIndicesByBlank(rawLines: readonly string[]): number[][];
119
+ export {};
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * These keep the transcript terse — one status line plus, for `retain`, one
6
6
  * `Remember: …` line per stored item — instead of the generic JSON arg tree,
7
- * which exploded multi-line memory blobs into an unreadable wall.
7
+ * which exploded multi-line memory blobs into an unreadable wall. The tool
8
+ * container is a transparent passthrough, so these renderers stay frameless:
9
+ * a status line with a couple of dim bullets reads far cleaner than boxing a
10
+ * one-line memory note.
8
11
  */
9
12
  import type { Component } from "@oh-my-pi/pi-tui";
10
13
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -1,16 +1,15 @@
1
1
  import type { ToolSession } from ".";
2
2
  /**
3
- * Resolve a write/edit target to its absolute filesystem path.
4
- *
5
- * In plan mode, transparently redirects `PLAN.md` aliases and targets whose
6
- * basename matches the plan file's basename to the canonical plan file
7
- * location at `state.planFilePath`. This lets `write` and `edit` accept the
8
- * habitual plan filename after approval even when the active artifact has a
9
- * titled path such as `local://APPROVED.md`.
10
- *
11
- * Outside plan mode (or when the basename does not match) this is a no-op.
3
+ * Resolve a write/edit target to its absolute filesystem path, honoring the
4
+ * `local://` and `vault://` schemes. Plain paths resolve against the session cwd.
12
5
  */
13
6
  export declare function resolvePlanPath(session: ToolSession, targetPath: string): string;
7
+ /**
8
+ * Plan mode keeps the working tree read-only while letting the agent draft its
9
+ * plan. Writes and edits to the `local://` artifact sandbox are allowed (that is
10
+ * where the plan and any scratch notes live); anything that would touch the
11
+ * working tree — or rename/delete a file — is rejected.
12
+ */
14
13
  export declare function enforcePlanModeWrite(session: ToolSession, targetPath: string, options?: {
15
14
  move?: string;
16
15
  op?: "create" | "update" | "delete";
@@ -46,8 +46,8 @@ export declare const TRUNCATE_LENGTHS: {
46
46
  /** Very short (task previews, badges) */
47
47
  readonly SHORT: 40;
48
48
  };
49
- /** Standard expand hint text */
50
- export declare const EXPAND_HINT = "(Ctrl+O for more)";
49
+ /** Human-readable key currently bound to tool-output expansion, e.g. `Ctrl+O`. */
50
+ export declare function expandKeyHint(): string;
51
51
  /**
52
52
  * Get first N lines of text as preview, with each line truncated.
53
53
  */
@@ -163,18 +163,14 @@ export declare function capParseErrors(errors: string[] | undefined, limit?: num
163
163
  errors: string[];
164
164
  total: number;
165
165
  };
166
- /**
167
- * Group `rawLines` by blank-line separators, mirroring the historical search /
168
- * ast-grep / ast-edit renderer behavior: if any blank line is present, splits on
169
- * runs of blank lines; otherwise collapses non-empty lines into a single group.
170
- */
171
- export declare function splitGroupsByBlankLine(rawLines: string[]): string[][];
172
166
  /**
173
167
  * Standard width+expand keyed render cache used by every search-style tool
174
168
  * renderer. `compute` re-runs only when the cache key changes; the returned
175
169
  * Component is the canonical `{ render, invalidate }` pair.
176
170
  */
177
- export declare function createCachedComponent(getExpanded: () => boolean, compute: (width: number, expanded: boolean) => string[]): Component;
171
+ export declare function createCachedComponent(getExpanded: () => boolean, compute: (width: number, expanded: boolean) => string[], options?: {
172
+ paddingX?: number;
173
+ }): Component;
178
174
  /**
179
175
  * Append the indented bullet list of parse errors (capped at
180
176
  * {@link PARSE_ERRORS_LIMIT}) to `lines`, with an overflow summary line if the
@@ -47,6 +47,10 @@ export interface SearchToolDetails {
47
47
  /** Absolute base directory used during search. Used by the renderer to resolve
48
48
  * display-relative paths to absolute paths for OSC 8 hyperlinks. */
49
49
  searchPath?: string;
50
+ /** Session cwd at search time. The renderer resolves the display-relative
51
+ * (cwd-relative) header/match paths against this for OSC 8 hyperlinks;
52
+ * `searchPath` is the scope label target, not the display-path base. */
53
+ cwd?: string;
50
54
  /** User-supplied paths whose base directory was missing on disk. The tool
51
55
  * skipped these and continued with the surviving entries; surfaced as a
52
56
  * non-fatal warning in the renderer and in the model-facing text. */
@@ -1,4 +1,5 @@
1
1
  import type { Database } from "bun:sqlite";
2
+ export declare function looksLikeSqlite(bytes: Uint8Array): boolean;
2
3
  export interface SqlitePathCandidate {
3
4
  sqlitePath: string;
4
5
  subPath: string;
@@ -146,14 +146,15 @@ export declare const TODO_STRIKE_HOLD_FRAMES = 2;
146
146
  export declare const TODO_STRIKE_REVEAL_FRAMES = 12;
147
147
  export declare const TODO_STRIKE_TOTAL_FRAMES: number;
148
148
  export declare const todoToolRenderer: {
149
- renderCall(args: TodoRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component;
149
+ renderCall(args: TodoRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component;
150
150
  renderResult(result: {
151
151
  content: Array<{
152
152
  type: string;
153
153
  text?: string;
154
154
  }>;
155
155
  details?: TodoToolDetails;
156
- }, options: RenderResultOptions, uiTheme: Theme, _args?: TodoRenderArgs): Component;
156
+ isError?: boolean;
157
+ }, options: RenderResultOptions, uiTheme: Theme, args?: TodoRenderArgs): Component;
157
158
  mergeCallAndResult: boolean;
158
159
  };
159
160
  export {};
@@ -17,6 +17,9 @@ export interface WriteToolDetails {
17
17
  meta?: OutputMeta;
18
18
  /** Set when the file was auto-chmod'd because content begins with a `#!` shebang. */
19
19
  madeExecutable?: boolean;
20
+ /** Absolute filesystem path the write resolved to. Used by the renderer to wrap
21
+ * the (possibly cwd-relative) header path in an OSC 8 `file://` hyperlink. */
22
+ resolvedPath?: string;
20
23
  }
21
24
  type WriteParams = WriteToolInput;
22
25
  /**
@@ -2,7 +2,7 @@
2
2
  * Bordered output container with optional header and sections.
3
3
  */
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
- import type { Theme } from "../modes/theme/theme";
5
+ import type { Theme, ThemeColor } from "../modes/theme/theme";
6
6
  import type { State } from "./types";
7
7
  export interface OutputBlockOptions {
8
8
  header?: string;
@@ -11,11 +11,16 @@ export interface OutputBlockOptions {
11
11
  sections?: Array<{
12
12
  label?: string;
13
13
  lines: string[];
14
+ separator?: boolean;
14
15
  }>;
15
16
  width: number;
16
17
  applyBg?: boolean;
18
+ contentPaddingLeft?: number;
17
19
  /** Animate the border with a sweeping dark segment (pending/running state). */
18
20
  animate?: boolean;
21
+ /** Override the state-derived border color. Used for muted "legacy" tool
22
+ * frames that should not visually compete with framed-output tools. */
23
+ borderColor?: ThemeColor;
19
24
  }
20
25
  declare const FRAMED_BLOCK_COMPONENT: unique symbol;
21
26
  export type FramedBlockComponent = Component & {
@@ -24,9 +29,9 @@ export type FramedBlockComponent = Component & {
24
29
  export declare function markFramedBlockComponent<T extends Component>(component: T): T & FramedBlockComponent;
25
30
  export declare function isFramedBlockComponent(component: Component): boolean;
26
31
  /**
27
- * Monotonic frame counter for animated borders, quantized to the TUI's ~16ms
28
- * render cap so the cache key advances once per ~60fps frame — fine enough for a
29
- * smooth segment sweep, coarse enough to coalesce multiple render passes that
32
+ * Monotonic frame counter for animated borders, quantized to the TUI's ~30fps
33
+ * render cap so the cache key advances once per animation frame — fine enough
34
+ * for a smooth segment sweep, coarse enough to coalesce multiple render passes
30
35
  * land inside the same frame.
31
36
  */
32
37
  export declare function borderShimmerTick(): number;
@@ -54,4 +59,11 @@ export declare class CachedOutputBlock {
54
59
  /** Invalidate the cache, forcing a rebuild on next render. */
55
60
  invalidate(): void;
56
61
  }
62
+ /**
63
+ * Build a self-framing tool component backed by a cached output block. The
64
+ * `build` callback returns the block options for a given width; the cache
65
+ * dedupes re-renders. Pass `borderColor: "borderMuted"` for the dim "legacy"
66
+ * look that does not compete with the state-colored framed tools.
67
+ */
68
+ export declare function framedBlock(theme: Theme, build: (width: number) => OutputBlockOptions): Component;
57
69
  export {};
@@ -5,6 +5,9 @@ import type { Theme, ThemeColor } from "../modes/theme/theme";
5
5
  import type { ToolUIStatus } from "../tools/render-utils";
6
6
  export interface StatusLineOptions {
7
7
  icon?: ToolUIStatus;
8
+ /** Pre-rendered glyph that replaces the status icon (e.g. a magnifier for
9
+ * search-family tools). Takes precedence over `icon`. */
10
+ iconOverride?: string;
8
11
  spinnerFrame?: number;
9
12
  title: string;
10
13
  titleColor?: ThemeColor;
@@ -0,0 +1,20 @@
1
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
2
+ export interface Osc5522Packet {
3
+ metadata: Map<string, string>;
4
+ payload: string;
5
+ }
6
+ export interface EnhancedPasteHandlers {
7
+ write(data: string): void;
8
+ pasteText(text: string): void;
9
+ pasteImage(image: ImageContent): void | Promise<void>;
10
+ showStatus(message: string): void;
11
+ }
12
+ export declare function isOsc5522Packet(data: string): boolean;
13
+ export declare function parseOsc5522Packet(data: string): Osc5522Packet | undefined;
14
+ export declare class EnhancedPasteController {
15
+ #private;
16
+ constructor(handlers: EnhancedPasteHandlers);
17
+ enable(): void;
18
+ disable(): void;
19
+ handleInput(data: string): boolean;
20
+ }
@@ -4,7 +4,7 @@
4
4
  * Uses Moonshot Kimi Code search API to retrieve web results.
5
5
  * Endpoint: POST https://api.kimi.com/coding/v1/search
6
6
  */
7
- import type { AuthStorage } from "@oh-my-pi/pi-ai";
7
+ import { type AuthStorage } from "@oh-my-pi/pi-ai";
8
8
  import type { SearchResponse } from "../../../web/search/types";
9
9
  import type { SearchParams } from "./base";
10
10
  import { SearchProvider } from "./base";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.10.0",
4
+ "version": "15.10.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,14 +47,14 @@
47
47
  "@agentclientprotocol/sdk": "0.22.1",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.10.0",
51
- "@oh-my-pi/omp-stats": "15.10.0",
52
- "@oh-my-pi/pi-agent-core": "15.10.0",
53
- "@oh-my-pi/pi-ai": "15.10.0",
54
- "@oh-my-pi/pi-mnemopi": "15.10.0",
55
- "@oh-my-pi/pi-natives": "15.10.0",
56
- "@oh-my-pi/pi-tui": "15.10.0",
57
- "@oh-my-pi/pi-utils": "15.10.0",
50
+ "@oh-my-pi/hashline": "15.10.1",
51
+ "@oh-my-pi/omp-stats": "15.10.1",
52
+ "@oh-my-pi/pi-agent-core": "15.10.1",
53
+ "@oh-my-pi/pi-ai": "15.10.1",
54
+ "@oh-my-pi/pi-mnemopi": "15.10.1",
55
+ "@oh-my-pi/pi-natives": "15.10.1",
56
+ "@oh-my-pi/pi-tui": "15.10.1",
57
+ "@oh-my-pi/pi-utils": "15.10.1",
58
58
  "@opentelemetry/api": "^1.9.1",
59
59
  "@opentelemetry/context-async-hooks": "^2.7.1",
60
60
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { type AssistantMessage, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
17
17
  import { prompt } from "@oh-my-pi/pi-utils";
18
+
18
19
  import type { ModelRegistry } from "../config/model-registry";
19
20
  import { resolveRoleSelection } from "../config/model-resolver";
20
21
  import type { Settings } from "../config/settings";
@@ -82,7 +83,10 @@ async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Prom
82
83
  messages: [{ role: "user", content: input, timestamp: Date.now() }],
83
84
  },
84
85
  {
85
- apiKey,
86
+ apiKey: deps.registry.resolver(model.provider, {
87
+ sessionId: deps.sessionId,
88
+ baseUrl: model.baseUrl,
89
+ }),
86
90
  maxTokens,
87
91
  disableReasoning: true,
88
92
  metadata,
@@ -1,8 +1,10 @@
1
1
  import type {
2
2
  Api,
3
+ ApiKeyResolver,
3
4
  AssistantMessage,
4
5
  AssistantMessageEvent,
5
6
  AssistantMessageEventStream,
7
+ AuthCredentialSnapshotEntry,
6
8
  Context,
7
9
  Model,
8
10
  OAuthAccess,
@@ -59,6 +61,12 @@ export interface DryBalanceAuthStorage {
59
61
  options?: DryBalanceAuthOptions,
60
62
  ): Promise<OAuthAccess | undefined>;
61
63
  getOAuthAccesses?(provider: string, options?: DryBalanceAuthOptions): Promise<OAuthAccessResolution[]>;
64
+ /**
65
+ * Force-refresh a single credential by id (step (b) of the auth-retry
66
+ * policy). The bench re-mints the failing account's token in place on a
67
+ * 401 rather than rotating accounts — it is measuring each account.
68
+ */
69
+ forceRefreshCredentialById?(id: number, signal?: AbortSignal): Promise<AuthCredentialSnapshotEntry>;
62
70
  }
63
71
 
64
72
  export interface DryBalanceModelRegistry {
@@ -152,6 +160,8 @@ export interface DryBalanceDependencies {
152
160
  now?: () => number;
153
161
  stdoutIsTTY?: boolean;
154
162
  stderrIsTTY?: boolean;
163
+ stdoutColumns?: number;
164
+ stderrColumns?: number;
155
165
  }
156
166
 
157
167
  type DryBalanceAttemptResult =
@@ -181,6 +191,7 @@ type DryBalanceBenchTarget =
181
191
  ok: true;
182
192
  account: string;
183
193
  accessToken: string;
194
+ credentialId?: number;
184
195
  }
185
196
  | {
186
197
  ok: false;
@@ -310,10 +321,11 @@ function renderBenchStatusLine(
310
321
  }
311
322
  }
312
323
 
313
- function createBenchProgressSink(
324
+ export function createBenchProgressSink(
314
325
  total: number,
315
326
  write: (text: string) => void,
316
327
  interactive: boolean,
328
+ columns: number,
317
329
  ): DryBalanceBenchProgressSink {
318
330
  const statuses: DryBalanceBenchProgressStatus[] = Array.from({ length: total }, () => ({ state: "waiting" }));
319
331
  if (!interactive) {
@@ -333,13 +345,21 @@ function createBenchProgressSink(
333
345
  let frame = 0;
334
346
  let lineCount = 0;
335
347
  let timer: NodeJS.Timeout | undefined;
348
+ const width = Number.isFinite(columns) && columns > 0 ? Math.trunc(columns) : 80;
336
349
  const render = (): void => {
337
350
  const lines = [
338
351
  chalk.bold("bench requests"),
339
352
  ...statuses.map((status, index) => renderBenchStatusLine(status, index, total, frame)),
340
353
  ];
341
- if (lineCount > 0) write(`\x1b[${lineCount}A`);
342
- write(`${lines.map(line => `\x1b[2K${line}`).join("\n")}\n`);
354
+ // Anchor every redraw at column 0 and terminate each row with CRLF: a
355
+ // bare `\n` only returns to column 0 when the tty performs ONLCR
356
+ // translation, which is off whenever the terminal is in raw mode — there
357
+ // the old column-preserving cursor-up staircased each frame into
358
+ // scrollback. Cap each line to the terminal width so a wrapped row never
359
+ // desyncs the `\x1b[<n>A` cursor-up from the logical line count.
360
+ const move = lineCount > 0 ? `\x1b[${lineCount}A` : "";
361
+ const body = lines.map(line => `\x1b[2K${truncateToWidth(line, width)}`).join("\r\n");
362
+ write(`${move}\r${body}\r\n`);
343
363
  lineCount = lines.length;
344
364
  };
345
365
  render();
@@ -370,13 +390,23 @@ function createBenchProgressSink(
370
390
  async function runBenchRequest(
371
391
  model: Model<Api>,
372
392
  sessionId: string,
373
- account: string,
374
- accessToken: string,
393
+ target: Extract<DryBalanceBenchTarget, { ok: true }>,
394
+ authStorage: DryBalanceAuthStorage,
375
395
  streamFn: DryBalanceStreamSimple,
376
396
  now: () => number,
377
397
  ): Promise<DryBalanceBenchResult> {
398
+ const { account, accessToken, credentialId } = target;
378
399
  const startedAt = now();
379
400
  let firstTokenAt: number | undefined;
401
+ // Re-mint the cached token on a 401: a peer/broker may have rotated it out
402
+ // from under our snapshot (Anthropic rotates refresh tokens on every use).
403
+ // The bench measures one account, so the switch step intentionally declines.
404
+ const apiKey: ApiKeyResolver = async ({ lastChance, error }) => {
405
+ if (error === undefined) return accessToken;
406
+ if (lastChance || credentialId === undefined || !authStorage.forceRefreshCredentialById) return undefined;
407
+ const refreshed = await authStorage.forceRefreshCredentialById(credentialId);
408
+ return refreshed.credential.type === "oauth" ? refreshed.credential.access : undefined;
409
+ };
380
410
  try {
381
411
  const context: Context = {
382
412
  messages: [
@@ -389,7 +419,7 @@ async function runBenchRequest(
389
419
  ],
390
420
  };
391
421
  const stream = streamFn(model, context, {
392
- apiKey: accessToken,
422
+ apiKey,
393
423
  sessionId,
394
424
  maxTokens: resolveBenchMaxTokens(model),
395
425
  temperature: 0.2,
@@ -454,7 +484,7 @@ async function resolveBenchTargets(
454
484
  seen.add(key);
455
485
  const account = extractAccount(entry);
456
486
  if (entry.ok) {
457
- targets.push({ ok: true, account, accessToken: entry.accessToken });
487
+ targets.push({ ok: true, account, accessToken: entry.accessToken, credentialId: entry.credentialId });
458
488
  } else {
459
489
  targets.push({ ok: false, account, error: entry.error });
460
490
  }
@@ -465,6 +495,7 @@ async function resolveBenchTargets(
465
495
  async function runBenchTargets(
466
496
  model: Model<Api>,
467
497
  targets: DryBalanceBenchTarget[],
498
+ authStorage: DryBalanceAuthStorage,
468
499
  randomSessionId: () => string,
469
500
  progress: DryBalanceBenchProgressSink | undefined,
470
501
  streamFn: DryBalanceStreamSimple,
@@ -482,14 +513,7 @@ async function runBenchTargets(
482
513
  return result;
483
514
  }
484
515
  progress?.markRunning(index, target.account);
485
- const result = await runBenchRequest(
486
- model,
487
- randomSessionId(),
488
- target.account,
489
- target.accessToken,
490
- streamFn,
491
- now,
492
- );
516
+ const result = await runBenchRequest(model, randomSessionId(), target, authStorage, streamFn, now);
493
517
  progress?.complete(index, result);
494
518
  return result;
495
519
  }),
@@ -792,8 +816,19 @@ export async function runDryBalanceCommand(
792
816
  const progressInteractive = command.flags.json
793
817
  ? (deps.stderrIsTTY ?? process.stderr.isTTY === true)
794
818
  : (deps.stdoutIsTTY ?? process.stdout.isTTY === true);
795
- progress = createBenchProgressSink(targets.length, progressWrite, progressInteractive);
796
- benchResults = await runBenchTargets(model, targets, randomSessionId, progress, streamFn, now);
819
+ const progressColumns = command.flags.json
820
+ ? (deps.stderrColumns ?? process.stderr.columns ?? 80)
821
+ : (deps.stdoutColumns ?? process.stdout.columns ?? 80);
822
+ progress = createBenchProgressSink(targets.length, progressWrite, progressInteractive, progressColumns);
823
+ benchResults = await runBenchTargets(
824
+ model,
825
+ targets,
826
+ runtime.modelRegistry.authStorage,
827
+ randomSessionId,
828
+ progress,
829
+ streamFn,
830
+ now,
831
+ );
797
832
  results = targets.map(target =>
798
833
  target.ok ? { ok: true, account: target.account } : { ok: false, reason: target.error },
799
834
  );
@@ -196,7 +196,10 @@ export async function runGalleryCommand(args: GalleryCommandArgs): Promise<void>
196
196
  const expanded = args.expanded ?? false;
197
197
  const states = args.states && args.states.length > 0 ? args.states : [...GALLERY_STATES];
198
198
 
199
- const allNames = Object.keys(toolRenderers).sort();
199
+ // Renderer-registry tools plus fixture-only tools (no dedicated renderer,
200
+ // e.g. `report_tool_issue` / custom extension tools) so the gallery covers
201
+ // the generic fallback + custom-tool branches too.
202
+ const allNames = Array.from(new Set([...Object.keys(toolRenderers), ...Object.keys(galleryFixtures)])).sort();
200
203
  const names = args.tool ? allNames.filter(name => name === args.tool) : allNames;
201
204
  if (args.tool && names.length === 0) {
202
205
  process.stdout.write(`Unknown tool '${args.tool}'. Known tools: ${allNames.join(", ")}\n`);
@@ -218,4 +218,33 @@ export const miscFixtures: Record<string, GalleryFixture> = {
218
218
  },
219
219
  },
220
220
  },
221
+
222
+ // Built-in tool with no dedicated renderer — exercises the generic fallback
223
+ // (`#formatToolExecution`) path so its padded, state-tinted block is QA'd.
224
+ report_tool_issue: {
225
+ label: "Report Tool Issue",
226
+ streamingArgs: { tool: "lsp" },
227
+ args: {
228
+ tool: "lsp",
229
+ report: "Rename returned no edit for an exported symbol that has 12 references",
230
+ },
231
+ result: { content: [{ type: "text", text: "Noted, thanks!" }] },
232
+ errorResult: {
233
+ content: [{ type: "text", text: "Could not record the report: issue tracker unreachable" }],
234
+ isError: true,
235
+ },
236
+ },
237
+
238
+ // Stand-in for a custom/extension tool that ships no renderer — same generic
239
+ // fallback path most MCP/extension tools take.
240
+ custom: {
241
+ label: "Custom Tool",
242
+ streamingArgs: { query: "weather" },
243
+ args: { query: "weather in Tokyo", units: "metric" },
244
+ result: { content: [{ type: "text", text: "Tokyo: 22°C, partly cloudy, humidity 64%." }] },
245
+ errorResult: {
246
+ content: [{ type: "text", text: "Upstream provider returned 503 Service Unavailable" }],
247
+ isError: true,
248
+ },
249
+ },
221
250
  };
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
@@ -14,7 +14,7 @@ const ConventionalAnalysisTool = createConventionalAnalysisTool(
14
14
 
15
15
  export interface ConventionalAnalysisInput {
16
16
  model: Model<Api>;
17
- apiKey: string;
17
+ apiKey: ApiKey;
18
18
  thinkingLevel?: ThinkingLevel;
19
19
  contextFiles?: Array<{ path: string; content: string }>;
20
20
  userContext?: string;
@@ -1,5 +1,5 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
+ import type { Api, ApiKey, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import * as z from "zod/v4";
@@ -19,7 +19,7 @@ const SummaryTool = {
19
19
 
20
20
  export interface SummaryInput {
21
21
  model: Model<Api>;
22
- apiKey: string;
22
+ apiKey: ApiKey;
23
23
  thinkingLevel?: ThinkingLevel;
24
24
  commitType: string;
25
25
  scope: string | null;