lsd-pi 1.3.6 → 1.3.7

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 (83) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/lsd-settings-manager.d.ts +2 -0
  3. package/dist/lsd-settings-manager.js +5 -0
  4. package/dist/resource-loader.js +33 -3
  5. package/dist/resources/extensions/cache-timer/index.js +3 -2
  6. package/dist/welcome-screen.js +2 -2
  7. package/package.json +1 -1
  8. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts +2 -0
  9. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts.map +1 -0
  10. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js +35 -0
  11. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js.map +1 -0
  12. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  13. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  14. package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
  15. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  16. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +5 -0
  17. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  18. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +21 -0
  19. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  20. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +16 -1
  21. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  22. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +12 -4
  23. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
  24. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +7 -5
  25. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
  26. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +86 -28
  27. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  28. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts +2 -0
  29. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +16 -10
  31. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  33. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +26 -4
  35. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +14 -1
  37. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  38. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +111 -12
  39. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  40. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +1 -0
  41. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
  42. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +47 -3
  43. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
  44. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +137 -6
  45. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  47. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +40 -14
  48. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
  52. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  53. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -1
  55. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  56. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +70 -25
  57. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  58. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +4 -4
  59. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  60. package/packages/pi-coding-agent/package.json +1 -1
  61. package/packages/pi-coding-agent/src/core/settings-manager.collapse-tool-calls.test.ts +46 -0
  62. package/packages/pi-coding-agent/src/core/settings-manager.ts +18 -0
  63. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +20 -0
  64. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +26 -0
  65. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +14 -4
  66. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +105 -28
  67. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +13 -6
  68. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +31 -4
  69. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +119 -13
  70. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +59 -3
  71. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +174 -6
  72. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +50 -14
  73. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -1
  74. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
  75. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +73 -25
  76. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -4
  77. package/packages/pi-tui/dist/components/editor.js +3 -3
  78. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  79. package/packages/pi-tui/src/components/editor.ts +3 -3
  80. package/pkg/dist/modes/interactive/theme/themes.js +4 -4
  81. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  82. package/pkg/package.json +1 -1
  83. package/src/resources/extensions/cache-timer/index.ts +3 -2
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { describe, it } from "node:test";
6
- import { computeEditDiff, fuzzyFindText, generateDiffString, normalizeForFuzzyMatch, } from "./edit-diff.js";
6
+ import { computeEditDiff, computeWriteDiff, fuzzyFindText, generateDiffString, normalizeForFuzzyMatch, } from "./edit-diff.js";
7
7
  describe("edit-diff", () => {
8
8
  it("normalizes quotes, dashes, spaces, and trailing whitespace", () => {
9
9
  const input = "“hello”\u00A0world — test \nnext\t\t\n";
@@ -47,6 +47,21 @@ describe("edit-diff", () => {
47
47
  assert.ok(result.firstChangedLine !== undefined);
48
48
  assert.match(result.diff, /CHANGED/);
49
49
  });
50
+ it("computes diffs for write preview against existing files", async (t) => {
51
+ const dir = mkdtempSync(join(tmpdir(), "write-diff-test-"));
52
+ t.after(() => {
53
+ rmSync(dir, { recursive: true, force: true });
54
+ });
55
+ const file = join(dir, "sample.ts");
56
+ writeFileSync(file, "const title = \"Hello\";\n", "utf-8");
57
+ const result = await computeWriteDiff(file, "const title = \"Hi\";\n", dir);
58
+ assert.ok(!("error" in result), "expected a diff result");
59
+ if (!("error" in result)) {
60
+ assert.equal(result.firstChangedLine, 1);
61
+ assert.match(result.diff, /-1 const title = "Hello";/);
62
+ assert.match(result.diff, /\+1 const title = "Hi";/);
63
+ }
64
+ });
50
65
  it("computes diffs for preview without native helpers", async (t) => {
51
66
  const dir = mkdtempSync(join(tmpdir(), "edit-diff-test-"));
52
67
  t.after(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"edit-diff.test.js","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { describe, it } from \"node:test\";\n\nimport {\n\tcomputeEditDiff,\n\tfuzzyFindText,\n\tgenerateDiffString,\n\tnormalizeForFuzzyMatch,\n} from \"./edit-diff.js\";\n\ndescribe(\"edit-diff\", () => {\n\tit(\"normalizes quotes, dashes, spaces, and trailing whitespace\", () => {\n\t\tconst input = \"“hello”\\u00A0world — test \\nnext\\t\\t\\n\";\n\t\tassert.equal(normalizeForFuzzyMatch(input), \"\\\"hello\\\" world - test\\nnext\\n\");\n\t});\n\n\tit(\"falls back to fuzzy matching when unicode punctuation differs\", () => {\n\t\tconst result = fuzzyFindText(\"const title = “Hello”;\\n\", \"const title = \\\"Hello\\\";\\n\");\n\t\tassert.equal(result.found, true);\n\t\tassert.equal(result.usedFuzzyMatch, true);\n\t\tassert.equal(result.contentForReplacement, \"const title = \\\"Hello\\\";\\n\");\n\t});\n\n\tit(\"renders numbered diffs with the first changed line\", () => {\n\t\tconst result = generateDiffString(\"line 1\\nline 2\\nline 3\\n\", \"line 1\\nline two\\nline 3\\n\");\n\t\tassert.equal(result.firstChangedLine, 2);\n\t\tassert.match(result.diff, /-2 line 2/);\n\t\tassert.match(result.diff, /\\+2 line two/);\n\t});\n\n\tit(\"respects contextLines and inserts separators for distant changes\", () => {\n\t\tconst lines = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`);\n\t\tconst oldContent = lines.join(\"\\n\") + \"\\n\";\n\t\tconst modified = [...lines];\n\t\tmodified[1] = \"changed 2\"; // line 2\n\t\tmodified[17] = \"changed 18\"; // line 18\n\t\tconst newContent = modified.join(\"\\n\") + \"\\n\";\n\n\t\tconst result = generateDiffString(oldContent, newContent, 2);\n\t\t// Should contain separator between the two distant change regions\n\t\tassert.match(result.diff, /\\.\\.\\./);\n\t\t// Should NOT contain lines far from changes (e.g. line 10)\n\t\tassert.doesNotMatch(result.diff, /line 10/);\n\t\t// Should contain the changed lines\n\t\tassert.match(result.diff, /changed 2/);\n\t\tassert.match(result.diff, /changed 18/);\n\t});\n\n\tit(\"handles large files without OOM by falling back to linear diff\", () => {\n\t\t// Create files large enough to exceed the DP threshold\n\t\tconst lineCount = 3000;\n\t\tconst oldLines = Array.from({ length: lineCount }, (_, i) => `line ${i}`);\n\t\tconst newLines = [...oldLines];\n\t\tnewLines[1500] = \"CHANGED\";\n\t\tconst result = generateDiffString(oldLines.join(\"\\n\") + \"\\n\", newLines.join(\"\\n\") + \"\\n\");\n\t\tassert.ok(result.firstChangedLine !== undefined);\n\t\tassert.match(result.diff, /CHANGED/);\n\t});\n\n\tit(\"computes diffs for preview without native helpers\", async (t) => {\n\t\tconst dir = mkdtempSync(join(tmpdir(), \"edit-diff-test-\"));\n\t\tt.after(() => {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t});\n\n\t\tconst file = join(dir, \"sample.ts\");\n\t\twriteFileSync(file, \"const title = “Hello”;\\n\", \"utf-8\");\n\n\t\tconst result = await computeEditDiff(\n\t\t\tfile,\n\t\t\t\"const title = \\\"Hello\\\";\\n\",\n\t\t\t\"const title = \\\"Hi\\\";\\n\",\n\t\t\tdir,\n\t\t);\n\n\t\tassert.ok(!(\"error\" in result), \"expected a diff result\");\n\t\tif (!(\"error\" in result)) {\n\t\t\tassert.equal(result.firstChangedLine, 1);\n\t\t\tassert.match(result.diff, /\\+1 const title = \"Hi\";/);\n\t\t}\n\t});\n});\n"]}
1
+ {"version":3,"file":"edit-diff.test.js","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\";\nimport { mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { describe, it } from \"node:test\";\n\nimport {\n\tcomputeEditDiff,\n\tcomputeWriteDiff,\n\tfuzzyFindText,\n\tgenerateDiffString,\n\tnormalizeForFuzzyMatch,\n} from \"./edit-diff.js\";\n\ndescribe(\"edit-diff\", () => {\n\tit(\"normalizes quotes, dashes, spaces, and trailing whitespace\", () => {\n\t\tconst input = \"“hello”\\u00A0world — test \\nnext\\t\\t\\n\";\n\t\tassert.equal(normalizeForFuzzyMatch(input), \"\\\"hello\\\" world - test\\nnext\\n\");\n\t});\n\n\tit(\"falls back to fuzzy matching when unicode punctuation differs\", () => {\n\t\tconst result = fuzzyFindText(\"const title = “Hello”;\\n\", \"const title = \\\"Hello\\\";\\n\");\n\t\tassert.equal(result.found, true);\n\t\tassert.equal(result.usedFuzzyMatch, true);\n\t\tassert.equal(result.contentForReplacement, \"const title = \\\"Hello\\\";\\n\");\n\t});\n\n\tit(\"renders numbered diffs with the first changed line\", () => {\n\t\tconst result = generateDiffString(\"line 1\\nline 2\\nline 3\\n\", \"line 1\\nline two\\nline 3\\n\");\n\t\tassert.equal(result.firstChangedLine, 2);\n\t\tassert.match(result.diff, /-2 line 2/);\n\t\tassert.match(result.diff, /\\+2 line two/);\n\t});\n\n\tit(\"respects contextLines and inserts separators for distant changes\", () => {\n\t\tconst lines = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`);\n\t\tconst oldContent = lines.join(\"\\n\") + \"\\n\";\n\t\tconst modified = [...lines];\n\t\tmodified[1] = \"changed 2\"; // line 2\n\t\tmodified[17] = \"changed 18\"; // line 18\n\t\tconst newContent = modified.join(\"\\n\") + \"\\n\";\n\n\t\tconst result = generateDiffString(oldContent, newContent, 2);\n\t\t// Should contain separator between the two distant change regions\n\t\tassert.match(result.diff, /\\.\\.\\./);\n\t\t// Should NOT contain lines far from changes (e.g. line 10)\n\t\tassert.doesNotMatch(result.diff, /line 10/);\n\t\t// Should contain the changed lines\n\t\tassert.match(result.diff, /changed 2/);\n\t\tassert.match(result.diff, /changed 18/);\n\t});\n\n\tit(\"handles large files without OOM by falling back to linear diff\", () => {\n\t\t// Create files large enough to exceed the DP threshold\n\t\tconst lineCount = 3000;\n\t\tconst oldLines = Array.from({ length: lineCount }, (_, i) => `line ${i}`);\n\t\tconst newLines = [...oldLines];\n\t\tnewLines[1500] = \"CHANGED\";\n\t\tconst result = generateDiffString(oldLines.join(\"\\n\") + \"\\n\", newLines.join(\"\\n\") + \"\\n\");\n\t\tassert.ok(result.firstChangedLine !== undefined);\n\t\tassert.match(result.diff, /CHANGED/);\n\t});\n\n\tit(\"computes diffs for write preview against existing files\", async (t) => {\n\t\tconst dir = mkdtempSync(join(tmpdir(), \"write-diff-test-\"));\n\t\tt.after(() => {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t});\n\n\t\tconst file = join(dir, \"sample.ts\");\n\t\twriteFileSync(file, \"const title = \\\"Hello\\\";\\n\", \"utf-8\");\n\n\t\tconst result = await computeWriteDiff(file, \"const title = \\\"Hi\\\";\\n\", dir);\n\n\t\tassert.ok(!(\"error\" in result), \"expected a diff result\");\n\t\tif (!(\"error\" in result)) {\n\t\t\tassert.equal(result.firstChangedLine, 1);\n\t\t\tassert.match(result.diff, /-1 const title = \"Hello\";/);\n\t\t\tassert.match(result.diff, /\\+1 const title = \"Hi\";/);\n\t\t}\n\t});\n\n\tit(\"computes diffs for preview without native helpers\", async (t) => {\n\t\tconst dir = mkdtempSync(join(tmpdir(), \"edit-diff-test-\"));\n\t\tt.after(() => {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t});\n\n\t\tconst file = join(dir, \"sample.ts\");\n\t\twriteFileSync(file, \"const title = “Hello”;\\n\", \"utf-8\");\n\n\t\tconst result = await computeEditDiff(\n\t\t\tfile,\n\t\t\t\"const title = \\\"Hello\\\";\\n\",\n\t\t\t\"const title = \\\"Hi\\\";\\n\",\n\t\t\tdir,\n\t\t);\n\n\t\tassert.ok(!(\"error\" in result), \"expected a diff result\");\n\t\tif (!(\"error\" in result)) {\n\t\t\tassert.equal(result.firstChangedLine, 1);\n\t\t\tassert.match(result.diff, /\\+1 const title = \"Hi\";/);\n\t\t}\n\t});\n});\n"]}
@@ -5,16 +5,24 @@ import { ToolSummaryLine } from "../tool-summary-line.js";
5
5
  import { initTheme } from "../../theme/theme.js";
6
6
  initTheme("dark");
7
7
  describe("ToolSummaryLine", () => {
8
- it("aggregates repeated tools with tool-row style formatting", () => {
8
+ it("renders action-based summaries for grouped identical tools", () => {
9
9
  const summary = new ToolSummaryLine();
10
10
  summary.addTool("read", 600);
11
- summary.addTool("lsp", 250);
12
11
  summary.addTool("read", 150);
13
12
  const rendered = stripAnsi(summary.render(160).join("\n"));
14
- assert.match(rendered, /^ ● collapsed tools /);
15
- assert.ok(rendered.includes("read ×2 · lsp · 1.0s"));
13
+ assert.match(rendered, /^ ● /);
14
+ assert.ok(rendered.includes("reading 2 files · 0.8s"));
15
+ assert.equal(summary.canGroupWith("read"), true);
16
+ assert.equal(summary.canGroupWith("find"), false);
17
+ assert.equal(rendered.includes("collapsed tools"), false);
16
18
  assert.equal(rendered.includes("⎯"), false);
17
19
  });
20
+ it("keeps fallback format for unknown tools", () => {
21
+ const summary = new ToolSummaryLine();
22
+ summary.addTool("custom_tool", 100);
23
+ const rendered = stripAnsi(summary.render(160).join("\n"));
24
+ assert.ok(rendered.includes("custom_tool · 0.1s"));
25
+ });
18
26
  it("renders nothing when empty or hidden", () => {
19
27
  const summary = new ToolSummaryLine();
20
28
  assert.deepEqual(summary.render(80), []);
@@ -1 +1 @@
1
- {"version":3,"file":"tool-summary-line.test.js","sourceRoot":"","sources":["../../../../../src/modes/interactive/components/__tests__/tool-summary-line.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,SAAS,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,SAAS,CAAC,MAAM,CAAC,CAAC;AAElB,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QACnE,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;QACtC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAC5B,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAE7B,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,sBAAsB,CAAC,CAAC;QAC/C,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC;QACrD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC/C,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;QACtC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAEzC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport stripAnsi from \"strip-ansi\";\n\nimport { ToolSummaryLine } from \"../tool-summary-line.js\";\nimport { initTheme } from \"../../theme/theme.js\";\n\ninitTheme(\"dark\");\n\ndescribe(\"ToolSummaryLine\", () => {\n\tit(\"aggregates repeated tools with tool-row style formatting\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addTool(\"read\", 600);\n\t\tsummary.addTool(\"lsp\", 250);\n\t\tsummary.addTool(\"read\", 150);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.match(rendered, /^ ● collapsed tools /);\n\t\tassert.ok(rendered.includes(\"read ×2 · lsp · 1.0s\"));\n\t\tassert.equal(rendered.includes(\"⎯\"), false);\n\t});\n\n\tit(\"renders nothing when empty or hidden\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tassert.deepEqual(summary.render(80), []);\n\n\t\tsummary.addTool(\"grep\", 100);\n\t\tsummary.setHidden(true);\n\t\tassert.deepEqual(summary.render(80), []);\n\t});\n});\n"]}
1
+ {"version":3,"file":"tool-summary-line.test.js","sourceRoot":"","sources":["../../../../../src/modes/interactive/components/__tests__/tool-summary-line.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,SAAS,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,SAAS,CAAC,MAAM,CAAC,CAAC;AAElB,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACrE,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;QACtC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAE7B,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/B,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC,CAAC;QACvD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;QACjD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;QAClD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QAClD,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;QACtC,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;QAEpC,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC/C,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;QACtC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAEzC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC7B,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport stripAnsi from \"strip-ansi\";\n\nimport { ToolSummaryLine } from \"../tool-summary-line.js\";\nimport { initTheme } from \"../../theme/theme.js\";\n\ninitTheme(\"dark\");\n\ndescribe(\"ToolSummaryLine\", () => {\n\tit(\"renders action-based summaries for grouped identical tools\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addTool(\"read\", 600);\n\t\tsummary.addTool(\"read\", 150);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.match(rendered, /^ ● /);\n\t\tassert.ok(rendered.includes(\"reading 2 files · 0.8s\"));\n\t\tassert.equal(summary.canGroupWith(\"read\"), true);\n\t\tassert.equal(summary.canGroupWith(\"find\"), false);\n\t\tassert.equal(rendered.includes(\"collapsed tools\"), false);\n\t\tassert.equal(rendered.includes(\"⎯\"), false);\n\t});\n\n\tit(\"keeps fallback format for unknown tools\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tsummary.addTool(\"custom_tool\", 100);\n\n\t\tconst rendered = stripAnsi(summary.render(160).join(\"\\n\"));\n\t\tassert.ok(rendered.includes(\"custom_tool · 0.1s\"));\n\t});\n\n\tit(\"renders nothing when empty or hidden\", () => {\n\t\tconst summary = new ToolSummaryLine();\n\t\tassert.deepEqual(summary.render(80), []);\n\n\t\tsummary.addTool(\"grep\", 100);\n\t\tsummary.setHidden(true);\n\t\tassert.deepEqual(summary.render(80), []);\n\t});\n});\n"]}
@@ -2,11 +2,13 @@ export interface RenderDiffOptions {
2
2
  /** File path (unused, kept for API compatibility) */
3
3
  filePath?: string;
4
4
  }
5
+ export declare function renderDiffLines(diffText: string, width: number, _options?: RenderDiffOptions): string[];
5
6
  /**
6
- * Render a diff string with colored lines and intra-line change highlighting.
7
- * - Context lines: dim/gray
8
- * - Removed lines: red, with inverse on changed tokens
9
- * - Added lines: green, with inverse on changed tokens
7
+ * Render a diff string with Claude-like colored lines.
8
+ * - Context lines: muted gray
9
+ * - Removed lines: red text on subtle red background
10
+ * - Added lines: green text on subtle green background
11
+ * - Changed tokens: slightly stronger background + bold
10
12
  */
11
- export declare function renderDiff(diffText: string, _options?: RenderDiffOptions): string;
13
+ export declare function renderDiff(diffText: string, options?: RenderDiffOptions): string;
12
14
  //# sourceMappingURL=diff.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/diff.ts"],"names":[],"mappings":"AAmEA,MAAM,WAAW,iBAAiB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAE,iBAAsB,GAAG,MAAM,CAoErF"}
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/diff.ts"],"names":[],"mappings":"AAoHA,MAAM,WAAW,iBAAiB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAkBD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,iBAAsB,GAAG,MAAM,EAAE,CA2E3G;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAEpF"}
@@ -1,5 +1,13 @@
1
+ import { visibleWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
1
2
  import * as Diff from "diff";
3
+ import chalk from "chalk";
2
4
  import { theme } from "../theme/theme.js";
5
+ const DIFF_BG = {
6
+ addedLine: "#0f2f1a",
7
+ removedLine: "#3a1116",
8
+ addedToken: "#1b5e20",
9
+ removedToken: "#7f1d1d",
10
+ };
3
11
  /**
4
12
  * Parse diff line to extract prefix, line number, and content.
5
13
  * Format: "+123 content" or "-123 content" or " 123 content" or " ..."
@@ -16,10 +24,45 @@ function parseDiffLine(line) {
16
24
  function replaceTabs(text) {
17
25
  return text.replace(/\t/g, " ");
18
26
  }
27
+ function formatLineNum(raw, width) {
28
+ const trimmed = raw.trim();
29
+ if (!trimmed)
30
+ return "".padStart(width, " ");
31
+ return trimmed.padStart(width, " ");
32
+ }
33
+ function padToWidth(text, width) {
34
+ if (!Number.isFinite(width) || width > 10_000) {
35
+ return text;
36
+ }
37
+ const paddingNeeded = Math.max(0, width - visibleWidth(text));
38
+ return text + " ".repeat(paddingNeeded);
39
+ }
40
+ function styleRemovedPrefix(prefix) {
41
+ return chalk.hex("#fb7185")(prefix);
42
+ }
43
+ function styleAddedPrefix(prefix) {
44
+ return chalk.hex("#4ade80")(prefix);
45
+ }
46
+ function styleRemovedToken(value) {
47
+ return chalk.bgHex(DIFF_BG.removedToken).whiteBright(value);
48
+ }
49
+ function styleAddedToken(value) {
50
+ return chalk.bgHex(DIFF_BG.addedToken).whiteBright(value);
51
+ }
52
+ function styleAddedLine(text, width) {
53
+ const prefix = text.startsWith("+") ? styleAddedPrefix("+") : "";
54
+ const rest = prefix ? text.slice(1) : text;
55
+ return chalk.bgHex(DIFF_BG.addedLine).whiteBright(padToWidth(`${prefix}${rest}`, width));
56
+ }
57
+ function styleRemovedLine(text, width) {
58
+ const prefix = text.startsWith("-") ? styleRemovedPrefix("-") : "";
59
+ const rest = prefix ? text.slice(1) : text;
60
+ return chalk.bgHex(DIFF_BG.removedLine).whiteBright(padToWidth(`${prefix}${rest}`, width));
61
+ }
19
62
  /**
20
- * Compute word-level diff and render with inverse on changed parts.
63
+ * Compute word-level diff and render changed tokens with subtle emphasis.
21
64
  * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
22
- * Strips leading whitespace from inverse to avoid highlighting indentation.
65
+ * Strips leading whitespace from highlighted token spans.
23
66
  */
24
67
  function renderIntraLineDiff(oldContent, newContent) {
25
68
  const wordDiff = Diff.diffWords(oldContent, newContent);
@@ -30,7 +73,6 @@ function renderIntraLineDiff(oldContent, newContent) {
30
73
  for (const part of wordDiff) {
31
74
  if (part.removed) {
32
75
  let value = part.value;
33
- // Strip leading whitespace from the first removed part
34
76
  if (isFirstRemoved) {
35
77
  const leadingWs = value.match(/^(\s*)/)?.[1] || "";
36
78
  value = value.slice(leadingWs.length);
@@ -38,12 +80,11 @@ function renderIntraLineDiff(oldContent, newContent) {
38
80
  isFirstRemoved = false;
39
81
  }
40
82
  if (value) {
41
- removedLine += theme.inverse(value);
83
+ removedLine += styleRemovedToken(value);
42
84
  }
43
85
  }
44
86
  else if (part.added) {
45
87
  let value = part.value;
46
- // Strip leading whitespace from the first added part
47
88
  if (isFirstAdded) {
48
89
  const leadingWs = value.match(/^(\s*)/)?.[1] || "";
49
90
  value = value.slice(leadingWs.length);
@@ -51,7 +92,7 @@ function renderIntraLineDiff(oldContent, newContent) {
51
92
  isFirstAdded = false;
52
93
  }
53
94
  if (value) {
54
- addedLine += theme.inverse(value);
95
+ addedLine += styleAddedToken(value);
55
96
  }
56
97
  }
57
98
  else {
@@ -61,26 +102,39 @@ function renderIntraLineDiff(oldContent, newContent) {
61
102
  }
62
103
  return { removedLine, addedLine };
63
104
  }
64
- /**
65
- * Render a diff string with colored lines and intra-line change highlighting.
66
- * - Context lines: dim/gray
67
- * - Removed lines: red, with inverse on changed tokens
68
- * - Added lines: green, with inverse on changed tokens
69
- */
70
- export function renderDiff(diffText, _options = {}) {
105
+ function renderStyledLine(text, width, kind) {
106
+ const wrapWidth = Math.max(1, width);
107
+ const wrapped = wrapTextWithAnsi(text, wrapWidth);
108
+ if (kind === "added") {
109
+ return wrapped.map((line) => styleAddedLine(line, width));
110
+ }
111
+ if (kind === "removed") {
112
+ return wrapped.map((line) => styleRemovedLine(line, width));
113
+ }
114
+ return wrapped.map((line) => padToWidth(theme.fg("toolDiffContext", line), width));
115
+ }
116
+ export function renderDiffLines(diffText, width, _options = {}) {
71
117
  const lines = diffText.split("\n");
72
118
  const result = [];
119
+ const parsedLines = lines.map(parseDiffLine).filter((p) => !!p);
120
+ const lineNumWidth = Math.max(1, ...parsedLines
121
+ .map((p) => p.lineNum.trim())
122
+ .filter(Boolean)
123
+ .map((n) => n.length));
124
+ const formatLine = (prefix, lineNum, content) => {
125
+ const num = formatLineNum(lineNum, lineNumWidth);
126
+ return `${prefix}${num} ${replaceTabs(content)}`;
127
+ };
73
128
  let i = 0;
74
129
  while (i < lines.length) {
75
130
  const line = lines[i];
76
131
  const parsed = parseDiffLine(line);
77
132
  if (!parsed) {
78
- result.push(theme.fg("toolDiffContext", line));
133
+ result.push(...renderStyledLine(line, width, "context"));
79
134
  i++;
80
135
  continue;
81
136
  }
82
137
  if (parsed.prefix === "-") {
83
- // Collect consecutive removed lines
84
138
  const removedLines = [];
85
139
  while (i < lines.length) {
86
140
  const p = parseDiffLine(lines[i]);
@@ -89,7 +143,6 @@ export function renderDiff(diffText, _options = {}) {
89
143
  removedLines.push({ lineNum: p.lineNum, content: p.content });
90
144
  i++;
91
145
  }
92
- // Collect consecutive added lines
93
146
  const addedLines = [];
94
147
  while (i < lines.length) {
95
148
  const p = parseDiffLine(lines[i]);
@@ -98,36 +151,41 @@ export function renderDiff(diffText, _options = {}) {
98
151
  addedLines.push({ lineNum: p.lineNum, content: p.content });
99
152
  i++;
100
153
  }
101
- // Only do intra-line diffing when there's exactly one removed and one added line
102
- // (indicating a single line modification). Otherwise, show lines as-is.
103
154
  if (removedLines.length === 1 && addedLines.length === 1) {
104
155
  const removed = removedLines[0];
105
156
  const added = addedLines[0];
106
157
  const { removedLine, addedLine } = renderIntraLineDiff(replaceTabs(removed.content), replaceTabs(added.content));
107
- result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`));
108
- result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`));
158
+ result.push(...renderStyledLine(formatLine("-", removed.lineNum, removedLine), width, "removed"));
159
+ result.push(...renderStyledLine(formatLine("+", added.lineNum, addedLine), width, "added"));
109
160
  }
110
161
  else {
111
- // Show all removed lines first, then all added lines
112
162
  for (const removed of removedLines) {
113
- result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
163
+ result.push(...renderStyledLine(formatLine("-", removed.lineNum, removed.content), width, "removed"));
114
164
  }
115
165
  for (const added of addedLines) {
116
- result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
166
+ result.push(...renderStyledLine(formatLine("+", added.lineNum, added.content), width, "added"));
117
167
  }
118
168
  }
119
169
  }
120
170
  else if (parsed.prefix === "+") {
121
- // Standalone added line
122
- result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
171
+ result.push(...renderStyledLine(formatLine("+", parsed.lineNum, parsed.content), width, "added"));
123
172
  i++;
124
173
  }
125
174
  else {
126
- // Context line
127
- result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
175
+ result.push(...renderStyledLine(formatLine(" ", parsed.lineNum, parsed.content), width, "context"));
128
176
  i++;
129
177
  }
130
178
  }
131
- return result.join("\n");
179
+ return result;
180
+ }
181
+ /**
182
+ * Render a diff string with Claude-like colored lines.
183
+ * - Context lines: muted gray
184
+ * - Removed lines: red text on subtle red background
185
+ * - Added lines: green text on subtle green background
186
+ * - Changed tokens: slightly stronger background + bold
187
+ */
188
+ export function renderDiff(diffText, options = {}) {
189
+ return renderDiffLines(diffText, Number.MAX_SAFE_INTEGER, options).join("\n");
132
190
  }
133
191
  //# sourceMappingURL=diff.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/diff.ts"],"names":[],"mappings":"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","sourcesContent":["import * as Diff from \"diff\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Parse diff line to extract prefix, line number, and content.\n * Format: \"+123 content\" or \"-123 content\" or \" 123 content\" or \" ...\"\n */\nfunction parseDiffLine(line: string): { prefix: string; lineNum: string; content: string } | null {\n\tconst match = line.match(/^([+-\\s])(\\s*\\d*)\\s(.*)$/);\n\tif (!match) return null;\n\treturn { prefix: match[1], lineNum: match[2], content: match[3] };\n}\n\n/**\n * Replace tabs with spaces for consistent rendering.\n */\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\n/**\n * Compute word-level diff and render with inverse on changed parts.\n * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.\n * Strips leading whitespace from inverse to avoid highlighting indentation.\n */\nfunction renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {\n\tconst wordDiff = Diff.diffWords(oldContent, newContent);\n\n\tlet removedLine = \"\";\n\tlet addedLine = \"\";\n\tlet isFirstRemoved = true;\n\tlet isFirstAdded = true;\n\n\tfor (const part of wordDiff) {\n\t\tif (part.removed) {\n\t\t\tlet value = part.value;\n\t\t\t// Strip leading whitespace from the first removed part\n\t\t\tif (isFirstRemoved) {\n\t\t\t\tconst leadingWs = value.match(/^(\\s*)/)?.[1] || \"\";\n\t\t\t\tvalue = value.slice(leadingWs.length);\n\t\t\t\tremovedLine += leadingWs;\n\t\t\t\tisFirstRemoved = false;\n\t\t\t}\n\t\t\tif (value) {\n\t\t\t\tremovedLine += theme.inverse(value);\n\t\t\t}\n\t\t} else if (part.added) {\n\t\t\tlet value = part.value;\n\t\t\t// Strip leading whitespace from the first added part\n\t\t\tif (isFirstAdded) {\n\t\t\t\tconst leadingWs = value.match(/^(\\s*)/)?.[1] || \"\";\n\t\t\t\tvalue = value.slice(leadingWs.length);\n\t\t\t\taddedLine += leadingWs;\n\t\t\t\tisFirstAdded = false;\n\t\t\t}\n\t\t\tif (value) {\n\t\t\t\taddedLine += theme.inverse(value);\n\t\t\t}\n\t\t} else {\n\t\t\tremovedLine += part.value;\n\t\t\taddedLine += part.value;\n\t\t}\n\t}\n\n\treturn { removedLine, addedLine };\n}\n\nexport interface RenderDiffOptions {\n\t/** File path (unused, kept for API compatibility) */\n\tfilePath?: string;\n}\n\n/**\n * Render a diff string with colored lines and intra-line change highlighting.\n * - Context lines: dim/gray\n * - Removed lines: red, with inverse on changed tokens\n * - Added lines: green, with inverse on changed tokens\n */\nexport function renderDiff(diffText: string, _options: RenderDiffOptions = {}): string {\n\tconst lines = diffText.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tconst parsed = parseDiffLine(line);\n\n\t\tif (!parsed) {\n\t\t\tresult.push(theme.fg(\"toolDiffContext\", line));\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (parsed.prefix === \"-\") {\n\t\t\t// Collect consecutive removed lines\n\t\t\tconst removedLines: { lineNum: string; content: string }[] = [];\n\t\t\twhile (i < lines.length) {\n\t\t\t\tconst p = parseDiffLine(lines[i]);\n\t\t\t\tif (!p || p.prefix !== \"-\") break;\n\t\t\t\tremovedLines.push({ lineNum: p.lineNum, content: p.content });\n\t\t\t\ti++;\n\t\t\t}\n\n\t\t\t// Collect consecutive added lines\n\t\t\tconst addedLines: { lineNum: string; content: string }[] = [];\n\t\t\twhile (i < lines.length) {\n\t\t\t\tconst p = parseDiffLine(lines[i]);\n\t\t\t\tif (!p || p.prefix !== \"+\") break;\n\t\t\t\taddedLines.push({ lineNum: p.lineNum, content: p.content });\n\t\t\t\ti++;\n\t\t\t}\n\n\t\t\t// Only do intra-line diffing when there's exactly one removed and one added line\n\t\t\t// (indicating a single line modification). Otherwise, show lines as-is.\n\t\t\tif (removedLines.length === 1 && addedLines.length === 1) {\n\t\t\t\tconst removed = removedLines[0];\n\t\t\t\tconst added = addedLines[0];\n\n\t\t\t\tconst { removedLine, addedLine } = renderIntraLineDiff(\n\t\t\t\t\treplaceTabs(removed.content),\n\t\t\t\t\treplaceTabs(added.content),\n\t\t\t\t);\n\n\t\t\t\tresult.push(theme.fg(\"toolDiffRemoved\", `-${removed.lineNum} ${removedLine}`));\n\t\t\t\tresult.push(theme.fg(\"toolDiffAdded\", `+${added.lineNum} ${addedLine}`));\n\t\t\t} else {\n\t\t\t\t// Show all removed lines first, then all added lines\n\t\t\t\tfor (const removed of removedLines) {\n\t\t\t\t\tresult.push(theme.fg(\"toolDiffRemoved\", `-${removed.lineNum} ${replaceTabs(removed.content)}`));\n\t\t\t\t}\n\t\t\t\tfor (const added of addedLines) {\n\t\t\t\t\tresult.push(theme.fg(\"toolDiffAdded\", `+${added.lineNum} ${replaceTabs(added.content)}`));\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (parsed.prefix === \"+\") {\n\t\t\t// Standalone added line\n\t\t\tresult.push(theme.fg(\"toolDiffAdded\", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));\n\t\t\ti++;\n\t\t} else {\n\t\t\t// Context line\n\t\t\tresult.push(theme.fg(\"toolDiffContext\", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn result.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/diff.ts"],"names":[],"mappings":"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","sourcesContent":["import { visibleWidth, wrapTextWithAnsi } from \"@gsd/pi-tui\";\nimport * as Diff from \"diff\";\nimport chalk from \"chalk\";\nimport { theme } from \"../theme/theme.js\";\n\nconst DIFF_BG = {\n\taddedLine: \"#0f2f1a\",\n\tremovedLine: \"#3a1116\",\n\taddedToken: \"#1b5e20\",\n\tremovedToken: \"#7f1d1d\",\n} as const;\n\n/**\n * Parse diff line to extract prefix, line number, and content.\n * Format: \"+123 content\" or \"-123 content\" or \" 123 content\" or \" ...\"\n */\nfunction parseDiffLine(line: string): { prefix: string; lineNum: string; content: string } | null {\n\tconst match = line.match(/^([+-\\s])(\\s*\\d*)\\s(.*)$/);\n\tif (!match) return null;\n\treturn { prefix: match[1], lineNum: match[2], content: match[3] };\n}\n\n/**\n * Replace tabs with spaces for consistent rendering.\n */\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\nfunction formatLineNum(raw: string, width: number): string {\n\tconst trimmed = raw.trim();\n\tif (!trimmed) return \"\".padStart(width, \" \");\n\treturn trimmed.padStart(width, \" \");\n}\n\nfunction padToWidth(text: string, width: number): string {\n\tif (!Number.isFinite(width) || width > 10_000) {\n\t\treturn text;\n\t}\n\tconst paddingNeeded = Math.max(0, width - visibleWidth(text));\n\treturn text + \" \".repeat(paddingNeeded);\n}\n\nfunction styleRemovedPrefix(prefix: string): string {\n\treturn chalk.hex(\"#fb7185\")(prefix);\n}\n\nfunction styleAddedPrefix(prefix: string): string {\n\treturn chalk.hex(\"#4ade80\")(prefix);\n}\n\nfunction styleRemovedToken(value: string): string {\n\treturn chalk.bgHex(DIFF_BG.removedToken).whiteBright(value);\n}\n\nfunction styleAddedToken(value: string): string {\n\treturn chalk.bgHex(DIFF_BG.addedToken).whiteBright(value);\n}\n\nfunction styleAddedLine(text: string, width: number): string {\n\tconst prefix = text.startsWith(\"+\") ? styleAddedPrefix(\"+\") : \"\";\n\tconst rest = prefix ? text.slice(1) : text;\n\treturn chalk.bgHex(DIFF_BG.addedLine).whiteBright(padToWidth(`${prefix}${rest}`, width));\n}\n\nfunction styleRemovedLine(text: string, width: number): string {\n\tconst prefix = text.startsWith(\"-\") ? styleRemovedPrefix(\"-\") : \"\";\n\tconst rest = prefix ? text.slice(1) : text;\n\treturn chalk.bgHex(DIFF_BG.removedLine).whiteBright(padToWidth(`${prefix}${rest}`, width));\n}\n\n/**\n * Compute word-level diff and render changed tokens with subtle emphasis.\n * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.\n * Strips leading whitespace from highlighted token spans.\n */\nfunction renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {\n\tconst wordDiff = Diff.diffWords(oldContent, newContent);\n\n\tlet removedLine = \"\";\n\tlet addedLine = \"\";\n\tlet isFirstRemoved = true;\n\tlet isFirstAdded = true;\n\n\tfor (const part of wordDiff) {\n\t\tif (part.removed) {\n\t\t\tlet value = part.value;\n\t\t\tif (isFirstRemoved) {\n\t\t\t\tconst leadingWs = value.match(/^(\\s*)/)?.[1] || \"\";\n\t\t\t\tvalue = value.slice(leadingWs.length);\n\t\t\t\tremovedLine += leadingWs;\n\t\t\t\tisFirstRemoved = false;\n\t\t\t}\n\t\t\tif (value) {\n\t\t\t\tremovedLine += styleRemovedToken(value);\n\t\t\t}\n\t\t} else if (part.added) {\n\t\t\tlet value = part.value;\n\t\t\tif (isFirstAdded) {\n\t\t\t\tconst leadingWs = value.match(/^(\\s*)/)?.[1] || \"\";\n\t\t\t\tvalue = value.slice(leadingWs.length);\n\t\t\t\taddedLine += leadingWs;\n\t\t\t\tisFirstAdded = false;\n\t\t\t}\n\t\t\tif (value) {\n\t\t\t\taddedLine += styleAddedToken(value);\n\t\t\t}\n\t\t} else {\n\t\t\tremovedLine += part.value;\n\t\t\taddedLine += part.value;\n\t\t}\n\t}\n\n\treturn { removedLine, addedLine };\n}\n\nexport interface RenderDiffOptions {\n\t/** File path (unused, kept for API compatibility) */\n\tfilePath?: string;\n}\n\nfunction renderStyledLine(\n\ttext: string,\n\twidth: number,\n\tkind: \"context\" | \"added\" | \"removed\",\n): string[] {\n\tconst wrapWidth = Math.max(1, width);\n\tconst wrapped = wrapTextWithAnsi(text, wrapWidth);\n\tif (kind === \"added\") {\n\t\treturn wrapped.map((line) => styleAddedLine(line, width));\n\t}\n\tif (kind === \"removed\") {\n\t\treturn wrapped.map((line) => styleRemovedLine(line, width));\n\t}\n\treturn wrapped.map((line) => padToWidth(theme.fg(\"toolDiffContext\", line), width));\n}\n\nexport function renderDiffLines(diffText: string, width: number, _options: RenderDiffOptions = {}): string[] {\n\tconst lines = diffText.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tconst parsedLines = lines.map(parseDiffLine).filter((p): p is { prefix: string; lineNum: string; content: string } => !!p);\n\tconst lineNumWidth = Math.max(\n\t\t1,\n\t\t...parsedLines\n\t\t\t.map((p) => p.lineNum.trim())\n\t\t\t.filter(Boolean)\n\t\t\t.map((n) => n.length),\n\t);\n\n\tconst formatLine = (prefix: \"+\" | \"-\" | \" \", lineNum: string, content: string): string => {\n\t\tconst num = formatLineNum(lineNum, lineNumWidth);\n\t\treturn `${prefix}${num} ${replaceTabs(content)}`;\n\t};\n\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tconst parsed = parseDiffLine(line);\n\n\t\tif (!parsed) {\n\t\t\tresult.push(...renderStyledLine(line, width, \"context\"));\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (parsed.prefix === \"-\") {\n\t\t\tconst removedLines: { lineNum: string; content: string }[] = [];\n\t\t\twhile (i < lines.length) {\n\t\t\t\tconst p = parseDiffLine(lines[i]);\n\t\t\t\tif (!p || p.prefix !== \"-\") break;\n\t\t\t\tremovedLines.push({ lineNum: p.lineNum, content: p.content });\n\t\t\t\ti++;\n\t\t\t}\n\n\t\t\tconst addedLines: { lineNum: string; content: string }[] = [];\n\t\t\twhile (i < lines.length) {\n\t\t\t\tconst p = parseDiffLine(lines[i]);\n\t\t\t\tif (!p || p.prefix !== \"+\") break;\n\t\t\t\taddedLines.push({ lineNum: p.lineNum, content: p.content });\n\t\t\t\ti++;\n\t\t\t}\n\n\t\t\tif (removedLines.length === 1 && addedLines.length === 1) {\n\t\t\t\tconst removed = removedLines[0];\n\t\t\t\tconst added = addedLines[0];\n\n\t\t\t\tconst { removedLine, addedLine } = renderIntraLineDiff(\n\t\t\t\t\treplaceTabs(removed.content),\n\t\t\t\t\treplaceTabs(added.content),\n\t\t\t\t);\n\n\t\t\t\tresult.push(...renderStyledLine(formatLine(\"-\", removed.lineNum, removedLine), width, \"removed\"));\n\t\t\t\tresult.push(...renderStyledLine(formatLine(\"+\", added.lineNum, addedLine), width, \"added\"));\n\t\t\t} else {\n\t\t\t\tfor (const removed of removedLines) {\n\t\t\t\t\tresult.push(...renderStyledLine(formatLine(\"-\", removed.lineNum, removed.content), width, \"removed\"));\n\t\t\t\t}\n\t\t\t\tfor (const added of addedLines) {\n\t\t\t\t\tresult.push(...renderStyledLine(formatLine(\"+\", added.lineNum, added.content), width, \"added\"));\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (parsed.prefix === \"+\") {\n\t\t\tresult.push(...renderStyledLine(formatLine(\"+\", parsed.lineNum, parsed.content), width, \"added\"));\n\t\t\ti++;\n\t\t} else {\n\t\t\tresult.push(...renderStyledLine(formatLine(\" \", parsed.lineNum, parsed.content), width, \"context\"));\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Render a diff string with Claude-like colored lines.\n * - Context lines: muted gray\n * - Removed lines: red text on subtle red background\n * - Added lines: green text on subtle green background\n * - Changed tokens: slightly stronger background + bold\n */\nexport function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {\n\treturn renderDiffLines(diffText, Number.MAX_SAFE_INTEGER, options).join(\"\\n\");\n}\n"]}
@@ -18,10 +18,12 @@ export declare class FooterComponent implements Component {
18
18
  private autoCompactEnabled;
19
19
  private permissionMode;
20
20
  private notificationSoundEnabled;
21
+ private verboseFooterEnabled;
21
22
  constructor(session: AgentSession, footerData: ReadonlyFooterDataProvider);
22
23
  setAutoCompactEnabled(enabled: boolean): void;
23
24
  setPermissionMode(mode: PermissionMode): void;
24
25
  setNotificationSoundEnabled(enabled: boolean): void;
26
+ setVerboseFooterEnabled(enabled: boolean): void;
25
27
  /**
26
28
  * No-op: git branch caching now handled by provider.
27
29
  * Kept for compatibility with existing call sites in interactive-mode.
@@ -1 +1 @@
1
- {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,gCAAgC,CAAC;AA0BxF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrD;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAM/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IANnB,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,cAAc,CAAwC;IAC9D,OAAO,CAAC,wBAAwB,CAAS;gBAGhC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B;IAG/C,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI7C,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAI7C,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAInD;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB;;;OAGG;IACH,OAAO,IAAI,IAAI;IAIf,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CAyN/B"}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,gCAAgC,CAAC;AA0BxF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKrD;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAO/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAPnB,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,cAAc,CAAwC;IAC9D,OAAO,CAAC,wBAAwB,CAAS;IACzC,OAAO,CAAC,oBAAoB,CAAS;gBAG5B,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B;IAG/C,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI7C,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAI7C,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAInD,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI/C;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB;;;OAGG;IACH,OAAO,IAAI,IAAI;IAIf,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;CA2N/B"}
@@ -51,6 +51,7 @@ export class FooterComponent {
51
51
  this.autoCompactEnabled = true;
52
52
  this.permissionMode = "danger-full-access";
53
53
  this.notificationSoundEnabled = false;
54
+ this.verboseFooterEnabled = false;
54
55
  }
55
56
  setAutoCompactEnabled(enabled) {
56
57
  this.autoCompactEnabled = enabled;
@@ -61,6 +62,9 @@ export class FooterComponent {
61
62
  setNotificationSoundEnabled(enabled) {
62
63
  this.notificationSoundEnabled = enabled;
63
64
  }
65
+ setVerboseFooterEnabled(enabled) {
66
+ this.verboseFooterEnabled = enabled;
67
+ }
64
68
  /**
65
69
  * No-op: git branch caching now handled by provider.
66
70
  * Kept for compatibility with existing call sites in interactive-mode.
@@ -112,7 +116,7 @@ export class FooterComponent {
112
116
  const extensionStatuses = this.footerData.getExtensionStatuses();
113
117
  const cacheTimerStatusRaw = extensionStatuses.get("cache-timer");
114
118
  const cacheTimerStatus = cacheTimerStatusRaw ? sanitizeStatusText(cacheTimerStatusRaw) : "";
115
- const hotkeysHints = ["Ctrl+K • /hotkeys", "/hotkeys", "Ctrl+K"];
119
+ const hotkeysHints = ["/hotkeys"];
116
120
  const firstLineMinPadding = 2;
117
121
  const firstLineRightParts = cacheTimerStatus ? [cacheTimerStatus] : [];
118
122
  if (this.notificationSoundEnabled) {
@@ -141,14 +145,16 @@ export class FooterComponent {
141
145
  }
142
146
  // Build stats line
143
147
  const statsParts = [];
144
- if (totalInput)
145
- statsParts.push(`↑${formatTokens(totalInput)}`);
146
- if (totalOutput)
147
- statsParts.push(`↓${formatTokens(totalOutput)}`);
148
- if (totalCacheRead)
149
- statsParts.push(`Cache ${formatTokens(totalCacheRead)}`);
150
- if (totalCacheWrite)
151
- statsParts.push(`W${formatTokens(totalCacheWrite)}`);
148
+ if (this.verboseFooterEnabled) {
149
+ if (totalInput)
150
+ statsParts.push(`↑${formatTokens(totalInput)}`);
151
+ if (totalOutput)
152
+ statsParts.push(`↓${formatTokens(totalOutput)}`);
153
+ if (totalCacheRead)
154
+ statsParts.push(`Cache ${formatTokens(totalCacheRead)}`);
155
+ if (totalCacheWrite)
156
+ statsParts.push(`W${formatTokens(totalCacheWrite)}`);
157
+ }
152
158
  // Show cost with "(sub)" indicator if using OAuth subscription
153
159
  const usingSubscription = displayModel ? this.session.modelRegistry.isUsingOAuth(displayModel) : false;
154
160
  if (totalCost || usingSubscription) {
@@ -156,7 +162,7 @@ export class FooterComponent {
156
162
  statsParts.push(costStr);
157
163
  }
158
164
  // Per-prompt cost annotation (opt-in via show_token_cost preference, #1515)
159
- if (process.env.GSD_SHOW_TOKEN_COST === "1") {
165
+ if (this.verboseFooterEnabled && process.env.GSD_SHOW_TOKEN_COST === "1") {
160
166
  const lastTurnCost = this.session.getLastTurnCost();
161
167
  if (lastTurnCost > 0) {
162
168
  statsParts.push(`(last: ${formatPromptCost(lastTurnCost)})`);
@@ -1 +1 @@
1
- {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@gsd/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { getPermissionMode, type PermissionMode } from \"../../../core/tool-approval.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts (similar to web-ui)\n */\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Format a cost value for compact display.\n * Uses fewer decimal places for larger amounts.\n * @internal Exported for testing only.\n */\nexport function formatPromptCost(cost: number): string {\n\tif (cost < 0.001) return `$${cost.toFixed(4)}`;\n\tif (cost < 0.01) return `$${cost.toFixed(3)}`;\n\tif (cost < 1) return `$${cost.toFixed(3)}`;\n\treturn `$${cost.toFixed(2)}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate permissionMode: PermissionMode = \"danger-full-access\";\n\tprivate notificationSoundEnabled = false;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\n\t}\n\n\tsetNotificationSoundEnabled(enabled: boolean): void {\n\t\tthis.notificationSoundEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\tconst usageTotals = this.session.sessionManager.getUsageTotals();\n\t\tconst totalInput = usageTotals.input;\n\t\tconst totalOutput = usageTotals.output;\n\t\tconst totalCacheRead = usageTotals.cacheRead;\n\t\tconst totalCacheWrite = usageTotals.cacheWrite;\n\t\tconst totalCost = usageTotals.cost;\n\n\t\t// Use activeInferenceModel during streaming to show the model actually\n\t\t// being used, not the configured model which may have been switched mid-turn.\n\t\tconst displayModel = state.activeInferenceModel ?? state.model;\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextTokens = contextUsage?.tokens ?? null;\n\t\tconst contextWindow = contextUsage?.contextWindow ?? displayModel?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tconst cacheTimerStatusRaw = extensionStatuses.get(\"cache-timer\");\n\t\tconst cacheTimerStatus = cacheTimerStatusRaw ? sanitizeStatusText(cacheTimerStatusRaw) : \"\";\n\t\tconst hotkeysHints = [\"Ctrl+K • /hotkeys\", \"/hotkeys\", \"Ctrl+K\"];\n\t\tconst firstLineMinPadding = 2;\n\t\tconst firstLineRightParts = cacheTimerStatus ? [cacheTimerStatus] : [];\n\t\tif (this.notificationSoundEnabled) {\n\t\t\tfirstLineRightParts.push(theme.fg(\"success\", \"🔔\"));\n\t\t}\n\t\tconst firstLineRightBase = firstLineRightParts.join(\" \");\n\t\tconst hotkeysHint = hotkeysHints.find((hint) => {\n\t\t\tconst candidate = firstLineRightBase ? `${firstLineRightBase} ${hint}` : hint;\n\t\t\treturn visibleWidth(pwd) + firstLineMinPadding + visibleWidth(candidate) <= width;\n\t\t}) ?? \"\";\n\t\tif (hotkeysHint) {\n\t\t\tfirstLineRightParts.push(theme.fg(\"dim\", hotkeysHint));\n\t\t}\n\t\tconst firstLineRight = firstLineRightParts.join(\" \");\n\n\t\tlet pwdLine: string;\n\t\tif (firstLineRight) {\n\t\t\tconst rightWidth = visibleWidth(firstLineRight);\n\t\t\tconst availableForPwd = Math.max(0, width - rightWidth - firstLineMinPadding);\n\t\t\tconst truncatedPwd = truncateToWidth(theme.fg(\"dim\", pwd), availableForPwd, theme.fg(\"dim\", \"...\"));\n\t\t\tconst truncatedPwdWidth = visibleWidth(truncatedPwd);\n\t\t\tconst padding = \" \".repeat(Math.max(firstLineMinPadding, width - truncatedPwdWidth - rightWidth));\n\t\t\tpwdLine = truncatedPwd + padding + firstLineRight;\n\t\t} else {\n\t\t\tpwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`Cache ${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = displayModel ? this.session.modelRegistry.isUsingOAuth(displayModel) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Per-prompt cost annotation (opt-in via show_token_cost preference, #1515)\n\t\tif (process.env.GSD_SHOW_TOKEN_COST === \"1\") {\n\t\t\tconst lastTurnCost = this.session.getLastTurnCost();\n\t\t\tif (lastTurnCost > 0) {\n\t\t\t\tstatsParts.push(`(last: ${formatPromptCost(lastTurnCost)})`);\n\t\t\t}\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tconst currentPermissionMode = getPermissionMode();\n\t\tlet permissionModeLabel: string;\n\t\tif (currentPermissionMode === \"danger-full-access\") {\n\t\t\tpermissionModeLabel = theme.fg(\"error\", \"⚡ full-access\");\n\t\t} else if (currentPermissionMode === \"accept-on-edit\") {\n\t\t\tpermissionModeLabel = theme.fg(\"success\", \"✓ accept-edit\");\n\t\t} else if (currentPermissionMode === \"auto\") {\n\t\t\tpermissionModeLabel = theme.fg(\"warning\", \"🤖 auto\");\n\t\t} else {\n\t\t\tpermissionModeLabel = theme.fg(\"violet\", \"📝 plan\");\n\t\t}\n\t\tstatsParts.push(permissionModeLabel);\n\n\t\tconst sandboxStatus = this.footerData.getSandboxStatus();\n\t\tif (sandboxStatus === \"active\") {\n\t\t\tstatsParts.push(theme.fg(\"success\", \"🔒 sandboxed\"));\n\t\t} else if (sandboxStatus === \"unavailable\") {\n\t\t\tstatsParts.push(theme.fg(\"warning\", \"⚠ no sandbox\"));\n\t\t}\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = displayModel?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (displayModel?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel === \"off\") {\n\t\t\t\trightSideWithoutProvider = `${modelName} • thinking off`;\n\t\t\t} else if (thinkingLevel === \"adaptive\") {\n\t\t\t\tconst resolved = state.lastAdaptiveDecision?.level;\n\t\t\t\trightSideWithoutProvider = resolved\n\t\t\t\t\t? `${modelName} • adaptive→${resolved}`\n\t\t\t\t\t: `${modelName} • adaptive`;\n\t\t\t} else {\n\t\t\t\trightSideWithoutProvider = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && displayModel) {\n\t\t\trightSide = `(${displayModel.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically.\n\t\t// cache-timer is surfaced on the first line instead of this extension-status line.\n\t\tconst nonTimerStatuses = Array.from(extensionStatuses.entries()).filter(([key]) => key !== \"cache-timer\" && key !== \"usage-tips\");\n\t\tif (nonTimerStatuses.length > 0) {\n\t\t\tconst sortedStatuses = nonTimerStatuses\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Match the rest of the footer styling: extension statuses should render\n\t\t\t// in the same dim color as pwd/stats, with a dim ellipsis on truncation.\n\t\t\tlines.push(truncateToWidth(theme.fg(\"dim\", statusLine), width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@gsd/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { getPermissionMode, type PermissionMode } from \"../../../core/tool-approval.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts (similar to web-ui)\n */\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Format a cost value for compact display.\n * Uses fewer decimal places for larger amounts.\n * @internal Exported for testing only.\n */\nexport function formatPromptCost(cost: number): string {\n\tif (cost < 0.001) return `$${cost.toFixed(4)}`;\n\tif (cost < 0.01) return `$${cost.toFixed(3)}`;\n\tif (cost < 1) return `$${cost.toFixed(3)}`;\n\treturn `$${cost.toFixed(2)}`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate permissionMode: PermissionMode = \"danger-full-access\";\n\tprivate notificationSoundEnabled = false;\n\tprivate verboseFooterEnabled = false;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\n\t}\n\n\tsetNotificationSoundEnabled(enabled: boolean): void {\n\t\tthis.notificationSoundEnabled = enabled;\n\t}\n\n\tsetVerboseFooterEnabled(enabled: boolean): void {\n\t\tthis.verboseFooterEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\tconst usageTotals = this.session.sessionManager.getUsageTotals();\n\t\tconst totalInput = usageTotals.input;\n\t\tconst totalOutput = usageTotals.output;\n\t\tconst totalCacheRead = usageTotals.cacheRead;\n\t\tconst totalCacheWrite = usageTotals.cacheWrite;\n\t\tconst totalCost = usageTotals.cost;\n\n\t\t// Use activeInferenceModel during streaming to show the model actually\n\t\t// being used, not the configured model which may have been switched mid-turn.\n\t\tconst displayModel = state.activeInferenceModel ?? state.model;\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextTokens = contextUsage?.tokens ?? null;\n\t\tconst contextWindow = contextUsage?.contextWindow ?? displayModel?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tconst cacheTimerStatusRaw = extensionStatuses.get(\"cache-timer\");\n\t\tconst cacheTimerStatus = cacheTimerStatusRaw ? sanitizeStatusText(cacheTimerStatusRaw) : \"\";\n\t\tconst hotkeysHints = [\"/hotkeys\"];\n\t\tconst firstLineMinPadding = 2;\n\t\tconst firstLineRightParts = cacheTimerStatus ? [cacheTimerStatus] : [];\n\t\tif (this.notificationSoundEnabled) {\n\t\t\tfirstLineRightParts.push(theme.fg(\"success\", \"🔔\"));\n\t\t}\n\t\tconst firstLineRightBase = firstLineRightParts.join(\" \");\n\t\tconst hotkeysHint = hotkeysHints.find((hint) => {\n\t\t\tconst candidate = firstLineRightBase ? `${firstLineRightBase} ${hint}` : hint;\n\t\t\treturn visibleWidth(pwd) + firstLineMinPadding + visibleWidth(candidate) <= width;\n\t\t}) ?? \"\";\n\t\tif (hotkeysHint) {\n\t\t\tfirstLineRightParts.push(theme.fg(\"dim\", hotkeysHint));\n\t\t}\n\t\tconst firstLineRight = firstLineRightParts.join(\" \");\n\n\t\tlet pwdLine: string;\n\t\tif (firstLineRight) {\n\t\t\tconst rightWidth = visibleWidth(firstLineRight);\n\t\t\tconst availableForPwd = Math.max(0, width - rightWidth - firstLineMinPadding);\n\t\t\tconst truncatedPwd = truncateToWidth(theme.fg(\"dim\", pwd), availableForPwd, theme.fg(\"dim\", \"...\"));\n\t\t\tconst truncatedPwdWidth = visibleWidth(truncatedPwd);\n\t\t\tconst padding = \" \".repeat(Math.max(firstLineMinPadding, width - truncatedPwdWidth - rightWidth));\n\t\t\tpwdLine = truncatedPwd + padding + firstLineRight;\n\t\t} else {\n\t\t\tpwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (this.verboseFooterEnabled) {\n\t\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\t\tif (totalCacheRead) statsParts.push(`Cache ${formatTokens(totalCacheRead)}`);\n\t\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\t\t}\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = displayModel ? this.session.modelRegistry.isUsingOAuth(displayModel) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Per-prompt cost annotation (opt-in via show_token_cost preference, #1515)\n\t\tif (this.verboseFooterEnabled && process.env.GSD_SHOW_TOKEN_COST === \"1\") {\n\t\t\tconst lastTurnCost = this.session.getLastTurnCost();\n\t\t\tif (lastTurnCost > 0) {\n\t\t\t\tstatsParts.push(`(last: ${formatPromptCost(lastTurnCost)})`);\n\t\t\t}\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tconst currentPermissionMode = getPermissionMode();\n\t\tlet permissionModeLabel: string;\n\t\tif (currentPermissionMode === \"danger-full-access\") {\n\t\t\tpermissionModeLabel = theme.fg(\"error\", \"⚡ full-access\");\n\t\t} else if (currentPermissionMode === \"accept-on-edit\") {\n\t\t\tpermissionModeLabel = theme.fg(\"success\", \"✓ accept-edit\");\n\t\t} else if (currentPermissionMode === \"auto\") {\n\t\t\tpermissionModeLabel = theme.fg(\"warning\", \"🤖 auto\");\n\t\t} else {\n\t\t\tpermissionModeLabel = theme.fg(\"violet\", \"📝 plan\");\n\t\t}\n\t\tstatsParts.push(permissionModeLabel);\n\n\t\tconst sandboxStatus = this.footerData.getSandboxStatus();\n\t\tif (sandboxStatus === \"active\") {\n\t\t\tstatsParts.push(theme.fg(\"success\", \"🔒 sandboxed\"));\n\t\t} else if (sandboxStatus === \"unavailable\") {\n\t\t\tstatsParts.push(theme.fg(\"warning\", \"⚠ no sandbox\"));\n\t\t}\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = displayModel?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (displayModel?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\tif (thinkingLevel === \"off\") {\n\t\t\t\trightSideWithoutProvider = `${modelName} • thinking off`;\n\t\t\t} else if (thinkingLevel === \"adaptive\") {\n\t\t\t\tconst resolved = state.lastAdaptiveDecision?.level;\n\t\t\t\trightSideWithoutProvider = resolved\n\t\t\t\t\t? `${modelName} • adaptive→${resolved}`\n\t\t\t\t\t: `${modelName} • adaptive`;\n\t\t\t} else {\n\t\t\t\trightSideWithoutProvider = `${modelName} • ${thinkingLevel}`;\n\t\t\t}\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && displayModel) {\n\t\t\trightSide = `(${displayModel.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically.\n\t\t// cache-timer is surfaced on the first line instead of this extension-status line.\n\t\tconst nonTimerStatuses = Array.from(extensionStatuses.entries()).filter(([key]) => key !== \"cache-timer\" && key !== \"usage-tips\");\n\t\tif (nonTimerStatuses.length > 0) {\n\t\t\tconst sortedStatuses = nonTimerStatuses\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Match the rest of the footer styling: extension statuses should render\n\t\t\t// in the same dim color as pwd/stats, with a dim ellipsis on truncation.\n\t\t\tlines.push(truncateToWidth(theme.fg(\"dim\", statusLine), width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}