@juliusbrussee/caveman-tui 0.65.2
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/README.md +767 -0
- package/dist/autocomplete.d.ts +52 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +623 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/chord.d.ts +57 -0
- package/dist/chord.d.ts.map +1 -0
- package/dist/chord.js +97 -0
- package/dist/chord.js.map +1 -0
- package/dist/color-depth.d.ts +17 -0
- package/dist/color-depth.d.ts.map +1 -0
- package/dist/color-depth.js +147 -0
- package/dist/color-depth.js.map +1 -0
- package/dist/components/Chapters.d.ts +41 -0
- package/dist/components/Chapters.d.ts.map +1 -0
- package/dist/components/Chapters.js +103 -0
- package/dist/components/Chapters.js.map +1 -0
- package/dist/components/DiffView.d.ts +75 -0
- package/dist/components/DiffView.d.ts.map +1 -0
- package/dist/components/DiffView.js +170 -0
- package/dist/components/DiffView.js.map +1 -0
- package/dist/components/StatusLine.d.ts +135 -0
- package/dist/components/StatusLine.d.ts.map +1 -0
- package/dist/components/StatusLine.js +133 -0
- package/dist/components/StatusLine.js.map +1 -0
- package/dist/components/SubagentOverlay.d.ts +63 -0
- package/dist/components/SubagentOverlay.d.ts.map +1 -0
- package/dist/components/SubagentOverlay.js +124 -0
- package/dist/components/SubagentOverlay.js.map +1 -0
- package/dist/components/box.d.ts +22 -0
- package/dist/components/box.d.ts.map +1 -0
- package/dist/components/box.js +104 -0
- package/dist/components/box.js.map +1 -0
- package/dist/components/cancellable-loader.d.ts +22 -0
- package/dist/components/cancellable-loader.d.ts.map +1 -0
- package/dist/components/cancellable-loader.js +35 -0
- package/dist/components/cancellable-loader.js.map +1 -0
- package/dist/components/editor.d.ts +244 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +1861 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/grouped-select-list.d.ts +60 -0
- package/dist/components/grouped-select-list.d.ts.map +1 -0
- package/dist/components/grouped-select-list.js +312 -0
- package/dist/components/grouped-select-list.js.map +1 -0
- package/dist/components/image.d.ts +28 -0
- package/dist/components/image.d.ts.map +1 -0
- package/dist/components/image.js +69 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/input.d.ts +37 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +426 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/loader.d.ts +26 -0
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/loader.js +67 -0
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +95 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +663 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/select-list.d.ts +50 -0
- package/dist/components/select-list.d.ts.map +1 -0
- package/dist/components/select-list.js +159 -0
- package/dist/components/select-list.js.map +1 -0
- package/dist/components/settings-list.d.ts +50 -0
- package/dist/components/settings-list.d.ts.map +1 -0
- package/dist/components/settings-list.js +185 -0
- package/dist/components/settings-list.js.map +1 -0
- package/dist/components/spacer.d.ts +12 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +23 -0
- package/dist/components/spacer.js.map +1 -0
- package/dist/components/spinner.d.ts +35 -0
- package/dist/components/spinner.d.ts.map +1 -0
- package/dist/components/spinner.js +77 -0
- package/dist/components/spinner.js.map +1 -0
- package/dist/components/streaming-markdown.d.ts +39 -0
- package/dist/components/streaming-markdown.d.ts.map +1 -0
- package/dist/components/streaming-markdown.js +137 -0
- package/dist/components/streaming-markdown.js.map +1 -0
- package/dist/components/text.d.ts +19 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +89 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/truncated-text.d.ts +13 -0
- package/dist/components/truncated-text.d.ts.map +1 -0
- package/dist/components/truncated-text.js +51 -0
- package/dist/components/truncated-text.js.map +1 -0
- package/dist/editor-component.d.ts +39 -0
- package/dist/editor-component.d.ts.map +1 -0
- package/dist/editor-component.js +2 -0
- package/dist/editor-component.js.map +1 -0
- package/dist/fuzzy.d.ts +16 -0
- package/dist/fuzzy.d.ts.map +1 -0
- package/dist/fuzzy.js +107 -0
- package/dist/fuzzy.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/keybindings.d.ts +193 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +174 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/keys.d.ts +170 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +1124 -0
- package/dist/keys.js.map +1 -0
- package/dist/kill-ring.d.ts +28 -0
- package/dist/kill-ring.d.ts.map +1 -0
- package/dist/kill-ring.js +44 -0
- package/dist/kill-ring.js.map +1 -0
- package/dist/notifications.d.ts +35 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +62 -0
- package/dist/notifications.js.map +1 -0
- package/dist/osc52.d.ts +28 -0
- package/dist/osc52.d.ts.map +1 -0
- package/dist/osc52.js +53 -0
- package/dist/osc52.js.map +1 -0
- package/dist/scroll-buffer.d.ts +67 -0
- package/dist/scroll-buffer.d.ts.map +1 -0
- package/dist/scroll-buffer.js +222 -0
- package/dist/scroll-buffer.js.map +1 -0
- package/dist/spinners.d.ts +26 -0
- package/dist/spinners.d.ts.map +1 -0
- package/dist/spinners.js +136 -0
- package/dist/spinners.js.map +1 -0
- package/dist/stdin-buffer.d.ts +48 -0
- package/dist/stdin-buffer.d.ts.map +1 -0
- package/dist/stdin-buffer.js +317 -0
- package/dist/stdin-buffer.js.map +1 -0
- package/dist/sync-output.d.ts +58 -0
- package/dist/sync-output.d.ts.map +1 -0
- package/dist/sync-output.js +79 -0
- package/dist/sync-output.js.map +1 -0
- package/dist/terminal-detect.d.ts +66 -0
- package/dist/terminal-detect.d.ts.map +1 -0
- package/dist/terminal-detect.js +315 -0
- package/dist/terminal-detect.js.map +1 -0
- package/dist/terminal-image.d.ts +68 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +288 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal.d.ts +105 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +427 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tui.d.ts +268 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +1161 -0
- package/dist/tui.js.map +1 -0
- package/dist/undo-stack.d.ts +17 -0
- package/dist/undo-stack.d.ts.map +1 -0
- package/dist/undo-stack.js +25 -0
- package/dist/undo-stack.js.map +1 -0
- package/dist/utils.d.ts +78 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +960 -0
- package/dist/utils.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chapters — auto-fold transcript turns by detected intent.
|
|
3
|
+
*
|
|
4
|
+
* The TUI keeps a flat history; chapters group consecutive turns that share
|
|
5
|
+
* a detected intent ("debug", "refactor", "test", "explain", …) into a
|
|
6
|
+
* single foldable unit. Detection is heuristic (keyword + tool-pattern) so
|
|
7
|
+
* the UX stays predictable and predictable-feeling cheap.
|
|
8
|
+
*/
|
|
9
|
+
const RULES = [
|
|
10
|
+
{
|
|
11
|
+
intent: "debug",
|
|
12
|
+
keywords: /\b(bug|crash|stack trace|null|undefined|reproduce|fail(ed|ing)?|exception|TypeError|RangeError|why is|why does|broken)\b/i,
|
|
13
|
+
},
|
|
14
|
+
{ intent: "test", keywords: /\b(test|spec|vitest|jest|node:test|coverage|assertion)s?\b/i, tools: /Bash|Test/ },
|
|
15
|
+
{ intent: "refactor", keywords: /\b(refactor|rename|extract|inline|deduplicat|cleanup|tidy|simplif|reorganiz)/i },
|
|
16
|
+
{
|
|
17
|
+
intent: "implement",
|
|
18
|
+
keywords: /\b(implement|add|create|build|wire|hook up|introduce|scaffold)\b/i,
|
|
19
|
+
tools: /Edit|Write/,
|
|
20
|
+
},
|
|
21
|
+
{ intent: "explain", keywords: /\b(explain|what does|how does|walk me through|tell me about|what is)\b/i },
|
|
22
|
+
{ intent: "review", keywords: /\b(review|critique|feedback|audit|lgtm|nit:)/i },
|
|
23
|
+
{ intent: "plan", keywords: /\b(plan|outline|strategy|approach|design|architecture|sketch)\b/i },
|
|
24
|
+
{ intent: "memory", keywords: /\b(remember|memory|memorize|recall|forget)\b/i },
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Classify a single turn into an intent. The first matching rule wins; ties
|
|
28
|
+
* are broken by rule order, which mirrors how a user perceives priority
|
|
29
|
+
* (test/debug/refactor before generic implement).
|
|
30
|
+
*/
|
|
31
|
+
export function detectIntent(turn) {
|
|
32
|
+
for (const rule of RULES) {
|
|
33
|
+
if (rule.keywords.test(turn.text))
|
|
34
|
+
return rule.intent;
|
|
35
|
+
if (rule.tools && turn.tools && turn.tools.some((t) => rule.tools?.test(t)))
|
|
36
|
+
return rule.intent;
|
|
37
|
+
}
|
|
38
|
+
return "other";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Group a turn stream into chapters. Consecutive same-intent turns merge;
|
|
42
|
+
* tool-only turns inherit the previous user/assistant intent so they don't
|
|
43
|
+
* fragment a chapter.
|
|
44
|
+
*/
|
|
45
|
+
export function groupTurnsIntoChapters(turns) {
|
|
46
|
+
const out = [];
|
|
47
|
+
let current;
|
|
48
|
+
let lastNonToolIntent;
|
|
49
|
+
for (const turn of turns) {
|
|
50
|
+
let intent;
|
|
51
|
+
if (turn.role === "tool" && lastNonToolIntent) {
|
|
52
|
+
intent = lastNonToolIntent;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
intent = detectIntent(turn);
|
|
56
|
+
if (turn.role !== "tool")
|
|
57
|
+
lastNonToolIntent = intent;
|
|
58
|
+
}
|
|
59
|
+
if (current && current.intent === intent) {
|
|
60
|
+
current.turns.push(turn);
|
|
61
|
+
current.endedAt = turn.timestamp;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
current = {
|
|
65
|
+
id: `ch-${out.length + 1}`,
|
|
66
|
+
intent,
|
|
67
|
+
title: titleForIntent(intent, turn),
|
|
68
|
+
turns: [turn],
|
|
69
|
+
startedAt: turn.timestamp,
|
|
70
|
+
endedAt: turn.timestamp,
|
|
71
|
+
// Fold every chapter except the last by default; the renderer
|
|
72
|
+
// flips the most recent open after grouping.
|
|
73
|
+
folded: true,
|
|
74
|
+
};
|
|
75
|
+
out.push(current);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (out.length > 0)
|
|
79
|
+
out[out.length - 1].folded = false;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function titleForIntent(intent, firstTurn) {
|
|
83
|
+
const summary = firstTurn.text.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
84
|
+
const prefix = INTENT_LABELS[intent];
|
|
85
|
+
return summary.length > 0 ? `${prefix}: ${summary}` : prefix;
|
|
86
|
+
}
|
|
87
|
+
const INTENT_LABELS = {
|
|
88
|
+
implement: "Implement",
|
|
89
|
+
debug: "Debug",
|
|
90
|
+
refactor: "Refactor",
|
|
91
|
+
test: "Test",
|
|
92
|
+
explain: "Explain",
|
|
93
|
+
review: "Review",
|
|
94
|
+
plan: "Plan",
|
|
95
|
+
memory: "Memory",
|
|
96
|
+
other: "Notes",
|
|
97
|
+
};
|
|
98
|
+
/** Toggle a chapter's folded state. Returns a new array (immutable). */
|
|
99
|
+
export function toggleChapter(chapters, id) {
|
|
100
|
+
return chapters.map((c) => (c.id === id ? { ...c, folded: !c.folded } : c));
|
|
101
|
+
}
|
|
102
|
+
export const intentLabel = (intent) => INTENT_LABELS[intent];
|
|
103
|
+
//# sourceMappingURL=Chapters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chapters.js","sourceRoot":"","sources":["../../src/components/Chapters.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chapters — auto-fold transcript turns by detected intent.\n *\n * The TUI keeps a flat history; chapters group consecutive turns that share\n * a detected intent (\"debug\", \"refactor\", \"test\", \"explain\", …) into a\n * single foldable unit. Detection is heuristic (keyword + tool-pattern) so\n * the UX stays predictable and predictable-feeling cheap.\n */\n\nexport type Intent = \"implement\" | \"debug\" | \"refactor\" | \"test\" | \"explain\" | \"review\" | \"plan\" | \"memory\" | \"other\";\n\nexport interface Turn {\n\tid: string;\n\trole: \"user\" | \"assistant\" | \"tool\";\n\ttext: string;\n\ttools?: string[];\n\ttimestamp: number;\n}\n\nexport interface Chapter {\n\tid: string;\n\tintent: Intent;\n\ttitle: string;\n\tturns: Turn[];\n\tstartedAt: number;\n\tendedAt: number;\n\tfolded: boolean;\n}\n\ninterface IntentRule {\n\tintent: Intent;\n\tkeywords: RegExp;\n\ttools?: RegExp;\n}\n\nconst RULES: IntentRule[] = [\n\t{\n\t\tintent: \"debug\",\n\t\tkeywords:\n\t\t\t/\\b(bug|crash|stack trace|null|undefined|reproduce|fail(ed|ing)?|exception|TypeError|RangeError|why is|why does|broken)\\b/i,\n\t},\n\t{ intent: \"test\", keywords: /\\b(test|spec|vitest|jest|node:test|coverage|assertion)s?\\b/i, tools: /Bash|Test/ },\n\t{ intent: \"refactor\", keywords: /\\b(refactor|rename|extract|inline|deduplicat|cleanup|tidy|simplif|reorganiz)/i },\n\t{\n\t\tintent: \"implement\",\n\t\tkeywords: /\\b(implement|add|create|build|wire|hook up|introduce|scaffold)\\b/i,\n\t\ttools: /Edit|Write/,\n\t},\n\t{ intent: \"explain\", keywords: /\\b(explain|what does|how does|walk me through|tell me about|what is)\\b/i },\n\t{ intent: \"review\", keywords: /\\b(review|critique|feedback|audit|lgtm|nit:)/i },\n\t{ intent: \"plan\", keywords: /\\b(plan|outline|strategy|approach|design|architecture|sketch)\\b/i },\n\t{ intent: \"memory\", keywords: /\\b(remember|memory|memorize|recall|forget)\\b/i },\n];\n\n/**\n * Classify a single turn into an intent. The first matching rule wins; ties\n * are broken by rule order, which mirrors how a user perceives priority\n * (test/debug/refactor before generic implement).\n */\nexport function detectIntent(turn: Turn): Intent {\n\tfor (const rule of RULES) {\n\t\tif (rule.keywords.test(turn.text)) return rule.intent;\n\t\tif (rule.tools && turn.tools && turn.tools.some((t) => rule.tools?.test(t))) return rule.intent;\n\t}\n\treturn \"other\";\n}\n\n/**\n * Group a turn stream into chapters. Consecutive same-intent turns merge;\n * tool-only turns inherit the previous user/assistant intent so they don't\n * fragment a chapter.\n */\nexport function groupTurnsIntoChapters(turns: Turn[]): Chapter[] {\n\tconst out: Chapter[] = [];\n\tlet current: Chapter | undefined;\n\tlet lastNonToolIntent: Intent | undefined;\n\n\tfor (const turn of turns) {\n\t\tlet intent: Intent;\n\t\tif (turn.role === \"tool\" && lastNonToolIntent) {\n\t\t\tintent = lastNonToolIntent;\n\t\t} else {\n\t\t\tintent = detectIntent(turn);\n\t\t\tif (turn.role !== \"tool\") lastNonToolIntent = intent;\n\t\t}\n\n\t\tif (current && current.intent === intent) {\n\t\t\tcurrent.turns.push(turn);\n\t\t\tcurrent.endedAt = turn.timestamp;\n\t\t} else {\n\t\t\tcurrent = {\n\t\t\t\tid: `ch-${out.length + 1}`,\n\t\t\t\tintent,\n\t\t\t\ttitle: titleForIntent(intent, turn),\n\t\t\t\tturns: [turn],\n\t\t\t\tstartedAt: turn.timestamp,\n\t\t\t\tendedAt: turn.timestamp,\n\t\t\t\t// Fold every chapter except the last by default; the renderer\n\t\t\t\t// flips the most recent open after grouping.\n\t\t\t\tfolded: true,\n\t\t\t};\n\t\t\tout.push(current);\n\t\t}\n\t}\n\n\tif (out.length > 0) out[out.length - 1].folded = false;\n\treturn out;\n}\n\nfunction titleForIntent(intent: Intent, firstTurn: Turn): string {\n\tconst summary = firstTurn.text.replace(/\\s+/g, \" \").trim().slice(0, 60);\n\tconst prefix = INTENT_LABELS[intent];\n\treturn summary.length > 0 ? `${prefix}: ${summary}` : prefix;\n}\n\nconst INTENT_LABELS: Record<Intent, string> = {\n\timplement: \"Implement\",\n\tdebug: \"Debug\",\n\trefactor: \"Refactor\",\n\ttest: \"Test\",\n\texplain: \"Explain\",\n\treview: \"Review\",\n\tplan: \"Plan\",\n\tmemory: \"Memory\",\n\tother: \"Notes\",\n};\n\n/** Toggle a chapter's folded state. Returns a new array (immutable). */\nexport function toggleChapter(chapters: Chapter[], id: string): Chapter[] {\n\treturn chapters.map((c) => (c.id === id ? { ...c, folded: !c.folded } : c));\n}\n\nexport const intentLabel = (intent: Intent): string => INTENT_LABELS[intent];\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff view — side-by-side ≥ 100 cols, unified otherwise.
|
|
3
|
+
*
|
|
4
|
+
* Implements the "Diff rendering: side-by-side ≥ 100 cols, unified otherwise,
|
|
5
|
+
* AAA contrast" deliverable from WS10. This component takes a pre-computed
|
|
6
|
+
* line diff (additions / deletions / context) and lays it out for a given
|
|
7
|
+
* terminal width. Hunk parsing is the caller's job — the existing edit-format
|
|
8
|
+
* renderers in `coding-agent/src/core/edit-formats/` (WS8 territory) will
|
|
9
|
+
* produce these line-tagged inputs.
|
|
10
|
+
*/
|
|
11
|
+
import type { Component } from "../tui.js";
|
|
12
|
+
export type DiffLineKind = "context" | "add" | "del" | "header" | "hunk";
|
|
13
|
+
export interface DiffLine {
|
|
14
|
+
kind: DiffLineKind;
|
|
15
|
+
text: string;
|
|
16
|
+
oldLine?: number;
|
|
17
|
+
newLine?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface DiffViewTheme {
|
|
20
|
+
add: (text: string) => string;
|
|
21
|
+
del: (text: string) => string;
|
|
22
|
+
context: (text: string) => string;
|
|
23
|
+
header: (text: string) => string;
|
|
24
|
+
hunk: (text: string) => string;
|
|
25
|
+
gutterAdd: (text: string) => string;
|
|
26
|
+
gutterDel: (text: string) => string;
|
|
27
|
+
gutterContext: (text: string) => string;
|
|
28
|
+
separator: (text: string) => string;
|
|
29
|
+
}
|
|
30
|
+
/** Threshold below which we render unified rather than side-by-side. */
|
|
31
|
+
export declare const SIDE_BY_SIDE_MIN_WIDTH = 100;
|
|
32
|
+
export type DiffLayout = "unified" | "side-by-side";
|
|
33
|
+
export declare function pickLayout(width: number): DiffLayout;
|
|
34
|
+
export interface DiffViewOptions {
|
|
35
|
+
lines: DiffLine[];
|
|
36
|
+
theme?: DiffViewTheme;
|
|
37
|
+
/** Force a specific layout. Default: width-based per `pickLayout`. */
|
|
38
|
+
forceLayout?: DiffLayout;
|
|
39
|
+
/** Show line-number gutter. */
|
|
40
|
+
showLineNumbers?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare class DiffView implements Component {
|
|
43
|
+
private lines;
|
|
44
|
+
private theme;
|
|
45
|
+
private forceLayout?;
|
|
46
|
+
private showLineNumbers;
|
|
47
|
+
constructor(options: DiffViewOptions);
|
|
48
|
+
setLines(lines: DiffLine[]): void;
|
|
49
|
+
invalidate(): void;
|
|
50
|
+
render(width: number): string[];
|
|
51
|
+
private renderUnified;
|
|
52
|
+
private renderSideBySide;
|
|
53
|
+
private formatHalf;
|
|
54
|
+
private formatGutter;
|
|
55
|
+
}
|
|
56
|
+
interface DiffPair {
|
|
57
|
+
kind: DiffLineKind;
|
|
58
|
+
text: string;
|
|
59
|
+
left?: DiffLine;
|
|
60
|
+
right?: DiffLine;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pair up consecutive del/add runs for side-by-side layout. Within a hunk:
|
|
64
|
+
*
|
|
65
|
+
* [del A, del B, add C, add D, ctx X]
|
|
66
|
+
*
|
|
67
|
+
* becomes:
|
|
68
|
+
*
|
|
69
|
+
* [del A | add C, del B | add D, ctx X | ctx X]
|
|
70
|
+
*
|
|
71
|
+
* Mismatched lengths are padded with empty halves.
|
|
72
|
+
*/
|
|
73
|
+
export declare function pairUpHunks(lines: DiffLine[]): DiffPair[];
|
|
74
|
+
export {};
|
|
75
|
+
//# sourceMappingURL=DiffView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiffView.d.ts","sourceRoot":"","sources":["../../src/components/DiffView.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,QAAQ;IACxB,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACpC;AAcD,wEAAwE;AACxE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,cAAc,CAAC;AAEpD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAEpD;AAED,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,+BAA+B;IAC/B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,qBAAa,QAAS,YAAW,SAAS;IACzC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,eAAe,CAAU;IAEjC,YAAY,OAAO,EAAE,eAAe,EAKnC;IAED,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,CAEhC;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAG9B;IAED,OAAO,CAAC,aAAa;IA4BrB,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,YAAY;CAMpB;AAuBD,UAAU,QAAQ;IACjB,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAyCzD","sourcesContent":["/**\n * Diff view — side-by-side ≥ 100 cols, unified otherwise.\n *\n * Implements the \"Diff rendering: side-by-side ≥ 100 cols, unified otherwise,\n * AAA contrast\" deliverable from WS10. This component takes a pre-computed\n * line diff (additions / deletions / context) and lays it out for a given\n * terminal width. Hunk parsing is the caller's job — the existing edit-format\n * renderers in `coding-agent/src/core/edit-formats/` (WS8 territory) will\n * produce these line-tagged inputs.\n */\nimport type { Component } from \"../tui.js\";\nimport { truncateToWidth, visibleWidth } from \"../utils.js\";\n\nexport type DiffLineKind = \"context\" | \"add\" | \"del\" | \"header\" | \"hunk\";\n\nexport interface DiffLine {\n\tkind: DiffLineKind;\n\ttext: string;\n\toldLine?: number;\n\tnewLine?: number;\n}\n\nexport interface DiffViewTheme {\n\tadd: (text: string) => string;\n\tdel: (text: string) => string;\n\tcontext: (text: string) => string;\n\theader: (text: string) => string;\n\thunk: (text: string) => string;\n\tgutterAdd: (text: string) => string;\n\tgutterDel: (text: string) => string;\n\tgutterContext: (text: string) => string;\n\tseparator: (text: string) => string;\n}\n\nconst IDENTITY_THEME: DiffViewTheme = {\n\tadd: (s) => s,\n\tdel: (s) => s,\n\tcontext: (s) => s,\n\theader: (s) => s,\n\thunk: (s) => s,\n\tgutterAdd: (s) => s,\n\tgutterDel: (s) => s,\n\tgutterContext: (s) => s,\n\tseparator: (s) => s,\n};\n\n/** Threshold below which we render unified rather than side-by-side. */\nexport const SIDE_BY_SIDE_MIN_WIDTH = 100;\n\nexport type DiffLayout = \"unified\" | \"side-by-side\";\n\nexport function pickLayout(width: number): DiffLayout {\n\treturn width >= SIDE_BY_SIDE_MIN_WIDTH ? \"side-by-side\" : \"unified\";\n}\n\nexport interface DiffViewOptions {\n\tlines: DiffLine[];\n\ttheme?: DiffViewTheme;\n\t/** Force a specific layout. Default: width-based per `pickLayout`. */\n\tforceLayout?: DiffLayout;\n\t/** Show line-number gutter. */\n\tshowLineNumbers?: boolean;\n}\n\nexport class DiffView implements Component {\n\tprivate lines: DiffLine[];\n\tprivate theme: DiffViewTheme;\n\tprivate forceLayout?: DiffLayout;\n\tprivate showLineNumbers: boolean;\n\n\tconstructor(options: DiffViewOptions) {\n\t\tthis.lines = options.lines;\n\t\tthis.theme = options.theme ?? IDENTITY_THEME;\n\t\tthis.forceLayout = options.forceLayout;\n\t\tthis.showLineNumbers = options.showLineNumbers ?? true;\n\t}\n\n\tsetLines(lines: DiffLine[]): void {\n\t\tthis.lines = lines;\n\t}\n\n\tinvalidate(): void {\n\t\t// Stateless — recomputed each render call.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst layout = this.forceLayout ?? pickLayout(width);\n\t\treturn layout === \"side-by-side\" ? this.renderSideBySide(width) : this.renderUnified(width);\n\t}\n\n\tprivate renderUnified(width: number): string[] {\n\t\tconst out: string[] = [];\n\t\tconst gutterW = this.showLineNumbers ? 5 : 0;\n\t\tconst sepW = 1; // \" \"\n\t\tconst prefixW = 1; // \"+\", \"-\", \" \"\n\t\tconst contentW = Math.max(1, width - gutterW - sepW - prefixW);\n\n\t\tfor (const line of this.lines) {\n\t\t\tif (line.kind === \"header\") {\n\t\t\t\tout.push(this.theme.header(padToWidth(truncateToWidth(line.text, width), width)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (line.kind === \"hunk\") {\n\t\t\t\tout.push(this.theme.hunk(padToWidth(truncateToWidth(line.text, width), width)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst gutter = this.showLineNumbers ? this.formatGutter(line.oldLine, line.newLine, line.kind) : \"\";\n\t\t\tconst prefix = line.kind === \"add\" ? \"+\" : line.kind === \"del\" ? \"-\" : \" \";\n\t\t\tconst body = truncateToWidth(line.text, contentW);\n\n\t\t\tlet row = `${gutter}${prefix} ${body}`;\n\t\t\trow = padToWidth(row, width);\n\t\t\trow = applyKind(row, line.kind, this.theme);\n\t\t\tout.push(row);\n\t\t}\n\t\treturn out;\n\t}\n\n\tprivate renderSideBySide(width: number): string[] {\n\t\tconst sepW = 3; // \" │ \"\n\t\tconst colW = Math.floor((width - sepW) / 2);\n\t\tconst out: string[] = [];\n\t\tconst pairs = pairUpHunks(this.lines);\n\n\t\tfor (const pair of pairs) {\n\t\t\tif (pair.kind === \"header\") {\n\t\t\t\tout.push(this.theme.header(padToWidth(truncateToWidth(pair.text, width), width)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pair.kind === \"hunk\") {\n\t\t\t\tout.push(this.theme.hunk(padToWidth(truncateToWidth(pair.text, width), width)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst left = pair.left ? this.formatHalf(pair.left, colW) : padToWidth(\"\", colW);\n\t\t\tconst right = pair.right ? this.formatHalf(pair.right, colW) : padToWidth(\"\", colW);\n\t\t\tconst sep = this.theme.separator(\" │ \");\n\t\t\tout.push(`${left}${sep}${right}`);\n\t\t}\n\t\treturn out;\n\t}\n\n\tprivate formatHalf(line: DiffLine, colW: number): string {\n\t\tconst prefix = line.kind === \"add\" ? \"+\" : line.kind === \"del\" ? \"-\" : \" \";\n\t\tconst body = truncateToWidth(line.text, Math.max(1, colW - 2));\n\t\tconst row = padToWidth(`${prefix} ${body}`, colW);\n\t\treturn applyKind(row, line.kind, this.theme);\n\t}\n\n\tprivate formatGutter(oldLine: number | undefined, newLine: number | undefined, kind: DiffLineKind): string {\n\t\tconst o = oldLine !== undefined ? String(oldLine) : \"\";\n\t\tconst n = newLine !== undefined ? String(newLine) : \"\";\n\t\tconst cell = kind === \"add\" ? n : kind === \"del\" ? o : n || o;\n\t\treturn `${cell.padStart(4, \" \")} `;\n\t}\n}\n\nfunction applyKind(row: string, kind: DiffLineKind, theme: DiffViewTheme): string {\n\tswitch (kind) {\n\t\tcase \"add\":\n\t\t\treturn theme.add(row);\n\t\tcase \"del\":\n\t\t\treturn theme.del(row);\n\t\tcase \"context\":\n\t\t\treturn theme.context(row);\n\t\tcase \"header\":\n\t\t\treturn theme.header(row);\n\t\tcase \"hunk\":\n\t\t\treturn theme.hunk(row);\n\t}\n}\n\nfunction padToWidth(s: string, width: number): string {\n\tconst w = visibleWidth(s);\n\tif (w >= width) return s;\n\treturn s + \" \".repeat(width - w);\n}\n\ninterface DiffPair {\n\tkind: DiffLineKind;\n\ttext: string;\n\tleft?: DiffLine;\n\tright?: DiffLine;\n}\n\n/**\n * Pair up consecutive del/add runs for side-by-side layout. Within a hunk:\n *\n * [del A, del B, add C, add D, ctx X]\n *\n * becomes:\n *\n * [del A | add C, del B | add D, ctx X | ctx X]\n *\n * Mismatched lengths are padded with empty halves.\n */\nexport function pairUpHunks(lines: DiffLine[]): DiffPair[] {\n\tconst out: DiffPair[] = [];\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tif (line.kind === \"header\") {\n\t\t\tout.push({ kind: \"header\", text: line.text });\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (line.kind === \"hunk\") {\n\t\t\tout.push({ kind: \"hunk\", text: line.text });\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (line.kind === \"context\") {\n\t\t\tout.push({ kind: \"context\", text: line.text, left: line, right: line });\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Collect run of dels then adds.\n\t\tconst dels: DiffLine[] = [];\n\t\tconst adds: DiffLine[] = [];\n\t\twhile (i < lines.length && lines[i].kind === \"del\") {\n\t\t\tdels.push(lines[i]);\n\t\t\ti++;\n\t\t}\n\t\twhile (i < lines.length && lines[i].kind === \"add\") {\n\t\t\tadds.push(lines[i]);\n\t\t\ti++;\n\t\t}\n\t\tconst max = Math.max(dels.length, adds.length);\n\t\tfor (let k = 0; k < max; k++) {\n\t\t\tconst left = dels[k];\n\t\t\tconst right = adds[k];\n\t\t\tconst kind: DiffLineKind = left ? \"del\" : \"add\";\n\t\t\tout.push({ kind, text: (left ?? right ?? { text: \"\" }).text, left, right });\n\t\t}\n\t}\n\treturn out;\n}\n"]}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "../utils.js";
|
|
2
|
+
const IDENTITY_THEME = {
|
|
3
|
+
add: (s) => s,
|
|
4
|
+
del: (s) => s,
|
|
5
|
+
context: (s) => s,
|
|
6
|
+
header: (s) => s,
|
|
7
|
+
hunk: (s) => s,
|
|
8
|
+
gutterAdd: (s) => s,
|
|
9
|
+
gutterDel: (s) => s,
|
|
10
|
+
gutterContext: (s) => s,
|
|
11
|
+
separator: (s) => s,
|
|
12
|
+
};
|
|
13
|
+
/** Threshold below which we render unified rather than side-by-side. */
|
|
14
|
+
export const SIDE_BY_SIDE_MIN_WIDTH = 100;
|
|
15
|
+
export function pickLayout(width) {
|
|
16
|
+
return width >= SIDE_BY_SIDE_MIN_WIDTH ? "side-by-side" : "unified";
|
|
17
|
+
}
|
|
18
|
+
export class DiffView {
|
|
19
|
+
lines;
|
|
20
|
+
theme;
|
|
21
|
+
forceLayout;
|
|
22
|
+
showLineNumbers;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.lines = options.lines;
|
|
25
|
+
this.theme = options.theme ?? IDENTITY_THEME;
|
|
26
|
+
this.forceLayout = options.forceLayout;
|
|
27
|
+
this.showLineNumbers = options.showLineNumbers ?? true;
|
|
28
|
+
}
|
|
29
|
+
setLines(lines) {
|
|
30
|
+
this.lines = lines;
|
|
31
|
+
}
|
|
32
|
+
invalidate() {
|
|
33
|
+
// Stateless — recomputed each render call.
|
|
34
|
+
}
|
|
35
|
+
render(width) {
|
|
36
|
+
const layout = this.forceLayout ?? pickLayout(width);
|
|
37
|
+
return layout === "side-by-side" ? this.renderSideBySide(width) : this.renderUnified(width);
|
|
38
|
+
}
|
|
39
|
+
renderUnified(width) {
|
|
40
|
+
const out = [];
|
|
41
|
+
const gutterW = this.showLineNumbers ? 5 : 0;
|
|
42
|
+
const sepW = 1; // " "
|
|
43
|
+
const prefixW = 1; // "+", "-", " "
|
|
44
|
+
const contentW = Math.max(1, width - gutterW - sepW - prefixW);
|
|
45
|
+
for (const line of this.lines) {
|
|
46
|
+
if (line.kind === "header") {
|
|
47
|
+
out.push(this.theme.header(padToWidth(truncateToWidth(line.text, width), width)));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (line.kind === "hunk") {
|
|
51
|
+
out.push(this.theme.hunk(padToWidth(truncateToWidth(line.text, width), width)));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const gutter = this.showLineNumbers ? this.formatGutter(line.oldLine, line.newLine, line.kind) : "";
|
|
55
|
+
const prefix = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
56
|
+
const body = truncateToWidth(line.text, contentW);
|
|
57
|
+
let row = `${gutter}${prefix} ${body}`;
|
|
58
|
+
row = padToWidth(row, width);
|
|
59
|
+
row = applyKind(row, line.kind, this.theme);
|
|
60
|
+
out.push(row);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
renderSideBySide(width) {
|
|
65
|
+
const sepW = 3; // " │ "
|
|
66
|
+
const colW = Math.floor((width - sepW) / 2);
|
|
67
|
+
const out = [];
|
|
68
|
+
const pairs = pairUpHunks(this.lines);
|
|
69
|
+
for (const pair of pairs) {
|
|
70
|
+
if (pair.kind === "header") {
|
|
71
|
+
out.push(this.theme.header(padToWidth(truncateToWidth(pair.text, width), width)));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (pair.kind === "hunk") {
|
|
75
|
+
out.push(this.theme.hunk(padToWidth(truncateToWidth(pair.text, width), width)));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const left = pair.left ? this.formatHalf(pair.left, colW) : padToWidth("", colW);
|
|
79
|
+
const right = pair.right ? this.formatHalf(pair.right, colW) : padToWidth("", colW);
|
|
80
|
+
const sep = this.theme.separator(" │ ");
|
|
81
|
+
out.push(`${left}${sep}${right}`);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
formatHalf(line, colW) {
|
|
86
|
+
const prefix = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
87
|
+
const body = truncateToWidth(line.text, Math.max(1, colW - 2));
|
|
88
|
+
const row = padToWidth(`${prefix} ${body}`, colW);
|
|
89
|
+
return applyKind(row, line.kind, this.theme);
|
|
90
|
+
}
|
|
91
|
+
formatGutter(oldLine, newLine, kind) {
|
|
92
|
+
const o = oldLine !== undefined ? String(oldLine) : "";
|
|
93
|
+
const n = newLine !== undefined ? String(newLine) : "";
|
|
94
|
+
const cell = kind === "add" ? n : kind === "del" ? o : n || o;
|
|
95
|
+
return `${cell.padStart(4, " ")} `;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function applyKind(row, kind, theme) {
|
|
99
|
+
switch (kind) {
|
|
100
|
+
case "add":
|
|
101
|
+
return theme.add(row);
|
|
102
|
+
case "del":
|
|
103
|
+
return theme.del(row);
|
|
104
|
+
case "context":
|
|
105
|
+
return theme.context(row);
|
|
106
|
+
case "header":
|
|
107
|
+
return theme.header(row);
|
|
108
|
+
case "hunk":
|
|
109
|
+
return theme.hunk(row);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function padToWidth(s, width) {
|
|
113
|
+
const w = visibleWidth(s);
|
|
114
|
+
if (w >= width)
|
|
115
|
+
return s;
|
|
116
|
+
return s + " ".repeat(width - w);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Pair up consecutive del/add runs for side-by-side layout. Within a hunk:
|
|
120
|
+
*
|
|
121
|
+
* [del A, del B, add C, add D, ctx X]
|
|
122
|
+
*
|
|
123
|
+
* becomes:
|
|
124
|
+
*
|
|
125
|
+
* [del A | add C, del B | add D, ctx X | ctx X]
|
|
126
|
+
*
|
|
127
|
+
* Mismatched lengths are padded with empty halves.
|
|
128
|
+
*/
|
|
129
|
+
export function pairUpHunks(lines) {
|
|
130
|
+
const out = [];
|
|
131
|
+
let i = 0;
|
|
132
|
+
while (i < lines.length) {
|
|
133
|
+
const line = lines[i];
|
|
134
|
+
if (line.kind === "header") {
|
|
135
|
+
out.push({ kind: "header", text: line.text });
|
|
136
|
+
i++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (line.kind === "hunk") {
|
|
140
|
+
out.push({ kind: "hunk", text: line.text });
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (line.kind === "context") {
|
|
145
|
+
out.push({ kind: "context", text: line.text, left: line, right: line });
|
|
146
|
+
i++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// Collect run of dels then adds.
|
|
150
|
+
const dels = [];
|
|
151
|
+
const adds = [];
|
|
152
|
+
while (i < lines.length && lines[i].kind === "del") {
|
|
153
|
+
dels.push(lines[i]);
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
while (i < lines.length && lines[i].kind === "add") {
|
|
157
|
+
adds.push(lines[i]);
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
const max = Math.max(dels.length, adds.length);
|
|
161
|
+
for (let k = 0; k < max; k++) {
|
|
162
|
+
const left = dels[k];
|
|
163
|
+
const right = adds[k];
|
|
164
|
+
const kind = left ? "del" : "add";
|
|
165
|
+
out.push({ kind, text: (left ?? right ?? { text: "" }).text, left, right });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=DiffView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiffView.js","sourceRoot":"","sources":["../../src/components/DiffView.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Diff view — side-by-side ≥ 100 cols, unified otherwise.\n *\n * Implements the \"Diff rendering: side-by-side ≥ 100 cols, unified otherwise,\n * AAA contrast\" deliverable from WS10. This component takes a pre-computed\n * line diff (additions / deletions / context) and lays it out for a given\n * terminal width. Hunk parsing is the caller's job — the existing edit-format\n * renderers in `coding-agent/src/core/edit-formats/` (WS8 territory) will\n * produce these line-tagged inputs.\n */\nimport type { Component } from \"../tui.js\";\nimport { truncateToWidth, visibleWidth } from \"../utils.js\";\n\nexport type DiffLineKind = \"context\" | \"add\" | \"del\" | \"header\" | \"hunk\";\n\nexport interface DiffLine {\n\tkind: DiffLineKind;\n\ttext: string;\n\toldLine?: number;\n\tnewLine?: number;\n}\n\nexport interface DiffViewTheme {\n\tadd: (text: string) => string;\n\tdel: (text: string) => string;\n\tcontext: (text: string) => string;\n\theader: (text: string) => string;\n\thunk: (text: string) => string;\n\tgutterAdd: (text: string) => string;\n\tgutterDel: (text: string) => string;\n\tgutterContext: (text: string) => string;\n\tseparator: (text: string) => string;\n}\n\nconst IDENTITY_THEME: DiffViewTheme = {\n\tadd: (s) => s,\n\tdel: (s) => s,\n\tcontext: (s) => s,\n\theader: (s) => s,\n\thunk: (s) => s,\n\tgutterAdd: (s) => s,\n\tgutterDel: (s) => s,\n\tgutterContext: (s) => s,\n\tseparator: (s) => s,\n};\n\n/** Threshold below which we render unified rather than side-by-side. */\nexport const SIDE_BY_SIDE_MIN_WIDTH = 100;\n\nexport type DiffLayout = \"unified\" | \"side-by-side\";\n\nexport function pickLayout(width: number): DiffLayout {\n\treturn width >= SIDE_BY_SIDE_MIN_WIDTH ? \"side-by-side\" : \"unified\";\n}\n\nexport interface DiffViewOptions {\n\tlines: DiffLine[];\n\ttheme?: DiffViewTheme;\n\t/** Force a specific layout. Default: width-based per `pickLayout`. */\n\tforceLayout?: DiffLayout;\n\t/** Show line-number gutter. */\n\tshowLineNumbers?: boolean;\n}\n\nexport class DiffView implements Component {\n\tprivate lines: DiffLine[];\n\tprivate theme: DiffViewTheme;\n\tprivate forceLayout?: DiffLayout;\n\tprivate showLineNumbers: boolean;\n\n\tconstructor(options: DiffViewOptions) {\n\t\tthis.lines = options.lines;\n\t\tthis.theme = options.theme ?? IDENTITY_THEME;\n\t\tthis.forceLayout = options.forceLayout;\n\t\tthis.showLineNumbers = options.showLineNumbers ?? true;\n\t}\n\n\tsetLines(lines: DiffLine[]): void {\n\t\tthis.lines = lines;\n\t}\n\n\tinvalidate(): void {\n\t\t// Stateless — recomputed each render call.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst layout = this.forceLayout ?? pickLayout(width);\n\t\treturn layout === \"side-by-side\" ? this.renderSideBySide(width) : this.renderUnified(width);\n\t}\n\n\tprivate renderUnified(width: number): string[] {\n\t\tconst out: string[] = [];\n\t\tconst gutterW = this.showLineNumbers ? 5 : 0;\n\t\tconst sepW = 1; // \" \"\n\t\tconst prefixW = 1; // \"+\", \"-\", \" \"\n\t\tconst contentW = Math.max(1, width - gutterW - sepW - prefixW);\n\n\t\tfor (const line of this.lines) {\n\t\t\tif (line.kind === \"header\") {\n\t\t\t\tout.push(this.theme.header(padToWidth(truncateToWidth(line.text, width), width)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (line.kind === \"hunk\") {\n\t\t\t\tout.push(this.theme.hunk(padToWidth(truncateToWidth(line.text, width), width)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst gutter = this.showLineNumbers ? this.formatGutter(line.oldLine, line.newLine, line.kind) : \"\";\n\t\t\tconst prefix = line.kind === \"add\" ? \"+\" : line.kind === \"del\" ? \"-\" : \" \";\n\t\t\tconst body = truncateToWidth(line.text, contentW);\n\n\t\t\tlet row = `${gutter}${prefix} ${body}`;\n\t\t\trow = padToWidth(row, width);\n\t\t\trow = applyKind(row, line.kind, this.theme);\n\t\t\tout.push(row);\n\t\t}\n\t\treturn out;\n\t}\n\n\tprivate renderSideBySide(width: number): string[] {\n\t\tconst sepW = 3; // \" │ \"\n\t\tconst colW = Math.floor((width - sepW) / 2);\n\t\tconst out: string[] = [];\n\t\tconst pairs = pairUpHunks(this.lines);\n\n\t\tfor (const pair of pairs) {\n\t\t\tif (pair.kind === \"header\") {\n\t\t\t\tout.push(this.theme.header(padToWidth(truncateToWidth(pair.text, width), width)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pair.kind === \"hunk\") {\n\t\t\t\tout.push(this.theme.hunk(padToWidth(truncateToWidth(pair.text, width), width)));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst left = pair.left ? this.formatHalf(pair.left, colW) : padToWidth(\"\", colW);\n\t\t\tconst right = pair.right ? this.formatHalf(pair.right, colW) : padToWidth(\"\", colW);\n\t\t\tconst sep = this.theme.separator(\" │ \");\n\t\t\tout.push(`${left}${sep}${right}`);\n\t\t}\n\t\treturn out;\n\t}\n\n\tprivate formatHalf(line: DiffLine, colW: number): string {\n\t\tconst prefix = line.kind === \"add\" ? \"+\" : line.kind === \"del\" ? \"-\" : \" \";\n\t\tconst body = truncateToWidth(line.text, Math.max(1, colW - 2));\n\t\tconst row = padToWidth(`${prefix} ${body}`, colW);\n\t\treturn applyKind(row, line.kind, this.theme);\n\t}\n\n\tprivate formatGutter(oldLine: number | undefined, newLine: number | undefined, kind: DiffLineKind): string {\n\t\tconst o = oldLine !== undefined ? String(oldLine) : \"\";\n\t\tconst n = newLine !== undefined ? String(newLine) : \"\";\n\t\tconst cell = kind === \"add\" ? n : kind === \"del\" ? o : n || o;\n\t\treturn `${cell.padStart(4, \" \")} `;\n\t}\n}\n\nfunction applyKind(row: string, kind: DiffLineKind, theme: DiffViewTheme): string {\n\tswitch (kind) {\n\t\tcase \"add\":\n\t\t\treturn theme.add(row);\n\t\tcase \"del\":\n\t\t\treturn theme.del(row);\n\t\tcase \"context\":\n\t\t\treturn theme.context(row);\n\t\tcase \"header\":\n\t\t\treturn theme.header(row);\n\t\tcase \"hunk\":\n\t\t\treturn theme.hunk(row);\n\t}\n}\n\nfunction padToWidth(s: string, width: number): string {\n\tconst w = visibleWidth(s);\n\tif (w >= width) return s;\n\treturn s + \" \".repeat(width - w);\n}\n\ninterface DiffPair {\n\tkind: DiffLineKind;\n\ttext: string;\n\tleft?: DiffLine;\n\tright?: DiffLine;\n}\n\n/**\n * Pair up consecutive del/add runs for side-by-side layout. Within a hunk:\n *\n * [del A, del B, add C, add D, ctx X]\n *\n * becomes:\n *\n * [del A | add C, del B | add D, ctx X | ctx X]\n *\n * Mismatched lengths are padded with empty halves.\n */\nexport function pairUpHunks(lines: DiffLine[]): DiffPair[] {\n\tconst out: DiffPair[] = [];\n\tlet i = 0;\n\twhile (i < lines.length) {\n\t\tconst line = lines[i];\n\t\tif (line.kind === \"header\") {\n\t\t\tout.push({ kind: \"header\", text: line.text });\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (line.kind === \"hunk\") {\n\t\t\tout.push({ kind: \"hunk\", text: line.text });\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (line.kind === \"context\") {\n\t\t\tout.push({ kind: \"context\", text: line.text, left: line, right: line });\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Collect run of dels then adds.\n\t\tconst dels: DiffLine[] = [];\n\t\tconst adds: DiffLine[] = [];\n\t\twhile (i < lines.length && lines[i].kind === \"del\") {\n\t\t\tdels.push(lines[i]);\n\t\t\ti++;\n\t\t}\n\t\twhile (i < lines.length && lines[i].kind === \"add\") {\n\t\t\tadds.push(lines[i]);\n\t\t\ti++;\n\t\t}\n\t\tconst max = Math.max(dels.length, adds.length);\n\t\tfor (let k = 0; k < max; k++) {\n\t\t\tconst left = dels[k];\n\t\t\tconst right = adds[k];\n\t\t\tconst kind: DiffLineKind = left ? \"del\" : \"add\";\n\t\t\tout.push({ kind, text: (left ?? right ?? { text: \"\" }).text, left, right });\n\t\t}\n\t}\n\treturn out;\n}\n"]}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status line — Claude Code v2.1.119 compatible.
|
|
3
|
+
*
|
|
4
|
+
* Schema (from `settings.json`):
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "statusLine": {
|
|
8
|
+
* "type": "command" | "default" | "detailed",
|
|
9
|
+
* "command": "/path/to/script.sh", // when type === "command"
|
|
10
|
+
* "padding": 0
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* When `type === "command"`, cave invokes the binary with a JSON context
|
|
15
|
+
* payload on stdin and renders stdout (single-line, ANSI-stripped to
|
|
16
|
+
* terminal width). The schema is identical to Claude Code so a user pasting
|
|
17
|
+
* `~/.claude/settings.json#statusLine` into `~/.cave/settings.json` Just
|
|
18
|
+
* Works.
|
|
19
|
+
*/
|
|
20
|
+
import type { Component } from "../tui.js";
|
|
21
|
+
/**
|
|
22
|
+
* Claude-Code-shaped statusLine setting. The `type` field is the union
|
|
23
|
+
* authority; unknown types fall back to "default".
|
|
24
|
+
*/
|
|
25
|
+
export interface StatusLineSettings {
|
|
26
|
+
type?: "command" | "default" | "detailed";
|
|
27
|
+
/** Shell command to run (only when `type === "command"`). */
|
|
28
|
+
command?: string;
|
|
29
|
+
/** Left padding in cells. */
|
|
30
|
+
padding?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* JSON context that cave pipes to a `command`-type status line on stdin.
|
|
34
|
+
* Matches Claude Code's documented shape closely enough that scripts written
|
|
35
|
+
* for Claude Code work with cave (and vice versa).
|
|
36
|
+
*/
|
|
37
|
+
export interface StatusLineContext {
|
|
38
|
+
hook_event_name: "Status";
|
|
39
|
+
session_id: string;
|
|
40
|
+
transcript_path?: string;
|
|
41
|
+
cwd: string;
|
|
42
|
+
model: {
|
|
43
|
+
id: string;
|
|
44
|
+
display_name?: string;
|
|
45
|
+
};
|
|
46
|
+
workspace: {
|
|
47
|
+
current_dir: string;
|
|
48
|
+
project_dir: string;
|
|
49
|
+
};
|
|
50
|
+
version?: string;
|
|
51
|
+
output_style?: {
|
|
52
|
+
name: string;
|
|
53
|
+
};
|
|
54
|
+
cost?: {
|
|
55
|
+
total_cost_usd: number;
|
|
56
|
+
total_duration_ms: number;
|
|
57
|
+
};
|
|
58
|
+
exceeds_200k_tokens?: boolean;
|
|
59
|
+
/** Cave-specific extras. Claude Code ignores keys it doesn't know. */
|
|
60
|
+
cave?: {
|
|
61
|
+
branch?: string;
|
|
62
|
+
gitDirty?: boolean;
|
|
63
|
+
queuedMessages?: number;
|
|
64
|
+
tokensIn?: number;
|
|
65
|
+
tokensOut?: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Result of rendering — separated from Component so callers can compose. */
|
|
69
|
+
export interface StatusLineResult {
|
|
70
|
+
text: string;
|
|
71
|
+
/** Where the text came from. Useful for surfacing errors in /doctor. */
|
|
72
|
+
source: "default" | "detailed" | "command" | "command-failed";
|
|
73
|
+
stderr?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the default (terse) status line. Format:
|
|
77
|
+
* <model> · <cwd-tail>
|
|
78
|
+
*/
|
|
79
|
+
export declare function renderDefault(ctx: StatusLineContext): string;
|
|
80
|
+
/**
|
|
81
|
+
* Build the detailed status line. Format:
|
|
82
|
+
* <model> · <branch>(<dirty>) · <cwd-tail> · q:<n> · $<cost>
|
|
83
|
+
*/
|
|
84
|
+
export declare function renderDetailed(ctx: StatusLineContext): string;
|
|
85
|
+
/**
|
|
86
|
+
* Truncate a filesystem path to the trailing N components.
|
|
87
|
+
*
|
|
88
|
+
* tailPath("/a/b/c/d", 2) → "c/d"
|
|
89
|
+
* tailPath("/usr", 2) → "/usr"
|
|
90
|
+
*/
|
|
91
|
+
export declare function tailPath(path: string, components: number): string;
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a status line settings block to text. Synchronous helpers only —
|
|
94
|
+
* the `command` type is async and lives on the StatusLine component below.
|
|
95
|
+
*/
|
|
96
|
+
export declare function renderStatusLineSync(settings: StatusLineSettings, ctx: StatusLineContext): StatusLineResult;
|
|
97
|
+
/**
|
|
98
|
+
* Validate and coerce an unknown settings.json `statusLine` value into a
|
|
99
|
+
* concrete StatusLineSettings. Returns `undefined` only when the value is
|
|
100
|
+
* malformed in a way that cannot be safely rendered.
|
|
101
|
+
*/
|
|
102
|
+
export declare function parseStatusLineSettings(raw: unknown): StatusLineSettings | undefined;
|
|
103
|
+
export interface StatusLineRenderer {
|
|
104
|
+
/**
|
|
105
|
+
* Async render — runs the configured command if any. Implementations are
|
|
106
|
+
* provided by the coding-agent (which has access to bash-executor); the
|
|
107
|
+
* TUI component takes a renderer and only does layout.
|
|
108
|
+
*/
|
|
109
|
+
render(ctx: StatusLineContext): Promise<StatusLineResult>;
|
|
110
|
+
}
|
|
111
|
+
export interface StatusLineComponentTheme {
|
|
112
|
+
bg: (text: string) => string;
|
|
113
|
+
muted: (text: string) => string;
|
|
114
|
+
error: (text: string) => string;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Single-line status component. Renders the current cached text. Callers
|
|
118
|
+
* call `setText()` when the renderer produces a new result.
|
|
119
|
+
*/
|
|
120
|
+
export declare class StatusLine implements Component {
|
|
121
|
+
private text;
|
|
122
|
+
private source;
|
|
123
|
+
private theme;
|
|
124
|
+
private padding;
|
|
125
|
+
constructor(options?: {
|
|
126
|
+
theme?: StatusLineComponentTheme;
|
|
127
|
+
padding?: number;
|
|
128
|
+
});
|
|
129
|
+
setText(result: StatusLineResult): void;
|
|
130
|
+
invalidate(): void;
|
|
131
|
+
render(width: number): string[];
|
|
132
|
+
}
|
|
133
|
+
/** Strip newlines/carriage returns; status line is single-row by contract. */
|
|
134
|
+
export declare function sanitizeOneLine(s: string): string;
|
|
135
|
+
//# sourceMappingURL=StatusLine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusLine.d.ts","sourceRoot":"","sources":["../../src/components/StatusLine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Status line — Claude Code v2.1.119 compatible.\n *\n * Schema (from `settings.json`):\n *\n * {\n * \"statusLine\": {\n * \"type\": \"command\" | \"default\" | \"detailed\",\n * \"command\": \"/path/to/script.sh\", // when type === \"command\"\n * \"padding\": 0\n * }\n * }\n *\n * When `type === \"command\"`, cave invokes the binary with a JSON context\n * payload on stdin and renders stdout (single-line, ANSI-stripped to\n * terminal width). The schema is identical to Claude Code so a user pasting\n * `~/.claude/settings.json#statusLine` into `~/.cave/settings.json` Just\n * Works.\n */\nimport type { Component } from \"../tui.js\";\nimport { truncateToWidth, visibleWidth } from \"../utils.js\";\n\n/**\n * Claude-Code-shaped statusLine setting. The `type` field is the union\n * authority; unknown types fall back to \"default\".\n */\nexport interface StatusLineSettings {\n\ttype?: \"command\" | \"default\" | \"detailed\";\n\t/** Shell command to run (only when `type === \"command\"`). */\n\tcommand?: string;\n\t/** Left padding in cells. */\n\tpadding?: number;\n}\n\n/**\n * JSON context that cave pipes to a `command`-type status line on stdin.\n * Matches Claude Code's documented shape closely enough that scripts written\n * for Claude Code work with cave (and vice versa).\n */\nexport interface StatusLineContext {\n\thook_event_name: \"Status\";\n\tsession_id: string;\n\ttranscript_path?: string;\n\tcwd: string;\n\tmodel: { id: string; display_name?: string };\n\tworkspace: { current_dir: string; project_dir: string };\n\tversion?: string;\n\toutput_style?: { name: string };\n\tcost?: { total_cost_usd: number; total_duration_ms: number };\n\texceeds_200k_tokens?: boolean;\n\t/** Cave-specific extras. Claude Code ignores keys it doesn't know. */\n\tcave?: {\n\t\tbranch?: string;\n\t\tgitDirty?: boolean;\n\t\tqueuedMessages?: number;\n\t\ttokensIn?: number;\n\t\ttokensOut?: number;\n\t};\n}\n\n/** Result of rendering — separated from Component so callers can compose. */\nexport interface StatusLineResult {\n\ttext: string;\n\t/** Where the text came from. Useful for surfacing errors in /doctor. */\n\tsource: \"default\" | \"detailed\" | \"command\" | \"command-failed\";\n\tstderr?: string;\n}\n\n/**\n * Build the default (terse) status line. Format:\n * <model> · <cwd-tail>\n */\nexport function renderDefault(ctx: StatusLineContext): string {\n\tconst model = ctx.model.display_name ?? ctx.model.id;\n\treturn `${model} · ${tailPath(ctx.workspace.current_dir, 2)}`;\n}\n\n/**\n * Build the detailed status line. Format:\n * <model> · <branch>(<dirty>) · <cwd-tail> · q:<n> · $<cost>\n */\nexport function renderDetailed(ctx: StatusLineContext): string {\n\tconst parts: string[] = [];\n\tconst model = ctx.model.display_name ?? ctx.model.id;\n\tparts.push(model);\n\n\tconst branch = ctx.cave?.branch;\n\tif (branch) {\n\t\tparts.push(`${branch}${ctx.cave?.gitDirty ? \"*\" : \"\"}`);\n\t}\n\n\tparts.push(tailPath(ctx.workspace.current_dir, 2));\n\n\tconst queued = ctx.cave?.queuedMessages ?? 0;\n\tif (queued > 0) parts.push(`q:${queued}`);\n\n\tif (ctx.cost && ctx.cost.total_cost_usd > 0) {\n\t\tparts.push(`$${ctx.cost.total_cost_usd.toFixed(4)}`);\n\t}\n\n\tif (ctx.exceeds_200k_tokens) parts.push(\"⚠ 200k\");\n\n\treturn parts.join(\" · \");\n}\n\n/**\n * Truncate a filesystem path to the trailing N components.\n *\n * tailPath(\"/a/b/c/d\", 2) → \"c/d\"\n * tailPath(\"/usr\", 2) → \"/usr\"\n */\nexport function tailPath(path: string, components: number): string {\n\tif (!path) return \"\";\n\tconst parts = path.split(/[\\\\/]/).filter(Boolean);\n\tif (parts.length <= components) return path;\n\treturn parts.slice(-components).join(\"/\");\n}\n\n/**\n * Resolve a status line settings block to text. Synchronous helpers only —\n * the `command` type is async and lives on the StatusLine component below.\n */\nexport function renderStatusLineSync(settings: StatusLineSettings, ctx: StatusLineContext): StatusLineResult {\n\tconst type = settings.type ?? \"default\";\n\tif (type === \"detailed\") return { text: renderDetailed(ctx), source: \"detailed\" };\n\tif (type === \"command\") {\n\t\t// Sync caller cannot run the command; surface the default.\n\t\treturn { text: renderDefault(ctx), source: \"default\" };\n\t}\n\treturn { text: renderDefault(ctx), source: \"default\" };\n}\n\n/**\n * Validate and coerce an unknown settings.json `statusLine` value into a\n * concrete StatusLineSettings. Returns `undefined` only when the value is\n * malformed in a way that cannot be safely rendered.\n */\nexport function parseStatusLineSettings(raw: unknown): StatusLineSettings | undefined {\n\tif (raw === undefined || raw === null) return undefined;\n\tif (typeof raw !== \"object\" || Array.isArray(raw)) return undefined;\n\tconst o = raw as Record<string, unknown>;\n\n\tconst out: StatusLineSettings = {};\n\n\tif (typeof o.type === \"string\") {\n\t\tif (o.type === \"command\" || o.type === \"default\" || o.type === \"detailed\") {\n\t\t\tout.type = o.type;\n\t\t} else {\n\t\t\tout.type = \"default\";\n\t\t}\n\t}\n\tif (typeof o.command === \"string\" && o.command.trim().length > 0) {\n\t\tout.command = o.command;\n\t}\n\tif (typeof o.padding === \"number\" && Number.isFinite(o.padding) && o.padding >= 0) {\n\t\tout.padding = Math.floor(o.padding);\n\t}\n\n\t// If type is \"command\" but no command field, downgrade to \"default\".\n\tif (out.type === \"command\" && !out.command) {\n\t\tout.type = \"default\";\n\t}\n\n\treturn out;\n}\n\nexport interface StatusLineRenderer {\n\t/**\n\t * Async render — runs the configured command if any. Implementations are\n\t * provided by the coding-agent (which has access to bash-executor); the\n\t * TUI component takes a renderer and only does layout.\n\t */\n\trender(ctx: StatusLineContext): Promise<StatusLineResult>;\n}\n\nexport interface StatusLineComponentTheme {\n\tbg: (text: string) => string;\n\tmuted: (text: string) => string;\n\terror: (text: string) => string;\n}\n\nconst IDENTITY_THEME: StatusLineComponentTheme = {\n\tbg: (s) => s,\n\tmuted: (s) => s,\n\terror: (s) => s,\n};\n\n/**\n * Single-line status component. Renders the current cached text. Callers\n * call `setText()` when the renderer produces a new result.\n */\nexport class StatusLine implements Component {\n\tprivate text = \"\";\n\tprivate source: StatusLineResult[\"source\"] = \"default\";\n\tprivate theme: StatusLineComponentTheme;\n\tprivate padding: number;\n\n\tconstructor(options: { theme?: StatusLineComponentTheme; padding?: number } = {}) {\n\t\tthis.theme = options.theme ?? IDENTITY_THEME;\n\t\tthis.padding = options.padding ?? 0;\n\t}\n\n\tsetText(result: StatusLineResult): void {\n\t\tthis.text = sanitizeOneLine(result.text);\n\t\tthis.source = result.source;\n\t}\n\n\tinvalidate(): void {\n\t\t// Stateless render — nothing to clear.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst pad = \" \".repeat(this.padding);\n\t\tconst inner = `${pad}${this.text}`;\n\t\tconst truncated = truncateToWidth(inner, width);\n\t\t// Pad to width so the status line owns its row even with diff backgrounds.\n\t\tconst w = visibleWidth(truncated);\n\t\tconst padded = w < width ? truncated + \" \".repeat(width - w) : truncated;\n\t\tconst styled = this.source === \"command-failed\" ? this.theme.error(padded) : this.theme.bg(padded);\n\t\treturn [styled];\n\t}\n}\n\n/** Strip newlines/carriage returns; status line is single-row by contract. */\nexport function sanitizeOneLine(s: string): string {\n\treturn s.replace(/[\\r\\n]+/g, \" \").trim();\n}\n"]}
|