gsd-pi 2.25.0 → 2.26.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 (122) hide show
  1. package/README.md +11 -2
  2. package/dist/headless.js +24 -4
  3. package/dist/resources/extensions/async-jobs/index.ts +9 -1
  4. package/dist/resources/extensions/bg-shell/index.ts +3 -2
  5. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
  6. package/dist/resources/extensions/gsd/auto-worktree.ts +14 -3
  7. package/dist/resources/extensions/gsd/auto.ts +81 -12
  8. package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
  9. package/dist/resources/extensions/gsd/doctor.ts +24 -1
  10. package/dist/resources/extensions/gsd/files.ts +13 -2
  11. package/dist/resources/extensions/gsd/guided-flow.ts +19 -9
  12. package/dist/resources/extensions/gsd/index.ts +48 -7
  13. package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
  14. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  15. package/dist/resources/extensions/gsd/preferences.ts +2 -1
  16. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  17. package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
  18. package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
  19. package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
  20. package/dist/resources/extensions/gsd/state.ts +17 -6
  21. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  22. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  23. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  24. package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  25. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  26. package/dist/resources/extensions/gsd/types.ts +2 -0
  27. package/dist/resources/extensions/search-the-web/native-search.ts +4 -0
  28. package/dist/resources/extensions/shared/path-display.ts +19 -0
  29. package/package.json +1 -6
  30. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  31. package/packages/pi-ai/dist/providers/anthropic.js +25 -0
  32. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  33. package/packages/pi-ai/src/providers/anthropic.ts +27 -0
  34. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
  35. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
  37. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
  39. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
  42. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
  45. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
  47. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
  49. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  51. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  53. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  55. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  56. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  57. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
  59. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  60. package/packages/pi-coding-agent/dist/index.d.ts +2 -1
  61. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/index.js +5 -1
  63. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +63 -30
  71. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
  73. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
  75. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
  78. package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
  80. package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
  81. package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
  82. package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
  83. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
  84. package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
  85. package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
  86. package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
  87. package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
  88. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  89. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  90. package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
  91. package/packages/pi-coding-agent/src/index.ts +15 -0
  92. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
  93. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +40 -4
  94. package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
  95. package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
  96. package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
  97. package/src/resources/extensions/async-jobs/index.ts +9 -1
  98. package/src/resources/extensions/bg-shell/index.ts +3 -2
  99. package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
  100. package/src/resources/extensions/gsd/auto-worktree.ts +14 -3
  101. package/src/resources/extensions/gsd/auto.ts +81 -12
  102. package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
  103. package/src/resources/extensions/gsd/doctor.ts +24 -1
  104. package/src/resources/extensions/gsd/files.ts +13 -2
  105. package/src/resources/extensions/gsd/guided-flow.ts +19 -9
  106. package/src/resources/extensions/gsd/index.ts +48 -7
  107. package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
  108. package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  109. package/src/resources/extensions/gsd/preferences.ts +2 -1
  110. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  111. package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
  112. package/src/resources/extensions/gsd/prompts/queue.md +2 -2
  113. package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
  114. package/src/resources/extensions/gsd/state.ts +17 -6
  115. package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  116. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  117. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  118. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  119. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  120. package/src/resources/extensions/gsd/types.ts +2 -0
  121. package/src/resources/extensions/search-the-web/native-search.ts +4 -0
  122. package/src/resources/extensions/shared/path-display.ts +19 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Cross-platform path display tests.
3
+ *
4
+ * Verifies that toPosixPath correctly normalizes Windows paths and that
5
+ * the system prompt builder produces forward-slash paths for LLM consumption.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=path-display.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-display.test.d.ts","sourceRoot":"","sources":["../../src/tests/path-display.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Cross-platform path display tests.
3
+ *
4
+ * Verifies that toPosixPath correctly normalizes Windows paths and that
5
+ * the system prompt builder produces forward-slash paths for LLM consumption.
6
+ */
7
+ import test from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { toPosixPath } from "../utils/path-display.js";
10
+ import { buildSystemPrompt } from "../core/system-prompt.js";
11
+ // ─── toPosixPath ────────────────────────────────────────────────────────────
12
+ test("toPosixPath: converts Windows backslash paths to forward slashes", () => {
13
+ assert.equal(toPosixPath("C:\\Users\\name\\project"), "C:/Users/name/project");
14
+ });
15
+ test("toPosixPath: handles mixed separators", () => {
16
+ assert.equal(toPosixPath("C:\\Users/name\\project/src"), "C:/Users/name/project/src");
17
+ });
18
+ test("toPosixPath: no-op for Unix paths", () => {
19
+ assert.equal(toPosixPath("/home/user/project"), "/home/user/project");
20
+ });
21
+ test("toPosixPath: handles empty string", () => {
22
+ assert.equal(toPosixPath(""), "");
23
+ });
24
+ test("toPosixPath: handles Windows UNC paths", () => {
25
+ assert.equal(toPosixPath("\\\\server\\share\\dir"), "//server/share/dir");
26
+ });
27
+ test("toPosixPath: handles .gsd/worktrees path on Windows", () => {
28
+ assert.equal(toPosixPath("C:\\Users\\name\\project\\.gsd\\worktrees\\M001"), "C:/Users/name/project/.gsd/worktrees/M001");
29
+ });
30
+ // ─── System prompt path normalization ───────────────────────────────────────
31
+ test("buildSystemPrompt: cwd uses forward slashes even with Windows input", () => {
32
+ const prompt = buildSystemPrompt({
33
+ cwd: "C:\\Users\\name\\development\\app-name",
34
+ });
35
+ assert.ok(prompt.includes("C:/Users/name/development/app-name"), "System prompt should contain forward-slash path");
36
+ assert.ok(!prompt.includes("C:\\Users\\name\\development\\app-name"), "System prompt must NOT contain backslash path");
37
+ });
38
+ test("buildSystemPrompt: Unix paths pass through unchanged", () => {
39
+ const prompt = buildSystemPrompt({
40
+ cwd: "/home/user/project",
41
+ });
42
+ assert.ok(prompt.includes("/home/user/project"));
43
+ });
44
+ // ─── Regression: no backslash paths in LLM-visible text ────────────────────
45
+ /**
46
+ * Pattern that matches Windows-style absolute paths with backslashes.
47
+ * Catches: C:\Users\..., D:\Projects\..., \\server\share\...
48
+ * Does not match: escaped chars in regex, JSON strings, etc.
49
+ */
50
+ const WINDOWS_ABS_PATH_RE = /[A-Z]:\\[A-Za-z]/;
51
+ test("buildSystemPrompt: no Windows absolute paths with backslashes in output", () => {
52
+ // Simulate a Windows-like cwd
53
+ const prompt = buildSystemPrompt({
54
+ cwd: "D:\\Projects\\my-app\\.gsd\\worktrees\\M002",
55
+ });
56
+ const lines = prompt.split("\n");
57
+ const violations = lines.filter(line => WINDOWS_ABS_PATH_RE.test(line));
58
+ assert.equal(violations.length, 0, `System prompt contains Windows backslash paths:\n${violations.join("\n")}`);
59
+ });
60
+ //# sourceMappingURL=path-display.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-display.test.js","sourceRoot":"","sources":["../../src/tests/path-display.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,+EAA+E;AAE/E,IAAI,CAAC,kEAAkE,EAAE,GAAG,EAAE;IAC7E,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,0BAA0B,CAAC,EAAE,uBAAuB,CAAC,CAAC;AAChF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE;IAClD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,6BAA6B,CAAC,EAAE,2BAA2B,CAAC,CAAC;AACvF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mCAAmC,EAAE,GAAG,EAAE;IAC9C,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC,CAAC;AACvE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mCAAmC,EAAE,GAAG,EAAE;IAC9C,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wCAAwC,EAAE,GAAG,EAAE;IACnD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,wBAAwB,CAAC,EAAE,oBAAoB,CAAC,CAAC;AAC3E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qDAAqD,EAAE,GAAG,EAAE;IAChE,MAAM,CAAC,KAAK,CACX,WAAW,CAAC,iDAAiD,CAAC,EAC9D,2CAA2C,CAC3C,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,+EAA+E;AAE/E,IAAI,CAAC,qEAAqE,EAAE,GAAG,EAAE;IAChF,MAAM,MAAM,GAAG,iBAAiB,CAAC;QAChC,GAAG,EAAE,wCAAwC;KAC7C,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,CACR,MAAM,CAAC,QAAQ,CAAC,oCAAoC,CAAC,EACrD,iDAAiD,CACjD,CAAC;IACF,MAAM,CAAC,EAAE,CACR,CAAC,MAAM,CAAC,QAAQ,CAAC,wCAAwC,CAAC,EAC1D,+CAA+C,CAC/C,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sDAAsD,EAAE,GAAG,EAAE;IACjE,MAAM,MAAM,GAAG,iBAAiB,CAAC;QAChC,GAAG,EAAE,oBAAoB;KACzB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;AAE/C,IAAI,CAAC,yEAAyE,EAAE,GAAG,EAAE;IACpF,8BAA8B;IAC9B,MAAM,MAAM,GAAG,iBAAiB,CAAC;QAChC,GAAG,EAAE,6CAA6C;KAClD,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACxE,MAAM,CAAC,KAAK,CACX,UAAU,CAAC,MAAM,EAAE,CAAC,EACpB,oDAAoD,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC3E,CAAC;AACH,CAAC,CAAC,CAAC","sourcesContent":["/**\n * Cross-platform path display tests.\n *\n * Verifies that toPosixPath correctly normalizes Windows paths and that\n * the system prompt builder produces forward-slash paths for LLM consumption.\n */\n\nimport test from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { toPosixPath } from \"../utils/path-display.js\";\nimport { buildSystemPrompt } from \"../core/system-prompt.js\";\n\n// ─── toPosixPath ────────────────────────────────────────────────────────────\n\ntest(\"toPosixPath: converts Windows backslash paths to forward slashes\", () => {\n\tassert.equal(toPosixPath(\"C:\\\\Users\\\\name\\\\project\"), \"C:/Users/name/project\");\n});\n\ntest(\"toPosixPath: handles mixed separators\", () => {\n\tassert.equal(toPosixPath(\"C:\\\\Users/name\\\\project/src\"), \"C:/Users/name/project/src\");\n});\n\ntest(\"toPosixPath: no-op for Unix paths\", () => {\n\tassert.equal(toPosixPath(\"/home/user/project\"), \"/home/user/project\");\n});\n\ntest(\"toPosixPath: handles empty string\", () => {\n\tassert.equal(toPosixPath(\"\"), \"\");\n});\n\ntest(\"toPosixPath: handles Windows UNC paths\", () => {\n\tassert.equal(toPosixPath(\"\\\\\\\\server\\\\share\\\\dir\"), \"//server/share/dir\");\n});\n\ntest(\"toPosixPath: handles .gsd/worktrees path on Windows\", () => {\n\tassert.equal(\n\t\ttoPosixPath(\"C:\\\\Users\\\\name\\\\project\\\\.gsd\\\\worktrees\\\\M001\"),\n\t\t\"C:/Users/name/project/.gsd/worktrees/M001\",\n\t);\n});\n\n// ─── System prompt path normalization ───────────────────────────────────────\n\ntest(\"buildSystemPrompt: cwd uses forward slashes even with Windows input\", () => {\n\tconst prompt = buildSystemPrompt({\n\t\tcwd: \"C:\\\\Users\\\\name\\\\development\\\\app-name\",\n\t});\n\tassert.ok(\n\t\tprompt.includes(\"C:/Users/name/development/app-name\"),\n\t\t\"System prompt should contain forward-slash path\",\n\t);\n\tassert.ok(\n\t\t!prompt.includes(\"C:\\\\Users\\\\name\\\\development\\\\app-name\"),\n\t\t\"System prompt must NOT contain backslash path\",\n\t);\n});\n\ntest(\"buildSystemPrompt: Unix paths pass through unchanged\", () => {\n\tconst prompt = buildSystemPrompt({\n\t\tcwd: \"/home/user/project\",\n\t});\n\tassert.ok(prompt.includes(\"/home/user/project\"));\n});\n\n// ─── Regression: no backslash paths in LLM-visible text ────────────────────\n\n/**\n * Pattern that matches Windows-style absolute paths with backslashes.\n * Catches: C:\\Users\\..., D:\\Projects\\..., \\\\server\\share\\...\n * Does not match: escaped chars in regex, JSON strings, etc.\n */\nconst WINDOWS_ABS_PATH_RE = /[A-Z]:\\\\[A-Za-z]/;\n\ntest(\"buildSystemPrompt: no Windows absolute paths with backslashes in output\", () => {\n\t// Simulate a Windows-like cwd\n\tconst prompt = buildSystemPrompt({\n\t\tcwd: \"D:\\\\Projects\\\\my-app\\\\.gsd\\\\worktrees\\\\M002\",\n\t});\n\tconst lines = prompt.split(\"\\n\");\n\tconst violations = lines.filter(line => WINDOWS_ABS_PATH_RE.test(line));\n\tassert.equal(\n\t\tviolations.length, 0,\n\t\t`System prompt contains Windows backslash paths:\\n${violations.join(\"\\n\")}`,\n\t);\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard-image.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard-image.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAQF,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE9E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAazE;AAsHD,wBAAsB,kBAAkB,CAAC,OAAO,CAAC,EAAE;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyCjC"}
1
+ {"version":3,"file":"clipboard-image.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard-image.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAQF,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE9E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAazE;AAiJD,wBAAsB,kBAAkB,CAAC,OAAO,CAAC,EAAE;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyCjC"}
@@ -86,14 +86,40 @@ function readClipboardImageViaWlPaste() {
86
86
  .map((t) => t.trim())
87
87
  .filter(Boolean);
88
88
  const selectedType = selectPreferredImageMimeType(types);
89
- if (!selectedType) {
90
- return null;
89
+ if (selectedType) {
90
+ const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
91
+ if (data.ok && data.stdout.length > 0) {
92
+ return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
93
+ }
91
94
  }
92
- const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
93
- if (!data.ok || data.stdout.length === 0) {
94
- return null;
95
+ // Fallback for WSLg/BMP: when only image/bmp is available, ask wl-paste
96
+ // to convert to PNG on the fly. wl-paste supports format conversion for
97
+ // some compositor types. If that fails, try reading BMP and converting
98
+ // via ImageMagick (#813).
99
+ const hasBmp = types.some((t) => baseMimeType(t) === "image/bmp");
100
+ if (!selectedType && hasBmp) {
101
+ // Try requesting PNG directly — wl-paste may convert
102
+ const pngData = runCommand("wl-paste", ["--type", "image/png", "--no-newline"]);
103
+ if (pngData.ok && pngData.stdout.length > 0) {
104
+ return { bytes: pngData.stdout, mimeType: "image/png" };
105
+ }
106
+ // Try reading BMP and converting via ImageMagick convert
107
+ const bmpData = runCommand("wl-paste", ["--type", "image/bmp", "--no-newline"]);
108
+ if (bmpData.ok && bmpData.stdout.length > 0) {
109
+ const converted = spawnSync("convert", ["bmp:-", "png:-"], {
110
+ input: bmpData.stdout,
111
+ timeout: 5000,
112
+ maxBuffer: DEFAULT_MAX_BUFFER_BYTES,
113
+ });
114
+ if (!converted.error && converted.status === 0 && converted.stdout.length > 0) {
115
+ const stdout = Buffer.isBuffer(converted.stdout)
116
+ ? converted.stdout
117
+ : Buffer.from(converted.stdout);
118
+ return { bytes: stdout, mimeType: "image/png" };
119
+ }
120
+ }
95
121
  }
96
- return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
122
+ return null;
97
123
  }
98
124
  function readClipboardImageViaXclip() {
99
125
  const targets = runCommand("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], {
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard-image.js","sourceRoot":"","sources":["../../src/utils/clipboard-image.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"child_process\";\n\nimport { readImageFromClipboard as nativeReadImage } from \"@gsd/native/clipboard\";\nimport { ImageFormat, parseImage } from \"@gsd/native/image\";\n\nexport type ClipboardImage = {\n\tbytes: Uint8Array;\n\tmimeType: string;\n};\n\nconst SUPPORTED_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\n\nconst DEFAULT_LIST_TIMEOUT_MS = 1000;\nconst DEFAULT_READ_TIMEOUT_MS = 3000;\nconst DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\nexport function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === \"wayland\";\n}\n\nfunction baseMimeType(mimeType: string): string {\n\treturn mimeType.split(\";\")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();\n}\n\nexport function extensionForImageMimeType(mimeType: string): string | null {\n\tswitch (baseMimeType(mimeType)) {\n\t\tcase \"image/png\":\n\t\t\treturn \"png\";\n\t\tcase \"image/jpeg\":\n\t\t\treturn \"jpg\";\n\t\tcase \"image/webp\":\n\t\t\treturn \"webp\";\n\t\tcase \"image/gif\":\n\t\t\treturn \"gif\";\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nfunction selectPreferredImageMimeType(mimeTypes: string[]): string | null {\n\tconst normalized = mimeTypes\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean)\n\t\t.map((t) => ({ raw: t, base: baseMimeType(t) }));\n\n\tfor (const preferred of SUPPORTED_IMAGE_MIME_TYPES) {\n\t\tconst match = normalized.find((t) => t.base === preferred);\n\t\tif (match) {\n\t\t\treturn match.raw;\n\t\t}\n\t}\n\n\tconst anyImage = normalized.find((t) => t.base.startsWith(\"image/\"));\n\treturn anyImage?.raw ?? null;\n}\n\nfunction isSupportedImageMimeType(mimeType: string): boolean {\n\tconst base = baseMimeType(mimeType);\n\treturn SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);\n}\n\n/**\n * Convert unsupported image formats to PNG using the native Rust image module.\n * Returns null if conversion fails.\n */\nasync function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> {\n\ttry {\n\t\tconst image = await parseImage(bytes);\n\t\tconst pngBytes = await image.encode(ImageFormat.PNG, 100);\n\t\treturn new Uint8Array(pngBytes);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction runCommand(\n\tcommand: string,\n\targs: string[],\n\toptions?: { timeoutMs?: number; maxBufferBytes?: number },\n): { stdout: Buffer; ok: boolean } {\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;\n\tconst maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n\n\tconst result = spawnSync(command, args, {\n\t\ttimeout: timeoutMs,\n\t\tmaxBuffer: maxBufferBytes,\n\t});\n\n\tif (result.error) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tif (result.status !== 0) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tconst stdout = Buffer.isBuffer(result.stdout)\n\t\t? result.stdout\n\t\t: Buffer.from(result.stdout ?? \"\", typeof result.stdout === \"string\" ? \"utf-8\" : undefined);\n\n\treturn { ok: true, stdout };\n}\n\nfunction readClipboardImageViaWlPaste(): ClipboardImage | null {\n\tconst list = runCommand(\"wl-paste\", [\"--list-types\"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\tif (!list.ok) {\n\t\treturn null;\n\t}\n\n\tconst types = list.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\n\tconst selectedType = selectPreferredImageMimeType(types);\n\tif (!selectedType) {\n\t\treturn null;\n\t}\n\n\tconst data = runCommand(\"wl-paste\", [\"--type\", selectedType, \"--no-newline\"]);\n\tif (!data.ok || data.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn { bytes: data.stdout, mimeType: baseMimeType(selectedType) };\n}\n\nfunction readClipboardImageViaXclip(): ClipboardImage | null {\n\tconst targets = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", \"TARGETS\", \"-o\"], {\n\t\ttimeoutMs: DEFAULT_LIST_TIMEOUT_MS,\n\t});\n\n\tlet candidateTypes: string[] = [];\n\tif (targets.ok) {\n\t\tcandidateTypes = targets.stdout\n\t\t\t.toString(\"utf-8\")\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t}\n\n\tconst preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;\n\tconst tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];\n\n\tfor (const mimeType of tryTypes) {\n\t\tconst data = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", mimeType, \"-o\"]);\n\t\tif (data.ok && data.stdout.length > 0) {\n\t\t\treturn { bytes: data.stdout, mimeType: baseMimeType(mimeType) };\n\t\t}\n\t}\n\n\treturn null;\n}\n\nexport async function readClipboardImage(options?: {\n\tenv?: NodeJS.ProcessEnv;\n\tplatform?: NodeJS.Platform;\n}): Promise<ClipboardImage | null> {\n\tconst env = options?.env ?? process.env;\n\tconst platform = options?.platform ?? process.platform;\n\n\tif (env.TERMUX_VERSION) {\n\t\treturn null;\n\t}\n\n\tlet image: ClipboardImage | null = null;\n\n\tif (platform === \"linux\" && isWaylandSession(env)) {\n\t\t// Wayland: use CLI tools (wl-paste/xclip) since native arboard\n\t\t// may not have access to the Wayland compositor from a terminal.\n\t\timage = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();\n\t} else {\n\t\t// macOS, Windows, Linux X11: use native Rust clipboard (arboard)\n\t\ttry {\n\t\t\tconst nativeImage = await nativeReadImage();\n\t\t\tif (!nativeImage || nativeImage.data.length === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\timage = { bytes: nativeImage.data, mimeType: nativeImage.mimeType };\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tif (!image) {\n\t\treturn null;\n\t}\n\n\t// Convert unsupported formats (e.g., BMP from WSLg) to PNG\n\tif (!isSupportedImageMimeType(image.mimeType)) {\n\t\tconst pngBytes = await convertToPng(image.bytes);\n\t\tif (!pngBytes) {\n\t\t\treturn null;\n\t\t}\n\t\treturn { bytes: pngBytes, mimeType: \"image/png\" };\n\t}\n\n\treturn image;\n}\n"]}
1
+ {"version":3,"file":"clipboard-image.js","sourceRoot":"","sources":["../../src/utils/clipboard-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAE,sBAAsB,IAAI,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAO5D,MAAM,0BAA0B,GAAG,CAAC,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,CAAU,CAAC;AAEnG,MAAM,uBAAuB,GAAG,IAAI,CAAC;AACrC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AACrC,MAAM,wBAAwB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,MAAM,UAAU,gBAAgB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACpE,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,GAAG,CAAC,gBAAgB,KAAK,SAAS,CAAC;AAC3E,CAAC;AAED,SAAS,YAAY,CAAC,QAAgB;IACrC,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,QAAgB;IACzD,QAAQ,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChC,KAAK,WAAW;YACf,OAAO,KAAK,CAAC;QACd,KAAK,YAAY;YAChB,OAAO,KAAK,CAAC;QACd,KAAK,YAAY;YAChB,OAAO,MAAM,CAAC;QACf,KAAK,WAAW;YACf,OAAO,KAAK,CAAC;QACd;YACC,OAAO,IAAI,CAAC;IACd,CAAC;AACF,CAAC;AAED,SAAS,4BAA4B,CAAC,SAAmB;IACxD,MAAM,UAAU,GAAG,SAAS;SAC1B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAElD,KAAK,MAAM,SAAS,IAAI,0BAA0B,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QAC3D,IAAI,KAAK,EAAE,CAAC;YACX,OAAO,KAAK,CAAC,GAAG,CAAC;QAClB,CAAC;IACF,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrE,OAAO,QAAQ,EAAE,GAAG,IAAI,IAAI,CAAC;AAC9B,CAAC;AAED,SAAS,wBAAwB,CAAC,QAAgB;IACjD,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACpC,OAAO,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,YAAY,CAAC,KAAiB;IAC5C,IAAI,CAAC;QACJ,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC1D,OAAO,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED,SAAS,UAAU,CAClB,OAAe,EACf,IAAc,EACd,OAAyD;IAEzD,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,uBAAuB,CAAC;IAChE,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,wBAAwB,CAAC;IAE3E,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE;QACvC,OAAO,EAAE,SAAS;QAClB,SAAS,EAAE,cAAc;KACzB,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/C,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/C,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;QAC5C,CAAC,CAAC,MAAM,CAAC,MAAM;QACf,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAE7F,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC;AAED,SAAS,4BAA4B;IACpC,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,CAAC,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAC,CAAC;IAC9F,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACd,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM;SACvB,QAAQ,CAAC,OAAO,CAAC;SACjB,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IAElB,MAAM,YAAY,GAAG,4BAA4B,CAAC,KAAK,CAAC,CAAC;IACzD,IAAI,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC,CAAC;QAC9E,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC;QACrE,CAAC;IACF,CAAC;IAED,wEAAwE;IACxE,wEAAwE;IACxE,uEAAuE;IACvE,0BAA0B;IAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC;IAClE,IAAI,CAAC,YAAY,IAAI,MAAM,EAAE,CAAC;QAC7B,qDAAqD;QACrD,MAAM,OAAO,GAAG,UAAU,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;QAChF,IAAI,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7C,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;QACzD,CAAC;QAED,yDAAyD;QACzD,MAAM,OAAO,GAAG,UAAU,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;QAChF,IAAI,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7C,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE;gBAC1D,KAAK,EAAE,OAAO,CAAC,MAAM;gBACrB,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,wBAAwB;aACnC,CAAC,CAAC;YACH,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/E,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC;oBAC/C,CAAC,CAAC,SAAS,CAAC,MAAM;oBAClB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBACjC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;YACjD,CAAC;QACF,CAAC;IACF,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC;AAED,SAAS,0BAA0B;IAClC,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE;QACvF,SAAS,EAAE,uBAAuB;KAClC,CAAC,CAAC;IAEH,IAAI,cAAc,GAAa,EAAE,CAAC;IAClC,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,cAAc,GAAG,OAAO,CAAC,MAAM;aAC7B,QAAQ,CAAC,OAAO,CAAC;aACjB,KAAK,CAAC,OAAO,CAAC;aACd,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,CAAC;IAED,MAAM,SAAS,GAAG,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,4BAA4B,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClG,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,GAAG,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,0BAA0B,CAAC,CAAC;IAE1G,KAAK,MAAM,QAAQ,IAAI,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;QACpF,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjE,CAAC;IACF,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAGxC;IACA,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACxC,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAEvD,IAAI,GAAG,CAAC,cAAc,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAI,KAAK,GAA0B,IAAI,CAAC;IAExC,IAAI,QAAQ,KAAK,OAAO,IAAI,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;QACnD,+DAA+D;QAC/D,iEAAiE;QACjE,KAAK,GAAG,4BAA4B,EAAE,IAAI,0BAA0B,EAAE,CAAC;IACxE,CAAC;SAAM,CAAC;QACP,iEAAiE;QACjE,IAAI,CAAC;YACJ,MAAM,WAAW,GAAG,MAAM,eAAe,EAAE,CAAC;YAC5C,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACnD,OAAO,IAAI,CAAC;YACb,CAAC;YACD,KAAK,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,QAAQ,EAAE,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IAED,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACb,CAAC;IAED,2DAA2D;IAC3D,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,OAAO,IAAI,CAAC;QACb,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;IACnD,CAAC;IAED,OAAO,KAAK,CAAC;AACd,CAAC","sourcesContent":["import { spawnSync } from \"child_process\";\n\nimport { readImageFromClipboard as nativeReadImage } from \"@gsd/native/clipboard\";\nimport { ImageFormat, parseImage } from \"@gsd/native/image\";\n\nexport type ClipboardImage = {\n\tbytes: Uint8Array;\n\tmimeType: string;\n};\n\nconst SUPPORTED_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\n\nconst DEFAULT_LIST_TIMEOUT_MS = 1000;\nconst DEFAULT_READ_TIMEOUT_MS = 3000;\nconst DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\nexport function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === \"wayland\";\n}\n\nfunction baseMimeType(mimeType: string): string {\n\treturn mimeType.split(\";\")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();\n}\n\nexport function extensionForImageMimeType(mimeType: string): string | null {\n\tswitch (baseMimeType(mimeType)) {\n\t\tcase \"image/png\":\n\t\t\treturn \"png\";\n\t\tcase \"image/jpeg\":\n\t\t\treturn \"jpg\";\n\t\tcase \"image/webp\":\n\t\t\treturn \"webp\";\n\t\tcase \"image/gif\":\n\t\t\treturn \"gif\";\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nfunction selectPreferredImageMimeType(mimeTypes: string[]): string | null {\n\tconst normalized = mimeTypes\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean)\n\t\t.map((t) => ({ raw: t, base: baseMimeType(t) }));\n\n\tfor (const preferred of SUPPORTED_IMAGE_MIME_TYPES) {\n\t\tconst match = normalized.find((t) => t.base === preferred);\n\t\tif (match) {\n\t\t\treturn match.raw;\n\t\t}\n\t}\n\n\tconst anyImage = normalized.find((t) => t.base.startsWith(\"image/\"));\n\treturn anyImage?.raw ?? null;\n}\n\nfunction isSupportedImageMimeType(mimeType: string): boolean {\n\tconst base = baseMimeType(mimeType);\n\treturn SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);\n}\n\n/**\n * Convert unsupported image formats to PNG using the native Rust image module.\n * Returns null if conversion fails.\n */\nasync function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> {\n\ttry {\n\t\tconst image = await parseImage(bytes);\n\t\tconst pngBytes = await image.encode(ImageFormat.PNG, 100);\n\t\treturn new Uint8Array(pngBytes);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction runCommand(\n\tcommand: string,\n\targs: string[],\n\toptions?: { timeoutMs?: number; maxBufferBytes?: number },\n): { stdout: Buffer; ok: boolean } {\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;\n\tconst maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n\n\tconst result = spawnSync(command, args, {\n\t\ttimeout: timeoutMs,\n\t\tmaxBuffer: maxBufferBytes,\n\t});\n\n\tif (result.error) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tif (result.status !== 0) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tconst stdout = Buffer.isBuffer(result.stdout)\n\t\t? result.stdout\n\t\t: Buffer.from(result.stdout ?? \"\", typeof result.stdout === \"string\" ? \"utf-8\" : undefined);\n\n\treturn { ok: true, stdout };\n}\n\nfunction readClipboardImageViaWlPaste(): ClipboardImage | null {\n\tconst list = runCommand(\"wl-paste\", [\"--list-types\"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\tif (!list.ok) {\n\t\treturn null;\n\t}\n\n\tconst types = list.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\n\tconst selectedType = selectPreferredImageMimeType(types);\n\tif (selectedType) {\n\t\tconst data = runCommand(\"wl-paste\", [\"--type\", selectedType, \"--no-newline\"]);\n\t\tif (data.ok && data.stdout.length > 0) {\n\t\t\treturn { bytes: data.stdout, mimeType: baseMimeType(selectedType) };\n\t\t}\n\t}\n\n\t// Fallback for WSLg/BMP: when only image/bmp is available, ask wl-paste\n\t// to convert to PNG on the fly. wl-paste supports format conversion for\n\t// some compositor types. If that fails, try reading BMP and converting\n\t// via ImageMagick (#813).\n\tconst hasBmp = types.some((t) => baseMimeType(t) === \"image/bmp\");\n\tif (!selectedType && hasBmp) {\n\t\t// Try requesting PNG directly — wl-paste may convert\n\t\tconst pngData = runCommand(\"wl-paste\", [\"--type\", \"image/png\", \"--no-newline\"]);\n\t\tif (pngData.ok && pngData.stdout.length > 0) {\n\t\t\treturn { bytes: pngData.stdout, mimeType: \"image/png\" };\n\t\t}\n\n\t\t// Try reading BMP and converting via ImageMagick convert\n\t\tconst bmpData = runCommand(\"wl-paste\", [\"--type\", \"image/bmp\", \"--no-newline\"]);\n\t\tif (bmpData.ok && bmpData.stdout.length > 0) {\n\t\t\tconst converted = spawnSync(\"convert\", [\"bmp:-\", \"png:-\"], {\n\t\t\t\tinput: bmpData.stdout,\n\t\t\t\ttimeout: 5000,\n\t\t\t\tmaxBuffer: DEFAULT_MAX_BUFFER_BYTES,\n\t\t\t});\n\t\t\tif (!converted.error && converted.status === 0 && converted.stdout.length > 0) {\n\t\t\t\tconst stdout = Buffer.isBuffer(converted.stdout)\n\t\t\t\t\t? converted.stdout\n\t\t\t\t\t: Buffer.from(converted.stdout);\n\t\t\t\treturn { bytes: stdout, mimeType: \"image/png\" };\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction readClipboardImageViaXclip(): ClipboardImage | null {\n\tconst targets = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", \"TARGETS\", \"-o\"], {\n\t\ttimeoutMs: DEFAULT_LIST_TIMEOUT_MS,\n\t});\n\n\tlet candidateTypes: string[] = [];\n\tif (targets.ok) {\n\t\tcandidateTypes = targets.stdout\n\t\t\t.toString(\"utf-8\")\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t}\n\n\tconst preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;\n\tconst tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];\n\n\tfor (const mimeType of tryTypes) {\n\t\tconst data = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", mimeType, \"-o\"]);\n\t\tif (data.ok && data.stdout.length > 0) {\n\t\t\treturn { bytes: data.stdout, mimeType: baseMimeType(mimeType) };\n\t\t}\n\t}\n\n\treturn null;\n}\n\nexport async function readClipboardImage(options?: {\n\tenv?: NodeJS.ProcessEnv;\n\tplatform?: NodeJS.Platform;\n}): Promise<ClipboardImage | null> {\n\tconst env = options?.env ?? process.env;\n\tconst platform = options?.platform ?? process.platform;\n\n\tif (env.TERMUX_VERSION) {\n\t\treturn null;\n\t}\n\n\tlet image: ClipboardImage | null = null;\n\n\tif (platform === \"linux\" && isWaylandSession(env)) {\n\t\t// Wayland: use CLI tools (wl-paste/xclip) since native arboard\n\t\t// may not have access to the Wayland compositor from a terminal.\n\t\timage = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();\n\t} else {\n\t\t// macOS, Windows, Linux X11: use native Rust clipboard (arboard)\n\t\ttry {\n\t\t\tconst nativeImage = await nativeReadImage();\n\t\t\tif (!nativeImage || nativeImage.data.length === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\timage = { bytes: nativeImage.data, mimeType: nativeImage.mimeType };\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tif (!image) {\n\t\treturn null;\n\t}\n\n\t// Convert unsupported formats (e.g., BMP from WSLg) to PNG\n\tif (!isSupportedImageMimeType(image.mimeType)) {\n\t\tconst pngBytes = await convertToPng(image.bytes);\n\t\tif (!pngBytes) {\n\t\t\treturn null;\n\t\t}\n\t\treturn { bytes: pngBytes, mimeType: \"image/png\" };\n\t}\n\n\treturn image;\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Cross-platform path display utilities.
3
+ *
4
+ * Paths injected into LLM prompts, tool results, or any text the model
5
+ * processes must use forward slashes. Windows backslash paths cause bash
6
+ * failures when the model copies them into shell commands — bash interprets
7
+ * backslashes as escape characters, silently stripping them.
8
+ *
9
+ * Node's `path` module and `fs` module handle native separators correctly
10
+ * for filesystem operations. This module is ONLY for paths that enter
11
+ * text consumed by the LLM or interpreted by a shell.
12
+ *
13
+ * Usage:
14
+ * import { toPosixPath } from "./path-display.js";
15
+ * prompt += `Current working directory: ${toPosixPath(cwd)}`;
16
+ *
17
+ * NOT for:
18
+ * fs.readFile(path) — use native path as-is
19
+ * path.join(a, b) — use native path module
20
+ * spawn(cmd, { cwd: path }) — Node handles this correctly
21
+ */
22
+ /**
23
+ * Convert a filesystem path to forward-slash (POSIX) form for display.
24
+ *
25
+ * On Unix this is a no-op. On Windows it converts `C:\Users\name\project`
26
+ * to `C:/Users/name/project`, which is valid in:
27
+ * - Git Bash / MSYS2
28
+ * - WSL bash
29
+ * - PowerShell
30
+ * - Node.js APIs (which accept both separators)
31
+ * - Most Windows programs
32
+ */
33
+ export declare function toPosixPath(fsPath: string): string;
34
+ //# sourceMappingURL=path-display.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-display.d.ts","sourceRoot":"","sources":["../../src/utils/path-display.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElD"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Cross-platform path display utilities.
3
+ *
4
+ * Paths injected into LLM prompts, tool results, or any text the model
5
+ * processes must use forward slashes. Windows backslash paths cause bash
6
+ * failures when the model copies them into shell commands — bash interprets
7
+ * backslashes as escape characters, silently stripping them.
8
+ *
9
+ * Node's `path` module and `fs` module handle native separators correctly
10
+ * for filesystem operations. This module is ONLY for paths that enter
11
+ * text consumed by the LLM or interpreted by a shell.
12
+ *
13
+ * Usage:
14
+ * import { toPosixPath } from "./path-display.js";
15
+ * prompt += `Current working directory: ${toPosixPath(cwd)}`;
16
+ *
17
+ * NOT for:
18
+ * fs.readFile(path) — use native path as-is
19
+ * path.join(a, b) — use native path module
20
+ * spawn(cmd, { cwd: path }) — Node handles this correctly
21
+ */
22
+ /**
23
+ * Convert a filesystem path to forward-slash (POSIX) form for display.
24
+ *
25
+ * On Unix this is a no-op. On Windows it converts `C:\Users\name\project`
26
+ * to `C:/Users/name/project`, which is valid in:
27
+ * - Git Bash / MSYS2
28
+ * - WSL bash
29
+ * - PowerShell
30
+ * - Node.js APIs (which accept both separators)
31
+ * - Most Windows programs
32
+ */
33
+ export function toPosixPath(fsPath) {
34
+ return fsPath.replaceAll("\\", "/");
35
+ }
36
+ //# sourceMappingURL=path-display.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-display.js","sourceRoot":"","sources":["../../src/utils/path-display.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IACzC,OAAO,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACrC,CAAC","sourcesContent":["/**\n * Cross-platform path display utilities.\n *\n * Paths injected into LLM prompts, tool results, or any text the model\n * processes must use forward slashes. Windows backslash paths cause bash\n * failures when the model copies them into shell commands — bash interprets\n * backslashes as escape characters, silently stripping them.\n *\n * Node's `path` module and `fs` module handle native separators correctly\n * for filesystem operations. This module is ONLY for paths that enter\n * text consumed by the LLM or interpreted by a shell.\n *\n * Usage:\n * import { toPosixPath } from \"./path-display.js\";\n * prompt += `Current working directory: ${toPosixPath(cwd)}`;\n *\n * NOT for:\n * fs.readFile(path) — use native path as-is\n * path.join(a, b) — use native path module\n * spawn(cmd, { cwd: path }) — Node handles this correctly\n */\n\n/**\n * Convert a filesystem path to forward-slash (POSIX) form for display.\n *\n * On Unix this is a no-op. On Windows it converts `C:\\Users\\name\\project`\n * to `C:/Users/name/project`, which is valid in:\n * - Git Bash / MSYS2\n * - WSL bash\n * - PowerShell\n * - Node.js APIs (which accept both separators)\n * - Most Windows programs\n */\nexport function toPosixPath(fsPath: string): string {\n\treturn fsPath.replaceAll(\"\\\\\", \"/\");\n}\n"]}
@@ -774,6 +774,42 @@ export class AgentSession {
774
774
  );
775
775
  }
776
776
 
777
+ /**
778
+ * Switch edit mode between standard (text-match) and hashline (LINE#ID anchors).
779
+ * Swaps the active read/edit tools and rebuilds the system prompt.
780
+ */
781
+ setEditMode(mode: "standard" | "hashline"): void {
782
+ this.settingsManager.setEditMode(mode);
783
+
784
+ // Get current active tool registry keys
785
+ const currentKeys = new Set<string>();
786
+ for (const [key, tool] of this._toolRegistry.entries()) {
787
+ if (this.agent.state.tools.includes(tool)) {
788
+ currentKeys.add(key);
789
+ }
790
+ }
791
+
792
+ // Swap read tools
793
+ if (mode === "hashline") {
794
+ currentKeys.delete("read");
795
+ currentKeys.add("hashline_read");
796
+ currentKeys.delete("edit");
797
+ currentKeys.add("hashline_edit");
798
+ } else {
799
+ currentKeys.delete("hashline_read");
800
+ currentKeys.add("read");
801
+ currentKeys.delete("hashline_edit");
802
+ currentKeys.add("edit");
803
+ }
804
+
805
+ this.setActiveToolsByName([...currentKeys]);
806
+ }
807
+
808
+ /** Current edit mode */
809
+ get editMode(): "standard" | "hashline" {
810
+ return this.settingsManager.getEditMode();
811
+ }
812
+
777
813
  /** All messages including custom types like BashExecutionMessage */
778
814
  get messages(): AgentMessage[] {
779
815
  return this.agent.state.messages;
@@ -65,7 +65,7 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
65
65
  externalEditor: "ctrl+g",
66
66
  followUp: "alt+enter",
67
67
  dequeue: "alt+up",
68
- pasteImage: process.platform === "win32" ? "alt+v" : "ctrl+v",
68
+ pasteImage: process.platform === "win32" ? "alt+v" : ["ctrl+v", "alt+v"],
69
69
  newSession: [],
70
70
  tree: [],
71
71
  fork: [],
@@ -842,7 +842,17 @@ export async function sendNotification(client: LspClient, method: string, params
842
842
  };
843
843
 
844
844
  client.lastActivity = Date.now();
845
- await writeMessage(client.proc.stdin, notification);
845
+ try {
846
+ await writeMessage(client.proc.stdin, notification);
847
+ } catch (err: unknown) {
848
+ // EPIPE means the LSP process died (e.g. after lsp.reload killed it).
849
+ // Swallow so callers don't crash — the next getOrCreateClient call
850
+ // will spawn a fresh server (#815).
851
+ if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'EPIPE') {
852
+ return;
853
+ }
854
+ throw err;
855
+ }
846
856
  }
847
857
 
848
858
  /**
@@ -211,6 +211,13 @@ async function reloadServer(client: LspClient, serverName: string, signal?: Abor
211
211
  }
212
212
  if (output.startsWith("Restarted")) {
213
213
  client.proc.kill();
214
+ // Wait for the process to actually exit so the crash recovery handler
215
+ // removes the client from the cache. Without this, the next
216
+ // getOrCreateClient call may return the dead client (#815).
217
+ await Promise.race([
218
+ client.proc.exited,
219
+ new Promise(r => setTimeout(r, 3000)),
220
+ ]);
214
221
  }
215
222
  return output;
216
223
  }
@@ -30,6 +30,12 @@ import {
30
30
  editTool,
31
31
  findTool,
32
32
  grepTool,
33
+ hashlineCodingTools,
34
+ hashlineEditTool,
35
+ hashlineReadTool,
36
+ createHashlineCodingTools,
37
+ createHashlineEditTool,
38
+ createHashlineReadTool,
33
39
  lsTool,
34
40
  readOnlyTools,
35
41
  readTool,
@@ -119,6 +125,13 @@ export {
119
125
  createGrepTool,
120
126
  createFindTool,
121
127
  createLsTool,
128
+ // Hashline edit mode
129
+ hashlineCodingTools,
130
+ hashlineEditTool,
131
+ hashlineReadTool,
132
+ createHashlineCodingTools,
133
+ createHashlineEditTool,
134
+ createHashlineReadTool,
122
135
  };
123
136
 
124
137
  // Helper Functions
@@ -238,7 +251,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
238
251
  thinkingLevel = "off";
239
252
  }
240
253
 
241
- const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write", "lsp"];
254
+ const editMode = settingsManager.getEditMode();
255
+ const defaultActiveToolNames: ToolName[] = editMode === "hashline"
256
+ ? ["hashline_read", "bash", "hashline_edit", "write", "lsp"]
257
+ : ["read", "bash", "edit", "write", "lsp"];
242
258
  const initialActiveToolNames: ToolName[] = options.tools
243
259
  ? options.tools.map((t) => t.name).filter((n): n is ToolName => n in allTools)
244
260
  : defaultActiveToolNames;
@@ -142,6 +142,7 @@ export interface Settings {
142
142
  taskIsolation?: TaskIsolationSettings;
143
143
  fallback?: FallbackSettings;
144
144
  modelDiscovery?: ModelDiscoverySettings;
145
+ editMode?: "standard" | "hashline"; // Edit tool mode: "standard" (text match) or "hashline" (LINE#ID anchors). Default: "standard"
145
146
  }
146
147
 
147
148
  /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
@@ -1127,4 +1128,14 @@ export class SettingsManager {
1127
1128
  this.markModified("modelDiscovery", "enabled");
1128
1129
  this.save();
1129
1130
  }
1131
+
1132
+ getEditMode(): "standard" | "hashline" {
1133
+ return this.settings.editMode ?? "standard";
1134
+ }
1135
+
1136
+ setEditMode(mode: "standard" | "hashline"): void {
1137
+ this.globalSettings.editMode = mode;
1138
+ this.markModified("editMode");
1139
+ this.save();
1140
+ }
1130
1141
  }
@@ -36,5 +36,6 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
36
36
  { name: "resume", description: "Resume a different session" },
37
37
  { name: "reload", description: "Reload extensions, skills, prompts, and themes" },
38
38
  { name: "thinking", description: "Set thinking level (off/minimal/low/medium/high/xhigh)" },
39
+ { name: "edit-mode", description: "Toggle edit mode (standard/hashline)" },
39
40
  { name: "quit", description: "Quit pi" },
40
41
  ];
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
6
+ import { toPosixPath } from "../utils/path-display.js";
6
7
  import { formatSkillsForPrompt, type Skill } from "./skills.js";
7
8
 
8
9
  /** Tool descriptions for system prompt */
@@ -48,7 +49,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
48
49
  contextFiles: providedContextFiles,
49
50
  skills: providedSkills,
50
51
  } = options;
51
- const resolvedCwd = cwd ?? process.cwd();
52
+ const resolvedCwd = toPosixPath(cwd ?? process.cwd());
52
53
 
53
54
  const now = new Date();
54
55
  const dateTime = now.toLocaleString("en-US", {
@@ -279,6 +279,19 @@ export {
279
279
  type WriteToolInput,
280
280
  type WriteToolOptions,
281
281
  writeTool,
282
+ // Hashline edit mode tools
283
+ hashlineEditTool,
284
+ hashlineReadTool,
285
+ hashlineCodingTools,
286
+ createHashlineEditTool,
287
+ createHashlineReadTool,
288
+ createHashlineCodingTools,
289
+ type HashlineEditInput,
290
+ type HashlineEditToolDetails,
291
+ type HashlineEditToolOptions,
292
+ type HashlineReadToolDetails,
293
+ type HashlineReadToolInput,
294
+ type HashlineReadToolOptions,
282
295
  } from "./core/tools/index.js";
283
296
  // Main entry point
284
297
  export { main } from "./main.js";
@@ -348,3 +361,5 @@ export { copyToClipboard } from "./utils/clipboard.js";
348
361
  export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
349
362
  // Shell utilities
350
363
  export { getShellConfig, sanitizeCommand } from "./utils/shell.js";
364
+ // Cross-platform path display
365
+ export { toPosixPath } from "./utils/path-display.js";