pi-cockpit 0.18.0 → 0.20.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/README.md +2 -2
- package/package.json +6 -6
- package/src/agent-bar.ts +15 -5
- package/src/ambient.ts +70 -4
- package/src/config.ts +3 -1
- package/src/editor/input-history-store.ts +43 -11
- package/src/endpoint-store.ts +1 -1
- package/src/icons.ts +4 -6
- package/src/index.ts +147 -15
- package/src/input-routing.ts +4 -0
- package/src/maestro-store.ts +18 -0
- package/src/public/v1/events.ts +15 -0
- package/src/render.ts +11 -0
- package/src/settings/cockpit-provider.ts +4 -4
- package/src/stack-widget.ts +1 -1
- package/src/todo-store.ts +16 -1
- package/src/tui-i18n.ts +18 -0
- package/src/types.ts +1 -1
- package/src/usage/extension.ts +997 -924
- package/src/viewport-stability.ts +28 -17
- package/src/window-autocomplete.ts +192 -0
- package/src/window-bar.ts +6 -4
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ The tab title is `frame + pi - <session> - <working state>`. The session part fo
|
|
|
120
120
|
- `theme`: named theme override; empty string follows the Pi session theme.
|
|
121
121
|
- `usage.enabled`: master switch for the usage bars (quota, balance, spend). On by default; off hides the footer segment and disables the `/usage` command. Ports the [`hknet/pi-usage-bars`](https://github.com/hknet/pi-usage-bars) extension.
|
|
122
122
|
- `usage.footer`: when `true`, render the live quota bar on a dedicated footer line. The `/usage` command works regardless of this toggle. On by default.
|
|
123
|
-
- `usage.pollIntervalMs`: how often to refresh usage data, in milliseconds. Clamped to `30000`..`1800000` (30s..30min)
|
|
123
|
+
- `usage.pollIntervalMs`: how often to refresh usage data, in milliseconds. Clamped to `30000`..`1800000` (30s..30min); `0` switches to manual refresh — no background polling and no footer bar; the `/usage` overlay fetches on open and re-fetches with `r`. Default `120000` (2min). Lower values hit provider APIs more often.
|
|
124
124
|
- `usage.barWidth`: characters per quota bar in the footer. Clamped to `4`..`16`. Default `8`.
|
|
125
125
|
- `usage.commandKey`: the `/`-command that opens the usage selector overlay. Default `"usage"`. Requires `/reload` to take effect (commands register at session start).
|
|
126
126
|
|
|
@@ -136,7 +136,7 @@ The tab title is `frame + pi - <session> - <working state>`. The session part fo
|
|
|
136
136
|
- `/cockpit sidebar auto|on|off` — selects dock behavior.
|
|
137
137
|
- `/cockpit sidebar resize` or `Ctrl+Shift+R` — enters temporary Resize mode. Left/Right adjusts one column, Shift+Left/Shift+Right adjusts four, Enter accepts, and Escape rolls back. Mouse reporting is active only during Resize mode.
|
|
138
138
|
- `/cockpit quiet` — toggles quiet mode; `/cockpit bg` shows background jobs.
|
|
139
|
-
- `/usage` — opens the usage selector overlay: quota, balance, and spend for every configured provider, with the active one marked `✓`. Search filters by name; arrows/`j`/`k` move, Enter expands the selected provider, Esc closes. Expanding a provider also shows its **7-day token trend** — a sparkline of daily token usage, a `total tok · $cost · turns/day` summary, and the top 2 models by token share — read from the optional `pi-maestro-flow` usage-history store (`~/.pi/agent/usage-history`), so it appears once that extension has recorded turns and is omitted on bare Pi. The command key is configurable via `usage.commandKey` (default `"usage"`, requires `/reload`). Disabled while `usage.enabled` is off.
|
|
139
|
+
- `/usage` — opens the usage selector overlay: quota, balance, and spend for every configured provider, with the active one marked `✓`. Search filters by name; arrows/`j`/`k` move, Enter expands the selected provider, Esc closes; `r` (with an empty search box) re-fetches all providers; `p` (with an empty search box) toggles background polling — persisted to `usage.pollIntervalMs` (off writes `0`, on restores the default `120000`), so the footer bar starts/stops. Expanding a provider also shows its **7-day token trend** — a sparkline of daily token usage, a `total tok · $cost · turns/day` summary, and the top 2 models by token share — read from the optional `pi-maestro-flow` usage-history store (`~/.pi/agent/usage-history`), so it appears once that extension has recorded turns and is omitted on bare Pi. The command key is configurable via `usage.commandKey` (default `"usage"`, requires `/reload`). Disabled while `usage.enabled` is off.
|
|
140
140
|
- `/theme` — switch theme with live preview; `/theme <name>` applies directly. `cockpit-zen` is the restrained warm-gold theme designed for the Zen stack; selecting it does not change `stackStyle`. Pi ships no standalone theme command; cockpit provides one.
|
|
141
141
|
|
|
142
142
|
## Sidebar compatibility
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cockpit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -67,12 +67,12 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@earendil-works/pi-agent-core": "0.
|
|
71
|
-
"@earendil-works/pi-ai": "0.
|
|
72
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
73
|
-
"@earendil-works/pi-tui": "0.
|
|
70
|
+
"@earendil-works/pi-agent-core": "0.84.4",
|
|
71
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
73
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
74
74
|
"@types/node": "^22.0.0",
|
|
75
|
-
"pi-maestro-teammate": "2.
|
|
75
|
+
"pi-maestro-teammate": "2.3.0",
|
|
76
76
|
"typebox": "1.3.7",
|
|
77
77
|
"typescript": "^5.5.0"
|
|
78
78
|
},
|
package/src/agent-bar.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { AgentRow } from "./types.ts";
|
|
|
15
15
|
import { effectiveAgentStatus, isCliAgent, type AgentDisplayStatus } from "./agents-store.ts";
|
|
16
16
|
import { visibleAgentRows } from "./stack-widget.ts";
|
|
17
17
|
import { tuiT } from "./tui-i18n.ts";
|
|
18
|
+
import { ownerDisplayToken } from "./window-autocomplete.ts";
|
|
18
19
|
|
|
19
20
|
export const SESSION_BAR_WIDGET_KEY = "cockpit-session-bar";
|
|
20
21
|
export const AGENT_BAR_WIDGET_KEY = SESSION_BAR_WIDGET_KEY;
|
|
@@ -37,12 +38,19 @@ export interface SessionBarDeps {
|
|
|
37
38
|
getCurrentSession?: () => { label: string; color: ThemeColor } | undefined;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export interface SessionBarHint {
|
|
42
|
+
text: string;
|
|
43
|
+
color?: ThemeColor;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SessionBarHintValue = string | SessionBarHint;
|
|
47
|
+
|
|
40
48
|
export interface AgentBarDeps {
|
|
41
49
|
getEndpoints: () => readonly CockpitEndpoint[];
|
|
42
50
|
getState: () => SessionUiState;
|
|
43
51
|
getNow: () => number;
|
|
44
52
|
isMainRunning?: () => boolean;
|
|
45
|
-
getShortcutHint?: () =>
|
|
53
|
+
getShortcutHint?: () => SessionBarHintValue | undefined;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export const AGENT_SESSION_COLORS = [
|
|
@@ -154,7 +162,7 @@ function endpointColor(endpoint: CockpitEndpoint, status: AgentBarStatus, now: n
|
|
|
154
162
|
export interface AgentBarRenderOptions {
|
|
155
163
|
mainRunning?: boolean;
|
|
156
164
|
now?: number;
|
|
157
|
-
shortcutHint?:
|
|
165
|
+
shortcutHint?: SessionBarHintValue;
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
/** Chip-to-chip separator in the agent bar. */
|
|
@@ -231,11 +239,12 @@ export function renderSessionBarLine(
|
|
|
231
239
|
renderContent: (width: number) => string,
|
|
232
240
|
width: number,
|
|
233
241
|
theme: Theme,
|
|
234
|
-
shortcutHint?:
|
|
242
|
+
shortcutHint?: SessionBarHintValue,
|
|
235
243
|
): string {
|
|
236
244
|
const w = Math.max(1, width);
|
|
237
245
|
if (!shortcutHint) return renderContent(w);
|
|
238
|
-
const
|
|
246
|
+
const hintSpec = typeof shortcutHint === "string" ? { text: shortcutHint, color: "dim" as const } : shortcutHint;
|
|
247
|
+
const hint = theme.fg(hintSpec.color ?? "dim", hintSpec.text);
|
|
239
248
|
const hintWidth = visibleWidth(hint);
|
|
240
249
|
const gap = 2;
|
|
241
250
|
if (w < hintWidth + gap + 8) return renderContent(w);
|
|
@@ -269,9 +278,10 @@ export function renderAgentBar(
|
|
|
269
278
|
const outcome = status === "done" || status === "failed" || status === "terminated" || status === "sleeping"
|
|
270
279
|
? endpoint.agentRow?.lastOutcome
|
|
271
280
|
: undefined;
|
|
281
|
+
const ownerId = endpoint.kind === "root" ? endpoint.registryEndpoint?.ownerId : undefined;
|
|
272
282
|
return {
|
|
273
283
|
id: endpoint.id,
|
|
274
|
-
label: endpoint.label,
|
|
284
|
+
label: ownerId ? ownerDisplayToken(endpoint.label, ownerId) : endpoint.label,
|
|
275
285
|
ordinal: endpoint.ordinal,
|
|
276
286
|
unread: state.endpoint(endpoint.id).unread,
|
|
277
287
|
endpoint,
|
package/src/ambient.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface AmbientState {
|
|
|
16
16
|
agents: readonly AgentRow[];
|
|
17
17
|
jobs: readonly BashBgJob[];
|
|
18
18
|
running: boolean;
|
|
19
|
+
waitingForInput?: boolean;
|
|
19
20
|
cwd?: string;
|
|
20
21
|
activeTool?: string;
|
|
21
22
|
workingStartedAt?: number;
|
|
@@ -54,17 +55,32 @@ function failedJobs(jobs: readonly BashBgJob[]): BashBgJob[] {
|
|
|
54
55
|
return jobs.filter((job) => job.status === "failed" || (job.exitCode !== null && job.exitCode !== 0));
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
export function nextUiPromptDepth(depth: number, event: "start" | "end"): number {
|
|
59
|
+
return event === "start" ? depth + 1 : Math.max(0, depth - 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function shouldHideWorkingDuration(
|
|
63
|
+
mode: "regular" | "fullscreen" | undefined,
|
|
64
|
+
staticMode: boolean,
|
|
65
|
+
): boolean {
|
|
66
|
+
// Pi's regular renderer shares scrollback between the transcript and status
|
|
67
|
+
// row, so a moving live timer can be preserved as transcript height changes.
|
|
68
|
+
return staticMode || mode !== "fullscreen";
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
/**
|
|
58
72
|
* The streaming working line.
|
|
59
73
|
*
|
|
60
|
-
* Cockpit hides the host indicator and renders the active state
|
|
61
|
-
* elapsed value
|
|
74
|
+
* Cockpit hides the host indicator and renders the active state. Fullscreen may
|
|
75
|
+
* add a live elapsed value; the regular main screen keeps this row stable.
|
|
62
76
|
*/
|
|
63
77
|
export function workingMessage(state: AmbientState, now = Date.now()): string | undefined {
|
|
64
|
-
const label = state.
|
|
78
|
+
const label = state.waitingForInput
|
|
79
|
+
? tuiT("ambient.waitingForInput")
|
|
80
|
+
: state.activeTool ?? (state.running ? tuiT("ambient.working") : undefined);
|
|
65
81
|
if (!label) return undefined;
|
|
66
82
|
const sep = state.separator ?? " ";
|
|
67
|
-
const text = (state.hideLiveDuration || state.workingStartedAt === undefined)
|
|
83
|
+
const text = (state.waitingForInput || state.hideLiveDuration || state.workingStartedAt === undefined)
|
|
68
84
|
? label
|
|
69
85
|
: `${label}${sep}${formatThinkingDuration(now - state.workingStartedAt)}`;
|
|
70
86
|
return `\x1b[3m${text}\x1b[23m`;
|
|
@@ -100,6 +116,8 @@ export function titleFor(
|
|
|
100
116
|
let title: string;
|
|
101
117
|
if (broken > 0) {
|
|
102
118
|
title = `${marks.fail} ${base}${sep}${tuiT("ambient.failed", { count: broken })}`;
|
|
119
|
+
} else if (state.waitingForInput) {
|
|
120
|
+
title = `${prefix}${base}${sep}${tuiT("ambient.waitingForInput")}`;
|
|
103
121
|
} else if (state.running) {
|
|
104
122
|
const live = liveAgents(state.agents).length;
|
|
105
123
|
title = live > 0
|
|
@@ -135,3 +153,51 @@ export function titleFor(
|
|
|
135
153
|
export function statusText(problem: string | undefined, mark: string): string | undefined {
|
|
136
154
|
return problem ? `${mark} cockpit: ${problem}` : undefined;
|
|
137
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Some host UI setters invalidate the whole TUI even when the value did not
|
|
159
|
+
* change. Cache the last ambient values so the 250ms Cockpit tick only mutates
|
|
160
|
+
* host surfaces when there is a real text change.
|
|
161
|
+
*/
|
|
162
|
+
export class AmbientSurfaceCache {
|
|
163
|
+
private workingMessageKnown = false;
|
|
164
|
+
private workingMessage: string | undefined;
|
|
165
|
+
private titleKnown = false;
|
|
166
|
+
private title: string | undefined;
|
|
167
|
+
private readonly statuses = new Map<string, string | undefined>();
|
|
168
|
+
|
|
169
|
+
reset(): void {
|
|
170
|
+
this.workingMessageKnown = false;
|
|
171
|
+
this.workingMessage = undefined;
|
|
172
|
+
this.titleKnown = false;
|
|
173
|
+
this.title = undefined;
|
|
174
|
+
this.statuses.clear();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
setWorkingMessage(setter: (message: string | undefined) => void, message: string | undefined): boolean {
|
|
178
|
+
if (this.workingMessageKnown && this.workingMessage === message) return false;
|
|
179
|
+
this.workingMessageKnown = true;
|
|
180
|
+
this.workingMessage = message;
|
|
181
|
+
setter(message);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setTitle(setter: (title: string) => void, title: string): boolean {
|
|
186
|
+
if (this.titleKnown && this.title === title) return false;
|
|
187
|
+
this.titleKnown = true;
|
|
188
|
+
this.title = title;
|
|
189
|
+
setter(title);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setStatus(
|
|
194
|
+
key: string,
|
|
195
|
+
setter: (key: string, text: string | undefined) => void,
|
|
196
|
+
text: string | undefined,
|
|
197
|
+
): boolean {
|
|
198
|
+
if (this.statuses.has(key) && this.statuses.get(key) === text) return false;
|
|
199
|
+
this.statuses.set(key, text);
|
|
200
|
+
setter(key, text);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -23,7 +23,9 @@ function mergeUsageConfig(base: UsageConfig, raw: unknown): UsageConfig {
|
|
|
23
23
|
const isFiniteInt = (v: unknown): v is number =>
|
|
24
24
|
typeof v === "number" && Number.isFinite(v) && Number.isSafeInteger(v);
|
|
25
25
|
const pollIntervalMs = isFiniteInt(o.pollIntervalMs)
|
|
26
|
-
?
|
|
26
|
+
? o.pollIntervalMs <= 0
|
|
27
|
+
? 0 // 0 = manual refresh: no background poll, /usage fetches on open and on `r`
|
|
28
|
+
: Math.min(USAGE_POLL_MAX_MS, Math.max(USAGE_POLL_MIN_MS, o.pollIntervalMs))
|
|
27
29
|
: base.pollIntervalMs;
|
|
28
30
|
const barWidth = isFiniteInt(o.barWidth)
|
|
29
31
|
? Math.min(USAGE_BAR_MAX_WIDTH, Math.max(USAGE_BAR_MIN_WIDTH, o.barWidth))
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* so a brand new session picks up where the previous one left off.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
11
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { basename, join, resolve } from "node:path";
|
|
@@ -19,6 +19,9 @@ const DEFAULT_MAX_ENTRIES = 100;
|
|
|
19
19
|
const DEFAULT_SAVE_DEBOUNCE_MS = 250;
|
|
20
20
|
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
21
21
|
const PRIVATE_FILE_MODE = 0o600;
|
|
22
|
+
const DEFAULT_SAVE_RETRY_DELAYS_MS = Object.freeze([25, 75, 150]);
|
|
23
|
+
|
|
24
|
+
type RenameFile = (oldPath: string, newPath: string) => Promise<void>;
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Serializes every save across store instances so a fresh store's load() never
|
|
@@ -32,6 +35,8 @@ export interface InputHistoryStoreOptions {
|
|
|
32
35
|
maxEntries?: number;
|
|
33
36
|
debounceMs?: number;
|
|
34
37
|
onError?: (error: unknown) => void;
|
|
38
|
+
renameFile?: RenameFile;
|
|
39
|
+
retryDelaysMs?: readonly number[];
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/**
|
|
@@ -59,6 +64,8 @@ export class InputHistoryStore {
|
|
|
59
64
|
private readonly maxEntries: number;
|
|
60
65
|
private readonly debounceMs: number;
|
|
61
66
|
private readonly onError: ((error: unknown) => void) | undefined;
|
|
67
|
+
private readonly renameFile: RenameFile;
|
|
68
|
+
private readonly retryDelaysMs: readonly number[];
|
|
62
69
|
private entries: string[] = [];
|
|
63
70
|
private timer: NodeJS.Timeout | undefined;
|
|
64
71
|
private writes: Promise<void> = Promise.resolve();
|
|
@@ -70,6 +77,8 @@ export class InputHistoryStore {
|
|
|
70
77
|
this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES);
|
|
71
78
|
this.debounceMs = Math.max(0, options.debounceMs ?? DEFAULT_SAVE_DEBOUNCE_MS);
|
|
72
79
|
this.onError = options.onError;
|
|
80
|
+
this.renameFile = options.renameFile ?? rename;
|
|
81
|
+
this.retryDelaysMs = retryDelays(options.retryDelaysMs);
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
/** Newest first. */
|
|
@@ -124,21 +133,30 @@ export class InputHistoryStore {
|
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
private async save(): Promise<void> {
|
|
136
|
+
await mkdir(this.dir, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
137
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
138
|
+
await this.mergeDiskEntries();
|
|
139
|
+
const temp = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
140
|
+
try {
|
|
141
|
+
const payload = JSON.stringify({ version: 1, entries: this.entries }, null, 2);
|
|
142
|
+
await writeFile(temp, payload, { mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
143
|
+
await this.renameFile(temp, this.filePath);
|
|
144
|
+
return;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
147
|
+
const retryDelay = this.retryDelaysMs[attempt];
|
|
148
|
+
if (retryDelay === undefined || !isTransientReplaceError(error)) throw error;
|
|
149
|
+
await delay(retryDelay);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async mergeDiskEntries(): Promise<void> {
|
|
127
155
|
// Another pi window in the same workspace may have written since we loaded;
|
|
128
156
|
// keep whatever it added instead of overwriting it.
|
|
129
157
|
const known = new Set(this.entries);
|
|
130
158
|
const disk = (await this.readFromDisk()).filter((value) => !known.has(value));
|
|
131
159
|
this.entries = capped([...this.entries, ...disk], this.maxEntries);
|
|
132
|
-
await mkdir(this.dir, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
133
|
-
const temp = `${this.filePath}.${process.pid}.tmp`;
|
|
134
|
-
try {
|
|
135
|
-
const payload = JSON.stringify({ version: 1, entries: this.entries }, null, 2);
|
|
136
|
-
await writeFile(temp, payload, { mode: PRIVATE_FILE_MODE });
|
|
137
|
-
await rename(temp, this.filePath);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
await rm(temp, { force: true });
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
160
|
}
|
|
143
161
|
|
|
144
162
|
private async readFromDisk(): Promise<string[]> {
|
|
@@ -171,3 +189,17 @@ function parseEntries(raw: unknown): string[] {
|
|
|
171
189
|
function capped(entries: string[], maxEntries: number): string[] {
|
|
172
190
|
return entries.length > maxEntries ? entries.slice(0, maxEntries) : entries;
|
|
173
191
|
}
|
|
192
|
+
|
|
193
|
+
function retryDelays(value: readonly number[] | undefined): readonly number[] {
|
|
194
|
+
const source = value ?? DEFAULT_SAVE_RETRY_DELAYS_MS;
|
|
195
|
+
return Object.freeze(source.map((delayMs) => Math.max(0, delayMs)).filter(Number.isFinite));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isTransientReplaceError(error: unknown): boolean {
|
|
199
|
+
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
200
|
+
return code === "EPERM" || code === "EBUSY" || code === "EACCES";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function delay(ms: number): Promise<void> {
|
|
204
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
205
|
+
}
|
package/src/endpoint-store.ts
CHANGED
|
@@ -507,7 +507,7 @@ export class EndpointStore {
|
|
|
507
507
|
id: `${MONITOR_CONTROL_ENDPOINT_PREFIX}${encodeURIComponent(root.id)}`,
|
|
508
508
|
logicalKey: `monitor-control:${root.ownerId}:${root.ownerNonce}`,
|
|
509
509
|
kind: "window",
|
|
510
|
-
label:
|
|
510
|
+
label: `control·${root.ownerId.slice(0, 6)}`,
|
|
511
511
|
ordinal: Number.MIN_SAFE_INTEGER,
|
|
512
512
|
status: root.status,
|
|
513
513
|
contentRevision: revisionOf([root.contentRevision, controlActivityRevision]),
|
package/src/icons.ts
CHANGED
|
@@ -149,13 +149,11 @@ export function resolveGlyphs(mode: IconMode): IconGlyphs {
|
|
|
149
149
|
/**
|
|
150
150
|
* One source of truth for animation cadence.
|
|
151
151
|
*
|
|
152
|
-
* The frame clock and
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* 4-frame ASCII spinner degenerated into a two-glyph flip (| ↔ -), which reads
|
|
156
|
-
* as blinking rather than rotation.
|
|
152
|
+
* The frame clock and redraw tick stay aligned so terminal animation advances
|
|
153
|
+
* one frame per paint. A half-second cadence remains legible without keeping
|
|
154
|
+
* the terminal renderer busy during long-running work.
|
|
157
155
|
*/
|
|
158
|
-
export const ANIMATION_PERIOD_MS =
|
|
156
|
+
export const ANIMATION_PERIOD_MS = 500;
|
|
159
157
|
|
|
160
158
|
/**
|
|
161
159
|
* The frame to draw at `now`, or a stable glyph when nothing is driving redraws.
|