agent-sh 0.15.2 → 0.15.4
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/dist/agent/agent-loop.js +1 -1
- package/dist/agent/events.d.ts +1 -0
- package/dist/agent/index.js +10 -5
- package/dist/agent/providers/openai-compatible.d.ts +4 -2
- package/dist/agent/providers/openai-compatible.js +5 -0
- package/dist/agent/subagent.js +1 -1
- package/dist/utils/diff-renderer.js +27 -26
- package/dist/utils/palette.d.ts +1 -0
- package/dist/utils/palette.js +5 -4
- package/examples/extensions/ashi/src/frontend.ts +17 -4
- package/examples/extensions/ashi/src/schema.ts +3 -2
- package/package.json +1 -1
- package/src/agent/agent-loop.ts +1 -1
- package/src/agent/events.ts +1 -1
- package/src/agent/index.ts +9 -4
- package/src/agent/providers/openai-compatible.ts +10 -2
- package/src/agent/subagent.ts +1 -1
- package/src/utils/diff-renderer.ts +28 -30
- package/src/utils/palette.ts +6 -4
package/dist/agent/agent-loop.js
CHANGED
|
@@ -967,7 +967,7 @@ export class AgentLoop {
|
|
|
967
967
|
// Execute via handler — extensions can advise to add safe-mode,
|
|
968
968
|
// logging, metrics, custom permission policies, etc.
|
|
969
969
|
const defaultOnChunk = (chunk) => {
|
|
970
|
-
this.bus.emit("agent:tool-output-chunk", { chunk });
|
|
970
|
+
this.bus.emit("agent:tool-output-chunk", { chunk, toolCallId: tc.id });
|
|
971
971
|
};
|
|
972
972
|
const result = await this.handlers.call("tool:execute", { name: tc.name, id: tc.id, args, tool, onChunk: defaultOnChunk,
|
|
973
973
|
batchIndex, batchTotal: batchTotal > 1 ? batchTotal : undefined,
|
package/dist/agent/events.d.ts
CHANGED
package/dist/agent/index.js
CHANGED
|
@@ -89,11 +89,15 @@ export default function agentBackend(ctx) {
|
|
|
89
89
|
const providerContribs = new Map();
|
|
90
90
|
// Settings overlay — fields here win over contributing extensions' payloads.
|
|
91
91
|
const settingsProviders = new Map();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
const refreshSettingsProviders = () => {
|
|
93
|
+
settingsProviders.clear();
|
|
94
|
+
for (const name of getProviderNames()) {
|
|
95
|
+
const p = resolveProvider(name);
|
|
96
|
+
if (p)
|
|
97
|
+
settingsProviders.set(name, p);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
refreshSettingsProviders();
|
|
97
101
|
const providerHooks = new Map();
|
|
98
102
|
// Bakes model id so ModelEndpoint.buildReasoningParams keeps its (level) signature.
|
|
99
103
|
const bindReasoning = (shapeId, model) => {
|
|
@@ -341,6 +345,7 @@ export default function agentBackend(ctx) {
|
|
|
341
345
|
let agentLoop = null;
|
|
342
346
|
let loadedExtensionNames = [];
|
|
343
347
|
bus.on("agent:providers:changed", () => {
|
|
348
|
+
refreshSettingsProviders();
|
|
344
349
|
resolvedProviders = computeResolvedProviders();
|
|
345
350
|
if (!resolved)
|
|
346
351
|
return;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI Chat Completions-compatible local/3rd-party server (Ollama, LM
|
|
3
|
-
* Studio, vLLM, llama.cpp, …).
|
|
4
|
-
*
|
|
3
|
+
* Studio, vLLM, llama.cpp, …). Emits the common `reasoning_effort` shape,
|
|
4
|
+
* with `"none"` to disable — the value most local servers honor. A server
|
|
5
|
+
* wanting a different disable token can override via `reasoningShape` or a
|
|
6
|
+
* user extension.
|
|
5
7
|
*/
|
|
6
8
|
import type { AgentContext } from "../host-types.js";
|
|
7
9
|
export default function activate(ctx: AgentContext): void;
|
|
@@ -5,6 +5,11 @@ export default function activate(ctx) {
|
|
|
5
5
|
// Local servers often need no key; SDK still wants a non-empty string.
|
|
6
6
|
const apiKey = process.env.OPENAI_API_KEY || "no-key";
|
|
7
7
|
const id = "openai-compatible";
|
|
8
|
+
ctx.agent.providers.configure(id, {
|
|
9
|
+
reasoningParams: (level) => level === "off"
|
|
10
|
+
? { reasoning_effort: "none" }
|
|
11
|
+
: { reasoning_effort: level === "xhigh" ? "high" : level },
|
|
12
|
+
});
|
|
8
13
|
ctx.agent.providers.register({ id, apiKey, baseURL, models: [] });
|
|
9
14
|
fetchModels(baseURL, apiKey).then((models) => {
|
|
10
15
|
if (models.length === 0)
|
package/dist/agent/subagent.js
CHANGED
|
@@ -72,7 +72,7 @@ export async function runSubagent(opts) {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
const onChunk = bus && tool.showOutput !== false
|
|
75
|
-
? (chunk) => { bus.emit("agent:tool-output-chunk", { chunk }); }
|
|
75
|
+
? (chunk) => { bus.emit("agent:tool-output-chunk", { chunk, toolCallId: tc.id }); }
|
|
76
76
|
: undefined;
|
|
77
77
|
const result = await tool.execute(args, onChunk);
|
|
78
78
|
if (bus) {
|
|
@@ -157,7 +157,7 @@ function highlightInlineChanges(oldLine, newLine, oldPalette, newPalette, useTru
|
|
|
157
157
|
}
|
|
158
158
|
else {
|
|
159
159
|
// Changed tokens: emphasis background, no syntax highlighting (emphasis stands out)
|
|
160
|
-
result += palette.emphBg +
|
|
160
|
+
result += palette.emphBg + tokens[i].text + p.reset;
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
return result;
|
|
@@ -246,55 +246,56 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
246
246
|
const { noW, lineTextW, textWidth, useTrueColor, gutterLine, lang, removedPalette, addedPalette } = layout;
|
|
247
247
|
const out = [];
|
|
248
248
|
const pairs = findChangePairs(hunk);
|
|
249
|
-
const renderedAsPartOfPair = new Set();
|
|
250
249
|
const bgWidth = Math.max(1, textWidth - noW - 3);
|
|
251
250
|
const gutter = (n) => `${p.dim}${n} │${p.reset} `;
|
|
252
251
|
const change = (no, sigil, bg, fg, text) => {
|
|
253
252
|
if (!gutterLine) {
|
|
254
|
-
return `${bg}${fg}${
|
|
253
|
+
return `${bg}${padToWidth(`${fg}${no} ${sigil}${p.diffText} ${preserveBg(text, bg)}`, textWidth)}${p.reset}`;
|
|
255
254
|
}
|
|
256
255
|
if (useTrueColor)
|
|
257
|
-
return gutter(no) + padToWidth(`${bg}${fg}${sigil} ${preserveBg(text, bg)}`, bgWidth) + p.reset;
|
|
256
|
+
return gutter(no) + padToWidth(`${bg}${fg}${sigil}${p.diffText} ${preserveBg(text, bg)}`, bgWidth) + p.reset;
|
|
258
257
|
return `${gutter(no)}${fg}${sigil} ${text}${p.reset}`;
|
|
259
258
|
};
|
|
259
|
+
const hlCache = new Map();
|
|
260
|
+
const highlightedPair = (pair) => {
|
|
261
|
+
let h = hlCache.get(pair);
|
|
262
|
+
if (!h) {
|
|
263
|
+
h = highlightInlineChanges(pair.removed.text, pair.added.text, removedPalette, addedPalette, useTrueColor, lang);
|
|
264
|
+
hlCache.set(pair, h);
|
|
265
|
+
}
|
|
266
|
+
return h;
|
|
267
|
+
};
|
|
260
268
|
for (let i = 0; i < hunk.lines.length; i++) {
|
|
261
269
|
const line = hunk.lines[i];
|
|
262
270
|
const no = String(line.type === "removed" ? (line.oldNo ?? "") : (line.newNo ?? line.oldNo ?? "")).padStart(noW);
|
|
263
271
|
if (line.type === "context") {
|
|
264
272
|
const raw = truncateText(line.text, lineTextW);
|
|
265
273
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
266
|
-
// The flush gutter dims only the line number; the code stays normal/highlighted.
|
|
267
274
|
out.push(!gutterLine ? `${p.dim}${no}${p.reset} ${text}` : `${gutter(no)} ${p.dim}${text}${p.reset}`);
|
|
268
275
|
continue;
|
|
269
276
|
}
|
|
277
|
+
const pair = pairs.get(i);
|
|
270
278
|
if (line.type === "removed") {
|
|
271
|
-
const pair = pairs.get(i);
|
|
272
279
|
let removedText;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (pair && pair.removedIdx === i) {
|
|
276
|
-
const highlighted = highlightInlineChanges(line.text, pair.added.text, removedPalette, addedPalette, useTrueColor, lang);
|
|
277
|
-
removedText = truncateText(highlighted.old, lineTextW);
|
|
278
|
-
addedText = truncateText(highlighted.new, lineTextW);
|
|
279
|
-
addedNo = String(pair.added.newNo ?? "").padStart(noW);
|
|
280
|
-
renderedAsPartOfPair.add(pair.addedIdx);
|
|
280
|
+
if (pair) {
|
|
281
|
+
removedText = truncateText(highlightedPair(pair).old, lineTextW);
|
|
281
282
|
}
|
|
282
283
|
else {
|
|
283
284
|
const raw = truncateText(line.text, lineTextW);
|
|
284
285
|
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
285
286
|
}
|
|
286
287
|
out.push(change(no, "-", p.errorBg, p.error, removedText));
|
|
287
|
-
if (addedText !== null && addedNo !== null) {
|
|
288
|
-
out.push(change(addedNo, "+", p.successBg, p.success, addedText));
|
|
289
|
-
}
|
|
290
|
-
continue;
|
|
291
288
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
else {
|
|
290
|
+
let addedText;
|
|
291
|
+
if (pair) {
|
|
292
|
+
addedText = truncateText(highlightedPair(pair).new, lineTextW);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const raw = truncateText(line.text, lineTextW);
|
|
296
|
+
addedText = lang ? highlightLine(raw, lang) : raw;
|
|
297
|
+
}
|
|
298
|
+
out.push(change(no, "+", p.successBg, p.success, addedText));
|
|
298
299
|
}
|
|
299
300
|
}
|
|
300
301
|
return out;
|
|
@@ -362,7 +363,7 @@ function renderSplitHunk(hunk, layout) {
|
|
|
362
363
|
}
|
|
363
364
|
else if (row.left.type === "removed") {
|
|
364
365
|
if (useTrueColor) {
|
|
365
|
-
leftCol = padToWidth(`${p.errorBg}${p.error}${leftNo}
|
|
366
|
+
leftCol = padToWidth(`${p.errorBg}${p.error}${leftNo} │${p.diffText} ${preserveBg(leftText, p.errorBg)}`, colWidth) + p.reset;
|
|
366
367
|
}
|
|
367
368
|
else {
|
|
368
369
|
leftCol = padToWidth(`${p.error}${leftNo} │ ${leftText}${p.reset}`, colWidth);
|
|
@@ -376,7 +377,7 @@ function renderSplitHunk(hunk, layout) {
|
|
|
376
377
|
}
|
|
377
378
|
else if (row.right.type === "added") {
|
|
378
379
|
if (useTrueColor) {
|
|
379
|
-
rightCol = padToWidth(`${p.successBg}${p.success}${rightNo}
|
|
380
|
+
rightCol = padToWidth(`${p.successBg}${p.success}${rightNo} │${p.diffText} ${preserveBg(rightText, p.successBg)}`, colWidth) + p.reset;
|
|
380
381
|
}
|
|
381
382
|
else {
|
|
382
383
|
rightCol = padToWidth(`${p.success}${rightNo} │ ${rightText}${p.reset}`, colWidth);
|
package/dist/utils/palette.d.ts
CHANGED
package/dist/utils/palette.js
CHANGED
|
@@ -14,10 +14,11 @@ const defaultPalette = {
|
|
|
14
14
|
warning: "\x1b[33m", // yellow
|
|
15
15
|
error: "\x1b[31m", // red
|
|
16
16
|
muted: "\x1b[90m", // gray
|
|
17
|
-
successBg: "\x1b[48;2;
|
|
18
|
-
errorBg: "\x1b[48;2;
|
|
19
|
-
successBgEmph: "\x1b[48;2;
|
|
20
|
-
errorBgEmph: "\x1b[48;2;
|
|
17
|
+
successBg: "\x1b[48;2;26;70;34m",
|
|
18
|
+
errorBg: "\x1b[48;2;92;32;42m",
|
|
19
|
+
successBgEmph: "\x1b[48;2;38;104;56m",
|
|
20
|
+
errorBgEmph: "\x1b[48;2;124;50;64m",
|
|
21
|
+
diffText: "\x1b[97m", // bright white — readable on the red/green tints
|
|
21
22
|
bold: "\x1b[1m",
|
|
22
23
|
dim: "\x1b[2m",
|
|
23
24
|
italic: "\x1b[3m",
|
|
@@ -318,6 +318,7 @@ export function mountAshi(
|
|
|
318
318
|
| { t: "thinking"; ctrl: ThinkingBlock }
|
|
319
319
|
| { t: "assistant"; ctrl: AssistantMessage }
|
|
320
320
|
| { t: "pair"; result: ToolResultView }
|
|
321
|
+
| { t: "user" }
|
|
321
322
|
| { t: "plain" };
|
|
322
323
|
const chatEntries: ChatEntry[] = [];
|
|
323
324
|
const appendEntry = (node: RenderNode, entry: ChatEntry): void => {
|
|
@@ -580,7 +581,7 @@ export function mountAshi(
|
|
|
580
581
|
.join("")
|
|
581
582
|
: "";
|
|
582
583
|
if (raw.startsWith("[Compacted conversation summary]")) return;
|
|
583
|
-
appendEntry(renderUserMessage(stripContextWrappers(raw)), { t: "
|
|
584
|
+
appendEntry(renderUserMessage(stripContextWrappers(raw)), { t: "user" });
|
|
584
585
|
} else if (m.role === "assistant") {
|
|
585
586
|
const reasoning = readReasoning(m);
|
|
586
587
|
if (reasoning) {
|
|
@@ -661,7 +662,7 @@ export function mountAshi(
|
|
|
661
662
|
bus.on("agent:query", ({ query }) => {
|
|
662
663
|
app.commitScrollback?.();
|
|
663
664
|
sealOpenGroup();
|
|
664
|
-
appendEntry(renderUserMessage(query), { t: "
|
|
665
|
+
appendEntry(renderUserMessage(query), { t: "user" });
|
|
665
666
|
activeAssistant = null;
|
|
666
667
|
app.requestRender();
|
|
667
668
|
});
|
|
@@ -763,7 +764,13 @@ export function mountAshi(
|
|
|
763
764
|
app.requestRender();
|
|
764
765
|
});
|
|
765
766
|
|
|
766
|
-
bus.on("agent:tool-output-chunk", ({ chunk }) => {
|
|
767
|
+
bus.on("agent:tool-output-chunk", ({ chunk, toolCallId }) => {
|
|
768
|
+
const owner = toolCallId ? activeTools.get(toolCallId) : undefined;
|
|
769
|
+
if (owner?.kind === "pair") {
|
|
770
|
+
owner.pair.result.appendChunk(chunk);
|
|
771
|
+
app.requestRender();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
767
774
|
for (const entry of [...activeTools.values()].reverse()) {
|
|
768
775
|
if (entry.kind === "pair") {
|
|
769
776
|
entry.pair.result.appendChunk(chunk);
|
|
@@ -1262,7 +1269,13 @@ export function mountAshi(
|
|
|
1262
1269
|
}
|
|
1263
1270
|
if (key.matches("ctrl+o")) {
|
|
1264
1271
|
allExpanded = !allExpanded;
|
|
1265
|
-
|
|
1272
|
+
// Toggle only the latest turn; re-rendering the whole transcript is O(history).
|
|
1273
|
+
let start = 0;
|
|
1274
|
+
for (let i = chatEntries.length - 1; i >= 0; i--) {
|
|
1275
|
+
if (chatEntries[i]!.t === "user") { start = i; break; }
|
|
1276
|
+
}
|
|
1277
|
+
for (let i = start; i < chatEntries.length; i++) {
|
|
1278
|
+
const e = chatEntries[i]!;
|
|
1266
1279
|
if (e.t === "group") e.group.setExpanded(allExpanded);
|
|
1267
1280
|
else if (e.t === "pair") e.result.setExpanded(allExpanded);
|
|
1268
1281
|
}
|
|
@@ -214,10 +214,11 @@ function renderStream(buffer: string, env: Env): string {
|
|
|
214
214
|
const lines = display.split("\n");
|
|
215
215
|
const trimmed = lines.slice(-env.previewLines).join("\n");
|
|
216
216
|
const remaining = Math.max(0, lines.length - env.previewLines);
|
|
217
|
+
// The preview is the tail, so the hidden lines come before it — note goes above.
|
|
217
218
|
const overflow = remaining > 0
|
|
218
|
-
?
|
|
219
|
+
? `${theme.fg("muted", `... (${remaining} earlier ${remaining === 1 ? "line" : "lines"})`)}\n`
|
|
219
220
|
: "";
|
|
220
|
-
return `${theme.fg("toolOutput", trimmed)}
|
|
221
|
+
return `${overflow}${theme.fg("toolOutput", trimmed)}`;
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
function lineCountHint(buffer: string): string {
|
package/package.json
CHANGED
package/src/agent/agent-loop.ts
CHANGED
|
@@ -1124,7 +1124,7 @@ export class AgentLoop implements AgentBackend {
|
|
|
1124
1124
|
// Execute via handler — extensions can advise to add safe-mode,
|
|
1125
1125
|
// logging, metrics, custom permission policies, etc.
|
|
1126
1126
|
const defaultOnChunk = (chunk: string) => {
|
|
1127
|
-
this.bus.emit("agent:tool-output-chunk", { chunk });
|
|
1127
|
+
this.bus.emit("agent:tool-output-chunk", { chunk, toolCallId: tc.id });
|
|
1128
1128
|
};
|
|
1129
1129
|
const result = await this.handlers.call(
|
|
1130
1130
|
"tool:execute",
|
package/src/agent/events.ts
CHANGED
|
@@ -88,7 +88,7 @@ declare module "../core/event-bus.js" {
|
|
|
88
88
|
kind?: string;
|
|
89
89
|
resultDisplay?: ToolResultDisplay;
|
|
90
90
|
};
|
|
91
|
-
"agent:tool-output-chunk": { chunk: string };
|
|
91
|
+
"agent:tool-output-chunk": { chunk: string; toolCallId?: string };
|
|
92
92
|
|
|
93
93
|
"tool:interactive-start": Record<string, never>;
|
|
94
94
|
"tool:interactive-end": Record<string, never>;
|
package/src/agent/index.ts
CHANGED
|
@@ -116,10 +116,14 @@ export default function agentBackend(ctx: ExtensionContext): void {
|
|
|
116
116
|
|
|
117
117
|
// Settings overlay — fields here win over contributing extensions' payloads.
|
|
118
118
|
const settingsProviders = new Map<string, ResolvedProvider>();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
const refreshSettingsProviders = () => {
|
|
120
|
+
settingsProviders.clear();
|
|
121
|
+
for (const name of getProviderNames()) {
|
|
122
|
+
const p = resolveProvider(name);
|
|
123
|
+
if (p) settingsProviders.set(name, p);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
refreshSettingsProviders();
|
|
123
127
|
|
|
124
128
|
const providerHooks = new Map<string, {
|
|
125
129
|
reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
|
|
@@ -368,6 +372,7 @@ export default function agentBackend(ctx: ExtensionContext): void {
|
|
|
368
372
|
let loadedExtensionNames: string[] = [];
|
|
369
373
|
|
|
370
374
|
bus.on("agent:providers:changed", () => {
|
|
375
|
+
refreshSettingsProviders();
|
|
371
376
|
resolvedProviders = computeResolvedProviders();
|
|
372
377
|
if (!resolved) return;
|
|
373
378
|
bus.emit("agent:models-changed", {});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI Chat Completions-compatible local/3rd-party server (Ollama, LM
|
|
3
|
-
* Studio, vLLM, llama.cpp, …).
|
|
4
|
-
*
|
|
3
|
+
* Studio, vLLM, llama.cpp, …). Emits the common `reasoning_effort` shape,
|
|
4
|
+
* with `"none"` to disable — the value most local servers honor. A server
|
|
5
|
+
* wanting a different disable token can override via `reasoningShape` or a
|
|
6
|
+
* user extension.
|
|
5
7
|
*/
|
|
6
8
|
import type { AgentContext } from "../host-types.js";
|
|
7
9
|
|
|
@@ -13,6 +15,12 @@ export default function activate(ctx: AgentContext): void {
|
|
|
13
15
|
const apiKey = process.env.OPENAI_API_KEY || "no-key";
|
|
14
16
|
const id = "openai-compatible";
|
|
15
17
|
|
|
18
|
+
ctx.agent.providers.configure(id, {
|
|
19
|
+
reasoningParams: (level) =>
|
|
20
|
+
level === "off"
|
|
21
|
+
? { reasoning_effort: "none" }
|
|
22
|
+
: { reasoning_effort: level === "xhigh" ? "high" : level },
|
|
23
|
+
});
|
|
16
24
|
ctx.agent.providers.register({ id, apiKey, baseURL, models: [] });
|
|
17
25
|
fetchModels(baseURL, apiKey).then((models) => {
|
|
18
26
|
if (models.length === 0) return;
|
package/src/agent/subagent.ts
CHANGED
|
@@ -157,7 +157,7 @@ export async function runSubagent(opts: SubagentOptions): Promise<string> {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
const onChunk = bus && tool.showOutput !== false
|
|
160
|
-
? (chunk: string) => { bus.emit("agent:tool-output-chunk", { chunk }); }
|
|
160
|
+
? (chunk: string) => { bus.emit("agent:tool-output-chunk", { chunk, toolCallId: tc.id }); }
|
|
161
161
|
: undefined;
|
|
162
162
|
|
|
163
163
|
const result = await tool.execute(args, onChunk);
|
|
@@ -216,7 +216,7 @@ function highlightInlineChanges(
|
|
|
216
216
|
result += palette.rowBg + preserveBg(text, palette.rowBg);
|
|
217
217
|
} else {
|
|
218
218
|
// Changed tokens: emphasis background, no syntax highlighting (emphasis stands out)
|
|
219
|
-
result += palette.emphBg +
|
|
219
|
+
result += palette.emphBg + tokens[i].text + p.reset;
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
return result;
|
|
@@ -337,18 +337,29 @@ function renderUnifiedHunk(hunk: DiffHunk, layout: UnifiedLayout): string[] {
|
|
|
337
337
|
const out: string[] = [];
|
|
338
338
|
|
|
339
339
|
const pairs = findChangePairs(hunk);
|
|
340
|
-
const renderedAsPartOfPair = new Set<number>();
|
|
341
340
|
const bgWidth = Math.max(1, textWidth - noW - 3);
|
|
342
341
|
const gutter = (n: string): string => `${p.dim}${n} │${p.reset} `;
|
|
343
342
|
|
|
344
343
|
const change = (no: string, sigil: string, bg: string, fg: string, text: string): string => {
|
|
345
344
|
if (!gutterLine) {
|
|
346
|
-
return `${bg}${fg}${
|
|
345
|
+
return `${bg}${padToWidth(`${fg}${no} ${sigil}${p.diffText} ${preserveBg(text, bg)}`, textWidth)}${p.reset}`;
|
|
347
346
|
}
|
|
348
|
-
if (useTrueColor) return gutter(no) + padToWidth(`${bg}${fg}${sigil} ${preserveBg(text, bg)}`, bgWidth) + p.reset;
|
|
347
|
+
if (useTrueColor) return gutter(no) + padToWidth(`${bg}${fg}${sigil}${p.diffText} ${preserveBg(text, bg)}`, bgWidth) + p.reset;
|
|
349
348
|
return `${gutter(no)}${fg}${sigil} ${text}${p.reset}`;
|
|
350
349
|
};
|
|
351
350
|
|
|
351
|
+
const hlCache = new Map<ChangePair, { old: string; new: string }>();
|
|
352
|
+
const highlightedPair = (pair: ChangePair): { old: string; new: string } => {
|
|
353
|
+
let h = hlCache.get(pair);
|
|
354
|
+
if (!h) {
|
|
355
|
+
h = highlightInlineChanges(
|
|
356
|
+
pair.removed.text, pair.added.text, removedPalette, addedPalette, useTrueColor, lang,
|
|
357
|
+
);
|
|
358
|
+
hlCache.set(pair, h);
|
|
359
|
+
}
|
|
360
|
+
return h;
|
|
361
|
+
};
|
|
362
|
+
|
|
352
363
|
for (let i = 0; i < hunk.lines.length; i++) {
|
|
353
364
|
const line = hunk.lines[i];
|
|
354
365
|
const no = String(
|
|
@@ -358,42 +369,29 @@ function renderUnifiedHunk(hunk: DiffHunk, layout: UnifiedLayout): string[] {
|
|
|
358
369
|
if (line.type === "context") {
|
|
359
370
|
const raw = truncateText(line.text, lineTextW);
|
|
360
371
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
361
|
-
// The flush gutter dims only the line number; the code stays normal/highlighted.
|
|
362
372
|
out.push(!gutterLine ? `${p.dim}${no}${p.reset} ${text}` : `${gutter(no)} ${p.dim}${text}${p.reset}`);
|
|
363
373
|
continue;
|
|
364
374
|
}
|
|
365
375
|
|
|
376
|
+
const pair = pairs.get(i);
|
|
366
377
|
if (line.type === "removed") {
|
|
367
|
-
const pair = pairs.get(i);
|
|
368
378
|
let removedText: string;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (pair && pair.removedIdx === i) {
|
|
373
|
-
const highlighted = highlightInlineChanges(
|
|
374
|
-
line.text, pair.added.text, removedPalette, addedPalette, useTrueColor, lang,
|
|
375
|
-
);
|
|
376
|
-
removedText = truncateText(highlighted.old, lineTextW);
|
|
377
|
-
addedText = truncateText(highlighted.new, lineTextW);
|
|
378
|
-
addedNo = String(pair.added.newNo ?? "").padStart(noW);
|
|
379
|
-
renderedAsPartOfPair.add(pair.addedIdx);
|
|
379
|
+
if (pair) {
|
|
380
|
+
removedText = truncateText(highlightedPair(pair).old, lineTextW);
|
|
380
381
|
} else {
|
|
381
382
|
const raw = truncateText(line.text, lineTextW);
|
|
382
383
|
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
383
384
|
}
|
|
384
|
-
|
|
385
385
|
out.push(change(no, "-", p.errorBg, p.error, removedText));
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
} else {
|
|
387
|
+
let addedText: string;
|
|
388
|
+
if (pair) {
|
|
389
|
+
addedText = truncateText(highlightedPair(pair).new, lineTextW);
|
|
390
|
+
} else {
|
|
391
|
+
const raw = truncateText(line.text, lineTextW);
|
|
392
|
+
addedText = lang ? highlightLine(raw, lang) : raw;
|
|
388
393
|
}
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (line.type === "added") {
|
|
393
|
-
if (renderedAsPartOfPair.has(i)) continue;
|
|
394
|
-
const raw = truncateText(line.text, lineTextW);
|
|
395
|
-
const text = lang ? highlightLine(raw, lang) : raw;
|
|
396
|
-
out.push(change(no, "+", p.successBg, p.success, text));
|
|
394
|
+
out.push(change(no, "+", p.successBg, p.success, addedText));
|
|
397
395
|
}
|
|
398
396
|
}
|
|
399
397
|
return out;
|
|
@@ -478,7 +476,7 @@ function renderSplitHunk(hunk: DiffHunk, layout: SplitLayout): string[] {
|
|
|
478
476
|
} else if (row.left.type === "removed") {
|
|
479
477
|
if (useTrueColor) {
|
|
480
478
|
leftCol = padToWidth(
|
|
481
|
-
`${p.errorBg}${p.error}${leftNo}
|
|
479
|
+
`${p.errorBg}${p.error}${leftNo} │${p.diffText} ${preserveBg(leftText, p.errorBg)}`, colWidth,
|
|
482
480
|
) + p.reset;
|
|
483
481
|
} else {
|
|
484
482
|
leftCol = padToWidth(`${p.error}${leftNo} │ ${leftText}${p.reset}`, colWidth);
|
|
@@ -492,7 +490,7 @@ function renderSplitHunk(hunk: DiffHunk, layout: SplitLayout): string[] {
|
|
|
492
490
|
} else if (row.right.type === "added") {
|
|
493
491
|
if (useTrueColor) {
|
|
494
492
|
rightCol = padToWidth(
|
|
495
|
-
`${p.successBg}${p.success}${rightNo}
|
|
493
|
+
`${p.successBg}${p.success}${rightNo} │${p.diffText} ${preserveBg(rightText, p.successBg)}`, colWidth,
|
|
496
494
|
) + p.reset;
|
|
497
495
|
} else {
|
|
498
496
|
rightCol = padToWidth(`${p.success}${rightNo} │ ${rightText}${p.reset}`, colWidth);
|
package/src/utils/palette.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface ColorPalette {
|
|
|
22
22
|
errorBg: string; // subtle red tint for removed lines
|
|
23
23
|
successBgEmph: string; // stronger green for changed tokens
|
|
24
24
|
errorBgEmph: string; // stronger red for changed tokens
|
|
25
|
+
diffText: string; // legible fg for diff row text on the tinted rows
|
|
25
26
|
|
|
26
27
|
// ── Style modifiers ───────────────────────────────────────
|
|
27
28
|
bold: string;
|
|
@@ -38,10 +39,11 @@ const defaultPalette: ColorPalette = {
|
|
|
38
39
|
error: "\x1b[31m", // red
|
|
39
40
|
muted: "\x1b[90m", // gray
|
|
40
41
|
|
|
41
|
-
successBg: "\x1b[48;2;
|
|
42
|
-
errorBg: "\x1b[48;2;
|
|
43
|
-
successBgEmph: "\x1b[48;2;
|
|
44
|
-
errorBgEmph: "\x1b[48;2;
|
|
42
|
+
successBg: "\x1b[48;2;26;70;34m",
|
|
43
|
+
errorBg: "\x1b[48;2;92;32;42m",
|
|
44
|
+
successBgEmph: "\x1b[48;2;38;104;56m",
|
|
45
|
+
errorBgEmph: "\x1b[48;2;124;50;64m",
|
|
46
|
+
diffText: "\x1b[97m", // bright white — readable on the red/green tints
|
|
45
47
|
|
|
46
48
|
bold: "\x1b[1m",
|
|
47
49
|
dim: "\x1b[2m",
|