threadwell 0.0.5 → 0.0.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 (93) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +4 -8
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -7
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/config-selector.d.ts.map +1 -1
  7. package/dist/cli/config-selector.js +4 -2
  8. package/dist/cli/config-selector.js.map +1 -1
  9. package/dist/core/settings-manager.d.ts +3 -0
  10. package/dist/core/settings-manager.d.ts.map +1 -1
  11. package/dist/core/settings-manager.js +5 -0
  12. package/dist/core/settings-manager.js.map +1 -1
  13. package/dist/core/slash-commands.d.ts.map +1 -1
  14. package/dist/core/slash-commands.js +1 -1
  15. package/dist/core/slash-commands.js.map +1 -1
  16. package/dist/core/tools/edit.d.ts.map +1 -1
  17. package/dist/core/tools/edit.js +7 -1
  18. package/dist/core/tools/edit.js.map +1 -1
  19. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  20. package/dist/modes/interactive/components/assistant-message.js +12 -4
  21. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  22. package/dist/modes/interactive/components/bash-execution.d.ts +2 -0
  23. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  24. package/dist/modes/interactive/components/bash-execution.js +43 -4
  25. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  26. package/dist/modes/interactive/components/branch-summary-message.d.ts +4 -3
  27. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  28. package/dist/modes/interactive/components/branch-summary-message.js +31 -8
  29. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  30. package/dist/modes/interactive/components/compaction-summary-message.d.ts +4 -3
  31. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  32. package/dist/modes/interactive/components/compaction-summary-message.js +32 -8
  33. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  34. package/dist/modes/interactive/components/config-selector.d.ts +1 -1
  35. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  36. package/dist/modes/interactive/components/config-selector.js +1 -9
  37. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  38. package/dist/modes/interactive/components/custom-message.d.ts +3 -0
  39. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  40. package/dist/modes/interactive/components/custom-message.js +33 -20
  41. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  42. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  43. package/dist/modes/interactive/components/diff.js +70 -0
  44. package/dist/modes/interactive/components/diff.js.map +1 -1
  45. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  46. package/dist/modes/interactive/components/footer.js +61 -49
  47. package/dist/modes/interactive/components/footer.js.map +1 -1
  48. package/dist/modes/interactive/components/framed-message.d.ts +17 -0
  49. package/dist/modes/interactive/components/framed-message.d.ts.map +1 -0
  50. package/dist/modes/interactive/components/framed-message.js +39 -0
  51. package/dist/modes/interactive/components/framed-message.js.map +1 -0
  52. package/dist/modes/interactive/components/settings-selector.d.ts +0 -4
  53. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  54. package/dist/modes/interactive/components/settings-selector.js +0 -20
  55. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  56. package/dist/modes/interactive/components/skill-invocation-message.d.ts +4 -3
  57. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  58. package/dist/modes/interactive/components/skill-invocation-message.js +31 -9
  59. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  60. package/dist/modes/interactive/components/split-info-card.d.ts.map +1 -1
  61. package/dist/modes/interactive/components/split-info-card.js +24 -17
  62. package/dist/modes/interactive/components/split-info-card.js.map +1 -1
  63. package/dist/modes/interactive/components/thinking-card.d.ts +6 -0
  64. package/dist/modes/interactive/components/thinking-card.d.ts.map +1 -0
  65. package/dist/modes/interactive/components/thinking-card.js +17 -0
  66. package/dist/modes/interactive/components/thinking-card.js.map +1 -0
  67. package/dist/modes/interactive/components/tool-execution.d.ts +5 -0
  68. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/tool-execution.js +72 -2
  70. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  71. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  72. package/dist/modes/interactive/components/user-message.js +16 -0
  73. package/dist/modes/interactive/components/user-message.js.map +1 -1
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.js +86 -73
  76. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  77. package/dist/modes/interactive/theme/visual-profile.d.ts +69 -0
  78. package/dist/modes/interactive/theme/visual-profile.d.ts.map +1 -0
  79. package/dist/modes/interactive/theme/visual-profile.js +131 -0
  80. package/dist/modes/interactive/theme/visual-profile.js.map +1 -0
  81. package/dist/package-manager-cli.d.ts.map +1 -1
  82. package/dist/package-manager-cli.js +1 -1
  83. package/dist/package-manager-cli.js.map +1 -1
  84. package/docs/docs.json +1 -5
  85. package/docs/extensions.md +7 -16
  86. package/docs/index.md +6 -5
  87. package/docs/packages.md +8 -30
  88. package/docs/quickstart.md +1 -1
  89. package/docs/sdk.md +2 -3
  90. package/docs/settings.md +24 -7
  91. package/docs/themes.md +5 -295
  92. package/docs/usage.md +14 -17
  93. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"custom-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAO,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,iBAAiB,CAAC;AAC7F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAC,CAAY;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAAS;IAE1B,YACC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAC/B,cAAc,CAAC,EAAE,eAAe,EAChC,aAAa,GAAE,aAAkC,EAajD;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;CAiDf","sourcesContent":["import type { TextContent } from \"@threadwell/ai\";\nimport type { Component } from \"@threadwell/tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@threadwell/tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.js\";\nimport type { CustomMessage } from \"../../../core/messages.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create box with purple background (used for default rendering)\n\t\tthis.box = new Box(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\t// Remove previous content component\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\t// Try custom renderer first - it handles its own styling\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Custom renderer provides its own styled component\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering\n\t\t\t}\n\t\t}\n\n\t\t// Default rendering uses our box\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\t// Default rendering: label + content\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[${this.message.customType}]\\x1b[22m`);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\t// Extract text content\n\t\tlet text: string;\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\ttext = this.message.content;\n\t\t} else {\n\t\t\ttext = this.message.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(text, 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAO,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,iBAAiB,CAAC;AAC7F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAK/D;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAC,CAAY;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAAS;IAE1B,YACC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAC/B,cAAc,CAAC,EAAE,eAAe,EAChC,aAAa,GAAE,aAAkC,EAWjD;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;IA2Bf,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,uBAAuB;IAa/B,OAAO,CAAC,uBAAuB;CAa/B","sourcesContent":["import type { TextContent } from \"@threadwell/ai\";\nimport type { Component } from \"@threadwell/tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@threadwell/tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.js\";\nimport type { CustomMessage } from \"../../../core/messages.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\nimport { getVisualTokens } from \"../theme/visual-profile.js\";\nimport { FramedMessage } from \"./framed-message.js\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering.\n\t\t\t}\n\t\t}\n\n\t\tif (getVisualTokens().enhanced) {\n\t\t\tthis.rebuildEnhancedFallback();\n\t\t\treturn;\n\t\t}\n\t\tthis.rebuildBaselineFallback();\n\t}\n\n\tprivate getTextContent(): string {\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\treturn this.message.content;\n\t\t}\n\t\treturn this.message.content\n\t\t\t.filter((content): content is TextContent => content.type === \"text\")\n\t\t\t.map((content) => content.text)\n\t\t\t.join(\"\\n\");\n\t}\n\n\tprivate rebuildEnhancedFallback(): void {\n\t\tconst visual = getVisualTokens();\n\t\tthis.customComponent = new FramedMessage({\n\t\t\ttitle: `${visual.glyphs.info} ${this.message.customType}`,\n\t\t\ttext: this.getTextContent(),\n\t\t\tmarkdownTheme: this.markdownTheme,\n\t\t\tborderColor: (content) => theme.fg(\"borderMuted\", content),\n\t\t\ttitleColor: (content) => theme.fg(\"customMessageLabel\", theme.bold(content)),\n\t\t\tcontentColor: (content) => theme.fg(\"customMessageText\", content),\n\t\t});\n\t\tthis.addChild(this.customComponent);\n\t}\n\n\tprivate rebuildBaselineFallback(): void {\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[${this.message.customType}]\\x1b[22m`);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(this.getTextContent(), 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
@@ -1,5 +1,7 @@
1
1
  import { Box, Container, Markdown, Spacer, Text } from "@threadwell/tui";
2
2
  import { getMarkdownTheme, theme } from "../theme/theme.js";
3
+ import { getVisualTokens } from "../theme/visual-profile.js";
4
+ import { FramedMessage } from "./framed-message.js";
3
5
  /**
4
6
  * Component that renders a custom message entry from extensions.
5
7
  * Uses distinct styling to differentiate from user messages.
@@ -17,8 +19,7 @@ export class CustomMessageComponent extends Container {
17
19
  this.customRenderer = customRenderer;
18
20
  this.markdownTheme = markdownTheme;
19
21
  this.addChild(new Spacer(1));
20
- // Create box with purple background (used for default rendering)
21
- this.box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
22
+ this.box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
22
23
  this.rebuild();
23
24
  }
24
25
  setExpanded(expanded) {
@@ -32,46 +33,58 @@ export class CustomMessageComponent extends Container {
32
33
  this.rebuild();
33
34
  }
34
35
  rebuild() {
35
- // Remove previous content component
36
36
  if (this.customComponent) {
37
37
  this.removeChild(this.customComponent);
38
38
  this.customComponent = undefined;
39
39
  }
40
40
  this.removeChild(this.box);
41
- // Try custom renderer first - it handles its own styling
42
41
  if (this.customRenderer) {
43
42
  try {
44
43
  const component = this.customRenderer(this.message, { expanded: this._expanded }, theme);
45
44
  if (component) {
46
- // Custom renderer provides its own styled component
47
45
  this.customComponent = component;
48
46
  this.addChild(component);
49
47
  return;
50
48
  }
51
49
  }
52
50
  catch {
53
- // Fall through to default rendering
51
+ // Fall through to default rendering.
54
52
  }
55
53
  }
56
- // Default rendering uses our box
54
+ if (getVisualTokens().enhanced) {
55
+ this.rebuildEnhancedFallback();
56
+ return;
57
+ }
58
+ this.rebuildBaselineFallback();
59
+ }
60
+ getTextContent() {
61
+ if (typeof this.message.content === "string") {
62
+ return this.message.content;
63
+ }
64
+ return this.message.content
65
+ .filter((content) => content.type === "text")
66
+ .map((content) => content.text)
67
+ .join("\n");
68
+ }
69
+ rebuildEnhancedFallback() {
70
+ const visual = getVisualTokens();
71
+ this.customComponent = new FramedMessage({
72
+ title: `${visual.glyphs.info} ${this.message.customType}`,
73
+ text: this.getTextContent(),
74
+ markdownTheme: this.markdownTheme,
75
+ borderColor: (content) => theme.fg("borderMuted", content),
76
+ titleColor: (content) => theme.fg("customMessageLabel", theme.bold(content)),
77
+ contentColor: (content) => theme.fg("customMessageText", content),
78
+ });
79
+ this.addChild(this.customComponent);
80
+ }
81
+ rebuildBaselineFallback() {
57
82
  this.addChild(this.box);
58
83
  this.box.clear();
59
- // Default rendering: label + content
60
84
  const label = theme.fg("customMessageLabel", `\x1b[1m[${this.message.customType}]\x1b[22m`);
61
85
  this.box.addChild(new Text(label, 0, 0));
62
86
  this.box.addChild(new Spacer(1));
63
- // Extract text content
64
- let text;
65
- if (typeof this.message.content === "string") {
66
- text = this.message.content;
67
- }
68
- else {
69
- text = this.message.content
70
- .filter((c) => c.type === "text")
71
- .map((c) => c.text)
72
- .join("\n");
73
- }
74
- this.box.addChild(new Markdown(text, 0, 0, this.markdownTheme, {
87
+ this.box.addChild(new Markdown(this.getTextContent(), 0, 0, this.markdownTheme, {
75
88
  color: (text) => theme.fg("customMessageText", text),
76
89
  }));
77
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"custom-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TextContent } from \"@threadwell/ai\";\nimport type { Component } from \"@threadwell/tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@threadwell/tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.js\";\nimport type { CustomMessage } from \"../../../core/messages.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create box with purple background (used for default rendering)\n\t\tthis.box = new Box(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\t// Remove previous content component\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\t// Try custom renderer first - it handles its own styling\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Custom renderer provides its own styled component\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering\n\t\t\t}\n\t\t}\n\n\t\t// Default rendering uses our box\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\t// Default rendering: label + content\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[${this.message.customType}]\\x1b[22m`);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\t// Extract text content\n\t\tlet text: string;\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\ttext = this.message.content;\n\t\t} else {\n\t\t\ttext = this.message.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(text, 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAsB,MAAM,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAG7F,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;GAGG;AACH,MAAM,OAAO,sBAAuB,SAAQ,SAAS;IAC5C,OAAO,CAAyB;IAChC,cAAc,CAAmB;IACjC,GAAG,CAAM;IACT,eAAe,CAAa;IAC5B,aAAa,CAAgB;IAC7B,SAAS,GAAG,KAAK,CAAC;IAE1B,YACC,OAA+B,EAC/B,cAAgC,EAChC,aAAa,GAAkB,gBAAgB,EAAE,EAChD;QACD,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QAEnC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC,CAAC;QAEtE,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAED,WAAW,CAAC,QAAiB,EAAQ;QACpC,IAAI,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;YAC1B,IAAI,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC;IAAA,CACD;IAEQ,UAAU,GAAS;QAC3B,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAEO,OAAO,GAAS;QACvB,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC1B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YACvC,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QAClC,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAE3B,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,IAAI,CAAC;gBACJ,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,KAAK,CAAC,CAAC;gBACzF,IAAI,SAAS,EAAE,CAAC;oBACf,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;oBACjC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;oBACzB,OAAO;gBACR,CAAC;YACF,CAAC;YAAC,MAAM,CAAC;gBACR,qCAAqC;YACtC,CAAC;QACF,CAAC;QAED,IAAI,eAAe,EAAE,CAAC,QAAQ,EAAE,CAAC;YAChC,IAAI,CAAC,uBAAuB,EAAE,CAAC;YAC/B,OAAO;QACR,CAAC;QACD,IAAI,CAAC,uBAAuB,EAAE,CAAC;IAAA,CAC/B;IAEO,cAAc,GAAW;QAChC,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC9C,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;QAC7B,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO;aACzB,MAAM,CAAC,CAAC,OAAO,EAA0B,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC;aACpE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;aAC9B,IAAI,CAAC,IAAI,CAAC,CAAC;IAAA,CACb;IAEO,uBAAuB,GAAS;QACvC,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,IAAI,aAAa,CAAC;YACxC,KAAK,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;YACzD,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE;YAC3B,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,WAAW,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,CAAC;YAC1D,UAAU,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,oBAAoB,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC5E,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,OAAO,CAAC;SACjE,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAAA,CACpC;IAEO,uBAAuB,GAAS;QACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QAEjB,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,oBAAoB,EAAE,WAAW,IAAI,CAAC,OAAO,CAAC,UAAU,WAAW,CAAC,CAAC;QAC5F,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAChB,IAAI,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE;YAC7D,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,EAAE,IAAI,CAAC;SAC5D,CAAC,CACF,CAAC;IAAA,CACF;CACD","sourcesContent":["import type { TextContent } from \"@threadwell/ai\";\nimport type { Component } from \"@threadwell/tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@threadwell/tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.js\";\nimport type { CustomMessage } from \"../../../core/messages.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\nimport { getVisualTokens } from \"../theme/visual-profile.js\";\nimport { FramedMessage } from \"./framed-message.js\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering.\n\t\t\t}\n\t\t}\n\n\t\tif (getVisualTokens().enhanced) {\n\t\t\tthis.rebuildEnhancedFallback();\n\t\t\treturn;\n\t\t}\n\t\tthis.rebuildBaselineFallback();\n\t}\n\n\tprivate getTextContent(): string {\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\treturn this.message.content;\n\t\t}\n\t\treturn this.message.content\n\t\t\t.filter((content): content is TextContent => content.type === \"text\")\n\t\t\t.map((content) => content.text)\n\t\t\t.join(\"\\n\");\n\t}\n\n\tprivate rebuildEnhancedFallback(): void {\n\t\tconst visual = getVisualTokens();\n\t\tthis.customComponent = new FramedMessage({\n\t\t\ttitle: `${visual.glyphs.info} ${this.message.customType}`,\n\t\t\ttext: this.getTextContent(),\n\t\t\tmarkdownTheme: this.markdownTheme,\n\t\t\tborderColor: (content) => theme.fg(\"borderMuted\", content),\n\t\t\ttitleColor: (content) => theme.fg(\"customMessageLabel\", theme.bold(content)),\n\t\t\tcontentColor: (content) => theme.fg(\"customMessageText\", content),\n\t\t});\n\t\tthis.addChild(this.customComponent);\n\t}\n\n\tprivate rebuildBaselineFallback(): void {\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[${this.message.customType}]\\x1b[22m`);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(this.getTextContent(), 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
@@ -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","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.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/diff.ts"],"names":[],"mappings":"AAoEA,MAAM,WAAW,iBAAiB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAkFD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAE,iBAAsB,GAAG,MAAM,CAwErF","sourcesContent":["import * as Diff from \"diff\";\nimport { theme } from \"../theme/theme.js\";\nimport { getVisualTokens } from \"../theme/visual-profile.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\nfunction renderEnhancedDiffLine(prefix: string, lineNum: string, content: string): string {\n\tconst visual = getVisualTokens();\n\tconst line = replaceTabs(content);\n\tif (prefix === \"+\") {\n\t\treturn theme.fg(\"toolDiffAdded\", `${visual.glyphs.plus} ${lineNum.trim().padStart(4)} │ ${line}`);\n\t}\n\tif (prefix === \"-\") {\n\t\treturn theme.fg(\"toolDiffRemoved\", `${visual.glyphs.minus} ${lineNum.trim().padStart(4)} │ ${line}`);\n\t}\n\treturn theme.fg(\"toolDiffContext\", ` ${lineNum.trim().padStart(4)} │ ${line}`);\n}\n\nfunction renderEnhancedDiff(diffText: string): string {\n\tconst lines = diffText.split(\"\\n\");\n\tconst result: string[] = [];\n\tconst visual = getVisualTokens();\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\tif (line.trim()) {\n\t\t\t\tresult.push(theme.fg(\"muted\", `${visual.glyphs.diff} ${line}`));\n\t\t\t}\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\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\t\t\t\tresult.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"toolDiffRemoved\",\n\t\t\t\t\t\t`${visual.glyphs.minus} ${removed.lineNum.trim().padStart(4)} │ ${removedLine}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tresult.push(\n\t\t\t\t\ttheme.fg(\"toolDiffAdded\", `${visual.glyphs.plus} ${added.lineNum.trim().padStart(4)} │ ${addedLine}`),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tfor (const removed of removedLines) {\n\t\t\t\t\tresult.push(renderEnhancedDiffLine(\"-\", removed.lineNum, removed.content));\n\t\t\t\t}\n\t\t\t\tfor (const added of addedLines) {\n\t\t\t\t\tresult.push(renderEnhancedDiffLine(\"+\", added.lineNum, added.content));\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tresult.push(renderEnhancedDiffLine(parsed.prefix, parsed.lineNum, parsed.content));\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn result.join(\"\\n\");\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\tif (getVisualTokens().enhanced) {\n\t\treturn renderEnhancedDiff(diffText);\n\t}\n\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,5 +1,6 @@
1
1
  import * as Diff from "diff";
2
2
  import { theme } from "../theme/theme.js";
3
+ import { getVisualTokens } from "../theme/visual-profile.js";
3
4
  /**
4
5
  * Parse diff line to extract prefix, line number, and content.
5
6
  * Format: "+123 content" or "-123 content" or " 123 content" or " ..."
@@ -61,6 +62,72 @@ function renderIntraLineDiff(oldContent, newContent) {
61
62
  }
62
63
  return { removedLine, addedLine };
63
64
  }
65
+ function renderEnhancedDiffLine(prefix, lineNum, content) {
66
+ const visual = getVisualTokens();
67
+ const line = replaceTabs(content);
68
+ if (prefix === "+") {
69
+ return theme.fg("toolDiffAdded", `${visual.glyphs.plus} ${lineNum.trim().padStart(4)} │ ${line}`);
70
+ }
71
+ if (prefix === "-") {
72
+ return theme.fg("toolDiffRemoved", `${visual.glyphs.minus} ${lineNum.trim().padStart(4)} │ ${line}`);
73
+ }
74
+ return theme.fg("toolDiffContext", ` ${lineNum.trim().padStart(4)} │ ${line}`);
75
+ }
76
+ function renderEnhancedDiff(diffText) {
77
+ const lines = diffText.split("\n");
78
+ const result = [];
79
+ const visual = getVisualTokens();
80
+ let i = 0;
81
+ while (i < lines.length) {
82
+ const line = lines[i];
83
+ const parsed = parseDiffLine(line);
84
+ if (!parsed) {
85
+ if (line.trim()) {
86
+ result.push(theme.fg("muted", `${visual.glyphs.diff} ${line}`));
87
+ }
88
+ i++;
89
+ continue;
90
+ }
91
+ if (parsed.prefix === "-") {
92
+ const removedLines = [];
93
+ while (i < lines.length) {
94
+ const p = parseDiffLine(lines[i]);
95
+ if (!p || p.prefix !== "-")
96
+ break;
97
+ removedLines.push({ lineNum: p.lineNum, content: p.content });
98
+ i++;
99
+ }
100
+ const addedLines = [];
101
+ while (i < lines.length) {
102
+ const p = parseDiffLine(lines[i]);
103
+ if (!p || p.prefix !== "+")
104
+ break;
105
+ addedLines.push({ lineNum: p.lineNum, content: p.content });
106
+ i++;
107
+ }
108
+ if (removedLines.length === 1 && addedLines.length === 1) {
109
+ const removed = removedLines[0];
110
+ const added = addedLines[0];
111
+ const { removedLine, addedLine } = renderIntraLineDiff(replaceTabs(removed.content), replaceTabs(added.content));
112
+ result.push(theme.fg("toolDiffRemoved", `${visual.glyphs.minus} ${removed.lineNum.trim().padStart(4)} │ ${removedLine}`));
113
+ result.push(theme.fg("toolDiffAdded", `${visual.glyphs.plus} ${added.lineNum.trim().padStart(4)} │ ${addedLine}`));
114
+ }
115
+ else {
116
+ for (const removed of removedLines) {
117
+ result.push(renderEnhancedDiffLine("-", removed.lineNum, removed.content));
118
+ }
119
+ for (const added of addedLines) {
120
+ result.push(renderEnhancedDiffLine("+", added.lineNum, added.content));
121
+ }
122
+ }
123
+ }
124
+ else {
125
+ result.push(renderEnhancedDiffLine(parsed.prefix, parsed.lineNum, parsed.content));
126
+ i++;
127
+ }
128
+ }
129
+ return result.join("\n");
130
+ }
64
131
  /**
65
132
  * Render a diff string with colored lines and intra-line change highlighting.
66
133
  * - Context lines: dim/gray
@@ -68,6 +135,9 @@ function renderIntraLineDiff(oldContent, newContent) {
68
135
  * - Added lines: green, with inverse on changed tokens
69
136
  */
70
137
  export function renderDiff(diffText, _options = {}) {
138
+ if (getVisualTokens().enhanced) {
139
+ return renderEnhancedDiff(diffText);
140
+ }
71
141
  const lines = diffText.split("\n");
72
142
  const result = [];
73
143
  let i = 0;
@@ -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 * as Diff from \"diff\";\nimport { theme } from \"../theme/theme.js\";\nimport { getVisualTokens } from \"../theme/visual-profile.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\nfunction renderEnhancedDiffLine(prefix: string, lineNum: string, content: string): string {\n\tconst visual = getVisualTokens();\n\tconst line = replaceTabs(content);\n\tif (prefix === \"+\") {\n\t\treturn theme.fg(\"toolDiffAdded\", `${visual.glyphs.plus} ${lineNum.trim().padStart(4)} │ ${line}`);\n\t}\n\tif (prefix === \"-\") {\n\t\treturn theme.fg(\"toolDiffRemoved\", `${visual.glyphs.minus} ${lineNum.trim().padStart(4)} │ ${line}`);\n\t}\n\treturn theme.fg(\"toolDiffContext\", ` ${lineNum.trim().padStart(4)} │ ${line}`);\n}\n\nfunction renderEnhancedDiff(diffText: string): string {\n\tconst lines = diffText.split(\"\\n\");\n\tconst result: string[] = [];\n\tconst visual = getVisualTokens();\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\tif (line.trim()) {\n\t\t\t\tresult.push(theme.fg(\"muted\", `${visual.glyphs.diff} ${line}`));\n\t\t\t}\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\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\t\t\t\tresult.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"toolDiffRemoved\",\n\t\t\t\t\t\t`${visual.glyphs.minus} ${removed.lineNum.trim().padStart(4)} │ ${removedLine}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tresult.push(\n\t\t\t\t\ttheme.fg(\"toolDiffAdded\", `${visual.glyphs.plus} ${added.lineNum.trim().padStart(4)} │ ${addedLine}`),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tfor (const removed of removedLines) {\n\t\t\t\t\tresult.push(renderEnhancedDiffLine(\"-\", removed.lineNum, removed.content));\n\t\t\t\t}\n\t\t\t\tfor (const added of addedLines) {\n\t\t\t\t\tresult.push(renderEnhancedDiffLine(\"+\", added.lineNum, added.content));\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tresult.push(renderEnhancedDiffLine(parsed.prefix, parsed.lineNum, parsed.content));\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn result.join(\"\\n\");\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\tif (getVisualTokens().enhanced) {\n\t\treturn renderEnhancedDiff(diffText);\n\t}\n\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 +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,iBAAiB,CAAC;AAChF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAa,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAkGnG;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAI/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAJnB,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YACS,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC3C;IAEJ,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA2J9B;CACD","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@threadwell/tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { GitStatus, ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { DISTRO_IDENTITY } from \"../../../core/identity.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 compact UI displays)\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\nfunction getContextUsageColor(percent: number): (text: string) => string {\n\tif (percent > 90) return (text) => theme.fg(\"error\", text);\n\tif (percent > 70) return (text) => theme.fg(\"warning\", text);\n\treturn (text) => theme.fg(\"dim\", text);\n}\n\nfunction formatGitBadge(status: GitStatus | null): string | null {\n\tif (!status) return null;\n\n\tconst parts = [`git ${status.branch}`];\n\tif (status.conflicts > 0) {\n\t\tparts.push(theme.fg(\"error\", `conflicts ${status.conflicts}`));\n\t\treturn parts.join(\" \");\n\t}\n\tif (status.clean) {\n\t\tparts.push(theme.fg(\"accent\", \"clean\"));\n\t\treturn parts.join(\" \");\n\t}\n\n\tif (status.staged > 0) parts.push(theme.fg(\"accent\", `+${status.staged}`));\n\tif (status.unstaged > 0) parts.push(theme.fg(\"error\", `-${status.unstaged}`));\n\tif (status.untracked > 0) parts.push(theme.fg(\"warning\", `?${status.untracked}`));\n\tif (status.ahead > 0) parts.push(theme.fg(\"muted\", `↑${status.ahead}`));\n\tif (status.behind > 0) parts.push(theme.fg(\"muted\", `↓${status.behind}`));\n\treturn parts.join(\" \");\n}\n\nfunction renderContextBar(width: number, percent: number | null, autoCompactEnabled: boolean): string {\n\tif (width < 2) return truncateToWidth(\"██\", width, \"\");\n\n\tconst innerWidth = Math.max(0, width - 2);\n\tconst clampedPercent = percent === null ? 0 : Math.max(0, Math.min(100, percent));\n\tconst exactFilledWidth = (clampedPercent / 100) * innerWidth;\n\tconst filledWidth = Math.min(innerWidth, Math.floor(exactFilledWidth));\n\tconst partial = exactFilledWidth - filledWidth;\n\tconst partialChar = partial <= 0 ? \"\" : partial < 1 / 3 ? \"░\" : partial < 2 / 3 ? \"▒\" : \"▓\";\n\tconst fillColor = getContextUsageColor(clampedPercent);\n\tconst label = `${percent === null ? \"?\" : `${clampedPercent.toFixed(1)}%`} · Autocompact ${autoCompactEnabled ? \"On\" : \"Off\"}`;\n\tconst labelStart = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));\n\tconst labelChars = [...label];\n\tlet labelIndex = 0;\n\tlet result = \"\";\n\n\tfor (let column = 0; column < width; column++) {\n\t\tconst labelEnd = labelStart + labelChars.length;\n\t\tif (column >= labelStart && column < labelEnd) {\n\t\t\tconst labelChar = labelChars[labelIndex++] ?? \"\";\n\t\t\tconst innerColumn = column - 1;\n\t\t\tconst isOverFill = column > 0 && column < width - 1 && innerColumn < exactFilledWidth;\n\t\t\tresult += isOverFill ? theme.inverse(fillColor(labelChar)) : theme.fg(\"dim\", labelChar);\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (column === 0 || column === width - 1) {\n\t\t\tresult += theme.fg(\"dim\", \"█\");\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst innerColumn = column - 1;\n\t\tif (innerColumn < filledWidth) {\n\t\t\tresult += fillColor(\"█\");\n\t\t} else if (innerColumn === filledWidth && partialChar) {\n\t\t\tresult += fillColor(partialChar);\n\t\t} else {\n\t\t\tresult += \" \";\n\t\t}\n\t}\n\n\treturn result;\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\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = 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\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\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 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 = this.session.sessionManager.getCwd();\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 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\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(`R${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 = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : 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\tconst continuityStatus = this.session.settingsManager.getContinuityEnabled()\n\t\t\t? \"continuity active\"\n\t\t\t: \"continuity off\";\n\t\tlet statsLeft = [theme.fg(\"accent\", DISTRO_IDENTITY.name), theme.fg(\"dim\", continuityStatus), ...statsParts].join(\n\t\t\t\" \",\n\t\t);\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.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 (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\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 && state.model) {\n\t\t\trightSide = `(${state.model!.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 gitBadge = formatGitBadge(this.footerData.getGitStatus());\n\t\tlet pwdLine: string;\n\t\tif (gitBadge) {\n\t\t\tconst left = theme.fg(\"dim\", pwd);\n\t\t\tconst leftWidth = visibleWidth(left);\n\t\t\tconst badgeWidth = visibleWidth(gitBadge);\n\t\t\tif (leftWidth + 2 + badgeWidth <= width) {\n\t\t\t\tpwdLine = left + \" \".repeat(width - leftWidth - badgeWidth) + gitBadge;\n\t\t\t} else {\n\t\t\t\tconst truncatedLeft = truncateToWidth(left, Math.max(0, width - badgeWidth - 2), theme.fg(\"dim\", \"...\"));\n\t\t\t\tpwdLine = `${truncatedLeft} ${gitBadge}`;\n\t\t\t}\n\t\t} else {\n\t\t\tpwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\t}\n\t\tconst contextBarLine = renderContextBar(\n\t\t\twidth,\n\t\t\tcontextPercent === \"?\" ? null : contextPercentValue,\n\t\t\tthis.autoCompactEnabled,\n\t\t);\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder, contextBarLine];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\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// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,iBAAiB,CAAC;AAChF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAkGxF;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAI/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAJnB,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YACS,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC3C;IAEJ,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiL9B;CACD","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@threadwell/tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { DISTRO_IDENTITY } from \"../../../core/identity.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { getVisualTokens } from \"../theme/visual-profile.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 compact UI displays)\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\nfunction getContextUsageColor(percent: number): (text: string) => string {\n\tif (percent > 90) return (text) => theme.fg(\"error\", text);\n\tif (percent > 70) return (text) => theme.fg(\"warning\", text);\n\treturn (text) => theme.fg(\"dim\", text);\n}\n\nfunction renderContextBar(width: number, percent: number | null, autoCompactEnabled: boolean): string {\n\tif (width < 2) return truncateToWidth(\"██\", width, \"\");\n\n\tconst visual = getVisualTokens();\n\tconst clampedPercent = percent === null ? 0 : Math.max(0, Math.min(100, percent));\n\tconst fillColor = getContextUsageColor(clampedPercent);\n\tconst separator = visual.enhanced ? visual.glyphs.separator : \"·\";\n\tconst label = ` context ${percent === null ? \"?\" : `${clampedPercent.toFixed(1)}%`} ${separator} auto ${autoCompactEnabled ? \"on\" : \"off\"} `;\n\n\tif (visual.enhanced) {\n\t\tconst labelWidth = visibleWidth(label);\n\t\tif (width <= labelWidth) return truncateToWidth(theme.fg(\"muted\", label.trim()), width, \"\");\n\n\t\tconst lineWidth = width - labelWidth;\n\t\tconst activeWidth = Math.floor((lineWidth * clampedPercent) / 100);\n\t\tconst leftWidth = Math.floor(lineWidth / 2);\n\t\tconst rightWidth = lineWidth - leftWidth;\n\t\tconst activeLeftWidth = Math.min(leftWidth, activeWidth);\n\t\tconst activeRightWidth = Math.max(0, activeWidth - leftWidth);\n\t\tconst left = fillColor(\"━\".repeat(activeLeftWidth)) + theme.fg(\"dim\", \"─\".repeat(leftWidth - activeLeftWidth));\n\t\tconst right =\n\t\t\tfillColor(\"━\".repeat(Math.min(rightWidth, activeRightWidth))) +\n\t\t\ttheme.fg(\"dim\", \"─\".repeat(Math.max(0, rightWidth - activeRightWidth)));\n\t\treturn `${left}${theme.fg(\"muted\", label)}${right}`;\n\t}\n\n\tconst innerWidth = Math.max(0, width - 2);\n\tconst exactFilledWidth = (clampedPercent / 100) * innerWidth;\n\tconst filledWidth = Math.min(innerWidth, Math.floor(exactFilledWidth));\n\tconst partial = exactFilledWidth - filledWidth;\n\tconst partialChar = partial <= 0 ? \"\" : partial < 1 / 3 ? \"░\" : partial < 2 / 3 ? \"▒\" : \"▓\";\n\tconst labelStart = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));\n\tconst labelChars = [...label];\n\tlet labelIndex = 0;\n\tlet result = \"\";\n\n\tfor (let column = 0; column < width; column++) {\n\t\tconst labelEnd = labelStart + labelChars.length;\n\t\tif (column >= labelStart && column < labelEnd) {\n\t\t\tconst labelChar = labelChars[labelIndex++] ?? \"\";\n\t\t\tconst innerColumn = column - 1;\n\t\t\tconst isOverFill = column > 0 && column < width - 1 && innerColumn < exactFilledWidth;\n\t\t\tresult += isOverFill ? theme.inverse(fillColor(labelChar)) : theme.fg(\"dim\", labelChar);\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (column === 0 || column === width - 1) {\n\t\t\tresult += theme.fg(\"dim\", \"█\");\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst innerColumn = column - 1;\n\t\tif (innerColumn < filledWidth) {\n\t\t\tresult += fillColor(\"█\");\n\t\t} else if (innerColumn === filledWidth && partialChar) {\n\t\t\tresult += fillColor(partialChar);\n\t\t} else {\n\t\t\tresult += \" \";\n\t\t}\n\t}\n\n\treturn result;\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\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = 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\t\tconst visual = getVisualTokens();\n\t\tconst glyphs = visual.glyphs;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\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 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 = this.session.sessionManager.getCwd();\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 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\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(`R${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 = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : 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\tconst continuityEnabled = this.session.settingsManager.getContinuityEnabled();\n\t\tconst continuityStatus = continuityEnabled\n\t\t\t? visual.enhanced\n\t\t\t\t? `${glyphs.continuity} continuity active`\n\t\t\t\t: \"continuity active\"\n\t\t\t: \"continuity off\";\n\t\tconst memorySettings = this.session.settingsManager.getMemorySettings();\n\t\tconst memoryStatus = memorySettings.enabled\n\t\t\t? visual.enhanced\n\t\t\t\t? `${glyphs.memory} ${memorySettings.inject ? \"memory injecting\" : \"memory on\"}`\n\t\t\t\t: memorySettings.inject\n\t\t\t\t\t? \"memory injecting\"\n\t\t\t\t\t: \"memory on\"\n\t\t\t: undefined;\n\t\tconst statsSeparator = visual.enhanced ? theme.fg(\"muted\", ` ${glyphs.separator} `) : \" \";\n\t\tlet statsLeft = visual.enhanced\n\t\t\t? [\n\t\t\t\t\ttheme.fg(\"accent\", DISTRO_IDENTITY.name),\n\t\t\t\t\ttheme.fg(continuityEnabled ? \"accent\" : \"dim\", continuityStatus),\n\t\t\t\t\t...(memoryStatus ? [theme.fg(memorySettings.inject ? \"accent\" : \"muted\", memoryStatus)] : []),\n\t\t\t\t\t...statsParts.map((part) => theme.fg(\"dim\", part)),\n\t\t\t\t].join(statsSeparator)\n\t\t\t: [\n\t\t\t\t\ttheme.fg(\"accent\", DISTRO_IDENTITY.name),\n\t\t\t\t\ttheme.fg(\"dim\", continuityStatus),\n\t\t\t\t\t...(memoryStatus ? [theme.fg(\"dim\", memoryStatus)] : []),\n\t\t\t\t\t...statsParts,\n\t\t\t\t].join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.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 (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\t\tif (visual.enhanced) {\n\t\t\tconst thinkingLevel = state.model?.reasoning ? state.thinkingLevel || \"off\" : undefined;\n\t\t\trightSideWithoutProvider = [\n\t\t\t\t`${theme.fg(\"dim\", glyphs.model)} ${theme.fg(\"muted\", modelName)}`,\n\t\t\t\t...(thinkingLevel ? [theme.fg(thinkingLevel === \"off\" ? \"dim\" : \"accent\", thinkingLevel)] : []),\n\t\t\t].join(statsSeparator);\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 && state.model) {\n\t\t\trightSide = `(${state.model!.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 = visual.enhanced ? statsLeft : theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = visual.enhanced ? remainder : theme.fg(\"dim\", remainder);\n\n\t\tconst folder = glyphs.folder;\n\t\tconst pwdLine = truncateToWidth(\n\t\t\t`${theme.fg(\"dim\", folder)} ${theme.fg(\"dim\", pwd)}`,\n\t\t\twidth,\n\t\t\ttheme.fg(\"dim\", \"...\"),\n\t\t);\n\t\tconst contextBarLine = renderContextBar(\n\t\t\twidth,\n\t\t\tcontextPercent === \"?\" ? null : contextPercentValue,\n\t\t\tthis.autoCompactEnabled,\n\t\t);\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder, contextBarLine];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\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// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { truncateToWidth, visibleWidth } from "@threadwell/tui";
2
2
  import { DISTRO_IDENTITY } from "../../../core/identity.js";
3
3
  import { theme } from "../theme/theme.js";
4
+ import { getVisualTokens } from "../theme/visual-profile.js";
4
5
  /**
5
6
  * Sanitize text for display in a single-line status.
6
7
  * Removes newlines, tabs, carriage returns, and other control characters.
@@ -33,41 +34,34 @@ function getContextUsageColor(percent) {
33
34
  return (text) => theme.fg("warning", text);
34
35
  return (text) => theme.fg("dim", text);
35
36
  }
36
- function formatGitBadge(status) {
37
- if (!status)
38
- return null;
39
- const parts = [`git ${status.branch}`];
40
- if (status.conflicts > 0) {
41
- parts.push(theme.fg("error", `conflicts ${status.conflicts}`));
42
- return parts.join(" ");
43
- }
44
- if (status.clean) {
45
- parts.push(theme.fg("accent", "clean"));
46
- return parts.join(" ");
47
- }
48
- if (status.staged > 0)
49
- parts.push(theme.fg("accent", `+${status.staged}`));
50
- if (status.unstaged > 0)
51
- parts.push(theme.fg("error", `-${status.unstaged}`));
52
- if (status.untracked > 0)
53
- parts.push(theme.fg("warning", `?${status.untracked}`));
54
- if (status.ahead > 0)
55
- parts.push(theme.fg("muted", `↑${status.ahead}`));
56
- if (status.behind > 0)
57
- parts.push(theme.fg("muted", `↓${status.behind}`));
58
- return parts.join(" ");
59
- }
60
37
  function renderContextBar(width, percent, autoCompactEnabled) {
61
38
  if (width < 2)
62
39
  return truncateToWidth("██", width, "");
63
- const innerWidth = Math.max(0, width - 2);
40
+ const visual = getVisualTokens();
64
41
  const clampedPercent = percent === null ? 0 : Math.max(0, Math.min(100, percent));
42
+ const fillColor = getContextUsageColor(clampedPercent);
43
+ const separator = visual.enhanced ? visual.glyphs.separator : "·";
44
+ const label = ` context ${percent === null ? "?" : `${clampedPercent.toFixed(1)}%`} ${separator} auto ${autoCompactEnabled ? "on" : "off"} `;
45
+ if (visual.enhanced) {
46
+ const labelWidth = visibleWidth(label);
47
+ if (width <= labelWidth)
48
+ return truncateToWidth(theme.fg("muted", label.trim()), width, "");
49
+ const lineWidth = width - labelWidth;
50
+ const activeWidth = Math.floor((lineWidth * clampedPercent) / 100);
51
+ const leftWidth = Math.floor(lineWidth / 2);
52
+ const rightWidth = lineWidth - leftWidth;
53
+ const activeLeftWidth = Math.min(leftWidth, activeWidth);
54
+ const activeRightWidth = Math.max(0, activeWidth - leftWidth);
55
+ const left = fillColor("━".repeat(activeLeftWidth)) + theme.fg("dim", "─".repeat(leftWidth - activeLeftWidth));
56
+ const right = fillColor("━".repeat(Math.min(rightWidth, activeRightWidth))) +
57
+ theme.fg("dim", "─".repeat(Math.max(0, rightWidth - activeRightWidth)));
58
+ return `${left}${theme.fg("muted", label)}${right}`;
59
+ }
60
+ const innerWidth = Math.max(0, width - 2);
65
61
  const exactFilledWidth = (clampedPercent / 100) * innerWidth;
66
62
  const filledWidth = Math.min(innerWidth, Math.floor(exactFilledWidth));
67
63
  const partial = exactFilledWidth - filledWidth;
68
64
  const partialChar = partial <= 0 ? "" : partial < 1 / 3 ? "░" : partial < 2 / 3 ? "▒" : "▓";
69
- const fillColor = getContextUsageColor(clampedPercent);
70
- const label = `${percent === null ? "?" : `${clampedPercent.toFixed(1)}%`} · Autocompact ${autoCompactEnabled ? "On" : "Off"}`;
71
65
  const labelStart = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
72
66
  const labelChars = [...label];
73
67
  let labelIndex = 0;
@@ -132,6 +126,8 @@ export class FooterComponent {
132
126
  }
133
127
  render(width) {
134
128
  const state = this.session.state;
129
+ const visual = getVisualTokens();
130
+ const glyphs = visual.glyphs;
135
131
  // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
136
132
  let totalInput = 0;
137
133
  let totalOutput = 0;
@@ -179,10 +175,34 @@ export class FooterComponent {
179
175
  const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
180
176
  statsParts.push(costStr);
181
177
  }
182
- const continuityStatus = this.session.settingsManager.getContinuityEnabled()
183
- ? "continuity active"
178
+ const continuityEnabled = this.session.settingsManager.getContinuityEnabled();
179
+ const continuityStatus = continuityEnabled
180
+ ? visual.enhanced
181
+ ? `${glyphs.continuity} continuity active`
182
+ : "continuity active"
184
183
  : "continuity off";
185
- let statsLeft = [theme.fg("accent", DISTRO_IDENTITY.name), theme.fg("dim", continuityStatus), ...statsParts].join(" ");
184
+ const memorySettings = this.session.settingsManager.getMemorySettings();
185
+ const memoryStatus = memorySettings.enabled
186
+ ? visual.enhanced
187
+ ? `${glyphs.memory} ${memorySettings.inject ? "memory injecting" : "memory on"}`
188
+ : memorySettings.inject
189
+ ? "memory injecting"
190
+ : "memory on"
191
+ : undefined;
192
+ const statsSeparator = visual.enhanced ? theme.fg("muted", ` ${glyphs.separator} `) : " ";
193
+ let statsLeft = visual.enhanced
194
+ ? [
195
+ theme.fg("accent", DISTRO_IDENTITY.name),
196
+ theme.fg(continuityEnabled ? "accent" : "dim", continuityStatus),
197
+ ...(memoryStatus ? [theme.fg(memorySettings.inject ? "accent" : "muted", memoryStatus)] : []),
198
+ ...statsParts.map((part) => theme.fg("dim", part)),
199
+ ].join(statsSeparator)
200
+ : [
201
+ theme.fg("accent", DISTRO_IDENTITY.name),
202
+ theme.fg("dim", continuityStatus),
203
+ ...(memoryStatus ? [theme.fg("dim", memoryStatus)] : []),
204
+ ...statsParts,
205
+ ].join(" ");
186
206
  // Add model name on the right side, plus thinking level if model supports it
187
207
  const modelName = state.model?.id || "no-model";
188
208
  let statsLeftWidth = visibleWidth(statsLeft);
@@ -200,6 +220,13 @@ export class FooterComponent {
200
220
  rightSideWithoutProvider =
201
221
  thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;
202
222
  }
223
+ if (visual.enhanced) {
224
+ const thinkingLevel = state.model?.reasoning ? state.thinkingLevel || "off" : undefined;
225
+ rightSideWithoutProvider = [
226
+ `${theme.fg("dim", glyphs.model)} ${theme.fg("muted", modelName)}`,
227
+ ...(thinkingLevel ? [theme.fg(thinkingLevel === "off" ? "dim" : "accent", thinkingLevel)] : []),
228
+ ].join(statsSeparator);
229
+ }
203
230
  // Prepend the provider in parentheses if there are multiple providers and there's enough room
204
231
  let rightSide = rightSideWithoutProvider;
205
232
  if (this.footerData.getAvailableProviderCount() > 1 && state.model) {
@@ -234,26 +261,11 @@ export class FooterComponent {
234
261
  // Apply dim to each part separately. statsLeft may contain color codes (for context %)
235
262
  // that end with a reset, which would clear an outer dim wrapper. So we dim the parts
236
263
  // before and after the colored section independently.
237
- const dimStatsLeft = theme.fg("dim", statsLeft);
264
+ const dimStatsLeft = visual.enhanced ? statsLeft : theme.fg("dim", statsLeft);
238
265
  const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
239
- const dimRemainder = theme.fg("dim", remainder);
240
- const gitBadge = formatGitBadge(this.footerData.getGitStatus());
241
- let pwdLine;
242
- if (gitBadge) {
243
- const left = theme.fg("dim", pwd);
244
- const leftWidth = visibleWidth(left);
245
- const badgeWidth = visibleWidth(gitBadge);
246
- if (leftWidth + 2 + badgeWidth <= width) {
247
- pwdLine = left + " ".repeat(width - leftWidth - badgeWidth) + gitBadge;
248
- }
249
- else {
250
- const truncatedLeft = truncateToWidth(left, Math.max(0, width - badgeWidth - 2), theme.fg("dim", "..."));
251
- pwdLine = `${truncatedLeft} ${gitBadge}`;
252
- }
253
- }
254
- else {
255
- pwdLine = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
256
- }
266
+ const dimRemainder = visual.enhanced ? remainder : theme.fg("dim", remainder);
267
+ const folder = glyphs.folder;
268
+ const pwdLine = truncateToWidth(`${theme.fg("dim", folder)} ${theme.fg("dim", pwd)}`, width, theme.fg("dim", "..."));
257
269
  const contextBarLine = renderContextBar(width, contextPercent === "?" ? null : contextPercentValue, this.autoCompactEnabled);
258
270
  const lines = [pwdLine, dimStatsLeft + dimRemainder, contextBarLine];
259
271
  // Add extension statuses on a single line, sorted by key alphabetically