@pellux/goodvibes-tui 0.26.0 → 0.28.0
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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -4,20 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Line } from '../types/grid.ts';
|
|
6
6
|
import { createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
7
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
7
8
|
import { BasePanel } from './base-panel.ts';
|
|
8
9
|
import type { AgentEvent, TurnEvent } from '@/runtime/index.ts';
|
|
9
10
|
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
10
11
|
import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
11
12
|
import {
|
|
12
13
|
buildEmptyState,
|
|
14
|
+
buildKeyboardHints,
|
|
15
|
+
buildMeterLine,
|
|
13
16
|
buildPanelLine,
|
|
14
17
|
buildStyledPanelLine,
|
|
18
|
+
buildTable,
|
|
15
19
|
buildPanelWorkspace,
|
|
16
20
|
resolveScrollablePanelSection,
|
|
21
|
+
extendPalette,
|
|
17
22
|
DEFAULT_PANEL_PALETTE,
|
|
18
23
|
type PanelWorkspaceSection,
|
|
19
24
|
} from './polish.ts';
|
|
20
|
-
import {
|
|
25
|
+
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
26
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
21
27
|
|
|
22
28
|
// Pricing lookups are provided by ../export/cost-utils.ts (single source of truth).
|
|
23
29
|
|
|
@@ -29,9 +35,7 @@ function formatCost(usd: number): string {
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
function formatTokens(n: number): string {
|
|
32
|
-
|
|
33
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
34
|
-
return String(n);
|
|
38
|
+
return abbreviateCount(n);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
@@ -75,20 +79,13 @@ function buildSparkline(history: number[]): string {
|
|
|
75
79
|
// Colour palette
|
|
76
80
|
// ---------------------------------------------------------------------------
|
|
77
81
|
|
|
78
|
-
const C = {
|
|
79
|
-
header: '#ffffff',
|
|
80
|
-
label: '#aaaaaa',
|
|
81
|
-
value: '#00ff88',
|
|
82
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
82
83
|
cost: '#ffdd44',
|
|
83
|
-
alert: '#ff4444',
|
|
84
|
-
dim: '#555555',
|
|
85
84
|
model: '#88aaff',
|
|
86
85
|
running: '#88aaff',
|
|
87
|
-
done: '#00ff88',
|
|
88
|
-
failed: '#ff4444',
|
|
89
86
|
separator: '#333333',
|
|
90
87
|
bg: '',
|
|
91
|
-
}
|
|
88
|
+
});
|
|
92
89
|
|
|
93
90
|
// ---------------------------------------------------------------------------
|
|
94
91
|
// CostTrackerPanel
|
|
@@ -150,7 +147,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
150
147
|
agentEvents.on('AGENT_SPAWNING', (payload) => {
|
|
151
148
|
this.agents.set(payload.agentId, {
|
|
152
149
|
id: payload.agentId.slice(0, 8),
|
|
153
|
-
task: payload.task
|
|
150
|
+
task: truncateDisplay(payload.task, 40),
|
|
154
151
|
model: 'unknown',
|
|
155
152
|
inputTokens: 0,
|
|
156
153
|
outputTokens: 0,
|
|
@@ -172,8 +169,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
172
169
|
if (rec?.usage) {
|
|
173
170
|
entry.inputTokens = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
|
|
174
171
|
entry.outputTokens = rec.usage.outputTokens;
|
|
175
|
-
|
|
176
|
-
entry.cost = (entry.inputTokens * pricing.input + entry.outputTokens * pricing.output) / 1_000_000;
|
|
172
|
+
entry.cost = calcSessionCost(rec.usage.inputTokens, rec.usage.outputTokens, rec.usage.cacheReadTokens ?? 0, rec.usage.cacheWriteTokens ?? 0, rec.model ?? 'unknown');
|
|
177
173
|
if (rec.model && rec.model !== 'unknown') entry.model = rec.model;
|
|
178
174
|
}
|
|
179
175
|
}
|
|
@@ -205,9 +201,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
205
201
|
if (usage.model) this.sessionModel = usage.model;
|
|
206
202
|
|
|
207
203
|
// Record cost delta for sparkline
|
|
208
|
-
const
|
|
209
|
-
const billableInput = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
210
|
-
const totalCost = (billableInput * pricing.input + usage.output * pricing.output) / 1_000_000;
|
|
204
|
+
const totalCost = calcSessionCost(usage.input, usage.output, usage.cacheRead, usage.cacheWrite, this.sessionModel);
|
|
211
205
|
const delta = Math.max(0, totalCost - this.lastSessionCost);
|
|
212
206
|
this.lastSessionCost = totalCost;
|
|
213
207
|
this.costHistory.push(delta);
|
|
@@ -267,13 +261,12 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
267
261
|
render(width: number, height: number): Line[] {
|
|
268
262
|
if (height <= 0 || width <= 0) return [];
|
|
269
263
|
|
|
270
|
-
const pricing = getPricing(this.sessionModel);
|
|
271
264
|
const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
|
|
272
|
-
const sessionCost = (
|
|
265
|
+
const sessionCost = calcSessionCost(this.sessionUsage.input, this.sessionUsage.output, this.sessionUsage.cacheRead, this.sessionUsage.cacheWrite, this.sessionModel);
|
|
273
266
|
const overBudget = this.budgetThreshold > 0 && sessionCost > this.budgetThreshold;
|
|
274
267
|
const sparkline = buildSparkline(this.costHistory);
|
|
275
268
|
const costStr = formatCost(sessionCost);
|
|
276
|
-
const costFg = overBudget ? C.
|
|
269
|
+
const costFg = overBudget ? C.bad : C.cost;
|
|
277
270
|
const budgetStr = this.budgetThreshold > 0
|
|
278
271
|
? ` / ${formatCost(this.budgetThreshold)}`
|
|
279
272
|
: '';
|
|
@@ -281,7 +274,21 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
281
274
|
const sessionLines: Line[] = [
|
|
282
275
|
this.renderKeyValue(width, ' Total', `${costStr}${budgetStr}${alertStr}`, costFg),
|
|
283
276
|
];
|
|
284
|
-
|
|
277
|
+
// Budget meter — the single most important glance for this panel: how much
|
|
278
|
+
// of the configured budget the session has consumed. Only shown when a
|
|
279
|
+
// budget is set (otherwise the bar would be meaningless).
|
|
280
|
+
if (this.budgetThreshold > 0) {
|
|
281
|
+
const ratio = sessionCost / this.budgetThreshold;
|
|
282
|
+
const BAR_W = 24;
|
|
283
|
+
const filled = Math.max(0, Math.min(BAR_W, Math.round(ratio * BAR_W)));
|
|
284
|
+
const meterFg = overBudget ? C.bad : ratio >= 0.8 ? C.warn : C.good;
|
|
285
|
+
const pctStr = `${Math.round(ratio * 100)}%`;
|
|
286
|
+
sessionLines.push(buildMeterLine(width, filled, BAR_W,
|
|
287
|
+
{ filled: meterFg, empty: C.separator, label: C.label },
|
|
288
|
+
{ prefix: ' Budget [', suffix: `] ${pctStr}` },
|
|
289
|
+
));
|
|
290
|
+
}
|
|
291
|
+
if (sparkline.length > 0) sessionLines.push(this.renderLabeledLine(width, ' Trend', sparkline, C.good));
|
|
285
292
|
sessionLines.push(this.renderKeyValue(width, ' Input', formatTokens(this.sessionUsage.input), C.label));
|
|
286
293
|
sessionLines.push(this.renderKeyValue(width, ' Output', formatTokens(this.sessionUsage.output), C.label));
|
|
287
294
|
if (this.sessionUsage.cacheRead > 0 || this.sessionUsage.cacheWrite > 0) {
|
|
@@ -299,35 +306,63 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
299
306
|
];
|
|
300
307
|
|
|
301
308
|
const agentList = Array.from(this.agents.values());
|
|
309
|
+
const scrollableAgents = agentList.length > 5;
|
|
310
|
+
// Footer hints adapt to whether the agent list is long enough to scroll.
|
|
311
|
+
const hintRow = scrollableAgents
|
|
312
|
+
? buildKeyboardHints(width, [
|
|
313
|
+
{ keys: 'Up/Down', label: 'scroll agents' },
|
|
314
|
+
{ keys: 'PgUp/PgDn', label: 'page' },
|
|
315
|
+
], DEFAULT_PANEL_PALETTE)
|
|
316
|
+
: buildKeyboardHints(width, [
|
|
317
|
+
{ keys: '/cost budget <usd>', label: 'set budget alert' },
|
|
318
|
+
], DEFAULT_PANEL_PALETTE);
|
|
302
319
|
if (agentList.length > 0) {
|
|
303
320
|
const planCost = agentList.reduce((sum, a) => sum + a.cost, 0);
|
|
321
|
+
const running = agentList.filter((a) => a.status === 'running').length;
|
|
322
|
+
const failed = agentList.filter((a) => a.status === 'failed').length;
|
|
304
323
|
const agentRows: Line[] = [
|
|
305
|
-
|
|
306
|
-
|
|
324
|
+
buildStyledPanelLine(width, [
|
|
325
|
+
{ text: ' Plan total ', fg: C.label },
|
|
326
|
+
{ text: formatCost(planCost + sessionCost), fg: C.cost, bold: true },
|
|
327
|
+
{ text: ` ${agentList.length} agent${agentList.length === 1 ? '' : 's'}`, fg: C.dim },
|
|
328
|
+
...(running > 0 ? [{ text: ` ${running} running`, fg: C.running }] : []),
|
|
329
|
+
...(failed > 0 ? [{ text: ` ${failed} failed`, fg: C.bad }] : []),
|
|
330
|
+
]),
|
|
331
|
+
// Per-agent cost ledger as an aligned table — agent, model, tokens, cost
|
|
332
|
+
// line up in columns instead of wrapping across two ragged rows.
|
|
333
|
+
...buildTable(
|
|
334
|
+
width,
|
|
335
|
+
[
|
|
336
|
+
{ label: 'Agent', width: 14 },
|
|
337
|
+
{ label: 'Model', width: 18 },
|
|
338
|
+
{ label: 'In', width: 7, align: 'right' },
|
|
339
|
+
{ label: 'Out', width: 7, align: 'right' },
|
|
340
|
+
{ label: 'Cost', align: 'right' },
|
|
341
|
+
],
|
|
342
|
+
agentList.map((agent) => {
|
|
343
|
+
const statusFg = agent.status === 'running' ? C.running
|
|
344
|
+
: agent.status === 'failed' ? C.bad
|
|
345
|
+
: C.good;
|
|
346
|
+
const statusIcon = agent.status === 'running' ? '…'
|
|
347
|
+
: agent.status === 'failed' ? '✕'
|
|
348
|
+
: '✓';
|
|
349
|
+
return {
|
|
350
|
+
cells: [
|
|
351
|
+
{ text: `${statusIcon} ${agent.id}`, fg: statusFg },
|
|
352
|
+
{ text: agent.model, fg: C.model },
|
|
353
|
+
{ text: agent.inputTokens > 0 ? formatTokens(agent.inputTokens) : '-', fg: C.dim },
|
|
354
|
+
{ text: agent.outputTokens > 0 ? formatTokens(agent.outputTokens) : '-', fg: C.dim },
|
|
355
|
+
{ text: agent.cost > 0 ? formatCost(agent.cost) : '-', fg: agent.cost > 0 ? C.cost : C.dim },
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}),
|
|
359
|
+
DEFAULT_PANEL_PALETTE,
|
|
360
|
+
),
|
|
307
361
|
];
|
|
308
|
-
for (const agent of agentList) {
|
|
309
|
-
const statusFg = agent.status === 'running' ? C.running
|
|
310
|
-
: agent.status === 'failed' ? C.failed
|
|
311
|
-
: C.done;
|
|
312
|
-
const statusIcon = agent.status === 'running' ? '…'
|
|
313
|
-
: agent.status === 'failed' ? '✕'
|
|
314
|
-
: '✓';
|
|
315
|
-
|
|
316
|
-
const agentLabel = `${statusIcon} ${agent.id}`;
|
|
317
|
-
const taskText = agent.task;
|
|
318
|
-
agentRows.push(this.renderAgent(width, agentLabel, taskText, statusFg));
|
|
319
|
-
|
|
320
|
-
if (agent.inputTokens > 0 || agent.outputTokens > 0) {
|
|
321
|
-
const tokenInfo = ` in:${formatTokens(agent.inputTokens)} out:${formatTokens(agent.outputTokens)} ${formatCost(agent.cost)}`;
|
|
322
|
-
agentRows.push(this.renderLabeledLine(width, '', tokenInfo, C.dim));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
362
|
const sessionSection: PanelWorkspaceSection = sections[0]!;
|
|
326
363
|
const agentsSection = resolveScrollablePanelSection(width, height, {
|
|
327
364
|
intro: 'Track per-session and per-agent token spend using model pricing and live usage snapshots.',
|
|
328
|
-
footerLines: [
|
|
329
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll agents', DEFAULT_PANEL_PALETTE.dim]]),
|
|
330
|
-
],
|
|
365
|
+
footerLines: [hintRow],
|
|
331
366
|
palette: DEFAULT_PANEL_PALETTE,
|
|
332
367
|
beforeSections: [sessionSection],
|
|
333
368
|
section: {
|
|
@@ -335,6 +370,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
335
370
|
scrollableLines: agentRows,
|
|
336
371
|
scrollOffset: this.scrollOffset,
|
|
337
372
|
minRows: 4,
|
|
373
|
+
appendWindowSummary: scrollableAgents ? { dimColor: DEFAULT_PANEL_PALETTE.dim } : undefined,
|
|
338
374
|
},
|
|
339
375
|
});
|
|
340
376
|
this.scrollOffset = agentsSection.scrollOffset;
|
|
@@ -346,7 +382,9 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
346
382
|
width,
|
|
347
383
|
' No agents spawned this session',
|
|
348
384
|
'Agent-level cost estimates appear here once delegated or background agents start running.',
|
|
349
|
-
[
|
|
385
|
+
[
|
|
386
|
+
{ command: '/cost budget <usd>', summary: 'set a session budget alert to track spend against a cap' },
|
|
387
|
+
],
|
|
350
388
|
DEFAULT_PANEL_PALETTE,
|
|
351
389
|
),
|
|
352
390
|
});
|
|
@@ -356,9 +394,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
356
394
|
title: ' Cost Tracker',
|
|
357
395
|
intro: 'Track per-session and per-agent token spend using model pricing and live usage snapshots.',
|
|
358
396
|
sections,
|
|
359
|
-
footerLines: [
|
|
360
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll agents', DEFAULT_PANEL_PALETTE.dim]]),
|
|
361
|
-
],
|
|
397
|
+
footerLines: [hintRow],
|
|
362
398
|
palette: DEFAULT_PANEL_PALETTE,
|
|
363
399
|
});
|
|
364
400
|
}
|
|
@@ -370,7 +406,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
370
406
|
private renderKeyValue(width: number, label: string, value: string, valueFg: string): Line {
|
|
371
407
|
const LABEL_W = 10;
|
|
372
408
|
return buildStyledPanelLine(width, [
|
|
373
|
-
{ text: label
|
|
409
|
+
{ text: fitDisplay(label, LABEL_W), fg: C.label, bg: C.bg },
|
|
374
410
|
{ text: ': ', fg: C.dim, bg: C.bg },
|
|
375
411
|
{ text: value, fg: valueFg, bg: C.bg, bold: true },
|
|
376
412
|
]);
|
|
@@ -378,23 +414,9 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
378
414
|
|
|
379
415
|
private renderLabeledLine(width: number, label: string, value: string, valueFg: string): Line {
|
|
380
416
|
return buildStyledPanelLine(width, [
|
|
381
|
-
...(label.length > 0 ? [{ text: `${label
|
|
417
|
+
...(label.length > 0 ? [{ text: `${fitDisplay(label, 10)} `, fg: C.label }] : []),
|
|
382
418
|
{ text: value, fg: valueFg },
|
|
383
419
|
]);
|
|
384
420
|
}
|
|
385
421
|
|
|
386
|
-
private renderAgent(width: number, label: string, task: string, fg: string): Line {
|
|
387
|
-
const LABEL_W = 12;
|
|
388
|
-
const remaining = width - LABEL_W - 1;
|
|
389
|
-
const trimmed = task.length > remaining ? task.slice(0, remaining - 3) + '...' : task;
|
|
390
|
-
return buildStyledPanelLine(width, [
|
|
391
|
-
{ text: `${label.padEnd(LABEL_W).slice(0, LABEL_W)} `, fg, bold: true },
|
|
392
|
-
{ text: trimmed, fg: C.label },
|
|
393
|
-
]);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
private renderDivider(width: number): Line {
|
|
397
|
-
return buildStyledPanelLine(width, [{ text: '─'.repeat(width), fg: C.separator }]);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
422
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BasePanel } from './base-panel.ts';
|
|
2
2
|
import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
|
|
3
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import type { TurnEvent } from '@/runtime/index.ts';
|
|
4
5
|
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
5
6
|
import type { Orchestrator } from '../core/orchestrator';
|
|
@@ -11,8 +12,11 @@ import {
|
|
|
11
12
|
resolveScrollablePanelSection,
|
|
12
13
|
resolveStackedScrollableSections,
|
|
13
14
|
DEFAULT_PANEL_PALETTE,
|
|
15
|
+
extendPalette,
|
|
14
16
|
type PanelWorkspaceSection,
|
|
15
17
|
} from './polish.ts';
|
|
18
|
+
import { abbreviateCount } from '../utils/format-number.ts';
|
|
19
|
+
import { formatLatencyMs } from '../utils/format-duration.ts';
|
|
16
20
|
|
|
17
21
|
// ---------------------------------------------------------------------------
|
|
18
22
|
// Types
|
|
@@ -52,24 +56,12 @@ const MAX_ERROR_LOG = 20;
|
|
|
52
56
|
// Colors
|
|
53
57
|
// ---------------------------------------------------------------------------
|
|
54
58
|
|
|
55
|
-
const C = {
|
|
56
|
-
title: '#00ffff',
|
|
57
|
-
ok: '#5fd700',
|
|
58
|
-
error: '#ff5f5f',
|
|
59
|
-
warn: '#ffaf00',
|
|
60
|
-
label: '244',
|
|
61
|
-
value: '252',
|
|
62
|
-
dim: '240',
|
|
59
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
63
60
|
provName: '#e2e8f0',
|
|
64
|
-
separator: '#374151',
|
|
65
61
|
input: '#00ffff',
|
|
66
62
|
output: '#d000ff',
|
|
67
|
-
latGood: '#5fd700',
|
|
68
|
-
latWarn: '#ffaf00',
|
|
69
|
-
latBad: '#ff5f5f',
|
|
70
|
-
sectionHdr: '238',
|
|
71
63
|
colHdr: '242',
|
|
72
|
-
}
|
|
64
|
+
});
|
|
73
65
|
|
|
74
66
|
const LATENCY_WARN_MS = 2_000;
|
|
75
67
|
const LATENCY_BAD_MS = 5_000;
|
|
@@ -79,16 +71,11 @@ const LATENCY_BAD_MS = 5_000;
|
|
|
79
71
|
// ---------------------------------------------------------------------------
|
|
80
72
|
|
|
81
73
|
function fmtTok(n: number): string {
|
|
82
|
-
|
|
83
|
-
if (n < 1_000_000) return (n / 1000).toFixed(1) + 'k';
|
|
84
|
-
return (n / 1_000_000).toFixed(2) + 'M';
|
|
74
|
+
return abbreviateCount(n, { guard: 10_000, decimals: 1, mDecimals: 2 });
|
|
85
75
|
}
|
|
86
76
|
|
|
87
77
|
function fmtMs(ms: number): string {
|
|
88
|
-
|
|
89
|
-
if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
90
|
-
if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
|
|
91
|
-
return `${Math.round(ms)}ms`;
|
|
78
|
+
return formatLatencyMs(ms);
|
|
92
79
|
}
|
|
93
80
|
|
|
94
81
|
function fmtAgo(ts: number): string {
|
|
@@ -99,9 +86,9 @@ function fmtAgo(ts: number): string {
|
|
|
99
86
|
}
|
|
100
87
|
|
|
101
88
|
function latColor(ms: number): string {
|
|
102
|
-
if (ms >= LATENCY_BAD_MS) return C.
|
|
103
|
-
if (ms >= LATENCY_WARN_MS) return C.
|
|
104
|
-
return C.
|
|
89
|
+
if (ms >= LATENCY_BAD_MS) return C.bad;
|
|
90
|
+
if (ms >= LATENCY_WARN_MS) return C.warn;
|
|
91
|
+
return C.good;
|
|
105
92
|
}
|
|
106
93
|
|
|
107
94
|
function statusCodeFromError(msg: string): number {
|
|
@@ -326,10 +313,21 @@ export class DebugPanel extends BasePanel {
|
|
|
326
313
|
}
|
|
327
314
|
}
|
|
328
315
|
|
|
316
|
+
const latestError = this._errors[this._errors.length - 1];
|
|
317
|
+
const footerLines = latestError
|
|
318
|
+
? [buildStyledPanelLine(width, [
|
|
319
|
+
{ text: ' ✕ latest error ', fg: C.bad },
|
|
320
|
+
{ text: truncateDisplay(latestError.errorMessage ?? 'unknown error', Math.max(0, width - 18)), fg: C.dim },
|
|
321
|
+
])]
|
|
322
|
+
: [buildStyledPanelLine(width, [
|
|
323
|
+
{ text: ' Live feed — newest calls at the bottom; auto-follows.', fg: C.dim },
|
|
324
|
+
])];
|
|
325
|
+
|
|
329
326
|
return buildPanelWorkspace(width, height, {
|
|
330
327
|
title: ' API Debug',
|
|
331
328
|
intro: 'Recent provider calls, token deltas, latency, status codes, and error history.',
|
|
332
329
|
sections,
|
|
330
|
+
footerLines,
|
|
333
331
|
palette: DEFAULT_PANEL_PALETTE,
|
|
334
332
|
});
|
|
335
333
|
}
|
|
@@ -341,16 +339,42 @@ export class DebugPanel extends BasePanel {
|
|
|
341
339
|
private _renderSummary(width: number): Line[] {
|
|
342
340
|
const errCount = this._totalErrors;
|
|
343
341
|
const okCount = this._totalCalls - this._totalErrors;
|
|
344
|
-
|
|
342
|
+
const last = this._calls[this._calls.length - 1];
|
|
343
|
+
const recent = this._calls.slice(-10);
|
|
344
|
+
const avgLat = recent.length > 0
|
|
345
|
+
? Math.round(recent.reduce((s, c) => s + c.latencyMs, 0) / recent.length)
|
|
346
|
+
: 0;
|
|
347
|
+
const sessionTokens = this._calls.reduce((s, c) => s + c.inputTokens + c.outputTokens, 0);
|
|
348
|
+
|
|
349
|
+
const lines: Line[] = [
|
|
345
350
|
buildStyledPanelLine(width, [
|
|
346
|
-
{ text: ' Calls
|
|
351
|
+
{ text: ' Calls ', fg: C.label },
|
|
347
352
|
{ text: String(this._totalCalls), fg: C.value },
|
|
348
|
-
{ text: '
|
|
349
|
-
{ text: String(okCount), fg: C.
|
|
350
|
-
{ text: '
|
|
351
|
-
{ text: String(errCount), fg: errCount > 0 ? C.
|
|
353
|
+
{ text: ' OK ', fg: C.label },
|
|
354
|
+
{ text: String(okCount), fg: C.good },
|
|
355
|
+
{ text: ' Errors ', fg: C.label },
|
|
356
|
+
{ text: String(errCount), fg: errCount > 0 ? C.bad : C.dim },
|
|
357
|
+
{ text: ' Avg latency ', fg: C.label },
|
|
358
|
+
{ text: fmtMs(avgLat), fg: avgLat > 0 ? latColor(avgLat) : C.dim },
|
|
359
|
+
{ text: ' Tokens ', fg: C.label },
|
|
360
|
+
{ text: fmtTok(sessionTokens), fg: C.value },
|
|
352
361
|
]),
|
|
353
362
|
];
|
|
363
|
+
// Live status: most recent call (latency / age) or wiring hint.
|
|
364
|
+
if (last) {
|
|
365
|
+
lines.push(buildStyledPanelLine(width, [
|
|
366
|
+
{ text: ' Last ', fg: C.label },
|
|
367
|
+
{ text: last.status === 'ok' ? '✓ ' : '✕ ', fg: last.status === 'ok' ? C.good : C.bad },
|
|
368
|
+
{ text: fitDisplay(last.model, 22), fg: C.value },
|
|
369
|
+
{ text: ' ' + fmtMs(last.latencyMs), fg: latColor(last.latencyMs) },
|
|
370
|
+
{ text: ' ' + fmtAgo(last.ts), fg: C.dim },
|
|
371
|
+
]));
|
|
372
|
+
} else if (!this._orchestrator) {
|
|
373
|
+
lines.push(buildStyledPanelLine(width, [
|
|
374
|
+
{ text: ' Per-call token deltas need the orchestrator wired (wireOrchestrator).', fg: C.dim },
|
|
375
|
+
]));
|
|
376
|
+
}
|
|
377
|
+
return lines;
|
|
354
378
|
}
|
|
355
379
|
|
|
356
380
|
private _renderCallLog(width: number): Line[] {
|
|
@@ -366,15 +390,15 @@ export class DebugPanel extends BasePanel {
|
|
|
366
390
|
private _callLogHeader(width: number): Line {
|
|
367
391
|
// Layout: time(8) status(2) provider(12) model(20) in(8) out(8) lat(8)
|
|
368
392
|
const header = ' Time S Provider Model In Out Lat';
|
|
369
|
-
return this._textLine(header
|
|
393
|
+
return this._textLine(truncateDisplay(header, width), C.colHdr, width, { dim: true });
|
|
370
394
|
}
|
|
371
395
|
|
|
372
396
|
private _callLogRow(e: ApiCallEntry, width: number): Line {
|
|
373
397
|
const timeStr = fmtAgo(e.ts).padEnd(8);
|
|
374
398
|
const statusChar = e.status === 'ok' ? '✓' : '✕';
|
|
375
|
-
const statusFg = e.status === 'ok' ? C.
|
|
376
|
-
const provStr = e.provider
|
|
377
|
-
const modelStr = e.model
|
|
399
|
+
const statusFg = e.status === 'ok' ? C.good : C.bad;
|
|
400
|
+
const provStr = fitDisplay(e.provider, 12);
|
|
401
|
+
const modelStr = fitDisplay(e.model, 20);
|
|
378
402
|
const inStr = fmtTok(e.inputTokens).padStart(8);
|
|
379
403
|
const outStr = fmtTok(e.outputTokens).padStart(8);
|
|
380
404
|
const latStr = fmtMs(e.latencyMs).padStart(8);
|
|
@@ -391,7 +415,7 @@ export class DebugPanel extends BasePanel {
|
|
|
391
415
|
|
|
392
416
|
// Append status code for errors
|
|
393
417
|
if (e.status === 'error' && e.statusCode > 0) {
|
|
394
|
-
segments.push({ text: ` [${e.statusCode}]`, fg: C.
|
|
418
|
+
segments.push({ text: ` [${e.statusCode}]`, fg: C.bad });
|
|
395
419
|
}
|
|
396
420
|
|
|
397
421
|
return buildStyledPanelLine(
|
|
@@ -412,9 +436,9 @@ export class DebugPanel extends BasePanel {
|
|
|
412
436
|
private _errorRow(e: ApiCallEntry, width: number): Line {
|
|
413
437
|
const timeStr = fmtAgo(e.ts).padEnd(8);
|
|
414
438
|
const codeStr = e.statusCode > 0 ? `[${e.statusCode}] ` : '';
|
|
415
|
-
const msgStr = (e.errorMessage ?? 'unknown error'
|
|
439
|
+
const msgStr = truncateDisplay(e.errorMessage ?? 'unknown error', Math.max(0, width - 12 - codeStr.length));
|
|
416
440
|
const full = ` ${timeStr} ${codeStr}${msgStr}`;
|
|
417
|
-
return this._textLine(full
|
|
441
|
+
return this._textLine(truncateDisplay(full, width), C.bad, width);
|
|
418
442
|
}
|
|
419
443
|
|
|
420
444
|
// -------------------------------------------------------------------------
|
package/src/panels/diff-panel.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Line } from '../types/grid.ts';
|
|
6
6
|
import { createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
7
|
+
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
7
8
|
import { BasePanel } from './base-panel.ts';
|
|
8
9
|
import {
|
|
9
10
|
buildBodyText,
|
|
@@ -21,14 +22,18 @@ import {
|
|
|
21
22
|
|
|
22
23
|
const COLOR = {
|
|
23
24
|
addition: '#00ff88',
|
|
25
|
+
additionBg: '#001a0d',
|
|
24
26
|
deletion: '#ff4444',
|
|
27
|
+
deletionBg: '#1a0000',
|
|
25
28
|
hunk: '#88aaff',
|
|
29
|
+
hunkBg: '#0a0a1a',
|
|
26
30
|
header: '#aaaaaa',
|
|
27
31
|
lineNum: '#555555',
|
|
28
32
|
lineNumAdd: '#00aa55',
|
|
29
33
|
lineNumDel: '#aa2222',
|
|
30
34
|
filename: '#ffffff',
|
|
31
35
|
tabActive: '#ffffff',
|
|
36
|
+
tabActiveBg: '#333333',
|
|
32
37
|
tabInactive: '#666666',
|
|
33
38
|
tabBg: '#222222',
|
|
34
39
|
context: '#888888',
|
|
@@ -47,6 +52,17 @@ interface DiffEntry {
|
|
|
47
52
|
semanticSummary?: string;
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
/** +added / -removed line counts for a parsed diff entry. */
|
|
56
|
+
function diffStat(entry: DiffEntry): { added: number; removed: number } {
|
|
57
|
+
let added = 0;
|
|
58
|
+
let removed = 0;
|
|
59
|
+
for (const line of entry.lines) {
|
|
60
|
+
if (line.kind === 'addition') added++;
|
|
61
|
+
else if (line.kind === 'deletion') removed++;
|
|
62
|
+
}
|
|
63
|
+
return { added, removed };
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
type LineKind = 'addition' | 'deletion' | 'context' | 'hunk' | 'header';
|
|
51
67
|
|
|
52
68
|
interface ParsedLine {
|
|
@@ -147,9 +163,7 @@ function makeLine(
|
|
|
147
163
|
const LEFT_W = 5;
|
|
148
164
|
const usedForNums = LEFT_W + 1 + LEFT_W + 1 + 2; // 14
|
|
149
165
|
const contentWidth = Math.max(0, width - usedForNums);
|
|
150
|
-
const truncated = content
|
|
151
|
-
? content.slice(0, contentWidth)
|
|
152
|
-
: content;
|
|
166
|
+
const truncated = truncateDisplay(content, contentWidth);
|
|
153
167
|
return buildStyledPanelLine(width, [
|
|
154
168
|
{ text: leftNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
|
|
155
169
|
{ text: ' ', fg: '', bg },
|
|
@@ -161,8 +175,7 @@ function makeLine(
|
|
|
161
175
|
}
|
|
162
176
|
|
|
163
177
|
function renderText(width: number, text: string, fg: string, bg: string, bold = false): Line {
|
|
164
|
-
|
|
165
|
-
return buildStyledPanelLine(width, [{ text: truncated, fg, bg, bold }]);
|
|
178
|
+
return buildStyledPanelLine(width, [{ text: truncateDisplay(text, width), fg, bg, bold }]);
|
|
166
179
|
}
|
|
167
180
|
|
|
168
181
|
// ---------------------------------------------------------------------------
|
|
@@ -448,44 +461,60 @@ export class DiffPanel extends BasePanel {
|
|
|
448
461
|
|
|
449
462
|
private renderTabBar(width: number): Line {
|
|
450
463
|
const cells: Line = [];
|
|
464
|
+
const push = (ch: string, fg: string, bg: string, bold = false): void => {
|
|
465
|
+
const cw = getDisplayWidth(ch);
|
|
466
|
+
if (cells.length + cw > width) return;
|
|
467
|
+
cells.push(createStyledCell(ch, { fg, bg, bold }));
|
|
468
|
+
if (cw === 2 && cells.length < width) cells.push(createStyledCell('', { fg, bg }));
|
|
469
|
+
};
|
|
451
470
|
|
|
452
471
|
for (let i = 0; i < this.entries.length; i++) {
|
|
453
472
|
const entry = this.entries[i]!;
|
|
454
473
|
const active = i === this.selectedFile;
|
|
455
|
-
const
|
|
474
|
+
const stat = diffStat(entry);
|
|
456
475
|
const fg = active ? COLOR.tabActive : COLOR.tabInactive;
|
|
457
|
-
const bg = active ?
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
476
|
+
const bg = active ? COLOR.tabActiveBg : COLOR.tabBg;
|
|
477
|
+
// Active file gets a leading marker; every tab shows +adds/-dels at a glance.
|
|
478
|
+
const marker = active ? '▸ ' : ' ';
|
|
479
|
+
const label = `${marker}${basename(entry.filePath)} `;
|
|
480
|
+
for (const ch of label) push(ch, fg, bg, active);
|
|
481
|
+
if (stat.added > 0) for (const ch of `+${stat.added}`) push(ch, COLOR.addition, bg, active);
|
|
482
|
+
if (stat.added > 0 && stat.removed > 0) push(' ', fg, bg);
|
|
483
|
+
if (stat.removed > 0) for (const ch of `-${stat.removed}`) push(ch, COLOR.deletion, bg, active);
|
|
484
|
+
push(' ', fg, bg);
|
|
485
|
+
push('│', COLOR.lineNum, COLOR.tabBg);
|
|
467
486
|
}
|
|
468
487
|
|
|
469
|
-
// Fill remaining
|
|
470
488
|
while (cells.length < width) {
|
|
471
489
|
cells.push(createStyledCell(' ', { fg: '', bg: COLOR.tabBg }));
|
|
472
490
|
}
|
|
473
|
-
|
|
474
491
|
return cells.slice(0, width);
|
|
475
492
|
}
|
|
476
493
|
|
|
477
494
|
// ── Status bar ───────────────────────────────────────────────────────────
|
|
478
495
|
|
|
479
496
|
private renderStatusBar(width: number, entry: DiffEntry | null): Line {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
const text
|
|
488
|
-
|
|
497
|
+
if (!entry) {
|
|
498
|
+
return buildStyledPanelLine(width, [{ text: ' No file', fg: COLOR.tabInactive, bg: COLOR.statusBar }], { fillBg: COLOR.statusBar });
|
|
499
|
+
}
|
|
500
|
+
const stat = diffStat(entry);
|
|
501
|
+
// Keep the file path display-width-aware so a long/wide path can't overflow.
|
|
502
|
+
const pathBudget = Math.max(8, Math.floor(width / 2));
|
|
503
|
+
const fileInfo = truncateDisplay(entry.filePath, pathBudget);
|
|
504
|
+
const segments: Array<{ text: string; fg: string; bg?: string; bold?: boolean }> = [
|
|
505
|
+
{ text: ` ${fileInfo} `, fg: COLOR.filename, bg: COLOR.statusBar, bold: true },
|
|
506
|
+
{ text: `[${this.selectedFile + 1}/${this.entries.length}]`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
507
|
+
{ text: ' +', fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
508
|
+
{ text: String(stat.added), fg: COLOR.addition, bg: COLOR.statusBar },
|
|
509
|
+
{ text: ' -', fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
510
|
+
{ text: String(stat.removed), fg: COLOR.deletion, bg: COLOR.statusBar },
|
|
511
|
+
{ text: ` L${this.scrollOffset + 1}/${entry.lines.length}`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
512
|
+
{ text: ' Tab next ↑/↓ scroll', fg: COLOR.context, bg: COLOR.statusBar },
|
|
513
|
+
];
|
|
514
|
+
if (entry.semanticSummary) {
|
|
515
|
+
segments.push({ text: ` ◈ ${entry.semanticSummary}`, fg: COLOR.hunk, bg: COLOR.statusBar });
|
|
516
|
+
}
|
|
517
|
+
return buildStyledPanelLine(width, segments, { fillBg: COLOR.statusBar });
|
|
489
518
|
}
|
|
490
519
|
|
|
491
520
|
// ── Parsed line ──────────────────────────────────────────────────────────
|
|
@@ -496,11 +525,11 @@ export class DiffPanel extends BasePanel {
|
|
|
496
525
|
|
|
497
526
|
switch (pl.kind) {
|
|
498
527
|
case 'addition':
|
|
499
|
-
return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition,
|
|
528
|
+
return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, COLOR.additionBg, COLOR.lineNumAdd, true);
|
|
500
529
|
case 'deletion':
|
|
501
|
-
return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion,
|
|
530
|
+
return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, COLOR.deletionBg, COLOR.lineNumDel, false);
|
|
502
531
|
case 'hunk':
|
|
503
|
-
return renderText(width, pl.text, COLOR.hunk,
|
|
532
|
+
return renderText(width, pl.text, COLOR.hunk, COLOR.hunkBg, false);
|
|
504
533
|
case 'header':
|
|
505
534
|
return renderText(width, pl.text, COLOR.header, '', false);
|
|
506
535
|
case 'context':
|