pi-cockpit 0.6.0 → 0.7.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/package.json +4 -4
- package/src/agents-store.ts +103 -60
- package/src/ambient.ts +21 -12
- package/src/bash-bg-overlay.ts +30 -7
- package/src/bash-bg-widget.ts +54 -50
- package/src/config.ts +4 -0
- package/src/footer.ts +26 -3
- package/src/icons.ts +5 -1
- package/src/index.ts +156 -27
- package/src/layout.ts +181 -129
- package/src/quiet-tools.ts +367 -310
- package/src/render.ts +107 -25
- package/src/settings-view.ts +23 -0
- package/src/sidebar-controller.ts +317 -266
- package/src/sidebar-render.ts +409 -312
- package/src/split-pane.ts +382 -377
- package/src/stack-widget.ts +16 -12
- package/src/thinking-timer.ts +249 -224
- package/src/tick-policy.ts +54 -0
- package/src/types.ts +40 -5
- package/src/viewport-stability.ts +101 -0
package/src/footer.ts
CHANGED
|
@@ -42,7 +42,21 @@ export function emptyTotals(): UsageTotals {
|
|
|
42
42
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, latestCacheHitRate: undefined };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
interface UsageCacheEntry {
|
|
46
|
+
key: string;
|
|
47
|
+
totals: UsageTotals;
|
|
48
|
+
computedAt: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let usageCache: UsageCacheEntry | undefined;
|
|
52
|
+
// Static mode lowers the token refresh cadence. The getter reads live config so
|
|
53
|
+
// a toggle applies on the next render without re-registering.
|
|
54
|
+
let usageThrottleMs: (() => number) | undefined;
|
|
55
|
+
|
|
56
|
+
/** Set the wall-clock throttle for token totals; 0/undefined means uncapped. */
|
|
57
|
+
export function setUsageThrottle(getMs: () => number): void {
|
|
58
|
+
usageThrottleMs = getMs;
|
|
59
|
+
}
|
|
46
60
|
|
|
47
61
|
function entryIdentity(entry: unknown): string {
|
|
48
62
|
const e = entry as {
|
|
@@ -77,9 +91,18 @@ export function invalidateUsageCache(): void {
|
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
// Sum assistant-message usage across session entries (shape: entry.message.usage).
|
|
80
|
-
export function getUsageTotals(entries: readonly unknown[]): UsageTotals {
|
|
94
|
+
export function getUsageTotals(entries: readonly unknown[], now: number = Date.now()): UsageTotals {
|
|
81
95
|
const key = entriesKey(entries);
|
|
82
96
|
if (usageCache && usageCache.key === key) return usageCache.totals;
|
|
97
|
+
const throttleMs = Math.max(0, usageThrottleMs?.() ?? 0);
|
|
98
|
+
if (throttleMs > 0 && usageCache && now - usageCache.computedAt < throttleMs) {
|
|
99
|
+
// Within the window: keep the previous totals on screen. The cache entry is
|
|
100
|
+
// deliberately left untouched (key and computedAt stay put) so a later call
|
|
101
|
+
// past the window with these same entries still mismatches the cached key
|
|
102
|
+
// and recomputes — rebinding the new key here would pin stale totals to it
|
|
103
|
+
// forever, since the key-match fast path above would then win every time.
|
|
104
|
+
return usageCache.totals;
|
|
105
|
+
}
|
|
83
106
|
|
|
84
107
|
const t = emptyTotals();
|
|
85
108
|
for (const e of entries) {
|
|
@@ -98,7 +121,7 @@ export function getUsageTotals(entries: readonly unknown[]): UsageTotals {
|
|
|
98
121
|
}
|
|
99
122
|
}
|
|
100
123
|
}
|
|
101
|
-
usageCache = { key, totals: t };
|
|
124
|
+
usageCache = { key, totals: t, computedAt: now };
|
|
102
125
|
return t;
|
|
103
126
|
}
|
|
104
127
|
|
package/src/icons.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface IconGlyphs {
|
|
|
14
14
|
arrow: string;
|
|
15
15
|
/** Points at a dependency/upstream ("blocked by", "waits on"). */
|
|
16
16
|
depArrow: string;
|
|
17
|
+
/** Creator-to-assignee transfer indicator in actor labels. */
|
|
18
|
+
transferArrow: string;
|
|
17
19
|
/** Tree connectors. Routed through the glyph set so ascii mode stays ascii. */
|
|
18
20
|
treeBranch: string;
|
|
19
21
|
treeLast: string;
|
|
@@ -60,6 +62,7 @@ const NERD_GLYPHS: IconGlyphs = {
|
|
|
60
62
|
barPending: "░",
|
|
61
63
|
arrow: "»",
|
|
62
64
|
depArrow: "←",
|
|
65
|
+
transferArrow: "→",
|
|
63
66
|
treeBranch: "├─",
|
|
64
67
|
treeLast: "└─",
|
|
65
68
|
treeVertical: "│ ",
|
|
@@ -92,7 +95,8 @@ const ASCII_GLYPHS: IconGlyphs = {
|
|
|
92
95
|
barPending: "-",
|
|
93
96
|
arrow: ">",
|
|
94
97
|
depArrow: "<-",
|
|
95
|
-
|
|
98
|
+
transferArrow: "->",
|
|
99
|
+
treeBranch: "|",
|
|
96
100
|
treeLast: "`-",
|
|
97
101
|
treeVertical: "| ",
|
|
98
102
|
treeSpace: " ",
|
package/src/index.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { renderBashBgSummary } from "./bash-bg-widget.ts";
|
|
|
12
12
|
import { registerQuietTools } from "./quiet-tools.ts";
|
|
13
13
|
import { ensureThinkingFolded, readHideThinkingBlock } from "./thinking-fold.ts";
|
|
14
14
|
import { ThinkingFoldTimer } from "./thinking-timer.ts";
|
|
15
|
+
import { shouldAnimateFrames, shouldAnimateSidebar, shouldRunTick, type TickPolicyState } from "./tick-policy.ts";
|
|
15
16
|
import { TodoStore } from "./todo-store.ts";
|
|
16
17
|
import { makeTodoWidget, makeAgentWidget, terminalRows } from "./stack-widget.ts";
|
|
17
18
|
import { activeThemeName, ThemePicker } from "./theme-picker.ts";
|
|
18
|
-
import { getUsageTotals, invalidateUsageCache, renderFooter, type PaintTheme, type WidthUtils } from "./footer.ts";
|
|
19
|
+
import { getUsageTotals, invalidateUsageCache, renderFooter, setUsageThrottle, type PaintTheme, type WidthUtils } from "./footer.ts";
|
|
19
20
|
import { collectExtensionStatuses } from "./extension-status.ts";
|
|
20
21
|
import { ANIMATION_PERIOD_MS, resolveGlyphs, spinFrame } from "./icons.ts";
|
|
21
22
|
import { ensureConfigExists, loadConfig, saveConfig } from "./config.ts";
|
|
@@ -47,6 +48,8 @@ const BASH_BG_OVERLAY_KEY = "alt+j";
|
|
|
47
48
|
const SIDEBAR_RESIZE_KEY = "ctrl+shift+r";
|
|
48
49
|
const COCKPIT_STATUS_KEY = "cockpit";
|
|
49
50
|
const WIDTH_POLL_INTERVAL_MS = 250;
|
|
51
|
+
// Static mode keeps token totals fresh enough without recomputing on every message.
|
|
52
|
+
const USAGE_REFRESH_THROTTLE_MS = 10_000;
|
|
50
53
|
// Quiet mode's rename of pi's hidden-thinking label. The live thinking timer
|
|
51
54
|
// derives its final "thoughts · 8.4s" labels from the same word.
|
|
52
55
|
const QUIET_THINKING_LABEL = "thoughts";
|
|
@@ -80,6 +83,33 @@ function formatCwd(cwd: string): string {
|
|
|
80
83
|
return rel === "" ? "~" : `~${sep}${rel}`;
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
87
|
+
return typeof value === "object" && value !== null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isStartedPayload(value: unknown): value is StartedPayload {
|
|
91
|
+
return isRecord(value)
|
|
92
|
+
&& typeof value.correlationId === "string"
|
|
93
|
+
&& value.correlationId.length > 0
|
|
94
|
+
&& typeof value.agent === "string";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isProgressPayload(value: unknown): value is MessagePayload {
|
|
98
|
+
return isRecord(value)
|
|
99
|
+
&& value.isSend !== true
|
|
100
|
+
&& value.isInteraction !== true
|
|
101
|
+
&& typeof value.correlationId === "string"
|
|
102
|
+
&& value.correlationId.length > 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isCompletePayload(value: unknown): value is CompletePayload {
|
|
106
|
+
return isRecord(value)
|
|
107
|
+
&& typeof value.correlationId === "string"
|
|
108
|
+
&& value.correlationId.length > 0
|
|
109
|
+
&& typeof value.exitCode === "number"
|
|
110
|
+
&& Number.isFinite(value.exitCode);
|
|
111
|
+
}
|
|
112
|
+
|
|
83
113
|
export default function (pi: ExtensionAPI): void {
|
|
84
114
|
const agents = new AgentsStore();
|
|
85
115
|
const bashBg = new BashBgStore();
|
|
@@ -92,7 +122,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
92
122
|
let dockEffectiveVisible = false;
|
|
93
123
|
let surfaceState: CockpitSurfaceState = "disabled";
|
|
94
124
|
let running = false;
|
|
95
|
-
|
|
125
|
+
let runningStartedAt: number | undefined;
|
|
126
|
+
const activeTools = new Map<string, { name: string; startedAt: number }>();
|
|
96
127
|
let tick: ReturnType<typeof setInterval> | undefined;
|
|
97
128
|
// Persisted rather than toasted: a config that failed to load silently downgrades
|
|
98
129
|
// the whole session to defaults, so it belongs in a slot that does not scroll away.
|
|
@@ -109,14 +140,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
109
140
|
// AgentSession construction — before renderInitialMessages runs.
|
|
110
141
|
ensureConfigExists();
|
|
111
142
|
config = loadConfig();
|
|
143
|
+
// Reads config live, so toggling static mode re-throttles without re-registering.
|
|
144
|
+
setUsageThrottle(() => (config.staticMode ? USAGE_REFRESH_THROTTLE_MS : 0));
|
|
112
145
|
if (config.quietMode) {
|
|
113
146
|
registerQuietTools(pi, () => config);
|
|
114
147
|
quietToolsRegistered = true;
|
|
115
148
|
}
|
|
116
149
|
|
|
117
|
-
// Live
|
|
118
|
-
//
|
|
119
|
-
// Pi owns the fold; this only renames the label it already renders.
|
|
150
|
+
// Live elapsed for folded thinking rows — no spinner, just a running timer
|
|
151
|
+
// while the model thinks, then the settled duration. Pi owns the fold.
|
|
120
152
|
const thinkingTimer = new ThinkingFoldTimer({
|
|
121
153
|
getTui: () => capturedTui,
|
|
122
154
|
requestRender: () => {
|
|
@@ -130,6 +162,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
130
162
|
getGlyphs: () => resolveGlyphs(config.icons.mode),
|
|
131
163
|
isThinkingHidden: () => (lastCtx ? readHideThinkingBlock(lastCtx.cwd) : false),
|
|
132
164
|
isEnabled: () => config.enabled,
|
|
165
|
+
isStatic: () => config.staticMode,
|
|
133
166
|
setGlobalLabel: (label) => {
|
|
134
167
|
const ctx = lastCtx;
|
|
135
168
|
if (!ctx || !isTuiContext(ctx)) return;
|
|
@@ -153,16 +186,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
153
186
|
ctx.ui.setStatus(COCKPIT_STATUS_KEY, undefined);
|
|
154
187
|
return;
|
|
155
188
|
}
|
|
189
|
+
const activeTool = [...activeTools.values()].at(-1);
|
|
156
190
|
const state: AmbientState = {
|
|
157
191
|
todos: todos.snapshot(),
|
|
158
192
|
agents: agents.snapshot(),
|
|
159
193
|
jobs: bashBg.snapshot(),
|
|
160
194
|
running,
|
|
161
195
|
cwd: formatCwd(ctx.sessionManager.getCwd()),
|
|
162
|
-
activeTool:
|
|
196
|
+
activeTool: activeTool?.name,
|
|
197
|
+
workingStartedAt: activeTool?.startedAt ?? runningStartedAt,
|
|
198
|
+
hideLiveDuration: config.staticMode,
|
|
199
|
+
separator: ` ${g.separator} `,
|
|
163
200
|
};
|
|
164
201
|
ctx.ui.setWorkingMessage(workingMessage(state));
|
|
165
|
-
ctx.ui.setTitle(titleFor(state, { ok: g.check, fail: g.cross }));
|
|
202
|
+
ctx.ui.setTitle(titleFor(state, { ok: g.check, fail: g.cross }, g.separator));
|
|
166
203
|
ctx.ui.setStatus(COCKPIT_STATUS_KEY, statusText(configProblem, g.blocked));
|
|
167
204
|
} catch {
|
|
168
205
|
// ambient surfaces are best-effort; never let them break a render
|
|
@@ -178,18 +215,39 @@ export default function (pi: ExtensionAPI): void {
|
|
|
178
215
|
// tui may be gone between sessions
|
|
179
216
|
}
|
|
180
217
|
};
|
|
218
|
+
const policy = (): TickPolicyState => ({
|
|
219
|
+
staticMode: config.staticMode,
|
|
220
|
+
running,
|
|
221
|
+
bashActive: bashBg.hasActive(),
|
|
222
|
+
lingering: agents.hasLingering(),
|
|
223
|
+
ticking: tick !== undefined,
|
|
224
|
+
});
|
|
181
225
|
// True only while a redraw loop is actually running; widgets use it to avoid
|
|
182
226
|
// painting a frozen spinner frame that reads as a hung UI.
|
|
183
|
-
const isAnimating = (): boolean =>
|
|
227
|
+
const isAnimating = (): boolean => shouldAnimateFrames(policy());
|
|
184
228
|
// A failed agent lingers for a while after it completes, and the loop has to
|
|
185
229
|
// outlive the session that produced it — otherwise the row would sit there
|
|
186
|
-
// until some unrelated event happened to expire it.
|
|
187
|
-
|
|
230
|
+
// until some unrelated event happened to expire it. Static mode still keeps
|
|
231
|
+
// that loop alive: failure retention is correctness, not animation. What it
|
|
232
|
+
// drops is the running/job churn that used to repaint every quarter second.
|
|
233
|
+
const needsTick = (): boolean => shouldRunTick(policy());
|
|
188
234
|
const startTick = (): void => {
|
|
189
235
|
if (tick) return;
|
|
190
236
|
tick = setInterval(() => {
|
|
191
|
-
if (needsTick())
|
|
192
|
-
|
|
237
|
+
if (needsTick()) {
|
|
238
|
+
// Static mode keeps the loop alive only so lingering rows can expire
|
|
239
|
+
// (prune is read-driven). The rows themselves are static, so skip the
|
|
240
|
+
// full repaint and repaint just once when a prune removed something.
|
|
241
|
+
if (config.staticMode) {
|
|
242
|
+
const before = agents.size;
|
|
243
|
+
agents.snapshot();
|
|
244
|
+
if (agents.size !== before) req();
|
|
245
|
+
} else {
|
|
246
|
+
req();
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
syncTick();
|
|
250
|
+
}
|
|
193
251
|
}, ANIMATION_PERIOD_MS);
|
|
194
252
|
tick.unref?.();
|
|
195
253
|
};
|
|
@@ -267,6 +325,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
267
325
|
}
|
|
268
326
|
};
|
|
269
327
|
|
|
328
|
+
|
|
270
329
|
const clearWidgets = (ctx: ExtensionContext): void => {
|
|
271
330
|
ctx.ui.setWidget(STACK_WIDGET_KEY, undefined);
|
|
272
331
|
ctx.ui.setWidget(AGENT_WIDGET_KEY, undefined);
|
|
@@ -319,6 +378,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
319
378
|
getAgents: () => agents.snapshot(),
|
|
320
379
|
getJobs: () => bashBg.snapshot(),
|
|
321
380
|
getConfig: () => config,
|
|
381
|
+
shouldAnimate: () => shouldAnimateSidebar(policy()),
|
|
322
382
|
onVisibilityChange: (visible) => {
|
|
323
383
|
dockEffectiveVisible = visible;
|
|
324
384
|
const activeCtx = lastCtx;
|
|
@@ -366,6 +426,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
366
426
|
// Leaving these set would strand a title and a status line owned by an
|
|
367
427
|
// extension that is no longer painting anything.
|
|
368
428
|
ctx.ui.setWorkingMessage(undefined);
|
|
429
|
+
ctx.ui.setWorkingIndicator();
|
|
369
430
|
ctx.ui.setStatus(COCKPIT_STATUS_KEY, undefined);
|
|
370
431
|
} catch {
|
|
371
432
|
// ambient surfaces are best-effort
|
|
@@ -380,15 +441,30 @@ export default function (pi: ExtensionAPI): void {
|
|
|
380
441
|
uninstallUi(ctx);
|
|
381
442
|
return;
|
|
382
443
|
}
|
|
444
|
+
ctx.ui.setWorkingIndicator({ frames: [] });
|
|
383
445
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
384
446
|
capturedTui = tui;
|
|
385
447
|
let observedWidth = tui.terminal.columns;
|
|
386
448
|
const widthTimer = setInterval(() => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
449
|
+
try {
|
|
450
|
+
const nextWidth = tui.terminal.columns;
|
|
451
|
+
if (!Number.isFinite(nextWidth) || nextWidth <= 0 || nextWidth === observedWidth) return;
|
|
452
|
+
observedWidth = nextWidth;
|
|
453
|
+
// Deliberately skip tui.invalidate(): upstream pi 0.83.0 has a bug
|
|
454
|
+
// where ToolExecutionComponent.updateDisplay() can push undefined
|
|
455
|
+
// into Container.children (renderer returns undefined for a
|
|
456
|
+
// "working" tool). Container.invalidate() then does
|
|
457
|
+
// `child.invalidate?.()` on that undefined child → TypeError →
|
|
458
|
+
// uncaughtException in this timer callback → pi process exit.
|
|
459
|
+
// requestRender(true) already clears previousLines and forces a
|
|
460
|
+
// full re-render with the new width, which is all we need here.
|
|
461
|
+
tui.requestRender(true);
|
|
462
|
+
} catch {
|
|
463
|
+
// Last-resort guard: a synchronous exception in this timer
|
|
464
|
+
// callback would become an uncaughtException and kill the pi
|
|
465
|
+
// process. Swallow it so the session survives; the next tick
|
|
466
|
+
// will retry.
|
|
467
|
+
}
|
|
392
468
|
}, WIDTH_POLL_INTERVAL_MS);
|
|
393
469
|
widthTimer.unref?.();
|
|
394
470
|
const unsubscribeBranch = footerData.onBranchChange(() => tui.requestRender());
|
|
@@ -409,6 +485,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
409
485
|
glyphs,
|
|
410
486
|
spin: spinFrame(glyphs, now, isAnimating()),
|
|
411
487
|
now,
|
|
488
|
+
hideLiveDuration: config.staticMode,
|
|
412
489
|
},
|
|
413
490
|
)[0];
|
|
414
491
|
return renderFooter({
|
|
@@ -444,16 +521,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
444
521
|
};
|
|
445
522
|
|
|
446
523
|
// --- teammate lifecycle (custom event bus; subscribed once for the extension lifetime) ---
|
|
447
|
-
pi.events.on(TEAMMATE_STARTED_EVENT, (
|
|
448
|
-
|
|
524
|
+
pi.events.on(TEAMMATE_STARTED_EVENT, (payload) => {
|
|
525
|
+
if (!isStartedPayload(payload)) return;
|
|
526
|
+
agents.applyStarted(payload);
|
|
449
527
|
req();
|
|
450
528
|
});
|
|
451
|
-
pi.events.on(TEAMMATE_MESSAGE_EVENT, (
|
|
452
|
-
|
|
529
|
+
pi.events.on(TEAMMATE_MESSAGE_EVENT, (payload) => {
|
|
530
|
+
if (!isProgressPayload(payload)) return;
|
|
531
|
+
agents.applyMessage(payload);
|
|
453
532
|
req();
|
|
454
533
|
});
|
|
455
|
-
pi.events.on(TEAMMATE_COMPLETE_EVENT, (
|
|
456
|
-
|
|
534
|
+
pi.events.on(TEAMMATE_COMPLETE_EVENT, (payload) => {
|
|
535
|
+
if (!isCompletePayload(payload)) return;
|
|
536
|
+
agents.applyComplete(payload);
|
|
457
537
|
// A failure that arrives after the session went idle still needs a loop to
|
|
458
538
|
// expire it, so the tick is re-evaluated rather than assumed to be running.
|
|
459
539
|
syncTick();
|
|
@@ -478,7 +558,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
478
558
|
|
|
479
559
|
// --- foreground tool label + todo changes ---
|
|
480
560
|
pi.on("tool_execution_start", (e) => {
|
|
481
|
-
activeTools.set(e.toolCallId, e.toolName);
|
|
561
|
+
activeTools.set(e.toolCallId, { name: e.toolName, startedAt: Date.now() });
|
|
482
562
|
req();
|
|
483
563
|
});
|
|
484
564
|
pi.on("tool_execution_end", (e, ctx) => {
|
|
@@ -557,6 +637,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
557
637
|
pi.events.emit(COCKPIT_UI_OWNERSHIP_EVENT, released);
|
|
558
638
|
lastCtx = undefined;
|
|
559
639
|
running = false;
|
|
640
|
+
runningStartedAt = undefined;
|
|
560
641
|
activeTools.clear();
|
|
561
642
|
thinkingTimer.reset();
|
|
562
643
|
invalidateUsageCache();
|
|
@@ -567,26 +648,28 @@ export default function (pi: ExtensionAPI): void {
|
|
|
567
648
|
|
|
568
649
|
pi.on("agent_start", () => {
|
|
569
650
|
running = true;
|
|
651
|
+
runningStartedAt = Date.now();
|
|
570
652
|
startTick();
|
|
571
653
|
req();
|
|
572
654
|
});
|
|
573
655
|
pi.on("agent_end", () => {
|
|
574
656
|
running = false;
|
|
657
|
+
runningStartedAt = undefined;
|
|
575
658
|
activeTools.clear();
|
|
576
659
|
thinkingTimer.stop();
|
|
577
660
|
syncTick();
|
|
578
661
|
req();
|
|
579
662
|
});
|
|
580
663
|
|
|
581
|
-
// --- live
|
|
664
|
+
// --- live elapsed for folded thinking rows ---
|
|
582
665
|
pi.on("message_update", (e) => {
|
|
583
666
|
thinkingTimer.onAssistantMessageEvent(e.assistantMessageEvent);
|
|
584
667
|
});
|
|
585
668
|
|
|
586
669
|
// --- redraw triggers for the footer's live data ---
|
|
587
670
|
pi.on("message_end", (e, ctx) => {
|
|
588
|
-
// Settle an interrupted thinking run: message_end fires for aborted
|
|
589
|
-
//
|
|
671
|
+
// Settle an interrupted thinking run: message_end fires for aborted and
|
|
672
|
+
// failed messages too, while the row is still mounted.
|
|
590
673
|
if (e.message.role === "assistant") thinkingTimer.onAssistantMessageEnd();
|
|
591
674
|
invalidateUsageCache();
|
|
592
675
|
if (isTuiContext(ctx)) req();
|
|
@@ -609,6 +692,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
609
692
|
close: () => done(undefined),
|
|
610
693
|
theme,
|
|
611
694
|
glyphs: resolveGlyphs(config.icons.mode),
|
|
695
|
+
hideLiveDuration: config.staticMode,
|
|
612
696
|
getTerminalRows: () => terminalRows(tui),
|
|
613
697
|
}), {
|
|
614
698
|
overlay: true,
|
|
@@ -715,6 +799,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
715
799
|
req();
|
|
716
800
|
};
|
|
717
801
|
|
|
802
|
+
const setStaticMode = (target: boolean): boolean => {
|
|
803
|
+
if (config.staticMode === target) return true;
|
|
804
|
+
config = { ...config, staticMode: target };
|
|
805
|
+
const result = saveConfig(config);
|
|
806
|
+
if (!result.ok) {
|
|
807
|
+
configProblem = `static mode save failed: ${result.error ?? "unknown error"}`;
|
|
808
|
+
} else if (configProblem?.startsWith("static mode save failed:")) {
|
|
809
|
+
configProblem = undefined;
|
|
810
|
+
}
|
|
811
|
+
// Restart or stop the animation loop for the new mode: turning static on
|
|
812
|
+
// must kill a running tick, turning it off must revive one mid-run.
|
|
813
|
+
syncTick();
|
|
814
|
+
thinkingTimer.syncMode();
|
|
815
|
+
req();
|
|
816
|
+
return result.ok;
|
|
817
|
+
};
|
|
818
|
+
|
|
718
819
|
pi.registerShortcut(BASH_BG_OVERLAY_KEY, {
|
|
719
820
|
description: "Open background Bash jobs — live status, command, cwd, duration and output tail",
|
|
720
821
|
async handler(ctx) {
|
|
@@ -741,6 +842,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
741
842
|
const candidates = [
|
|
742
843
|
"quiet", "quiet on", "quiet off", "bg", "jobs", "todo", "todo expand", "todo collapse",
|
|
743
844
|
"sidebar", "sidebar auto", "sidebar on", "sidebar off", "sidebar resize",
|
|
845
|
+
"static", "static on", "static off",
|
|
744
846
|
];
|
|
745
847
|
const matches = candidates.filter((c) => c.startsWith(query));
|
|
746
848
|
return matches.length > 0 ? matches.map((c) => ({ value: c, label: c })) : null;
|
|
@@ -795,6 +897,28 @@ export default function (pi: ExtensionAPI): void {
|
|
|
795
897
|
req();
|
|
796
898
|
return;
|
|
797
899
|
}
|
|
900
|
+
if (action === "static" || action === "static toggle") {
|
|
901
|
+
const ok = setStaticMode(!config.staticMode);
|
|
902
|
+
ctx.ui.notify(
|
|
903
|
+
ok
|
|
904
|
+
? `Cockpit static mode ${config.staticMode ? "on — live updates reduced" : "off"}`
|
|
905
|
+
: "Cockpit static mode kept for this session; save failed",
|
|
906
|
+
ok ? "info" : "warning",
|
|
907
|
+
);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (action === "static on" || action === "static off") {
|
|
911
|
+
const target = action === "static on";
|
|
912
|
+
if (config.staticMode === target) return;
|
|
913
|
+
const ok = setStaticMode(target);
|
|
914
|
+
ctx.ui.notify(
|
|
915
|
+
ok
|
|
916
|
+
? `Cockpit static mode ${target ? "on — live updates reduced" : "off"}`
|
|
917
|
+
: "Cockpit static mode kept for this session; save failed",
|
|
918
|
+
ok ? "info" : "warning",
|
|
919
|
+
);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
798
922
|
await openSettings(ctx);
|
|
799
923
|
},
|
|
800
924
|
});
|
|
@@ -821,6 +945,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
821
945
|
const apply = (key: string): void => {
|
|
822
946
|
const wasEnabled = config.enabled;
|
|
823
947
|
const wasQuiet = config.quietMode;
|
|
948
|
+
const wasStatic = config.staticMode;
|
|
824
949
|
const wasSidebarMode = config.sidebar.mode;
|
|
825
950
|
if (key === "theme") {
|
|
826
951
|
// Delegate: the picker previews live and reverts on Esc, neither of
|
|
@@ -870,6 +995,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
870
995
|
if (wasQuiet !== config.quietMode) {
|
|
871
996
|
applyQuietMode(ctx, wasQuiet, config.quietMode);
|
|
872
997
|
}
|
|
998
|
+
if (wasStatic !== config.staticMode) {
|
|
999
|
+
syncTick();
|
|
1000
|
+
thinkingTimer.syncMode();
|
|
1001
|
+
}
|
|
873
1002
|
req();
|
|
874
1003
|
};
|
|
875
1004
|
const ui = {
|