threadwell 0.0.6 → 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.
- package/CHANGELOG.md +19 -0
- package/README.md +4 -8
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -7
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +4 -2
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +5 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +7 -1
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +12 -4
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +2 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +43 -4
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +4 -3
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +31 -8
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +4 -3
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +32 -8
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -9
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +3 -0
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +33 -20
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +70 -0
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +61 -49
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/framed-message.d.ts +17 -0
- package/dist/modes/interactive/components/framed-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/framed-message.js +39 -0
- package/dist/modes/interactive/components/framed-message.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +0 -4
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +0 -20
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts +4 -3
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +31 -9
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/split-info-card.d.ts.map +1 -1
- package/dist/modes/interactive/components/split-info-card.js +24 -17
- package/dist/modes/interactive/components/split-info-card.js.map +1 -1
- package/dist/modes/interactive/components/thinking-card.d.ts +6 -0
- package/dist/modes/interactive/components/thinking-card.d.ts.map +1 -0
- package/dist/modes/interactive/components/thinking-card.js +17 -0
- package/dist/modes/interactive/components/thinking-card.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +5 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +72 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +16 -0
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +86 -73
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/visual-profile.d.ts +69 -0
- package/dist/modes/interactive/theme/visual-profile.d.ts.map +1 -0
- package/dist/modes/interactive/theme/visual-profile.js +131 -0
- package/dist/modes/interactive/theme/visual-profile.js.map +1 -0
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +1 -1
- package/dist/package-manager-cli.js.map +1 -1
- package/docs/docs.json +1 -5
- package/docs/extensions.md +7 -16
- package/docs/index.md +6 -5
- package/docs/packages.md +8 -30
- package/docs/quickstart.md +1 -1
- package/docs/sdk.md +2 -3
- package/docs/settings.md +24 -7
- package/docs/themes.md +5 -295
- package/docs/usage.md +14 -17
- 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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"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;
|
|
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\";\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":"
|
|
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":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D;;;GAGG;AACH,SAAS,aAAa,CAAC,IAAY,EAA+D;IACjG,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IACrD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CAClE;AAED;;GAEG;AACH,SAAS,WAAW,CAAC,IAAY,EAAU;IAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAAA,CAClC;AAED;;;;GAIG;AACH,SAAS,mBAAmB,CAAC,UAAkB,EAAE,UAAkB,EAA8C;IAChH,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IAExD,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,IAAI,SAAS,GAAG,EAAE,CAAC;IACnB,IAAI,cAAc,GAAG,IAAI,CAAC;IAC1B,IAAI,YAAY,GAAG,IAAI,CAAC;IAExB,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YACvB,uDAAuD;YACvD,IAAI,cAAc,EAAE,CAAC;gBACpB,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBACnD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBACtC,WAAW,IAAI,SAAS,CAAC;gBACzB,cAAc,GAAG,KAAK,CAAC;YACxB,CAAC;YACD,IAAI,KAAK,EAAE,CAAC;gBACX,WAAW,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACrC,CAAC;QACF,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACvB,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YACvB,qDAAqD;YACrD,IAAI,YAAY,EAAE,CAAC;gBAClB,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBACnD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBACtC,SAAS,IAAI,SAAS,CAAC;gBACvB,YAAY,GAAG,KAAK,CAAC;YACtB,CAAC;YACD,IAAI,KAAK,EAAE,CAAC;gBACX,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACnC,CAAC;QACF,CAAC;aAAM,CAAC;YACP,WAAW,IAAI,IAAI,CAAC,KAAK,CAAC;YAC1B,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC;QACzB,CAAC;IACF,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;AAAA,CAClC;AAOD,SAAS,sBAAsB,CAAC,MAAc,EAAE,OAAe,EAAE,OAAe,EAAU;IACzF,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC,EAAE,CAAC,eAAe,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAM,IAAI,EAAE,CAAC,CAAC;IACnG,CAAC;IACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAM,IAAI,EAAE,CAAC,CAAC;IACtG,CAAC;IACD,OAAO,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAM,IAAI,EAAE,CAAC,CAAC;AAAA,CAChF;AAED,SAAS,kBAAkB,CAAC,QAAgB,EAAU;IACrD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IAEjC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAEnC,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YACjE,CAAC;YACD,CAAC,EAAE,CAAC;YACJ,SAAS;QACV,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC3B,MAAM,YAAY,GAA2C,EAAE,CAAC;YAChE,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;gBACzB,MAAM,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG;oBAAE,MAAM;gBAClC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC9D,CAAC,EAAE,CAAC;YACL,CAAC;YAED,MAAM,UAAU,GAA2C,EAAE,CAAC;YAC9D,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;gBACzB,MAAM,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG;oBAAE,MAAM;gBAClC,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC5D,CAAC,EAAE,CAAC;YACL,CAAC;YAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1D,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;gBAChC,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;gBAC5B,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,mBAAmB,CACrD,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,EAC5B,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAC1B,CAAC;gBACF,MAAM,CAAC,IAAI,CACV,KAAK,CAAC,EAAE,CACP,iBAAiB,EACjB,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAM,WAAW,EAAE,CAC/E,CACD,CAAC;gBACF,MAAM,CAAC,IAAI,CACV,KAAK,CAAC,EAAE,CAAC,eAAe,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAM,SAAS,EAAE,CAAC,CACrG,CAAC;YACH,CAAC;iBAAM,CAAC;gBACP,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;oBACpC,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC5E,CAAC;gBACD,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;oBAChC,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBACxE,CAAC;YACF,CAAC;QACF,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YACnF,CAAC,EAAE,CAAC;QACL,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACzB;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,QAAgB,EAAE,QAAQ,GAAsB,EAAE,EAAU;IACtF,IAAI,eAAe,EAAE,CAAC,QAAQ,EAAE,CAAC;QAChC,OAAO,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAEnC,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC,CAAC;YAC/C,CAAC,EAAE,CAAC;YACJ,SAAS;QACV,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC3B,oCAAoC;YACpC,MAAM,YAAY,GAA2C,EAAE,CAAC;YAChE,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;gBACzB,MAAM,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG;oBAAE,MAAM;gBAClC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC9D,CAAC,EAAE,CAAC;YACL,CAAC;YAED,kCAAkC;YAClC,MAAM,UAAU,GAA2C,EAAE,CAAC;YAC9D,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;gBACzB,MAAM,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG;oBAAE,MAAM;gBAClC,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC5D,CAAC,EAAE,CAAC;YACL,CAAC;YAED,iFAAiF;YACjF,wEAAwE;YACxE,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1D,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;gBAChC,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;gBAE5B,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,mBAAmB,CACrD,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,EAC5B,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAC1B,CAAC;gBAEF,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,OAAO,CAAC,OAAO,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC;gBAC/E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,eAAe,EAAE,IAAI,KAAK,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC;YAC1E,CAAC;iBAAM,CAAC;gBACP,qDAAqD;gBACrD,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;oBACpC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,OAAO,CAAC,OAAO,IAAI,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;gBACjG,CAAC;gBACD,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;oBAChC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,eAAe,EAAE,IAAI,KAAK,CAAC,OAAO,IAAI,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC3F,CAAC;YACF,CAAC;QACF,CAAC;aAAM,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAClC,wBAAwB;YACxB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,eAAe,EAAE,IAAI,MAAM,CAAC,OAAO,IAAI,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;YAC5F,CAAC,EAAE,CAAC;QACL,CAAC;aAAM,CAAC;YACP,eAAe;YACf,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,MAAM,CAAC,OAAO,IAAI,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;YAC9F,CAAC,EAAE,CAAC;QACL,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACzB","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
|
|
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
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
241
|
-
|
|
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
|