@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.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 (246) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
@@ -17,6 +17,7 @@ import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
  import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
19
  import { type AuthCredential, SqliteAuthCredentialStore, type TSchema } from "@oh-my-pi/pi-ai";
20
+ import { piEscapeRegexLiteral, piJoinPath } from "@oh-my-pi/pi-ai/providers/cursor-pi-args";
20
21
  import { getKeybindings, type Keybinding, Text } from "@oh-my-pi/pi-tui";
21
22
  import {
22
23
  getAgentDbPath,
@@ -139,6 +140,25 @@ export interface LsToolOptions {
139
140
  operations?: LsOperations;
140
141
  }
141
142
 
143
+ export interface EditOperations {
144
+ readFile: (absolutePath: string) => Promise<Buffer>;
145
+ writeFile: (absolutePath: string, content: string) => Promise<void>;
146
+ access: (absolutePath: string) => Promise<void>;
147
+ }
148
+
149
+ export interface EditToolOptions {
150
+ operations?: EditOperations;
151
+ }
152
+
153
+ export interface WriteOperations {
154
+ writeFile: (absolutePath: string, content: string) => Promise<void>;
155
+ mkdir: (dir: string) => Promise<void>;
156
+ }
157
+
158
+ export interface WriteToolOptions {
159
+ operations?: WriteOperations;
160
+ }
161
+
142
162
  const legacyBashSchema = Type.Object({
143
163
  command: Type.String({ description: "Bash command to execute" }),
144
164
  timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })),
@@ -298,16 +318,6 @@ function lineRangePath(readPath: string, offset: number | undefined, limit: numb
298
318
  return `${readPath}:${start}-${end}`;
299
319
  }
300
320
 
301
- function escapeRegexLiteral(value: string): string {
302
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
303
- }
304
-
305
- function joinLegacyGlob(searchPath: string, pattern: string): string {
306
- if (path.isAbsolute(pattern)) return pattern;
307
- if (!searchPath || searchPath === ".") return pattern;
308
- return path.join(searchPath, pattern);
309
- }
310
-
311
321
  function normalizeLegacyLimit(limit: number | undefined, fallback: number): number {
312
322
  if (limit === undefined || !Number.isFinite(limit)) return fallback;
313
323
  return Math.max(1, Math.floor(limit));
@@ -512,7 +522,7 @@ export function createGrepToolDefinition(cwd: string, options?: GrepToolOptions)
512
522
  renderResult: legacyRenderResult,
513
523
  execute: (toolCallId, params, signal, onUpdate) => {
514
524
  const rawPattern = stringField(params, "pattern") ?? "";
515
- const pattern = booleanField(params, "literal") ? escapeRegexLiteral(rawPattern) : rawPattern;
525
+ const pattern = booleanField(params, "literal") ? piEscapeRegexLiteral(rawPattern) : rawPattern;
516
526
  const searchPath = stringField(params, "path") ?? ".";
517
527
  const glob = stringField(params, "glob");
518
528
  const context = numberField(params, "context");
@@ -529,7 +539,7 @@ export function createGrepToolDefinition(cwd: string, options?: GrepToolOptions)
529
539
  toolCallId,
530
540
  {
531
541
  pattern,
532
- path: glob ? joinLegacyGlob(searchPath, glob) : searchPath,
542
+ path: glob ? piJoinPath(searchPath, glob) : searchPath,
533
543
  case: booleanField(params, "ignoreCase") ? false : undefined,
534
544
  },
535
545
  signal,
@@ -587,7 +597,7 @@ export function createFindToolDefinition(cwd: string, options?: FindToolOptions)
587
597
  }
588
598
  return tool.execute(
589
599
  toolCallId,
590
- { path: joinLegacyGlob(searchPath, pattern), hidden: true, gitignore: true, limit },
600
+ { path: piJoinPath(searchPath, pattern), hidden: true, gitignore: true, limit },
591
601
  signal,
592
602
  onUpdate,
593
603
  );
@@ -645,6 +655,40 @@ export function createLsTool(cwd: string, options?: LsToolOptions): ToolDefiniti
645
655
  return createLsToolDefinition(cwd, options);
646
656
  }
647
657
 
658
+ /** Create the legacy edit tool definition. */
659
+ export function createEditToolDefinition(cwd: string, options?: EditToolOptions): ToolDefinition {
660
+ if (options?.operations) {
661
+ throw new Error(
662
+ "Legacy EditToolOptions.operations is not supported: OMP's built-in edit tool writes the local " +
663
+ "filesystem natively and exposes no pluggable operations seam. Register a custom edit tool via " +
664
+ "defineTool() instead of passing operations to createEditTool()/createEditToolDefinition().",
665
+ );
666
+ }
667
+ return legacyBuiltinTool(cwd, "edit");
668
+ }
669
+
670
+ /** Create the legacy edit tool. */
671
+ export function createEditTool(cwd: string, options?: EditToolOptions): ToolDefinition {
672
+ return createEditToolDefinition(cwd, options);
673
+ }
674
+
675
+ /** Create the legacy write tool definition. */
676
+ export function createWriteToolDefinition(cwd: string, options?: WriteToolOptions): ToolDefinition {
677
+ if (options?.operations) {
678
+ throw new Error(
679
+ "Legacy WriteToolOptions.operations is not supported: OMP's built-in write tool writes the local " +
680
+ "filesystem natively and exposes no pluggable operations seam. Register a custom write tool via " +
681
+ "defineTool() instead of passing operations to createWriteTool()/createWriteToolDefinition().",
682
+ );
683
+ }
684
+ return legacyBuiltinTool(cwd, "write");
685
+ }
686
+
687
+ /** Create the legacy write tool. */
688
+ export function createWriteTool(cwd: string, options?: WriteToolOptions): ToolDefinition {
689
+ return createWriteToolDefinition(cwd, options);
690
+ }
691
+
648
692
  /** Create legacy read, bash, edit, and write tools. */
649
693
  export function createCodingTools(cwd: string): ToolDefinition[] {
650
694
  return LEGACY_CODING_TOOL_NAMES.map(name => legacyBuiltinTool(cwd, name));
@@ -1378,6 +1422,14 @@ export function getPackageDir(): string {
1378
1422
  // check during validation (issue #6583).
1379
1423
  export { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
1380
1424
 
1425
+ // Same barrel gap for two more legacy package-root exports: pi re-exported the
1426
+ // `CONFIG_DIR_NAME` constant and the CLI parser `parseArgs`. In omp
1427
+ // `CONFIG_DIR_NAME` lives in `@oh-my-pi/pi-utils` and `parseArgs` in
1428
+ // `../cli/args`, neither of which the barrel below forwards, so legacy
1429
+ // extensions importing either fail Bun's static export check during validation.
1430
+ export { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils";
1431
+ export { parseArgs } from "../cli/args";
1432
+
1381
1433
  export * from "../index";
1382
1434
  export { formatBytes as formatSize } from "../tools/render-utils";
1383
1435
  export { copyToClipboard } from "../utils/clipboard";
@@ -20,6 +20,7 @@ export * from "./omp-protocol";
20
20
  export * from "./parse";
21
21
  export * from "./router";
22
22
  export * from "./rule-protocol";
23
+ export * from "./security-protocol";
23
24
  export * from "./skill-protocol";
24
25
  export * from "./ssh-protocol";
25
26
  export type * from "./types";
@@ -390,8 +390,9 @@ export class LocalProtocolHandler implements ProtocolHandler {
390
390
 
391
391
  /**
392
392
  * Install a process-global override that wins over the AgentRegistry-based
393
- * derivation. Used by SDK consumers that wire `localProtocolOptions` on
394
- * `createAgentSession` and by subagents that share their parent's root.
393
+ * derivation. Used by top-level SDK consumers that wire
394
+ * `localProtocolOptions` on `createAgentSession`; subagents keep their
395
+ * inherited mapping session-bound.
395
396
  */
396
397
  static setOverride(value: LocalProtocolOptions | undefined): void {
397
398
  LocalProtocolHandler.#override = value;
@@ -97,7 +97,10 @@ function formatAvailableResources(mcpManager: MCPManager): string {
97
97
  .getConnectedServers()
98
98
  .flatMap(name => {
99
99
  const serverResources = mcpManager.getServerResources(name);
100
- return (serverResources?.resources ?? []).map(r => ` ${r.uri} (${name})`);
100
+ if (!serverResources) return [];
101
+ const concrete = serverResources.resources.map(r => ` ${r.uri} (${name})`);
102
+ const templates = serverResources.templates.map(t => ` ${t.uriTemplate} (${name}, template)`);
103
+ return [...concrete, ...templates];
101
104
  })
102
105
  .join("\n");
103
106
  return available || " (none)";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Internal URL router for internal protocols (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, `ssh://`, `vault://`, and `xd://`).
2
+ * Internal URL router for internal protocols (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `security://`, `skill://`, `ssh://`, `vault://`, and `xd://`).
3
3
  *
4
4
  * One process-global router with one handler per scheme. Access via
5
5
  * `InternalUrlRouter.instance()`. Handlers are stateless; per-session and
@@ -15,6 +15,7 @@ import { MemoryProtocolHandler } from "./memory-protocol";
15
15
  import { OmpProtocolHandler } from "./omp-protocol";
16
16
  import { extractUriScheme, parseInternalUrl } from "./parse";
17
17
  import { RuleProtocolHandler } from "./rule-protocol";
18
+ import { SecurityProtocolHandler } from "./security-protocol";
18
19
  import { SkillProtocolHandler } from "./skill-protocol";
19
20
  import { SshProtocolHandler } from "./ssh-protocol";
20
21
  import type {
@@ -42,6 +43,8 @@ export class InternalUrlRouter {
42
43
  this.register(new VaultProtocolHandler());
43
44
  this.register(new SkillProtocolHandler());
44
45
  this.register(new RuleProtocolHandler());
46
+ // Reserved OMP-owned security-analysis namespace; vendor adapters normalize into its store.
47
+ this.register(new SecurityProtocolHandler());
45
48
  this.register(new McpProtocolHandler());
46
49
  this.register(new IssueProtocolHandler());
47
50
  this.register(new PrProtocolHandler());
@@ -0,0 +1,261 @@
1
+ import * as path from "node:path";
2
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
3
+ import { isSettingsInitialized, settings } from "../config/settings";
4
+ import { getDefault } from "../config/settings-schema";
5
+ import type { SecurityFinding } from "../security/contracts";
6
+ import { createPublicSecurityScan, redactPrivateSecurityMetadata } from "../security/provenance";
7
+ import { createSecurityResource } from "../security/resource-output";
8
+ import type { SecurityScanSummary } from "../security/store";
9
+ import { SecurityStore } from "../security/store";
10
+ import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
11
+
12
+ export type SecurityStoreResolver = (cwd: string, signal?: AbortSignal) => Promise<SecurityStore>;
13
+
14
+ export function isSecurityEnabled(): boolean {
15
+ if (!isSettingsInitialized()) return getDefault("security.enabled");
16
+ try {
17
+ return settings.get("security.enabled");
18
+ } catch {
19
+ return getDefault("security.enabled");
20
+ }
21
+ }
22
+
23
+ function securityEnabledFromContext(context?: ResolveContext): boolean | undefined {
24
+ if (!context?.settings || typeof context.settings !== "object") return undefined;
25
+ try {
26
+ const get = Reflect.get(context.settings, "get");
27
+ if (typeof get !== "function") return undefined;
28
+ const enabled = Reflect.apply(get, context.settings, ["security.enabled"]);
29
+ return typeof enabled === "boolean" ? enabled : undefined;
30
+ } catch {
31
+ return undefined;
32
+ }
33
+ }
34
+
35
+ const SECURITY_DISABLED_MESSAGE =
36
+ "security:// is disabled. Enable it by setting `security.enabled = true` (Settings → Tools → Security).";
37
+
38
+ export class SecurityDisabledError extends Error {
39
+ constructor() {
40
+ super(SECURITY_DISABLED_MESSAGE);
41
+ this.name = "SecurityDisabledError";
42
+ }
43
+ }
44
+
45
+ function splitSecurityPath(url: InternalUrl): string[] {
46
+ const host = url.rawHost || url.hostname;
47
+ const pathname = (url.rawPathname ?? url.pathname).replace(/^\/+/, "");
48
+ return [host, ...pathname.split("/")].filter(Boolean).map(segment => decodeURIComponent(segment));
49
+ }
50
+
51
+ function formatScans(scans: SecurityScanSummary[]): string {
52
+ if (scans.length === 0) return "# Security scans\n\nNo scans are stored for this project.\n";
53
+ const rows = scans.map(
54
+ scan =>
55
+ `- \`${scan.id}\` — ${scan.status}; ${scan.findingCount} finding(s); ${scan.producer.name}; ${scan.createdAt}`,
56
+ );
57
+ return `# Security scans\n\n${rows.join("\n")}\n`;
58
+ }
59
+
60
+ function formatFinding(finding: SecurityFinding): string {
61
+ const locations = finding.occurrences.flatMap(occurrence => occurrence.locations);
62
+ const locationLines = locations.map(location => {
63
+ const end = location.endLine && location.endLine !== location.startLine ? `-${location.endLine}` : "";
64
+ return [
65
+ `- \`${sanitizeText(location.path)}:${location.startLine}${end}\``,
66
+ location.role ? ` (${sanitizeText(location.role)})` : "",
67
+ ].join("");
68
+ });
69
+ const evidence = finding.evidence.map(
70
+ item => `- **${sanitizeText(item.label)}** — ${sanitizeText(item.explanation)}`,
71
+ );
72
+ return [
73
+ `# ${sanitizeText(finding.title)}`,
74
+ "",
75
+ `- ID: \`${finding.id}\``,
76
+ `- Rule: \`${sanitizeText(finding.ruleId)}\``,
77
+ `- Severity: **${finding.severity.level}**`,
78
+ `- Confidence: **${finding.confidence.level}**`,
79
+ `- Disposition: **${finding.disposition.status}**`,
80
+ `- Fingerprint: \`${finding.fingerprint}\``,
81
+ "",
82
+ "## Summary",
83
+ "",
84
+ sanitizeText(finding.summary),
85
+ "",
86
+ "## Locations",
87
+ "",
88
+ ...(locationLines.length > 0 ? locationLines : ["No source locations recorded."]),
89
+ "",
90
+ "## Evidence",
91
+ "",
92
+ ...(evidence.length > 0 ? evidence : ["No expanded evidence recorded."]),
93
+ "",
94
+ "## Remediation",
95
+ "",
96
+ sanitizeText(finding.remediation ?? "No remediation guidance recorded."),
97
+ "",
98
+ ].join("\n");
99
+ }
100
+
101
+ export class SecurityProtocolHandler implements ProtocolHandler {
102
+ readonly scheme = "security";
103
+ readonly immutable = true;
104
+ readonly #resolveStore: SecurityStoreResolver;
105
+ readonly #enabled: () => boolean;
106
+
107
+ constructor(
108
+ resolveStore: SecurityStoreResolver = (cwd, signal) => SecurityStore.openForCwd(cwd, { signal }),
109
+ enabled: () => boolean = isSecurityEnabled,
110
+ ) {
111
+ this.#resolveStore = resolveStore;
112
+ this.#enabled = enabled;
113
+ }
114
+
115
+ async #store(context?: ResolveContext): Promise<SecurityStore> {
116
+ return this.#resolveStore(path.resolve(context?.cwd ?? process.cwd()), context?.signal);
117
+ }
118
+
119
+ async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
120
+ if (!(securityEnabledFromContext(context) ?? this.#enabled())) throw new SecurityDisabledError();
121
+ const parts = splitSecurityPath(url);
122
+ const store = await this.#store(context);
123
+ if (parts.length === 0) {
124
+ return createSecurityResource({
125
+ url: "security://",
126
+ content: [
127
+ "# Security",
128
+ "",
129
+ "OMP-owned software-security analysis resources. The namespace is read-only; use explicit security commands or tools for mutations.",
130
+ "",
131
+ "- `security://scans` — list scans",
132
+ "",
133
+ ].join("\n"),
134
+ contentType: "text/markdown",
135
+ isDirectory: true,
136
+ });
137
+ }
138
+ if (parts[0] !== "scans") throw new Error(`Unknown security resource: security://${parts.join("/")}`);
139
+ if (parts.length === 1) {
140
+ return createSecurityResource({
141
+ url: "security://scans",
142
+ content: formatScans(await store.listScans()),
143
+ contentType: "text/markdown",
144
+ isDirectory: true,
145
+ });
146
+ }
147
+ const scanId = parts[1];
148
+ const bundle = await store.getBundle(scanId);
149
+ if (!bundle) throw new Error(`Unknown security scan: ${scanId}`);
150
+ if (parts.length === 2) {
151
+ return createSecurityResource({
152
+ url: `security://scans/${scanId}`,
153
+ content: [
154
+ `# Security scan ${scanId}`,
155
+ "",
156
+ `- Status: **${bundle.scan.status}**`,
157
+ `- Producer: **${sanitizeText(bundle.scan.producer.name)}**`,
158
+ `- Findings: **${bundle.findings.length}**`,
159
+ `- Coverage: **${bundle.scan.coverage.completeness}**`,
160
+ `- Target: \`${sanitizeText(bundle.scan.target.displayName)}\``,
161
+ "",
162
+ "Resources: `manifest`, `findings`, `coverage`, `report`, `sarif`, `provenance`.",
163
+ "",
164
+ ].join("\n"),
165
+ contentType: "text/markdown",
166
+ isDirectory: true,
167
+ });
168
+ }
169
+ switch (parts[2]) {
170
+ case "manifest":
171
+ if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
172
+ return createSecurityResource({
173
+ url: `security://scans/${scanId}/manifest`,
174
+ content: `${JSON.stringify(createPublicSecurityScan(bundle.scan, { includePlan: true }), null, 2)}\n`,
175
+ contentType: "application/json",
176
+ });
177
+ case "findings": {
178
+ if (parts.length === 3) {
179
+ const listing = bundle.findings.map(finding =>
180
+ [
181
+ `- \`${finding.id}\` **${finding.severity.level}** — ${sanitizeText(finding.title)}`,
182
+ ` (\`${sanitizeText(finding.ruleId)}\`)`,
183
+ ].join(""),
184
+ );
185
+ return createSecurityResource({
186
+ url: `security://scans/${scanId}/findings`,
187
+ content: `# Findings for ${scanId}\n\n${listing.length > 0 ? listing.join("\n") : "No findings."}\n`,
188
+ contentType: "text/markdown",
189
+ isDirectory: true,
190
+ });
191
+ }
192
+ if (parts.length !== 4) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
193
+ const findingId = parts[3];
194
+ const finding = await store.getFinding(scanId, findingId);
195
+ if (!finding) throw new Error(`Unknown security finding: ${findingId}`);
196
+ return createSecurityResource({
197
+ url: `security://scans/${scanId}/findings/${findingId}`,
198
+ content: formatFinding(finding),
199
+ contentType: "text/markdown",
200
+ });
201
+ }
202
+ case "coverage":
203
+ if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
204
+ return createSecurityResource({
205
+ url: `security://scans/${scanId}/coverage`,
206
+ content: `${JSON.stringify(bundle.scan.coverage, null, 2)}\n`,
207
+ contentType: "application/json",
208
+ });
209
+ case "report":
210
+ if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
211
+ if (bundle.report === undefined) throw new Error(`Security scan ${scanId} has no report`);
212
+ return createSecurityResource({
213
+ url: `security://scans/${scanId}/report`,
214
+ content: bundle.report,
215
+ contentType: "text/markdown",
216
+ });
217
+ case "sarif":
218
+ if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
219
+ if (bundle.sarif === undefined) throw new Error(`Security scan ${scanId} has no SARIF export`);
220
+ return createSecurityResource({
221
+ url: `security://scans/${scanId}/sarif`,
222
+ content: `${JSON.stringify(bundle.sarif, null, 2)}\n`,
223
+ contentType: "application/json",
224
+ });
225
+ case "provenance":
226
+ if (parts.length !== 3) throw new Error(`Unknown security resource: security://${parts.join("/")}`);
227
+ return createSecurityResource({
228
+ url: `security://scans/${scanId}/provenance`,
229
+ content: `${JSON.stringify(redactPrivateSecurityMetadata(bundle.scan.provenance), null, 2)}\n`,
230
+ contentType: "application/json",
231
+ });
232
+ default:
233
+ throw new Error(`Unknown security resource: security://${parts.join("/")}`);
234
+ }
235
+ }
236
+
237
+ async complete(query = "", context?: ResolveContext): Promise<UrlCompletion[]> {
238
+ if (!(securityEnabledFromContext(context) ?? this.#enabled())) return [];
239
+ const store = await this.#store(context);
240
+ const scans = await store.listScans();
241
+ const candidates: UrlCompletion[] = [{ value: "scans", label: "Scans", description: "Stored security scans" }];
242
+ for (const scan of scans.slice(0, 50)) {
243
+ const prefix = `scans/${scan.id}`;
244
+ candidates.push({
245
+ value: prefix,
246
+ label: scan.id,
247
+ description: `${scan.status}; ${scan.findingCount} findings`,
248
+ });
249
+ for (const child of ["manifest", "findings", "coverage", "report", "sarif", "provenance"]) {
250
+ candidates.push({ value: `${prefix}/${child}`, label: `${scan.id}/${child}` });
251
+ }
252
+ }
253
+ const normalizedQuery = query.trim().toLowerCase();
254
+ if (!normalizedQuery) return candidates;
255
+ return candidates.filter(candidate =>
256
+ [candidate.value, candidate.label ?? "", candidate.description ?? ""].some(value =>
257
+ value.toLowerCase().includes(normalizedQuery),
258
+ ),
259
+ );
260
+ }
261
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Types for the internal URL routing system.
3
3
  *
4
- * Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, `ssh://`, `vault://`, and `xd://`) are resolved by tools like read,
4
+ * Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `security://`, `skill://`, `ssh://`, `vault://`, and `xd://`) are resolved by tools like read,
5
5
  * providing access to agent outputs and server resources without exposing filesystem paths.
6
6
  */
7
7
 
@@ -0,0 +1,53 @@
1
+ import { workerHostEntry } from "@oh-my-pi/pi-utils/worker-host";
2
+ import type { TerminalOutputOptions } from "./terminal-output";
3
+ import {
4
+ TERMINAL_OUTPUT_WORKER_ARG,
5
+ type TerminalOutputWorkerRequest,
6
+ type TerminalOutputWorkerResult,
7
+ } from "./terminal-output-worker-protocol";
8
+
9
+ /** Replay legacy broker PTY bytes without evaluating xterm in the client process. */
10
+ export async function renderTerminalOutputIsolated(
11
+ output: string,
12
+ options: TerminalOutputOptions,
13
+ ): Promise<string[] | undefined> {
14
+ const hostEntry = workerHostEntry();
15
+ const worker = hostEntry
16
+ ? new Worker(hostEntry, { type: "module", argv: [TERMINAL_OUTPUT_WORKER_ARG] })
17
+ : new Worker(new URL("./terminal-output-worker.ts", import.meta.url).href, { type: "module" });
18
+ const pending = Promise.withResolvers<string[] | undefined>();
19
+ const onMessage = (event: MessageEvent<TerminalOutputWorkerResult>): void => {
20
+ if (event.data.ok) pending.resolve(event.data.rows);
21
+ else pending.reject(new Error(event.data.error));
22
+ };
23
+ const onError = (event: ErrorEvent): void => {
24
+ pending.reject(event.error instanceof Error ? event.error : new Error(event.message));
25
+ };
26
+ const onClose = (): void => {
27
+ pending.reject(new Error("Terminal output worker exited before responding"));
28
+ };
29
+ worker.addEventListener("message", onMessage);
30
+ worker.addEventListener("error", onError);
31
+ worker.addEventListener("close", onClose);
32
+ try {
33
+ const request: TerminalOutputWorkerRequest = { output, options };
34
+ worker.postMessage(request);
35
+ return await pending.promise;
36
+ } finally {
37
+ worker.removeEventListener("message", onMessage);
38
+ worker.removeEventListener("error", onError);
39
+ worker.removeEventListener("close", onClose);
40
+ worker.terminate();
41
+ }
42
+ }
43
+
44
+ /** Distribution smoke for source, npm-bundle, and compiled worker routing. */
45
+ export async function smokeTestTerminalOutputWorker(): Promise<void> {
46
+ const rows = await renderTerminalOutputIsolated("old\r\x1b[2K\x1b[1;32mready\x1b[0m", {
47
+ head: false,
48
+ maxRows: 10,
49
+ });
50
+ if (rows?.length !== 1 || rows[0] !== "\x1b[0m\x1b[1;38;5;2mready") {
51
+ throw new Error("terminal output worker smoke mismatch");
52
+ }
53
+ }
@@ -0,0 +1,11 @@
1
+ import type { TerminalOutputOptions } from "./terminal-output";
2
+
3
+ /** Hidden CLI selector for legacy PTY replay outside the client process. */
4
+ export const TERMINAL_OUTPUT_WORKER_ARG = "__omp_worker_terminal_output";
5
+
6
+ export interface TerminalOutputWorkerRequest {
7
+ output: string;
8
+ options: TerminalOutputOptions;
9
+ }
10
+
11
+ export type TerminalOutputWorkerResult = { ok: true; rows: string[] | undefined } | { ok: false; error: string };
@@ -0,0 +1,23 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
3
+ import { renderTerminalOutput } from "./terminal-output";
4
+ import type { TerminalOutputWorkerRequest, TerminalOutputWorkerResult } from "./terminal-output-worker-protocol";
5
+
6
+ if (!parentPort) throw new Error("terminal-output-worker: missing parentPort");
7
+
8
+ const port = parentPort;
9
+ const inbox = consumeWorkerInbox();
10
+ const handle = async (message: unknown): Promise<void> => {
11
+ const request = message as TerminalOutputWorkerRequest;
12
+ let result: TerminalOutputWorkerResult;
13
+ try {
14
+ result = { ok: true, rows: await renderTerminalOutput(request.output, request.options) };
15
+ } catch (error) {
16
+ result = { ok: false, error: error instanceof Error ? error.message : String(error) };
17
+ }
18
+ port.postMessage(result);
19
+ port.close();
20
+ };
21
+
22
+ if (inbox) inbox.bind(message => void handle(message));
23
+ else port.on("message", message => void handle(message));
package/src/lsp/client.ts CHANGED
@@ -27,6 +27,7 @@ const fileOperationLocks = new Map<string, Promise<void>>();
27
27
  /** Negative cache of recent init failures so a broken server fails fast instead of re-spawning per call. */
28
28
  const INIT_FAILURE_BACKOFF_MS = 3 * 60 * 1000;
29
29
  const initFailures = new Map<string, { at: number; message: string }>();
30
+ const READER_EXIT_GRACE_MS = 100;
30
31
 
31
32
  // Idle timeout configuration (disabled by default)
32
33
  let idleTimeoutMs: number | null = null;
@@ -303,6 +304,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
303
304
 
304
305
  const framer = new MessageFramer(Buffer.from(client.messageBuffer));
305
306
 
307
+ let readerFailed = false;
306
308
  try {
307
309
  while (true) {
308
310
  const { done, value } = await reader.read();
@@ -391,6 +393,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
391
393
  }
392
394
  }
393
395
  } catch (err) {
396
+ readerFailed = true;
394
397
  // Connection closed or error - reject all pending requests
395
398
  for (const pending of Array.from(client.pendingRequests.values())) {
396
399
  pending.reject(new Error(`LSP connection closed: ${err}`));
@@ -401,6 +404,9 @@ async function startMessageReader(client: LspClient): Promise<void> {
401
404
  client.messageBuffer = framer.remainder();
402
405
  reader.releaseLock();
403
406
  client.isReading = false;
407
+ if (!readerFailed && client.proc.exitCode === null) {
408
+ await waitForExit(client, READER_EXIT_GRACE_MS);
409
+ }
404
410
  // Reader exited while the server process is still alive (unrecoverable
405
411
  // read error or bad stream state): nothing will route responses anymore,
406
412
  // so tear the client down — the next call respawns instead of timing out.
@@ -676,6 +682,15 @@ const PROJECT_LOAD_TIMEOUT_MS = 15_000;
676
682
  const SHUTDOWN_TIMEOUT_MS = 5_000;
677
683
  const EXIT_TIMEOUT_MS = 1_000;
678
684
 
685
+ function clientKey(config: ServerConfig, cwd: string): string {
686
+ return `${config.command}:${cwd}`;
687
+ }
688
+
689
+ /** Allow an explicit user reload to retry a matching initialization failure immediately. */
690
+ export function clearInitializationFailure(config: ServerConfig, cwd: string): void {
691
+ initFailures.delete(clientKey(config, cwd));
692
+ }
693
+
679
694
  /**
680
695
  * Get or create an LSP client for the given server configuration and working directory.
681
696
  * @param config - Server configuration
@@ -692,7 +707,7 @@ export async function getOrCreateClient(
692
707
  initTimeoutMs?: number,
693
708
  signal?: AbortSignal,
694
709
  ): Promise<LspClient> {
695
- const key = `${config.command}:${cwd}`;
710
+ const key = clientKey(config, cwd);
696
711
 
697
712
  // Check if client already exists
698
713
  const existingClient = clients.get(key);
@@ -865,13 +880,13 @@ export async function getActiveOrPendingClient(
865
880
  signal?: AbortSignal,
866
881
  ): Promise<LspClient | undefined> {
867
882
  throwIfAborted(signal);
868
- const client = clients.get(`${config.command}:${cwd}`);
883
+ const client = clients.get(clientKey(config, cwd));
869
884
  if (client) {
870
885
  client.lastActivity = Date.now();
871
886
  return client;
872
887
  }
873
888
 
874
- const pending = clientLocks.get(`${config.command}:${cwd}`);
889
+ const pending = clientLocks.get(clientKey(config, cwd));
875
890
  if (!pending) return undefined;
876
891
  try {
877
892
  return await untilAborted(signal, pending);
package/src/lsp/index.ts CHANGED
@@ -17,6 +17,7 @@ import { formatPathRelativeToCwd, resolveToCwd } from "../tools/path-utils";
17
17
  import { ToolAbortError, ToolError, throwIfAborted } from "../tools/tool-errors";
18
18
  import { clampTimeout } from "../tools/tool-timeouts";
19
19
  import {
20
+ clearInitializationFailure,
20
21
  ensureFileOpen,
21
22
  FileChangeType,
22
23
  getActiveClients,
@@ -1598,6 +1599,9 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1598
1599
  _context?: AgentToolContext,
1599
1600
  ): Promise<AgentToolResult<LspToolDetails>> {
1600
1601
  const { action, file, line, symbol, query, new_name, apply, timeout } = params;
1602
+ if (this.session.lspReadOnly && !LSP_READONLY_ACTIONS.has(action)) {
1603
+ throw new ToolError(`LSP action ${action} is disabled in this read-only session`);
1604
+ }
1601
1605
  const timeoutSec = clampTimeout("lsp", timeout, this.session.settings.get("tools.maxTimeout"));
1602
1606
  const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
1603
1607
  const callerSignal = signal;
@@ -2345,6 +2349,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2345
2349
  const outputs: string[] = [];
2346
2350
  for (const [workspaceServerName, workspaceServerConfig] of servers) {
2347
2351
  throwIfAborted(signal);
2352
+ clearInitializationFailure(workspaceServerConfig, this.session.cwd);
2348
2353
  try {
2349
2354
  const workspaceClient = await getOrCreateClient(
2350
2355
  workspaceServerConfig,
@@ -2377,6 +2382,8 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2377
2382
 
2378
2383
  const [serverName, serverConfig] = serverInfo;
2379
2384
 
2385
+ if (action === "reload") clearInitializationFailure(serverConfig, this.session.cwd);
2386
+
2380
2387
  try {
2381
2388
  const client = await getOrCreateClient(serverConfig, this.session.cwd, undefined, signal);
2382
2389
  const targetFile = resolvedFile;
package/src/lsp/render.ts CHANGED
@@ -582,7 +582,7 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
582
582
  for (let i = 0; i < lines.length; i++) {
583
583
  const isLast = i === lines.length - 1;
584
584
  const branch = isLast ? theme.tree.last : theme.tree.branch;
585
- output += `\n ${theme.fg("dim", branch)} ${lines[i]}`;
585
+ output += `\n ${theme.fg("dim", branch)} ${truncateToWidth(replaceTabs(lines[i]), TRUNCATE_LENGTHS.CONTENT)}`;
586
586
  }
587
587
  return output.split("\n");
588
588
  }