@pellux/goodvibes-tui 0.18.23 → 0.19.1
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 +71 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -3
- package/src/core/conversation-rendering.ts +8 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/daemon/cli.ts +54 -0
- package/src/input/commands/diff-runtime.ts +6 -5
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/shell-core.ts +3 -2
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/subscription-runtime.ts +4 -4
- package/src/input/handler.ts +8 -10
- package/src/input/model-picker.ts +6 -2
- package/src/input/panel-integration-actions.ts +2 -1
- package/src/input/settings-modal-types.ts +60 -0
- package/src/input/settings-modal.ts +83 -65
- package/src/main.ts +52 -0
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +1 -0
- package/src/panels/automation-control-panel.ts +1 -0
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +1 -0
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +1 -0
- package/src/panels/diff-panel.ts +2 -0
- package/src/panels/file-explorer-panel.ts +51 -31
- package/src/panels/file-preview-panel.ts +57 -35
- package/src/panels/git-panel.ts +12 -13
- package/src/panels/hooks-panel.ts +3 -1
- package/src/panels/incident-review-panel.ts +4 -2
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +1 -0
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +3 -1
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +1 -0
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +3 -0
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +1 -0
- package/src/panels/polish.ts +51 -2
- package/src/panels/provider-accounts-panel.ts +1 -0
- package/src/panels/provider-health-panel.ts +6 -8
- package/src/panels/routes-panel.ts +3 -1
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +19 -2
- package/src/panels/security-panel.ts +17 -15
- package/src/panels/services-panel.ts +6 -4
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +3 -1
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +1 -0
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +1 -0
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +1 -0
- package/src/panels/worktree-panel.ts +1 -0
- package/src/panels/wrfc-panel.ts +2 -0
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/buffer.ts +12 -1
- package/src/renderer/help-overlay.ts +14 -3
- package/src/renderer/model-picker-overlay.ts +9 -2
- package/src/renderer/settings-modal-helpers.ts +27 -0
- package/src/renderer/settings-modal.ts +18 -1
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +4 -8
- package/src/renderer/tool-call.ts +4 -3
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap.ts +7 -8
- package/src/runtime/diagnostics/panels/policy.ts +2 -1
- package/src/shell/ui-openers.ts +44 -3
- package/src/version.ts +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// FileExplorerPanel — collapsible project tree view
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { promises as fsPromises } from 'node:fs';
|
|
6
6
|
import { join, relative, basename } from 'node:path';
|
|
7
7
|
import type { Line } from '../types/grid.ts';
|
|
8
8
|
import { createEmptyLine } from '../types/grid.ts';
|
|
@@ -120,6 +120,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
120
120
|
private rootPath: string;
|
|
121
121
|
private readonly workingDirectory: string;
|
|
122
122
|
private cacheValid: boolean = false;
|
|
123
|
+
private readyPromise: Promise<void> | null = null;
|
|
123
124
|
|
|
124
125
|
// --- navigation ---
|
|
125
126
|
private cursor: number = 0;
|
|
@@ -139,7 +140,9 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
139
140
|
|
|
140
141
|
override onActivate(): void {
|
|
141
142
|
super.onActivate();
|
|
142
|
-
if (!this.cacheValid)
|
|
143
|
+
if (!this.cacheValid) {
|
|
144
|
+
void this._buildTreeAsync();
|
|
145
|
+
}
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
override onDestroy(): void {
|
|
@@ -153,8 +156,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
153
156
|
/** Force a full tree refresh from disk. */
|
|
154
157
|
refresh(): void {
|
|
155
158
|
this.cacheValid = false;
|
|
156
|
-
this.
|
|
157
|
-
this.markDirty();
|
|
159
|
+
void this._buildTreeAsync();
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
/** Currently focused node (or null). */
|
|
@@ -197,7 +199,6 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
197
199
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
198
200
|
|
|
199
201
|
render(width: number, height: number): Line[] {
|
|
200
|
-
if (!this.cacheValid) this._buildTree();
|
|
201
202
|
this.needsRender = false;
|
|
202
203
|
const searchLine = this.searchMode
|
|
203
204
|
? `/ ${this.searchQuery}_`
|
|
@@ -324,14 +325,29 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
324
325
|
|
|
325
326
|
// ── Private: tree building ─────────────────────────────────────────────────
|
|
326
327
|
|
|
327
|
-
private
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
328
|
+
private _buildTreeAsync(): Promise<void> {
|
|
329
|
+
const p = (async () => {
|
|
330
|
+
try {
|
|
331
|
+
await this.withLoading('Scanning directory\u2026', async () => {
|
|
332
|
+
this.root = await this._scanDirAsync(this.rootPath, 0);
|
|
333
|
+
this._rebuildFlat();
|
|
334
|
+
this.cacheValid = true;
|
|
335
|
+
});
|
|
336
|
+
} catch (err) {
|
|
337
|
+
this.setError(err instanceof Error ? err.message : String(err));
|
|
338
|
+
}
|
|
339
|
+
this.markDirty();
|
|
340
|
+
})();
|
|
341
|
+
this.readyPromise = p;
|
|
342
|
+
return p;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Resolves when the current tree build has settled. */
|
|
346
|
+
public awaitReady(): Promise<void> {
|
|
347
|
+
return this.readyPromise ?? Promise.resolve();
|
|
332
348
|
}
|
|
333
349
|
|
|
334
|
-
private
|
|
350
|
+
private async _scanDirAsync(dirPath: string, depth: number): Promise<TreeNode> {
|
|
335
351
|
const name = basename(dirPath);
|
|
336
352
|
const node: TreeNode = {
|
|
337
353
|
path: dirPath,
|
|
@@ -339,7 +355,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
339
355
|
isDir: true,
|
|
340
356
|
depth,
|
|
341
357
|
size: 0,
|
|
342
|
-
expanded: depth === 0,
|
|
358
|
+
expanded: depth === 0,
|
|
343
359
|
children: [],
|
|
344
360
|
loaded: false,
|
|
345
361
|
};
|
|
@@ -348,7 +364,7 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
348
364
|
|
|
349
365
|
let entries: string[];
|
|
350
366
|
try {
|
|
351
|
-
entries =
|
|
367
|
+
entries = await fsPromises.readdir(dirPath);
|
|
352
368
|
} catch {
|
|
353
369
|
return node;
|
|
354
370
|
}
|
|
@@ -356,31 +372,35 @@ export class FileExplorerPanel extends BasePanel {
|
|
|
356
372
|
node.loaded = true;
|
|
357
373
|
|
|
358
374
|
// Sort: dirs first, then files, alphabetically within each group
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
})
|
|
375
|
+
const filtered = entries.filter(e => !shouldSkip(e));
|
|
376
|
+
const statResults = await Promise.all(
|
|
377
|
+
filtered.map(async (e) => {
|
|
378
|
+
try {
|
|
379
|
+
const s = await fsPromises.stat(join(dirPath, e));
|
|
380
|
+
return { name: e, isDir: s.isDirectory(), size: s.size, stat: s };
|
|
381
|
+
} catch {
|
|
382
|
+
return { name: e, isDir: false, size: 0, stat: null };
|
|
383
|
+
}
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const sorted = statResults.sort((a, b) => {
|
|
388
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
389
|
+
return a.name.localeCompare(b.name);
|
|
390
|
+
});
|
|
369
391
|
|
|
370
392
|
for (const entry of sorted) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (stat.isDirectory()) {
|
|
376
|
-
node.children.push(this._scanDir(fullPath, depth + 1));
|
|
393
|
+
if (entry.stat === null) continue;
|
|
394
|
+
const fullPath = join(dirPath, entry.name);
|
|
395
|
+
if (entry.isDir) {
|
|
396
|
+
node.children.push(await this._scanDirAsync(fullPath, depth + 1));
|
|
377
397
|
} else {
|
|
378
398
|
node.children.push({
|
|
379
399
|
path: fullPath,
|
|
380
|
-
name: entry,
|
|
400
|
+
name: entry.name,
|
|
381
401
|
isDir: false,
|
|
382
402
|
depth: depth + 1,
|
|
383
|
-
size:
|
|
403
|
+
size: entry.size,
|
|
384
404
|
expanded: false,
|
|
385
405
|
children: [],
|
|
386
406
|
loaded: true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Stats } from 'node:fs';
|
|
2
|
+
import { promises as fsPromises, readFileSync, statSync } from 'node:fs';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import type { Line, Cell } from '../types/grid.ts';
|
|
4
5
|
import { createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
@@ -68,7 +69,7 @@ export class FilePreviewPanel extends BasePanel {
|
|
|
68
69
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
|
-
* Load a file into the preview. Reads
|
|
72
|
+
* Load a file into the preview. Reads asynchronously.
|
|
72
73
|
* Files larger than 100 KB show a warning instead of content.
|
|
73
74
|
*/
|
|
74
75
|
openFile(filePath: string): void {
|
|
@@ -79,51 +80,72 @@ export class FilePreviewPanel extends BasePanel {
|
|
|
79
80
|
|
|
80
81
|
this.filePath = filePath;
|
|
81
82
|
this.oversized = false;
|
|
82
|
-
this.
|
|
83
|
-
this.fenceTag = '';
|
|
83
|
+
this.fenceTag = extToFenceTag(filePath);
|
|
84
84
|
|
|
85
85
|
// Restore scroll position for this file, or start at top
|
|
86
86
|
this.scrollOffset = this.scrollMemory.get(filePath) ?? 0;
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Synchronously pre-populate fileLines for small files so that callers
|
|
89
|
+
// (e.g. syncSymbolOutlineFromPreview) can read getSource() immediately.
|
|
89
90
|
try {
|
|
90
|
-
stat =
|
|
91
|
+
const stat = statSync(filePath);
|
|
92
|
+
if (stat.size <= MAX_FILE_SIZE) {
|
|
93
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
94
|
+
this.fileLines = content.split('\n');
|
|
95
|
+
} else {
|
|
96
|
+
this.fileLines = [];
|
|
97
|
+
this.oversized = true;
|
|
98
|
+
}
|
|
91
99
|
} catch {
|
|
92
100
|
this.fileLines = [`(cannot open: ${filePath})`];
|
|
93
|
-
this.markDirty();
|
|
94
|
-
return;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.markDirty();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
103
|
+
void this._loadFileAsync(filePath);
|
|
104
|
+
}
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
private async _loadFileAsync(filePath: string): Promise<void> {
|
|
104
107
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
108
|
+
await this.withLoading('Loading…', async () => {
|
|
109
|
+
let stat: Stats;
|
|
110
|
+
try {
|
|
111
|
+
stat = await fsPromises.stat(filePath);
|
|
112
|
+
} catch {
|
|
113
|
+
this.fileLines = [`(cannot open: ${filePath})`];
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
118
|
+
this.oversized = true;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let content: string;
|
|
123
|
+
try {
|
|
124
|
+
content = await fsPromises.readFile(filePath, 'utf-8');
|
|
125
|
+
} catch {
|
|
126
|
+
this.fileLines = [`(read error: ${filePath})`];
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.fileLines = content.split('\n');
|
|
131
|
+
// Strip trailing empty line from final newline
|
|
132
|
+
if (this.fileLines.length > 0 && this.fileLines[this.fileLines.length - 1] === '') {
|
|
133
|
+
this.fileLines.pop();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.fenceTag = extToFenceTag(filePath);
|
|
137
|
+
|
|
138
|
+
// Kick off async tree-sitter parse so subsequent renders get highlighting
|
|
139
|
+
if (this.fenceTag) {
|
|
140
|
+
this.syntaxHighlighter.highlight(content, this.fenceTag);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Clamp scroll in case the new file is shorter
|
|
144
|
+
this.clampScroll(0);
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
this.setError(err instanceof Error ? err.message : String(err));
|
|
123
148
|
}
|
|
124
|
-
|
|
125
|
-
// Clamp scroll in case the new file is shorter
|
|
126
|
-
this.clampScroll(0);
|
|
127
149
|
this.markDirty();
|
|
128
150
|
}
|
|
129
151
|
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -103,7 +103,7 @@ export class GitPanel extends BasePanel {
|
|
|
103
103
|
/** Scroll offset for both main view and diff view. */
|
|
104
104
|
private scrollOffset = 0;
|
|
105
105
|
|
|
106
|
-
private
|
|
106
|
+
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
107
107
|
private loading = true;
|
|
108
108
|
private error: string | null = null;
|
|
109
109
|
|
|
@@ -119,20 +119,21 @@ export class GitPanel extends BasePanel {
|
|
|
119
119
|
override onActivate(): void {
|
|
120
120
|
super.onActivate();
|
|
121
121
|
void this.refresh();
|
|
122
|
-
this.
|
|
122
|
+
this.refreshTimerId = this.registerTimer(setInterval(() => {
|
|
123
123
|
void this.refresh();
|
|
124
|
-
}, 5_000);
|
|
124
|
+
}, 5_000));
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
override onDeactivate(): void {
|
|
128
|
-
if (this.
|
|
129
|
-
|
|
130
|
-
this.
|
|
128
|
+
if (this.refreshTimerId !== null) {
|
|
129
|
+
this.clearTimer(this.refreshTimerId);
|
|
130
|
+
this.refreshTimerId = null;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
override onDestroy(): void {
|
|
135
135
|
this.onDeactivate();
|
|
136
|
+
super.onDestroy();
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
// ---------------------------------------------------------------------------
|
|
@@ -326,18 +327,16 @@ export class GitPanel extends BasePanel {
|
|
|
326
327
|
const item = this.items[this.selectedIndex];
|
|
327
328
|
if (!item || item.kind !== 'file') return;
|
|
328
329
|
|
|
329
|
-
// I3:
|
|
330
|
-
this.startLoading('Loading diff...');
|
|
331
|
-
this.markDirty();
|
|
330
|
+
// I3: withLoading guarantees spinner is cleared even if diffFile throws
|
|
332
331
|
try {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
const raw = await this.withLoading('Loading diff…', async () => {
|
|
333
|
+
const git = new GitService(this.workingDirectory);
|
|
334
|
+
return git.diffFile(item.entry.path, item.entry.staged);
|
|
335
|
+
});
|
|
336
336
|
this.expandedDiff = raw ? raw.split('\n') : ['(no diff available)'];
|
|
337
337
|
this.scrollOffset = 0;
|
|
338
338
|
this.markDirty();
|
|
339
339
|
} catch (err) {
|
|
340
|
-
this.stopLoading();
|
|
341
340
|
this.expandedDiff = [`Error: ${summarizeError(err)}`];
|
|
342
341
|
this.scrollOffset = 0;
|
|
343
342
|
this.markDirty();
|
|
@@ -10,6 +10,7 @@ import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks/workben
|
|
|
10
10
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
11
11
|
import {
|
|
12
12
|
buildPanelLine,
|
|
13
|
+
buildStatusPill,
|
|
13
14
|
DEFAULT_PANEL_PALETTE,
|
|
14
15
|
} from './polish.ts';
|
|
15
16
|
|
|
@@ -66,6 +67,7 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
66
67
|
dataSource: HooksPanelDataSource = createDefaultDataSource(hookDispatcher, hookWorkbench, hookActivityTracker),
|
|
67
68
|
) {
|
|
68
69
|
super('hooks', 'Hooks', 'H', 'monitoring');
|
|
70
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
69
71
|
this.dataSource = dataSource;
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -88,7 +90,7 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
88
90
|
[' ', C.label, bg],
|
|
89
91
|
[truncateDisplay(entry.hook.name ?? '(unnamed)', 20).padEnd(20), C.value, bg],
|
|
90
92
|
[` ${truncateDisplay(entry.pattern, 28).padEnd(28)}`, C.info, bg],
|
|
91
|
-
|
|
93
|
+
...buildStatusPill(entry.hook.enabled === false ? 'warn' : 'good', ` ${(entry.hook.enabled === false ? 'DISABLED' : 'ENABLED').padEnd(8)}`, { bg }),
|
|
92
94
|
[` ${entry.hook.type}`, C.dim, bg],
|
|
93
95
|
]);
|
|
94
96
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
buildKeyValueLine,
|
|
9
9
|
buildPanelLine,
|
|
10
10
|
buildPanelWorkspace,
|
|
11
|
+
buildStatusPill,
|
|
11
12
|
DEFAULT_PANEL_PALETTE,
|
|
12
13
|
type PanelPalette,
|
|
13
14
|
} from './polish.ts';
|
|
@@ -40,6 +41,7 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
40
41
|
|
|
41
42
|
public constructor(registry?: ForensicsRegistry) {
|
|
42
43
|
super('incident', 'Incident Review', 'N', 'monitoring');
|
|
44
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
43
45
|
this.registry = registry;
|
|
44
46
|
this.unsub = registry ? registry.subscribe(() => this.markDirty()) : null;
|
|
45
47
|
}
|
|
@@ -137,7 +139,7 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
137
139
|
if (bundle.evidence.slowPhases.length > 0) {
|
|
138
140
|
footerLines.push(buildPanelLine(width, [
|
|
139
141
|
[' Slow phases: ', C.label],
|
|
140
|
-
|
|
142
|
+
...buildStatusPill('warn', bundle.evidence.slowPhases.join(', ').slice(0, Math.max(0, width - 15))),
|
|
141
143
|
]));
|
|
142
144
|
}
|
|
143
145
|
const rootCause = selected.causalChain.find((entry) => entry.isRootCause);
|
|
@@ -166,7 +168,7 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
166
168
|
: `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description}`;
|
|
167
169
|
footerLines.push(buildPanelLine(width, [
|
|
168
170
|
[' ', C.label],
|
|
169
|
-
|
|
171
|
+
...buildStatusPill('bad', replayDetail.slice(0, Math.max(0, width - 2))),
|
|
170
172
|
]));
|
|
171
173
|
} else {
|
|
172
174
|
const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
|