@oh-my-pi/pi-coding-agent 15.9.5 → 15.10.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 (192) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/gallery-cli.d.ts +43 -0
  4. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  5. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  6. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  8. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  9. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  10. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  11. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  12. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  15. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  16. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  17. package/dist/types/commands/gallery.d.ts +47 -0
  18. package/dist/types/config/keybindings.d.ts +10 -2
  19. package/dist/types/config/model-id-affixes.d.ts +2 -0
  20. package/dist/types/config/model-registry.d.ts +8 -1
  21. package/dist/types/config/settings-schema.d.ts +43 -7
  22. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  23. package/dist/types/eval/backend.d.ts +6 -6
  24. package/dist/types/eval/bridge-timeout.d.ts +27 -0
  25. package/dist/types/eval/idle-timeout.d.ts +16 -14
  26. package/dist/types/eval/js/executor.d.ts +3 -3
  27. package/dist/types/eval/py/executor.d.ts +2 -2
  28. package/dist/types/eval/py/spawn-options.d.ts +58 -0
  29. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  30. package/dist/types/lsp/types.d.ts +10 -0
  31. package/dist/types/main.d.ts +3 -2
  32. package/dist/types/memory-backend/index.d.ts +2 -1
  33. package/dist/types/memory-backend/resolve.d.ts +1 -1
  34. package/dist/types/memory-backend/types.d.ts +1 -1
  35. package/dist/types/modes/components/assistant-message.d.ts +5 -0
  36. package/dist/types/modes/components/copy-selector.d.ts +22 -0
  37. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  38. package/dist/types/modes/components/model-selector.d.ts +1 -0
  39. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  42. package/dist/types/modes/index.d.ts +5 -4
  43. package/dist/types/modes/interactive-mode.d.ts +2 -2
  44. package/dist/types/modes/setup-version.d.ts +11 -0
  45. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  46. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  47. package/dist/types/modes/types.d.ts +2 -2
  48. package/dist/types/modes/utils/copy-targets.d.ts +53 -0
  49. package/dist/types/sdk.d.ts +1 -1
  50. package/dist/types/task/executor.d.ts +7 -0
  51. package/dist/types/telemetry-export.d.ts +1 -1
  52. package/dist/types/tools/eval-render.d.ts +1 -0
  53. package/dist/types/tools/fetch.d.ts +15 -7
  54. package/dist/types/tools/render-utils.d.ts +33 -0
  55. package/dist/types/tools/renderers.d.ts +16 -2
  56. package/dist/types/tools/search.d.ts +1 -1
  57. package/dist/types/tools/write.d.ts +2 -0
  58. package/dist/types/tui/code-cell.d.ts +6 -0
  59. package/dist/types/tui/output-block.d.ts +11 -0
  60. package/dist/types/web/scrapers/github.d.ts +22 -0
  61. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +9 -9
  64. package/scripts/dev-launch +42 -0
  65. package/scripts/dev-launch-preload.ts +19 -0
  66. package/src/autoresearch/dashboard.ts +11 -21
  67. package/src/cli/args.ts +2 -2
  68. package/src/cli/claude-trace-cli.ts +13 -1
  69. package/src/cli/gallery-cli.ts +223 -0
  70. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  71. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  72. package/src/cli/gallery-fixtures/edit.ts +194 -0
  73. package/src/cli/gallery-fixtures/fs.ts +153 -0
  74. package/src/cli/gallery-fixtures/index.ts +40 -0
  75. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  76. package/src/cli/gallery-fixtures/memory.ts +81 -0
  77. package/src/cli/gallery-fixtures/misc.ts +221 -0
  78. package/src/cli/gallery-fixtures/search.ts +213 -0
  79. package/src/cli/gallery-fixtures/shell.ts +167 -0
  80. package/src/cli/gallery-fixtures/types.ts +41 -0
  81. package/src/cli/gallery-fixtures/web.ts +158 -0
  82. package/src/cli/gallery-screenshot.ts +279 -0
  83. package/src/cli-commands.ts +1 -0
  84. package/src/commands/gallery.ts +52 -0
  85. package/src/commands/launch.ts +1 -1
  86. package/src/config/keybindings.ts +68 -2
  87. package/src/config/model-equivalence.ts +35 -12
  88. package/src/config/model-id-affixes.ts +39 -22
  89. package/src/config/model-registry.ts +16 -16
  90. package/src/config/settings-schema.ts +29 -6
  91. package/src/config/settings.ts +11 -0
  92. package/src/dap/client.ts +14 -16
  93. package/src/debug/raw-sse.ts +18 -4
  94. package/src/edit/file-snapshot-store.ts +1 -1
  95. package/src/edit/index.ts +1 -1
  96. package/src/edit/renderer.ts +43 -55
  97. package/src/edit/streaming.ts +1 -1
  98. package/src/eval/__tests__/agent-bridge.test.ts +102 -58
  99. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  100. package/src/eval/__tests__/idle-timeout.test.ts +26 -12
  101. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  102. package/src/eval/__tests__/llm-bridge.test.ts +10 -10
  103. package/src/eval/agent-bridge.ts +38 -12
  104. package/src/eval/backend.ts +6 -6
  105. package/src/eval/bridge-timeout.ts +44 -0
  106. package/src/eval/idle-timeout.ts +33 -15
  107. package/src/eval/js/executor.ts +10 -10
  108. package/src/eval/llm-bridge.ts +4 -5
  109. package/src/eval/py/executor.ts +6 -6
  110. package/src/eval/py/kernel.ts +11 -1
  111. package/src/eval/py/spawn-options.ts +126 -0
  112. package/src/export/ttsr.ts +9 -0
  113. package/src/extensibility/extensions/runner.ts +3 -0
  114. package/src/extensibility/plugins/doctor.ts +0 -1
  115. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  116. package/src/goals/tools/goal-tool.ts +2 -2
  117. package/src/internal-urls/docs-index.generated.ts +7 -6
  118. package/src/lsp/client.ts +179 -52
  119. package/src/lsp/index.ts +38 -4
  120. package/src/lsp/render.ts +3 -3
  121. package/src/lsp/types.ts +10 -0
  122. package/src/main.ts +47 -52
  123. package/src/memory-backend/index.ts +13 -1
  124. package/src/memory-backend/resolve.ts +3 -5
  125. package/src/memory-backend/types.ts +1 -1
  126. package/src/modes/components/agent-dashboard.ts +13 -4
  127. package/src/modes/components/assistant-message.ts +22 -1
  128. package/src/modes/components/copy-selector.ts +249 -0
  129. package/src/modes/components/custom-editor.ts +10 -1
  130. package/src/modes/components/extensions/extension-list.ts +17 -8
  131. package/src/modes/components/history-search.ts +19 -11
  132. package/src/modes/components/model-selector.ts +125 -29
  133. package/src/modes/components/oauth-selector.ts +28 -12
  134. package/src/modes/components/session-observer-overlay.ts +13 -15
  135. package/src/modes/components/session-selector.ts +24 -13
  136. package/src/modes/components/status-line.ts +3 -5
  137. package/src/modes/components/tool-execution.ts +83 -24
  138. package/src/modes/components/tree-selector.ts +19 -7
  139. package/src/modes/components/user-message-selector.ts +25 -14
  140. package/src/modes/controllers/command-controller.ts +13 -118
  141. package/src/modes/controllers/event-controller.ts +26 -10
  142. package/src/modes/controllers/input-controller.ts +11 -3
  143. package/src/modes/controllers/selector-controller.ts +40 -3
  144. package/src/modes/index.ts +5 -4
  145. package/src/modes/interactive-mode.ts +21 -7
  146. package/src/modes/setup-version.ts +11 -0
  147. package/src/modes/setup-wizard/index.ts +3 -2
  148. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  149. package/src/modes/theme/theme.ts +46 -10
  150. package/src/modes/types.ts +2 -2
  151. package/src/modes/utils/context-usage.ts +10 -6
  152. package/src/modes/utils/copy-targets.ts +254 -0
  153. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  154. package/src/prompts/tools/ast-edit.md +1 -1
  155. package/src/prompts/tools/ast-grep.md +1 -1
  156. package/src/prompts/tools/read.md +1 -1
  157. package/src/prompts/tools/search.md +1 -1
  158. package/src/sdk.ts +21 -23
  159. package/src/session/agent-session.ts +13 -9
  160. package/src/slash-commands/builtin-registry.ts +4 -12
  161. package/src/slash-commands/helpers/usage-report.ts +2 -0
  162. package/src/task/executor.ts +20 -2
  163. package/src/task/render.ts +37 -11
  164. package/src/telemetry-export.ts +25 -7
  165. package/src/tools/bash.ts +18 -8
  166. package/src/tools/browser/render.ts +5 -4
  167. package/src/tools/debug.ts +3 -3
  168. package/src/tools/eval-backends.ts +6 -17
  169. package/src/tools/eval-render.ts +28 -10
  170. package/src/tools/eval.ts +19 -23
  171. package/src/tools/fetch.ts +99 -89
  172. package/src/tools/read.ts +7 -7
  173. package/src/tools/render-utils.ts +63 -3
  174. package/src/tools/renderers.ts +16 -1
  175. package/src/tools/report-tool-issue.ts +1 -1
  176. package/src/tools/search.ts +173 -81
  177. package/src/tools/ssh.ts +21 -8
  178. package/src/tools/todo.ts +20 -7
  179. package/src/tools/write.ts +39 -9
  180. package/src/tui/code-cell.ts +19 -4
  181. package/src/tui/output-block.ts +14 -0
  182. package/src/web/scrapers/github.ts +255 -3
  183. package/src/web/scrapers/youtube.ts +3 -2
  184. package/src/web/search/providers/perplexity.ts +199 -51
  185. package/src/web/search/render.ts +42 -57
  186. package/src/web/search/types.ts +5 -1
  187. package/dist/types/eval/heartbeat.d.ts +0 -45
  188. package/src/eval/__tests__/heartbeat.test.ts +0 -84
  189. package/src/eval/__tests__/shared-executors.test.ts +0 -609
  190. package/src/eval/heartbeat.ts +0 -74
  191. /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
  192. /package/dist/types/eval/__tests__/{shared-executors.test.d.ts → kernel-spawn.test.d.ts} +0 -0
@@ -4,34 +4,49 @@ const MODEL_ID_SEGMENT_PATTERN = /[a-z0-9.:-]+/g;
4
4
  const MODEL_FAMILY_PREFIX_PATTERN =
5
5
  /^(claude|gemini|gpt|grok|glm|qwen|deepseek|kimi|mimo|doubao|ernie|gpt-oss|gemma|minimax|step|command|jamba|llama|o[1345])/i;
6
6
 
7
- function hasDigit(value: string): boolean {
8
- return /\d/.test(value);
7
+ function normalizeModelIdWhitespace(value: string): string {
8
+ return value.trim().replace(/\s+/g, " ");
9
9
  }
10
10
 
11
+ /** Ordering for model-like segments: longest first, ties broken lexicographically. */
11
12
  function compareSegmentPreference(left: string, right: string): number {
12
- if (left.length !== right.length) {
13
- return right.length - left.length;
14
- }
15
- return left.localeCompare(right);
13
+ return left.length !== right.length ? right.length - left.length : left.localeCompare(right);
16
14
  }
17
15
 
18
16
  export function getModelLikeIdSegments(modelId: string): string[] {
19
- const normalized = normalizeModelIdWhitespace(modelId).toLowerCase();
20
- if (!normalized) return [];
21
- const segments = (normalized.match(MODEL_ID_SEGMENT_PATTERN) ?? []).filter(
22
- segment => MODEL_FAMILY_PREFIX_PATTERN.test(segment) && hasDigit(segment),
23
- );
24
- const unique = [...new Set(segments)];
25
- unique.sort(compareSegmentPreference);
26
- return unique;
17
+ const matches = normalizeModelIdWhitespace(modelId).toLowerCase().match(MODEL_ID_SEGMENT_PATTERN);
18
+ if (!matches) return [];
19
+ const segments = new Set<string>();
20
+ for (const segment of matches) {
21
+ if (MODEL_FAMILY_PREFIX_PATTERN.test(segment) && /\d/.test(segment)) segments.add(segment);
22
+ }
23
+ return [...segments].sort(compareSegmentPreference);
27
24
  }
28
25
 
29
26
  export function getLongestModelLikeIdSegment(modelId: string): string | undefined {
30
- return getModelLikeIdSegments(modelId)[0];
27
+ const matches = normalizeModelIdWhitespace(modelId).toLowerCase().match(MODEL_ID_SEGMENT_PATTERN);
28
+ if (!matches) return undefined;
29
+ let best: string | undefined;
30
+ for (const segment of matches) {
31
+ if (
32
+ MODEL_FAMILY_PREFIX_PATTERN.test(segment) &&
33
+ /\d/.test(segment) &&
34
+ (best === undefined || compareSegmentPreference(segment, best) < 0)
35
+ ) {
36
+ best = segment;
37
+ }
38
+ }
39
+ return best;
31
40
  }
32
41
 
33
- function normalizeModelIdWhitespace(value: string): string {
34
- return value.trim().replace(/\s+/g, " ");
42
+ function hasBracketAffixMarker(value: string): boolean {
43
+ for (let index = 0; index < value.length; index++) {
44
+ const code = value.charCodeAt(index);
45
+ if (code === 91 || code === 93 || code === 0x3010 || code === 0x3011) {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
35
50
  }
36
51
 
37
52
  /**
@@ -39,18 +54,20 @@ function normalizeModelIdWhitespace(value: string): string {
39
54
  * upstream model id, e.g.
40
55
  * "[Kiro] claude-opus-4-8" -> "claude-opus-4-8"
41
56
  * "[gcli转] gemini-3.1-pro-preview [假流]" -> "gemini-3.1-pro-preview"
57
+ *
58
+ * Candidates are returned most-stripped first: both ends, then leading-only, then trailing-only.
42
59
  */
43
60
  export function getBracketStrippedModelIdCandidates(modelId: string): string[] {
61
+ if (!hasBracketAffixMarker(modelId)) return [];
44
62
  const normalized = normalizeModelIdWhitespace(modelId);
45
63
  if (!normalized) return [];
46
64
 
47
- const candidates = new Set<string>();
48
- const withoutLeading = normalizeModelIdWhitespace(normalized.replace(LEADING_BRACKETED_AFFIX_PATTERN, ""));
65
+ const strippedLeading = normalized.replace(LEADING_BRACKETED_AFFIX_PATTERN, "");
66
+ const withoutLeading = normalizeModelIdWhitespace(strippedLeading);
49
67
  const withoutTrailing = normalizeModelIdWhitespace(normalized.replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""));
50
- const withoutBoth = normalizeModelIdWhitespace(
51
- normalized.replace(LEADING_BRACKETED_AFFIX_PATTERN, "").replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""),
52
- );
68
+ const withoutBoth = normalizeModelIdWhitespace(strippedLeading.replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""));
53
69
 
70
+ const candidates = new Set<string>();
54
71
  for (const candidate of [withoutBoth, withoutLeading, withoutTrailing]) {
55
72
  if (candidate && candidate !== normalized) {
56
73
  candidates.add(candidate);
@@ -1,27 +1,19 @@
1
1
  import * as path from "node:path";
2
+ import { registerCustomApi, unregisterCustomApis } from "@oh-my-pi/pi-ai/api-registry";
3
+ import { readModelCache } from "@oh-my-pi/pi-ai/model-cache";
4
+ import { createModelManager, type ModelManagerOptions, type ModelRefreshStrategy } from "@oh-my-pi/pi-ai/model-manager";
5
+ import { enrichModelThinking } from "@oh-my-pi/pi-ai/model-thinking";
6
+ import { getBundledModels, getBundledProviders } from "@oh-my-pi/pi-ai/models";
2
7
  import {
3
- type Api,
4
- type AssistantMessageEventStream,
5
- type Context,
6
- createModelManager,
7
- enrichModelThinking,
8
- getBundledModels,
9
- getBundledProviders,
10
8
  googleAntigravityModelManagerOptions,
11
9
  googleGeminiCliModelManagerOptions,
12
- type Model,
13
- type ModelManagerOptions,
14
- type ModelRefreshStrategy,
15
10
  openaiCodexModelManagerOptions,
16
11
  PROVIDER_DESCRIPTORS,
17
- readModelCache,
18
- registerCustomApi,
19
- type SimpleStreamOptions,
20
- type ThinkingConfig,
21
12
  UNK_CONTEXT_WINDOW,
22
13
  UNK_MAX_TOKENS,
23
- unregisterCustomApis,
24
- } from "@oh-my-pi/pi-ai";
14
+ } from "@oh-my-pi/pi-ai/provider-models";
15
+ import type { Api, Context, Model, SimpleStreamOptions, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
16
+ import type { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
25
17
 
26
18
  // Sentinel for local-only OAuth token (LM Studio, vLLM) — declared inline to avoid loading
27
19
  // any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
@@ -2609,6 +2601,14 @@ export class ModelRegistry {
2609
2601
  }
2610
2602
  return true;
2611
2603
  }
2604
+
2605
+ /**
2606
+ * Clear all cooldown suppressions recorded via {@link suppressSelector}.
2607
+ * Used to reset retry-fallback cooldown state without a full {@link refresh}.
2608
+ */
2609
+ clearSuppressedSelectors(): void {
2610
+ this.#suppressedSelectors.clear();
2611
+ }
2612
2612
  }
2613
2613
 
2614
2614
  /**
@@ -902,6 +902,15 @@ export const SETTINGS_SCHEMA = {
902
902
  "Maximum wait between retries, in ms. When the provider asks us to wait longer than this and no credential or model fallback succeeds, the request fails fast instead of sleeping (e.g. 3-hour Anthropic rate-limit windows).",
903
903
  },
904
904
  },
905
+ "retry.modelFallback": {
906
+ type: "boolean",
907
+ default: true,
908
+ ui: {
909
+ tab: "model",
910
+ label: "Retry Model Fallback",
911
+ description: "Allow retry recovery to switch to configured fallback models",
912
+ },
913
+ },
905
914
  "retry.fallbackChains": { type: "record", default: {} as Record<string, string[]> },
906
915
  "retry.fallbackRevertPolicy": {
907
916
  type: "enum",
@@ -1855,7 +1864,7 @@ export const SETTINGS_SCHEMA = {
1855
1864
  tab: "editing",
1856
1865
  label: "Hash Lines",
1857
1866
  description:
1858
- "Include snapshot-tag headers and line numbers in read output for hashline edit mode (PATH#tag plus LINE:content)",
1867
+ "Include snapshot-tag headers and line numbers in read output for hashline edit mode ([PATH#TAG] plus LINE:content)",
1859
1868
  },
1860
1869
  },
1861
1870
 
@@ -3050,13 +3059,26 @@ export const SETTINGS_SCHEMA = {
3050
3059
  ],
3051
3060
  },
3052
3061
  },
3053
- "providers.parallelFetch": {
3054
- type: "boolean",
3055
- default: true,
3062
+ "providers.fetch": {
3063
+ type: "enum",
3064
+ values: ["auto", "native", "trafilatura", "lynx", "parallel", "jina"] as const,
3065
+ default: "auto",
3056
3066
  ui: {
3057
3067
  tab: "providers",
3058
- label: "Parallel Fetch",
3059
- description: "Use Parallel extract API for URL fetching when credentials are available",
3068
+ label: "Fetch Provider",
3069
+ description: "Reader backend priority for the fetch/read URL tool",
3070
+ options: [
3071
+ {
3072
+ value: "auto",
3073
+ label: "Auto",
3074
+ description: "Priority: native > trafilatura > lynx > parallel > jina",
3075
+ },
3076
+ { value: "native", label: "Native", description: "In-process HTML→Markdown converter (always available)" },
3077
+ { value: "trafilatura", label: "Trafilatura", description: "Auto-installs via uv/pip" },
3078
+ { value: "lynx", label: "Lynx", description: "Requires lynx system package" },
3079
+ { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
3080
+ { value: "jina", label: "Jina", description: "Uses r.jina.ai reader (JINA_API_KEY optional)" },
3081
+ ],
3060
3082
  },
3061
3083
  },
3062
3084
  "provider.appendOnlyContext": {
@@ -3307,6 +3329,7 @@ export interface RetrySettings {
3307
3329
  maxRetries: number;
3308
3330
  baseDelayMs: number;
3309
3331
  maxDelayMs: number;
3332
+ modelFallback: boolean;
3310
3333
  }
3311
3334
 
3312
3335
  export interface MemoriesSettings {
@@ -712,6 +712,17 @@ export class Settings {
712
712
  }
713
713
  }
714
714
 
715
+ // providers.parallelFetch (boolean) replaced by the providers.fetch reader
716
+ // priority enum. The new default ("auto") supersedes both old values —
717
+ // Parallel is now a deep fallback in the auto chain rather than the first
718
+ // choice — so drop the legacy key (flat and nested) and let the enum
719
+ // default apply.
720
+ const providersObj = raw.providers as Record<string, unknown> | undefined;
721
+ if (providersObj && "parallelFetch" in providersObj) {
722
+ delete providersObj.parallelFetch;
723
+ }
724
+ delete raw["providers.parallelFetch"];
725
+
715
726
  // Map legacy `memories.enabled` boolean to the explicit `memory.backend`
716
727
  // enum if the latter hasn't been set yet. Idempotent: subsequent
717
728
  // migrations are no-ops once memory.backend is materialised.
package/src/dap/client.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { logger, ptree } from "@oh-my-pi/pi-utils";
1
+ import * as fs from "node:fs/promises";
2
+ import { isEnoent, logger, ptree } from "@oh-my-pi/pi-utils";
2
3
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
3
4
  import { ToolAbortError } from "../tools/tool-errors";
4
5
  import type {
@@ -165,19 +166,7 @@ export class DapClient {
165
166
  detached: true,
166
167
  });
167
168
 
168
- // Wait for the socket file to appear (dlv needs to start listening)
169
- await waitForCondition(
170
- () => {
171
- try {
172
- Bun.file(socketPath).size;
173
- return true;
174
- } catch {
175
- return false;
176
- }
177
- },
178
- 10_000,
179
- proc,
180
- );
169
+ await waitForCondition(() => isUnixSocketReady(socketPath), 10_000, proc);
181
170
 
182
171
  const { readable, writeSink, socket } = await connectSocket({ unix: socketPath });
183
172
  const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
@@ -553,15 +542,24 @@ export class DapClient {
553
542
  }
554
543
  }
555
544
 
545
+ async function isUnixSocketReady(socketPath: string): Promise<boolean> {
546
+ try {
547
+ return (await fs.stat(socketPath)).isSocket();
548
+ } catch (error) {
549
+ if (isEnoent(error)) return false;
550
+ throw error;
551
+ }
552
+ }
553
+
556
554
  /** Poll a condition until it returns true, or timeout/process exit. */
557
555
  async function waitForCondition(
558
- check: () => boolean,
556
+ check: () => boolean | Promise<boolean>,
559
557
  timeoutMs: number,
560
558
  proc: { exitCode: number | null },
561
559
  ): Promise<void> {
562
560
  const deadline = Date.now() + timeoutMs;
563
561
  while (Date.now() < deadline) {
564
- if (check()) return;
562
+ if (await check()) return;
565
563
  if (proc.exitCode !== null) {
566
564
  throw new Error("Adapter process exited before socket was ready");
567
565
  }
@@ -1,4 +1,12 @@
1
- import { type Component, matchesKey, padding, replaceTabs, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1
+ import {
2
+ type Component,
3
+ matchesKey,
4
+ padding,
5
+ replaceTabs,
6
+ ScrollView,
7
+ truncateToWidth,
8
+ visibleWidth,
9
+ } from "@oh-my-pi/pi-tui";
2
10
  import { sanitizeText } from "@oh-my-pi/pi-utils";
3
11
  import { theme } from "../modes/theme/theme";
4
12
  import { copyToClipboard } from "../utils/clipboard";
@@ -146,14 +154,20 @@ export class RawSseViewerComponent implements Component {
146
154
  const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
147
155
  const bodyHeight = this.#bodyHeight();
148
156
  const rawLines = this.#renderRawLines(innerWidth);
149
- const body = rawLines.slice(this.#scrollOffset, this.#scrollOffset + bodyHeight);
150
- while (body.length < bodyHeight) body.push("");
157
+ const sv = new ScrollView(rawLines.slice(this.#scrollOffset, this.#scrollOffset + bodyHeight), {
158
+ height: bodyHeight,
159
+ scrollbar: "auto",
160
+ totalRows: rawLines.length,
161
+ theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
162
+ });
163
+ sv.setScrollOffset(this.#scrollOffset);
164
+ const bodyRows = sv.render(innerWidth);
151
165
 
152
166
  return [
153
167
  this.#frameTop(innerWidth),
154
168
  this.#frameLine(this.#summaryText(), innerWidth),
155
169
  this.#frameSeparator(innerWidth),
156
- ...body.map(line => this.#frameLine(line, innerWidth)),
170
+ ...bodyRows.map(line => this.#frameLine(line, innerWidth)),
157
171
  this.#frameLine(this.#statusText(), innerWidth),
158
172
  this.#frameBottom(innerWidth),
159
173
  ];
@@ -14,7 +14,7 @@ import { normalizeToLF } from "./normalize";
14
14
  /**
15
15
  * Upper bound on the file size we snapshot. A section tag is a content hash of
16
16
  * the *whole* file, so minting one means holding the full normalized text in
17
- * the store. Files above this cap emit no path#tag` header — line-anchored
17
+ * the store. Files above this cap emit no `[path#tag]` header — line-anchored
18
18
  * editing of multi-megabyte files is out of scope under the full-content model.
19
19
  */
20
20
  export const SNAPSHOT_MAX_BYTES = 4 * 1024 * 1024;
package/src/edit/index.ts CHANGED
@@ -275,7 +275,7 @@ function extractApprovalPath(args: unknown): string {
275
275
  const record = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
276
276
  const input = typeof record.input === "string" ? record.input : undefined;
277
277
  if (input) {
278
- const hashlineMatch = /^(?:¶|§|@)([^\s#]+)/m.exec(input);
278
+ const hashlineMatch = /^\[([^#\r\n]+)(?:#[0-9a-fA-F]{4})?\]/m.exec(input);
279
279
  if (hashlineMatch?.[1]) return hashlineMatch[1];
280
280
 
281
281
  const applyPatchMatch = /^\*\*\* (?:Add|Update|Delete) File:\s*(.+)$/m.exec(input);
@@ -2,7 +2,7 @@
2
2
  * Edit tool renderer and LSP batching helpers.
3
3
  */
4
4
 
5
- import { HL_FILE_PREFIX } from "@oh-my-pi/hashline";
5
+ import { HL_FILE_PREFIX, HL_FILE_SUFFIX } from "@oh-my-pi/hashline";
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
7
  import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
8
8
  import { sanitizeText } from "@oh-my-pi/pi-utils";
@@ -179,11 +179,6 @@ function countEditFiles(edits: EditRenderEntry[]): number {
179
179
  return new Set(edits.map(edit => filePathFromEditEntry(edit.path)).filter(Boolean)).size;
180
180
  }
181
181
 
182
- function countLines(text: string): number {
183
- if (!text) return 0;
184
- return text.split("\n").length;
185
- }
186
-
187
182
  function getOperationTitle(op: Operation | undefined): string {
188
183
  return op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
189
184
  }
@@ -233,19 +228,22 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
233
228
  return preview.trimEnd();
234
229
  }
235
230
 
236
- function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, label = "streaming"): string {
231
+ function formatStreamingDiff(
232
+ diff: string,
233
+ rawPath: string,
234
+ uiTheme: Theme,
235
+ expanded: boolean,
236
+ label = "streaming",
237
+ ): string {
237
238
  if (!diff) return "";
238
- // "Cursor" tail window: pin the last EDIT_STREAMING_PREVIEW_LINES rows to the
239
- // bottom of the diff so freshly streamed changes stay on screen, and accept
240
- // the trailing rows "from the back" once the diff outgrows the window. The
241
- // whole-file diff is recomputed on every streamed chunk and its Myers
242
- // alignment is not monotonic in payload length, so a hunk-aware window that
243
- // kept whole change segments gained and lost rows tick to tick — the box
244
- // stuttered, and the earlier high-water fix traded that for a half-empty
245
- // rectangle. A strict fixed-height window keeps the box steady and always
246
- // full of real diff context instead of blank padding.
239
+ // Collapsed uses a "Cursor" tail window: pin the last
240
+ // EDIT_STREAMING_PREVIEW_LINES rows to the bottom so freshly streamed changes
241
+ // stay on screen. The whole-file diff is recomputed on every streamed chunk
242
+ // and its Myers alignment is not monotonic in payload length, so a hunk-aware
243
+ // window stutters as rows move between hunks. Expanded deliberately lifts that
244
+ // cap for the approval-time full view.
247
245
  const allLines = diff.replace(/\n+$/u, "").split("\n");
248
- const hiddenLines = Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
246
+ const hiddenLines = expanded ? 0 : Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
249
247
  const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
250
248
  let text = "\n\n";
251
249
  if (hiddenLines > 0) {
@@ -256,19 +254,11 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
256
254
  text += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
257
255
  }
258
256
  text += renderDiffColored(visible.join("\n"), { filePath: rawPath });
259
- text += uiTheme.fg("dim", `\n(${label})`);
257
+ if (!expanded || label !== "preview") text += uiTheme.fg("dim", `\n(${label})`);
260
258
  return text;
261
259
  }
262
260
 
263
- function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
264
- const icon = uiTheme.getLangIcon(language);
265
- if (lineCount !== null) {
266
- return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
267
- }
268
- return uiTheme.fg("dim", `${icon}`);
269
- }
270
-
271
- function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: Theme): string {
261
+ function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: Theme, expanded: boolean): string {
272
262
  const parts: string[] = [];
273
263
  for (const preview of previews) {
274
264
  if (!preview.diff && !preview.error) continue;
@@ -278,7 +268,7 @@ function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: T
278
268
  continue;
279
269
  }
280
270
  if (preview.diff) {
281
- parts.push(`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, "preview")}`);
271
+ parts.push(`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, expanded, "preview")}`);
282
272
  }
283
273
  }
284
274
  return parts.join("");
@@ -289,16 +279,17 @@ function getCallPreview(
289
279
  rawPath: string,
290
280
  uiTheme: Theme,
291
281
  renderContext: EditRenderContext | undefined,
282
+ expanded: boolean,
292
283
  ): string {
293
284
  const multi = renderContext?.perFileDiffPreview;
294
285
  if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
295
- return formatMultiFileStreamingDiff(multi, uiTheme);
286
+ return formatMultiFileStreamingDiff(multi, uiTheme, expanded);
296
287
  }
297
288
  if (args.previewDiff) {
298
- return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, "preview");
289
+ return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, expanded, "preview");
299
290
  }
300
291
  if (args.diff && args.op) {
301
- return formatStreamingDiff(args.diff, rawPath, uiTheme);
292
+ return formatStreamingDiff(args.diff, rawPath, uiTheme, expanded);
302
293
  }
303
294
  if (args.diff) {
304
295
  return renderPlainTextPreview(args.diff, uiTheme, rawPath);
@@ -328,12 +319,12 @@ function normalizeHashlineInputPreviewPath(rawPath: string): string {
328
319
  }
329
320
 
330
321
  function parseHashlineInputPreviewHeader(line: string): string | null {
331
- if (!line.startsWith(HL_FILE_PREFIX)) return null;
332
- // Mirror hashline/input.ts: strip every leading file marker so canonical
333
- // PATH` headers and stray `¶¶ PATH` / `¶¶¶PATH` runs render clean paths.
334
- let prefixEnd = 0;
335
- while (prefixEnd < line.length && line[prefixEnd] === HL_FILE_PREFIX) prefixEnd++;
336
- const body = line.slice(prefixEnd).trim();
322
+ const trimmed = line.trimEnd();
323
+ if (!trimmed.startsWith(HL_FILE_PREFIX)) return null;
324
+ // Keep streaming previews tolerant while the closing bracket is still
325
+ // being generated; the parser enforces the final `[path#TAG]` shape.
326
+ const bodyEnd = trimmed.endsWith(HL_FILE_SUFFIX) ? trimmed.length - HL_FILE_SUFFIX.length : trimmed.length;
327
+ const body = trimmed.slice(HL_FILE_PREFIX.length, bodyEnd).trim();
337
328
  const previewPath = normalizeHashlineInputPreviewPath(body);
338
329
  return previewPath.length > 0 ? previewPath : null;
339
330
  }
@@ -383,6 +374,13 @@ function getApplyPatchRenderSummary(
383
374
  }
384
375
  }
385
376
 
377
+ function formatDiffStatsSuffix(diff: string, uiTheme: Theme): string {
378
+ const { added, removed, hunks } = getDiffStats(diff);
379
+ const stats = formatDiffStats(added, removed, hunks, uiTheme);
380
+ if (!stats) return "";
381
+ return ` ${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${stats}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
382
+ }
383
+
386
384
  function renderDiffSection(
387
385
  diff: string,
388
386
  rawPath: string,
@@ -390,15 +388,6 @@ function renderDiffSection(
390
388
  uiTheme: Theme,
391
389
  renderDiffFn: (t: string, o?: { filePath?: string }) => string,
392
390
  ): string {
393
- let text = "";
394
- const diffStats = getDiffStats(diff);
395
- text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${formatDiffStats(
396
- diffStats.added,
397
- diffStats.removed,
398
- diffStats.hunks,
399
- uiTheme,
400
- )}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
401
-
402
391
  const {
403
392
  text: truncatedDiff,
404
393
  hiddenHunks,
@@ -407,7 +396,7 @@ function renderDiffSection(
407
396
  ? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
408
397
  : truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
409
398
 
410
- text += `\n\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
399
+ let text = `\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
411
400
  if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
412
401
  const remainder: string[] = [];
413
402
  if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
@@ -481,7 +470,7 @@ export const editToolRenderer = {
481
470
  if (fileCount > 1) {
482
471
  text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
483
472
  }
484
- text += getCallPreview(editArgs, rawPath, uiTheme, renderContext);
473
+ text += getCallPreview(editArgs, rawPath, uiTheme, renderContext, options.expanded);
485
474
  if (applyPatchSummary?.error) {
486
475
  text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), CALL_TEXT_PREVIEW_WIDTH))}`;
487
476
  }
@@ -528,11 +517,6 @@ function renderSingleFileResult(
528
517
  "";
529
518
  const op = args?.op || firstEdit?.op || details?.op;
530
519
  const rename = args?.rename || firstEdit?.rename || firstEdit?.move || details?.move;
531
- const { language } = formatEditDescription(rawPath, uiTheme, { rename });
532
-
533
- const editTextSource = args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch;
534
- const metadataLineCount = editTextSource ? countLines(editTextSource) : null;
535
- const metadataLine = op !== "delete" ? `\n${formatMetadataLine(metadataLineCount, language, uiTheme)}` : "";
536
520
 
537
521
  const displayErrorText = isError && details && "displayErrorText" in details ? details.displayErrorText : undefined;
538
522
  const errorText = isError
@@ -556,6 +540,11 @@ function renderSingleFileResult(
556
540
  (details && !isError ? details.firstChangedLine : undefined);
557
541
  const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine });
558
542
 
543
+ // Change stats ride inline on the header next to the path rather than a separate row.
544
+ const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
545
+ const headerDiff = isError ? undefined : details?.diff || previewDiff;
546
+ const statsSuffix = headerDiff ? formatDiffStatsSuffix(headerDiff, uiTheme) : "";
547
+
559
548
  const header = renderStatusLine(
560
549
  {
561
550
  icon: isError ? "error" : "success",
@@ -564,8 +553,7 @@ function renderSingleFileResult(
564
553
  },
565
554
  uiTheme,
566
555
  );
567
- let text = header;
568
- text += metadataLine;
556
+ let text = header + statsSuffix;
569
557
 
570
558
  if (isError) {
571
559
  if (errorText) {
@@ -424,7 +424,7 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
424
424
  return previews.length > 0 ? previews : null;
425
425
  },
426
426
  renderStreamingFallback() {
427
- // Never leak raw hashline syntax (`64:`, `|payload`, path#hash`)
427
+ // Never leak raw hashline syntax (`64:`, `|payload`, `[path#hash]`)
428
428
  // to the user — the streaming preview already projects every
429
429
  // parseable op onto the real file via applyPartialTo, and an
430
430
  // unparseable trailing chunk renders as "no preview yet" rather