fix(tui): surface visible feedback when copying a log entry
The [c] copy handler was writing a success/failure log entry to the buffer, but in expanded mode the panel hides the buffer so users got no indication the copy ran. Add a transient inline "Copied!" flash in both list and expanded views, and clamp the index to match the display cursor so the copy never silently misses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -493,13 +493,17 @@ function LogsPanel({
|
||||
const hiddenAbove = visibleStart;
|
||||
const hiddenBelow = entries.length - visibleEnd;
|
||||
|
||||
const panelTitle = state.clipboardFlash
|
||||
? `Logs (${state.logEntries.length}/1000) · ${state.clipboardFlash.ok ? "✓ Copied!" : "✗ Copy failed"}`
|
||||
: `Logs (${state.logEntries.length}/1000)`;
|
||||
return (
|
||||
<Panel title={`Logs (${state.logEntries.length}/1000)`} isFocused={isFocused} flexGrow={1}>
|
||||
<Panel title={panelTitle} isFocused={isFocused} flexGrow={1}>
|
||||
{logsExpandedMode && entries[cursor] ? (
|
||||
<ExpandedLog
|
||||
entry={entries[cursor]}
|
||||
index={cursor}
|
||||
total={entries.length}
|
||||
clipboardFlash={state.clipboardFlash}
|
||||
/>
|
||||
) : entries.length === 0 ? (
|
||||
<Text dimColor>No log entries yet.</Text>
|
||||
@@ -584,10 +588,27 @@ function LogsPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedLog({ entry, index, total }: { entry: LogEntry; index: number; total: number }) {
|
||||
function ExpandedLog({
|
||||
entry,
|
||||
index,
|
||||
total,
|
||||
clipboardFlash,
|
||||
}: {
|
||||
entry: LogEntry;
|
||||
index: number;
|
||||
total: number;
|
||||
clipboardFlash: { ok: boolean; at: number } | null;
|
||||
}) {
|
||||
return (
|
||||
<Box flexDirection="column" flexGrow={1} width="100%">
|
||||
<Text dimColor>Entry {index + 1}/{total} · [Enter/Esc] close · [c] copy</Text>
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text dimColor>Entry {index + 1}/{total} · [Enter/Esc] close · [c] copy</Text>
|
||||
{clipboardFlash && (
|
||||
<Text color={clipboardFlash.ok ? "greenBright" : "redBright"} bold>
|
||||
{clipboardFlash.ok ? "✓ Copied!" : "✗ Copy failed"}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box height={1} />
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text dimColor>Time:</Text>
|
||||
@@ -4039,12 +4060,20 @@ export function DashboardApp({ controller }: DashboardAppProps) {
|
||||
}
|
||||
|
||||
if (input === "c" || input === "C") {
|
||||
const target = filteredEntries[state.selectedLogIndex];
|
||||
// Clamp the index to match the display logic in LogsPanel — the cursor
|
||||
// shown is always Math.min(Math.max(idx, 0), entries.length - 1), so
|
||||
// we copy whatever the user is actually looking at instead of silently
|
||||
// hitting `undefined` when selectedLogIndex is briefly out of range.
|
||||
const idx = filteredEntries.length === 0
|
||||
? -1
|
||||
: Math.min(Math.max(state.selectedLogIndex, 0), filteredEntries.length - 1);
|
||||
const target = idx >= 0 ? filteredEntries[idx] : undefined;
|
||||
if (target) {
|
||||
const ts = formatTimestamp(target.timestamp);
|
||||
const prefix = target.prefix ? `[${target.prefix}] ` : "";
|
||||
const text = `${ts} ${target.level.toUpperCase()} ${prefix}${target.message}`;
|
||||
void copyToClipboard(text).then((ok) => {
|
||||
controller.flashClipboard(ok);
|
||||
if (ok) {
|
||||
controller.log("Log entry copied to clipboard.", "clipboard");
|
||||
} else {
|
||||
@@ -4054,6 +4083,9 @@ export function DashboardApp({ controller }: DashboardAppProps) {
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
controller.warn("No log entry to copy.", "clipboard");
|
||||
controller.flashClipboard(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ export class DashboardTUI {
|
||||
// Throttle so we don't spam kills while the sampler keeps firing during
|
||||
// sustained pressure (sampler runs every 2s).
|
||||
private lastAutoKillAt = 0;
|
||||
clipboardFlash: { ok: boolean; at: number } | null = null;
|
||||
private clipboardFlashTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
interactiveData: InteractiveData | null = null;
|
||||
interactiveView: InteractiveView = "board";
|
||||
interactiveInputLocked = false;
|
||||
@@ -162,6 +164,7 @@ export class DashboardTUI {
|
||||
autoKillVitestOnPressure: this.autoKillVitestOnPressure,
|
||||
vitestKillThreshold: this.vitestKillThreshold,
|
||||
updateStatus: this.updateStatus,
|
||||
clipboardFlash: this.clipboardFlash,
|
||||
};
|
||||
return this.cachedSnapshot;
|
||||
}
|
||||
@@ -421,6 +424,17 @@ export class DashboardTUI {
|
||||
this.addLog({ level: "info", message, prefix });
|
||||
}
|
||||
|
||||
flashClipboard(ok: boolean): void {
|
||||
this.clipboardFlash = { ok, at: Date.now() };
|
||||
if (this.clipboardFlashTimer) clearTimeout(this.clipboardFlashTimer);
|
||||
this.clipboardFlashTimer = setTimeout(() => {
|
||||
this.clipboardFlash = null;
|
||||
this.clipboardFlashTimer = null;
|
||||
this.notify();
|
||||
}, 1800);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
warn(message: string, prefix?: string): void {
|
||||
this.addLog({ level: "warn", message, prefix });
|
||||
}
|
||||
@@ -713,6 +727,10 @@ export class DashboardTUI {
|
||||
clearTimeout(this.resizeDebounceTimer);
|
||||
this.resizeDebounceTimer = null;
|
||||
}
|
||||
if (this.clipboardFlashTimer) {
|
||||
clearTimeout(this.clipboardFlashTimer);
|
||||
this.clipboardFlashTimer = null;
|
||||
}
|
||||
|
||||
if (this.inkInstance) {
|
||||
this.inkInstance.unmount();
|
||||
|
||||
@@ -350,6 +350,10 @@ export interface DashboardState {
|
||||
autoKillVitestOnPressure: boolean;
|
||||
vitestKillThreshold: number;
|
||||
updateStatus: UpdateStatus | null;
|
||||
// Transient flash shown after the user copies a log entry. `at` is a
|
||||
// monotonic timestamp so the view can render "Copied!" briefly before the
|
||||
// controller clears it via setTimeout.
|
||||
clipboardFlash: { ok: boolean; at: number } | null;
|
||||
}
|
||||
|
||||
export const SECTION_ORDER: SectionId[] = ["system", "logs", "utilities", "stats", "settings"];
|
||||
@@ -377,5 +381,6 @@ export function createInitialState(): DashboardState {
|
||||
autoKillVitestOnPressure: true,
|
||||
vitestKillThreshold: 0.9,
|
||||
updateStatus: null,
|
||||
clipboardFlash: null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user