@aemonge-dev/pi-stardeck 0.10.0 → 0.11.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 +1 -1
- package/packages/pi-stardeck/README.md +32 -65
- package/packages/pi-stardeck/index.ts +39 -202
- package/packages/pi-stardeck/src/deck.ts +15 -64
- package/packages/pi-stardeck/src/gauge.ts +0 -34
- package/packages/status/index.ts +18 -2
- package/packages/status/src/active-work.ts +12 -0
- package/packages/status/src/openspec.ts +14 -8
package/package.json
CHANGED
|
@@ -4,73 +4,40 @@ A dependency-free Pi extension that coordinates the prompt information architect
|
|
|
4
4
|
through Pi's supported widget and event APIs.
|
|
5
5
|
|
|
6
6
|
```text
|
|
7
|
-
─ ›
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
─ › ────────────────────────────────────────────────────────────────────
|
|
8
|
+
[] ~/projects/example › main › +0 ~1 1 Opus › high › 42%
|
|
9
|
+
──────────────────────────────────────────────────────────────────────
|
|
10
|
+
Stabilize runtime and focus in the cockpit planning 6/7 › 9/19
|
|
11
|
+
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
|
12
|
+
editing deck.ts 00:38'00 · 4.2k
|
|
13
|
+
└ (00'12) scout working
|
|
14
|
+
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
|
15
|
+
5h 52% › 3% › 48% › 2 › 268M › 38% › $5.16 abcd1234
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
The
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
and
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
and
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
The title has four styled groups: lifecycle, a structured active ancestry, focus, and
|
|
41
|
-
optional focus detail. Each activity-path segment receives the blue minor `›`; the
|
|
42
|
-
violet-pink major `⟩` divides runtime ancestry from the green narrative focus. Human
|
|
43
|
-
prompt text is never converted into focus automatically. OpenSpec may add its exact Task
|
|
44
|
-
after another green `›`. Focus selection prefers an exact OpenSpec Task, then
|
|
45
|
-
Session Work. Active turns without a selection use the explicit `OpenSpec › no focus`,
|
|
46
|
-
`no OpenSpec`, or `no focus` state supplied by the status producer; `Direct response` is
|
|
47
|
-
never presented as a focus. Empty states are dim while real focus remains bold semantic
|
|
48
|
-
green.
|
|
49
|
-
|
|
50
|
-
Wide rows render lifecycle → active ancestry → optional focus → optional focus detail,
|
|
51
|
-
for example `assure › checking quality › assuring tests ⟩ Implement global flow`. The
|
|
52
|
-
renderer generates every separator. Empty or rejected paths leave no separator or
|
|
53
|
-
placeholder whitespace. As width falls it tries full labels, compact labels, a collapsed
|
|
54
|
-
`… › deepest` ancestry, and finally compact lifecycle plus the deepest active segment.
|
|
55
|
-
Narrative focus disappears before meaningful active ancestry. Terminal display-cell
|
|
56
|
-
measurement and final truncation remain Unicode- and Nerd-Font-aware.
|
|
57
|
-
|
|
58
|
-
Runtime anchor and lifecycle are bold blue `#076678`; activity ancestry and its minor
|
|
59
|
-
`›` are regular blue; the major `⟩` is purple; focus is bold semantic success green; and
|
|
60
|
-
optional focus hierarchy is regular success green. Dim is reserved for the two empty
|
|
61
|
-
focus fallbacks; muted, text, cyan, yellow, and gray are never used in the title.
|
|
62
|
-
|
|
63
|
-
The extension hides Pi's separate built-in `Working...` loader row with
|
|
64
|
-
`ctx.ui.setWorkingVisible(false)`, including its vertical space. The header therefore
|
|
65
|
-
uses its complete width for lifecycle, bounded activity, and focus. Runtime age is
|
|
66
|
-
published for the bottom prompt rail as an eight-cell chronometer measuring time since
|
|
67
|
-
the most recent observed foreground provider, assistant, or tool event. It stays
|
|
68
|
-
fixed-width as it advances: `00:00'00`, `00:00'45`, `00:12'30`, or `12:34'56`; hour/day
|
|
69
|
-
fallbacks also remain eight cells. Active timing uses the high-contrast deep-cyan
|
|
70
|
-
`accent` color; settled states display dim `00:00'00` rather than repeating the header's
|
|
71
|
-
`wait`, `ready`, or other lifecycle word. A session-scoped ticker publishes only when
|
|
72
|
-
the formatted chronometer changes and is cleared on shutdown. No message content,
|
|
73
|
-
thinking, reasoning, prompts, tool arguments, or child-agent output enters the widget.
|
|
18
|
+
The deck renders four stable rows above the editor:
|
|
19
|
+
|
|
20
|
+
1. **Where and what** — identity and companion chips lead, then the project path
|
|
21
|
+
and Git segment; the right corner carries the engine group (model › thinking ›
|
|
22
|
+
quota) and any alert.
|
|
23
|
+
2. **Focus** — the session subject (auto-titled from the first real prompt,
|
|
24
|
+
settable through the `title` tool) with the OpenSpec work tile on the right.
|
|
25
|
+
3. **Pure activity band (optional)** — exists only while an observed activity is
|
|
26
|
+
live — `editing`, `reading`, `grepping`, `bash`, or `subagent` — with the turn
|
|
27
|
+
chronometer and token count riding on the right; subagent and bash child runs
|
|
28
|
+
keep their `└` span rows directly beneath. Between tools and at rest the band
|
|
29
|
+
retires entirely: Pi's own `Working…` indicator stays visible exactly as the
|
|
30
|
+
core renders it, and no phase words or `awaiting Human` filler are ever
|
|
31
|
+
generated.
|
|
32
|
+
4. **Machine and status** — quota, CPU/RAM, context, compactions, and session
|
|
33
|
+
cost yield right-to-left into the width the status end leaves: notices when
|
|
34
|
+
present, otherwise the dim ladybug sentinel wearing the short session id.
|
|
35
|
+
|
|
36
|
+
Rows appear only when they speak — a silent band and spanless state collapse
|
|
37
|
+
their seam. Every slot is bounded, sanitized display vocabulary; no message
|
|
38
|
+
content, thinking, reasoning, prompts, tool arguments, or child-agent output
|
|
39
|
+
enters the widget. Terminal display-cell measurement and final truncation remain
|
|
40
|
+
Unicode- and Nerd-Font-aware.
|
|
74
41
|
|
|
75
42
|
The additive header protocol projects selection independently from live work:
|
|
76
43
|
|
|
@@ -3,32 +3,16 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
Theme,
|
|
5
5
|
} from '@earendil-works/pi-coding-agent';
|
|
6
|
-
import { truncateToWidth } from '@earendil-works/pi-tui';
|
|
7
6
|
|
|
8
7
|
import {
|
|
9
|
-
buildAgentFooterWidgets,
|
|
10
|
-
buildPromptStatusSnapshot,
|
|
11
|
-
COMPACTION_WIDGET_ID,
|
|
12
|
-
CONTEXT_WIDGET_ID,
|
|
13
8
|
countCompactions,
|
|
14
|
-
ENVIRONMENT_WIDGET_ID,
|
|
15
|
-
FANCY_FOOTER_READY_CHANNEL,
|
|
16
9
|
FOOTER_TELEMETRY_CHANNEL,
|
|
17
|
-
FANCY_FOOTER_WIDGET_CHANNEL,
|
|
18
10
|
formatFooterBranch,
|
|
19
11
|
formatFooterPath,
|
|
20
12
|
GALACTICA_HEADER_CHANNEL,
|
|
21
|
-
GIT_BRANCH_WIDGET_ID,
|
|
22
|
-
GIT_CONFLICT_WIDGET_ID,
|
|
23
|
-
GIT_SCOPE_SEPARATOR_WIDGET_ID,
|
|
24
|
-
GIT_MODIFIED_WIDGET_ID,
|
|
25
|
-
GIT_STAGED_WIDGET_ID,
|
|
26
|
-
GIT_UNTRACKED_WIDGET_ID,
|
|
27
13
|
type FooterTelemetrySnapshot,
|
|
28
14
|
type GitStatusSummary,
|
|
29
15
|
type HeaderSnapshot,
|
|
30
|
-
LEGACY_GIT_WIDGET_ID,
|
|
31
|
-
LOCATION_WIDGET_ID,
|
|
32
16
|
normalizeContextPercent,
|
|
33
17
|
parseFooterTelemetry,
|
|
34
18
|
parseGitBranch,
|
|
@@ -37,24 +21,12 @@ import {
|
|
|
37
21
|
parsePiStatusSnapshot,
|
|
38
22
|
PI_NATIVE_STATUS_CHANNEL,
|
|
39
23
|
PI_STATUS_SNAPSHOT_CHANNEL,
|
|
40
|
-
PROMPT_STATUS_CHANNEL,
|
|
41
|
-
REASONING_WIDGET_ID,
|
|
42
|
-
SCOPE_SEPARATOR_WIDGET_ID,
|
|
43
|
-
runtimeHeaderWork,
|
|
44
24
|
parseVimMode,
|
|
45
25
|
VIM_MODE_CHANNEL,
|
|
46
26
|
type PiStatusSnapshot,
|
|
47
27
|
type VimMode,
|
|
48
28
|
} from './src/gauge.ts';
|
|
49
29
|
import {
|
|
50
|
-
buildRuntimeFooterWidgets,
|
|
51
|
-
formatRailChronometer,
|
|
52
|
-
RUNTIME_SCOPE_SEPARATOR_WIDGET_ID,
|
|
53
|
-
RUNTIME_STATUS_WIDGET_ID,
|
|
54
|
-
} from './src/activity-age.ts';
|
|
55
|
-
import {
|
|
56
|
-
buildCapabilityFooterWidget,
|
|
57
|
-
CAPABILITY_WIDGET_ID,
|
|
58
30
|
LSP_STATUS_EVENT,
|
|
59
31
|
MCP_STATUS_EVENT,
|
|
60
32
|
parseLspCapability,
|
|
@@ -64,11 +36,8 @@ import {
|
|
|
64
36
|
} from './src/capabilities.ts';
|
|
65
37
|
import { shouldRefreshGitAfterTool } from './src/git-refresh.ts';
|
|
66
38
|
import {
|
|
67
|
-
buildResourceFooterWidgets,
|
|
68
39
|
buildResourceTelemetry,
|
|
69
40
|
calculateCpuPercent,
|
|
70
|
-
CPU_WIDGET_ID,
|
|
71
|
-
MEMORY_WIDGET_ID,
|
|
72
41
|
RESOURCE_REFRESH_MS,
|
|
73
42
|
sampleProcessTreeResources,
|
|
74
43
|
updateCpuSmoothing,
|
|
@@ -77,7 +46,6 @@ import {
|
|
|
77
46
|
type ResourceTelemetry,
|
|
78
47
|
} from './src/process-resources.ts';
|
|
79
48
|
import { renderHeaderDeck } from './src/deck.ts';
|
|
80
|
-
import { layoutLifecycleTitle, renderLifecycleTitleSections } from './src/title.ts';
|
|
81
49
|
|
|
82
50
|
const WIDGET_KEY = 'galactica.work-identity';
|
|
83
51
|
const PI_STATUS_SOURCE_CHANNEL = 'pi-stardeck:status-source/v1';
|
|
@@ -103,7 +71,11 @@ export interface ContextHeaderDeckSurface {
|
|
|
103
71
|
}
|
|
104
72
|
|
|
105
73
|
export interface ContextHeaderOptions {
|
|
106
|
-
surface
|
|
74
|
+
/** The deck surface is the product: `'deck'` registers the above-editor
|
|
75
|
+
* widget, `'editor-deck'` renders into an embedder's component instead
|
|
76
|
+
* (vim border). The pre-deck legacy title line is retired
|
|
77
|
+
* (simplify-deck-defaults). */
|
|
78
|
+
surface?: 'deck' | 'editor-deck';
|
|
107
79
|
deckSurface?: ContextHeaderDeckSurface;
|
|
108
80
|
}
|
|
109
81
|
|
|
@@ -164,7 +136,6 @@ export default function galacticaContextHeader(
|
|
|
164
136
|
pi: ExtensionAPI,
|
|
165
137
|
options: ContextHeaderOptions = {},
|
|
166
138
|
): void {
|
|
167
|
-
const deckMode = options.surface === 'deck' || options.surface === 'editor-deck';
|
|
168
139
|
const editorDeckSurface =
|
|
169
140
|
options.surface === 'editor-deck' ? options.deckSurface : undefined;
|
|
170
141
|
let clearWidget: (() => void) | undefined;
|
|
@@ -176,7 +147,6 @@ export default function galacticaContextHeader(
|
|
|
176
147
|
let header: HeaderSnapshot | null = null;
|
|
177
148
|
let agentBusy = false;
|
|
178
149
|
let lastObservedActivityAt: number | null = null;
|
|
179
|
-
let lastActivityAgeSignature = '';
|
|
180
150
|
let activityAgeTimer: ReturnType<typeof setInterval> | undefined;
|
|
181
151
|
let sessionGeneration = 0;
|
|
182
152
|
let sessionShortId: string | undefined;
|
|
@@ -196,64 +166,9 @@ export default function galacticaContextHeader(
|
|
|
196
166
|
let piStatus: PiStatusSnapshot | undefined;
|
|
197
167
|
let nativeStatusBridgeAvailable = true;
|
|
198
168
|
let vimMode: VimMode = 'insert';
|
|
199
|
-
let lastCapabilitySignature = '';
|
|
200
169
|
|
|
201
170
|
const currentHeader = () => header;
|
|
202
171
|
|
|
203
|
-
const publishPromptStatus = () => {
|
|
204
|
-
if (deckMode) return;
|
|
205
|
-
pi.events.emit(
|
|
206
|
-
PROMPT_STATUS_CHANNEL,
|
|
207
|
-
buildPromptStatusSnapshot(currentHeader(), branch, gitStatus),
|
|
208
|
-
);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const publishFooter = () => {
|
|
212
|
-
if (deckMode) return;
|
|
213
|
-
const ctx = activeCtx;
|
|
214
|
-
if (!ctx) return;
|
|
215
|
-
const usage = ctx.getContextUsage();
|
|
216
|
-
const contextPercent = normalizeContextPercent(
|
|
217
|
-
usage?.percent,
|
|
218
|
-
usage?.tokens,
|
|
219
|
-
usage?.contextWindow ?? ctx.model?.contextWindow,
|
|
220
|
-
);
|
|
221
|
-
const thinking = pi.getThinkingLevel();
|
|
222
|
-
for (const message of buildAgentFooterWidgets({
|
|
223
|
-
cwd: formatFooterPath(activeCwd || ctx.cwd, process.env.HOME),
|
|
224
|
-
devbox: process.env.DEVBOX_PROJECT_ROOT !== undefined,
|
|
225
|
-
branch: formatFooterBranch(branch),
|
|
226
|
-
git: gitStatus,
|
|
227
|
-
gitAvailable,
|
|
228
|
-
model: ctx.model?.name || ctx.model?.id || 'no model',
|
|
229
|
-
...(thinking && thinking !== 'off' ? { thinking } : {}),
|
|
230
|
-
contextPercent,
|
|
231
|
-
compactionCount: countCompactions(ctx.sessionManager.getBranch()),
|
|
232
|
-
})) {
|
|
233
|
-
pi.events.emit(FANCY_FOOTER_WIDGET_CHANNEL, message);
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const publishLatestResourceFooter = () => {
|
|
238
|
-
if (deckMode || !latestResourceTelemetry) return;
|
|
239
|
-
for (const message of buildResourceFooterWidgets(latestResourceTelemetry)) {
|
|
240
|
-
pi.events.emit(FANCY_FOOTER_WIDGET_CHANNEL, message);
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const publishCapabilityFooter = (force = false) => {
|
|
245
|
-
if (deckMode) return;
|
|
246
|
-
const signature = [mcpCapability?.healthy ?? '', mcpCapability?.total ?? ''].join(
|
|
247
|
-
':',
|
|
248
|
-
);
|
|
249
|
-
if (!force && signature === lastCapabilitySignature) return;
|
|
250
|
-
lastCapabilitySignature = signature;
|
|
251
|
-
pi.events.emit(
|
|
252
|
-
FANCY_FOOTER_WIDGET_CHANNEL,
|
|
253
|
-
buildCapabilityFooterWidget(mcpCapability),
|
|
254
|
-
);
|
|
255
|
-
};
|
|
256
|
-
|
|
257
172
|
const refreshResources = async (): Promise<void> => {
|
|
258
173
|
if (!activeCtx || resourceRefreshInFlight) return;
|
|
259
174
|
resourceRefreshInFlight = true;
|
|
@@ -274,58 +189,28 @@ export default function galacticaContextHeader(
|
|
|
274
189
|
latestResourceTelemetry = telemetry;
|
|
275
190
|
if (signature === lastResourceSignature) return;
|
|
276
191
|
lastResourceSignature = signature;
|
|
277
|
-
|
|
278
|
-
else publishLatestResourceFooter();
|
|
192
|
+
requestRender?.();
|
|
279
193
|
} finally {
|
|
280
194
|
resourceRefreshInFlight = false;
|
|
281
195
|
}
|
|
282
196
|
};
|
|
283
197
|
|
|
284
|
-
const removeFooterWidgets = () => {
|
|
285
|
-
for (const id of [
|
|
286
|
-
LOCATION_WIDGET_ID,
|
|
287
|
-
LEGACY_GIT_WIDGET_ID,
|
|
288
|
-
GIT_BRANCH_WIDGET_ID,
|
|
289
|
-
GIT_SCOPE_SEPARATOR_WIDGET_ID,
|
|
290
|
-
GIT_STAGED_WIDGET_ID,
|
|
291
|
-
GIT_MODIFIED_WIDGET_ID,
|
|
292
|
-
GIT_UNTRACKED_WIDGET_ID,
|
|
293
|
-
GIT_CONFLICT_WIDGET_ID,
|
|
294
|
-
ENVIRONMENT_WIDGET_ID,
|
|
295
|
-
REASONING_WIDGET_ID,
|
|
296
|
-
CONTEXT_WIDGET_ID,
|
|
297
|
-
COMPACTION_WIDGET_ID,
|
|
298
|
-
CPU_WIDGET_ID,
|
|
299
|
-
MEMORY_WIDGET_ID,
|
|
300
|
-
RUNTIME_STATUS_WIDGET_ID,
|
|
301
|
-
RUNTIME_SCOPE_SEPARATOR_WIDGET_ID,
|
|
302
|
-
SCOPE_SEPARATOR_WIDGET_ID,
|
|
303
|
-
CAPABILITY_WIDGET_ID,
|
|
304
|
-
]) {
|
|
305
|
-
pi.events.emit(FANCY_FOOTER_WIDGET_CHANNEL, {
|
|
306
|
-
protocol: 1,
|
|
307
|
-
type: 'remove',
|
|
308
|
-
id,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
|
|
313
198
|
const currentActivityAge = (): number | null =>
|
|
314
199
|
agentBusy && lastObservedActivityAt !== null
|
|
315
200
|
? Math.max(0, Date.now() - lastObservedActivityAt)
|
|
316
201
|
: null;
|
|
317
202
|
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
203
|
+
const startActivityTicker = (): void => {
|
|
204
|
+
// Duty-cycled: the 50 ms chronometer ticks only while an activity can
|
|
205
|
+
// render — idle sessions cost zero wakeups (simplify-deck-defaults).
|
|
206
|
+
if (activityAgeTimer) return;
|
|
207
|
+
activityAgeTimer = setInterval(refreshActivityAge, 50);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const stopActivityTicker = (): void => {
|
|
211
|
+
if (activityAgeTimer === undefined) return;
|
|
212
|
+
clearInterval(activityAgeTimer);
|
|
213
|
+
activityAgeTimer = undefined;
|
|
329
214
|
};
|
|
330
215
|
|
|
331
216
|
const publishCapabilityHealth = () => {
|
|
@@ -363,12 +248,6 @@ export default function galacticaContextHeader(
|
|
|
363
248
|
});
|
|
364
249
|
};
|
|
365
250
|
|
|
366
|
-
const stopReady = pi.events.on(FANCY_FOOTER_READY_CHANNEL, () => {
|
|
367
|
-
publishFooter();
|
|
368
|
-
publishLatestResourceFooter();
|
|
369
|
-
publishRuntimeFooter();
|
|
370
|
-
publishCapabilityFooter(true);
|
|
371
|
-
});
|
|
372
251
|
const stopLspStatus = pi.events.on(LSP_STATUS_EVENT, (raw) => {
|
|
373
252
|
lspCapability = parseLspCapability(raw);
|
|
374
253
|
requestRender?.();
|
|
@@ -377,7 +256,6 @@ export default function galacticaContextHeader(
|
|
|
377
256
|
const stopMcpStatus = pi.events.on(MCP_STATUS_EVENT, (raw) => {
|
|
378
257
|
mcpCapability = parseMcpCapability(raw);
|
|
379
258
|
requestRender?.();
|
|
380
|
-
publishCapabilityFooter();
|
|
381
259
|
publishCapabilityHealth();
|
|
382
260
|
});
|
|
383
261
|
const stopFooterTelemetry = pi.events.on(FOOTER_TELEMETRY_CHANNEL, (raw) => {
|
|
@@ -401,33 +279,28 @@ export default function galacticaContextHeader(
|
|
|
401
279
|
const stopHeader = pi.events.on(GALACTICA_HEADER_CHANNEL, (raw) => {
|
|
402
280
|
header = parseHeaderSnapshot(raw);
|
|
403
281
|
requestRender?.();
|
|
404
|
-
publishPromptStatus();
|
|
405
|
-
publishRuntimeFooter();
|
|
406
282
|
});
|
|
407
283
|
|
|
408
284
|
const markObservedActivity = () => {
|
|
409
285
|
lastObservedActivityAt = Date.now();
|
|
410
|
-
|
|
286
|
+
startActivityTicker();
|
|
411
287
|
requestRender?.();
|
|
412
|
-
publishRuntimeFooter();
|
|
413
288
|
};
|
|
414
289
|
|
|
415
290
|
const clearObservedActivity = () => {
|
|
416
291
|
lastObservedActivityAt = null;
|
|
417
|
-
|
|
292
|
+
stopActivityTicker();
|
|
418
293
|
};
|
|
419
294
|
|
|
420
295
|
const refreshActivityAge = () => {
|
|
421
296
|
if (currentActivityAge() === null) return;
|
|
422
|
-
|
|
423
|
-
else publishRuntimeFooter();
|
|
297
|
+
requestRender?.();
|
|
424
298
|
};
|
|
425
299
|
|
|
426
300
|
pi.on('before_agent_start', (_event, ctx) => {
|
|
427
301
|
if (ctx.mode !== 'tui') return;
|
|
428
302
|
agentBusy = true;
|
|
429
303
|
markObservedActivity();
|
|
430
|
-
publishPromptStatus();
|
|
431
304
|
});
|
|
432
305
|
|
|
433
306
|
pi.on('turn_start', markObservedActivity);
|
|
@@ -480,11 +353,7 @@ export default function galacticaContextHeader(
|
|
|
480
353
|
gitStatus = gitAvailable
|
|
481
354
|
? parseGitStatus(String(statusResult?.stdout ?? ''))
|
|
482
355
|
: { ...EMPTY_GIT };
|
|
483
|
-
|
|
484
|
-
else {
|
|
485
|
-
publishPromptStatus();
|
|
486
|
-
publishFooter();
|
|
487
|
-
}
|
|
356
|
+
requestRender?.();
|
|
488
357
|
} while (gitRefreshQueued && activeCwd);
|
|
489
358
|
} finally {
|
|
490
359
|
gitRefreshInFlight = false;
|
|
@@ -495,9 +364,8 @@ export default function galacticaContextHeader(
|
|
|
495
364
|
clearWidget?.();
|
|
496
365
|
clearFooter?.();
|
|
497
366
|
clearFooter = undefined;
|
|
498
|
-
|
|
367
|
+
stopActivityTicker();
|
|
499
368
|
if (resourceTimer) clearInterval(resourceTimer);
|
|
500
|
-
activityAgeTimer = undefined;
|
|
501
369
|
resourceTimer = undefined;
|
|
502
370
|
++sessionGeneration;
|
|
503
371
|
activeCtx = ctx;
|
|
@@ -519,34 +387,23 @@ export default function galacticaContextHeader(
|
|
|
519
387
|
cpuSmoothing = undefined;
|
|
520
388
|
latestResourceTelemetry = undefined;
|
|
521
389
|
lastResourceSignature = '';
|
|
522
|
-
lastCapabilitySignature = '';
|
|
523
390
|
clearObservedActivity();
|
|
524
|
-
publishFooter();
|
|
525
|
-
publishPromptStatus();
|
|
526
|
-
publishRuntimeFooter();
|
|
527
|
-
publishCapabilityFooter();
|
|
528
391
|
publishCapabilityHealth();
|
|
529
|
-
if (ctx.mode === 'tui') {
|
|
530
|
-
ctx.ui.setWorkingVisible(false);
|
|
531
|
-
activityAgeTimer = setInterval(refreshActivityAge, 50);
|
|
532
|
-
}
|
|
533
392
|
if (!ctx.hasUI) return;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
);
|
|
549
|
-
}
|
|
393
|
+
clearFooter = installEmptyDeckFooter(
|
|
394
|
+
ctx,
|
|
395
|
+
(statuses) => {
|
|
396
|
+
pi.events.emit(PI_NATIVE_STATUS_CHANNEL, {
|
|
397
|
+
protocol: 1,
|
|
398
|
+
type: 'snapshot',
|
|
399
|
+
statuses,
|
|
400
|
+
});
|
|
401
|
+
},
|
|
402
|
+
(available) => {
|
|
403
|
+
nativeStatusBridgeAvailable = available;
|
|
404
|
+
publishCapabilityHealth();
|
|
405
|
+
},
|
|
406
|
+
);
|
|
550
407
|
void refreshResources();
|
|
551
408
|
resourceTimer = setInterval(() => void refreshResources(), RESOURCE_REFRESH_MS);
|
|
552
409
|
activeCwd = ctx.cwd;
|
|
@@ -610,16 +467,7 @@ export default function galacticaContextHeader(
|
|
|
610
467
|
requestRender = () => tui.requestRender();
|
|
611
468
|
return {
|
|
612
469
|
render(width: number): string[] {
|
|
613
|
-
|
|
614
|
-
const { work, idle } = runtimeHeaderWork(currentHeader()?.work, agentBusy);
|
|
615
|
-
const layout = layoutLifecycleTitle(work, width, agentBusy);
|
|
616
|
-
const line = renderLifecycleTitleSections(
|
|
617
|
-
layout,
|
|
618
|
-
work.lifecycle,
|
|
619
|
-
idle,
|
|
620
|
-
theme,
|
|
621
|
-
);
|
|
622
|
-
return [truncateToWidth(line, width, '…')];
|
|
470
|
+
return renderDeck(width, theme);
|
|
623
471
|
},
|
|
624
472
|
invalidate() {},
|
|
625
473
|
};
|
|
@@ -634,21 +482,17 @@ export default function galacticaContextHeader(
|
|
|
634
482
|
});
|
|
635
483
|
|
|
636
484
|
pi.on('model_select', () => {
|
|
637
|
-
publishFooter();
|
|
638
485
|
requestRender?.();
|
|
639
486
|
});
|
|
640
487
|
|
|
641
488
|
pi.on('thinking_level_select', () => {
|
|
642
|
-
|
|
643
|
-
else publishFooter();
|
|
489
|
+
requestRender?.();
|
|
644
490
|
});
|
|
645
491
|
pi.on('message_end', (event) => {
|
|
646
492
|
if (event.message.role === 'assistant') markObservedActivity();
|
|
647
|
-
publishFooter();
|
|
648
493
|
});
|
|
649
494
|
pi.on('session_compact', () => {
|
|
650
|
-
|
|
651
|
-
else publishFooter();
|
|
495
|
+
requestRender?.();
|
|
652
496
|
});
|
|
653
497
|
|
|
654
498
|
pi.on('tool_execution_end', (event) => {
|
|
@@ -662,9 +506,6 @@ export default function galacticaContextHeader(
|
|
|
662
506
|
agentBusy = false;
|
|
663
507
|
clearObservedActivity();
|
|
664
508
|
requestRender?.();
|
|
665
|
-
publishPromptStatus();
|
|
666
|
-
publishFooter();
|
|
667
|
-
publishRuntimeFooter();
|
|
668
509
|
void refreshGit();
|
|
669
510
|
});
|
|
670
511
|
|
|
@@ -675,9 +516,7 @@ export default function galacticaContextHeader(
|
|
|
675
516
|
gitAvailable = false;
|
|
676
517
|
agentBusy = false;
|
|
677
518
|
clearObservedActivity();
|
|
678
|
-
if (activityAgeTimer) clearInterval(activityAgeTimer);
|
|
679
519
|
if (resourceTimer) clearInterval(resourceTimer);
|
|
680
|
-
activityAgeTimer = undefined;
|
|
681
520
|
resourceTimer = undefined;
|
|
682
521
|
resourceRefreshInFlight = false;
|
|
683
522
|
previousResourceSample = undefined;
|
|
@@ -689,9 +528,7 @@ export default function galacticaContextHeader(
|
|
|
689
528
|
clearWidget = undefined;
|
|
690
529
|
clearFooter?.();
|
|
691
530
|
clearFooter = undefined;
|
|
692
|
-
removeFooterWidgets();
|
|
693
531
|
stopHeader();
|
|
694
|
-
stopReady();
|
|
695
532
|
stopLspStatus();
|
|
696
533
|
stopMcpStatus();
|
|
697
534
|
stopPiStatus();
|
|
@@ -5,7 +5,6 @@ import type { LspCapability, McpCapability } from './capabilities.ts';
|
|
|
5
5
|
import type {
|
|
6
6
|
FooterTelemetrySnapshot,
|
|
7
7
|
GitStatusSummary,
|
|
8
|
-
HeaderActivity,
|
|
9
8
|
HeaderSnapshot,
|
|
10
9
|
PiStatusSnapshot,
|
|
11
10
|
VimMode,
|
|
@@ -279,82 +278,34 @@ function spanTreeRows(state: HeaderDeckState, width: number, theme: Theme): stri
|
|
|
279
278
|
});
|
|
280
279
|
}
|
|
281
280
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
implementation: 'implementing',
|
|
290
|
-
'change-review': 'reviewing change',
|
|
291
|
-
regression: 'testing',
|
|
292
|
-
verification: 'checking',
|
|
293
|
-
'result-review': 'reviewing result',
|
|
294
|
-
answering: 'answering',
|
|
295
|
-
recovery: 'recovering',
|
|
296
|
-
'awaiting-validation': 'validation',
|
|
297
|
-
'awaiting-input': 'input',
|
|
298
|
-
'operation-aborted': 'stopped',
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
function activityLabel(state: HeaderDeckState): string {
|
|
302
|
-
const pathActivity = state.header?.work?.activityPath?.at(-1);
|
|
303
|
-
const pathLabel = safeText(pathActivity?.compact || pathActivity?.label || '');
|
|
304
|
-
if (pathLabel) return pathLabel;
|
|
305
|
-
const activity = state.header?.work?.activity;
|
|
306
|
-
// `awaiting-*` kinds are the rest face — they never speak for the band
|
|
307
|
-
// (a focused session still retires; quiet-activity-row amendment).
|
|
308
|
-
if (!activity || activity.kind.startsWith('awaiting-')) return '';
|
|
309
|
-
return ACTIVITY_LABELS[activity.kind];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** `editing deck.ts` / `reading schema.yaml` / `subagent: scout`. */
|
|
281
|
+
/** The band speaks only when an observed activity is live — one of the
|
|
282
|
+
* five title-state classes (editing/reading/grepping/bash/subagent).
|
|
283
|
+
* Fabricated phase words (`interpreting`, `planning`, …), the legacy
|
|
284
|
+
* activity projection, and the `awaiting Human` placeholder are gone
|
|
285
|
+
* (Human-directed simplify-deck-defaults): Pi's restored native working
|
|
286
|
+
* indicator owns mid-turn liveness, and the deck narrates only observed
|
|
287
|
+
* reality. */
|
|
313
288
|
function titleActivityLabel(activity: { class: string; target: string }): string {
|
|
314
289
|
return activity.class === 'subagent'
|
|
315
290
|
? `subagent: ${activity.target}`
|
|
316
291
|
: `${activity.class} ${activity.target}`;
|
|
317
292
|
}
|
|
318
293
|
|
|
319
|
-
/** The
|
|
320
|
-
*
|
|
321
|
-
* cost tokens or time. Both idle (`awaiting Human` + `— · 0 tok` or
|
|
322
|
-
* no meter) is zero activity — the band and its seam retire
|
|
323
|
-
* (Human-directed 2026-10-20, quiet-activity-row). */
|
|
324
|
-
/** Live phase word from the header's kind-based activity projection —
|
|
325
|
-
* fills the gaps between tools (thinking, planning, checking) so the
|
|
326
|
-
* band never claims `awaiting Human` mid-turn. The `awaiting-*` kinds
|
|
327
|
-
* are the REST face (awaiting Human review/input after settle): they
|
|
328
|
-
* never speak for the band, or a focused session would never rest
|
|
329
|
-
* (quiet-activity-row amendment). */
|
|
330
|
-
function liveKindLabel(state: HeaderDeckState): string {
|
|
331
|
-
const activity = state.header?.activity ?? null;
|
|
332
|
-
if (!activity || activity.kind.startsWith('awaiting-')) return '';
|
|
333
|
-
return ACTIVITY_LABELS[activity.kind] ?? '';
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** The band's current left label: the precise title-state activity when a
|
|
337
|
-
* tool is observed, else the live phase word; empty when truly idle. */
|
|
294
|
+
/** The band's current label: the live observed activity; empty when
|
|
295
|
+
* nothing is observed — the band then retires wholesale. */
|
|
338
296
|
function observedActivityLabel(state: HeaderDeckState): string {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
return title.activity ? titleActivityLabel(title.activity) : liveKindLabel(state);
|
|
342
|
-
}
|
|
343
|
-
return activityLabel(state) || liveKindLabel(state);
|
|
297
|
+
const activity = state.header?.title?.activity ?? null;
|
|
298
|
+
return activity ? titleActivityLabel(activity) : '';
|
|
344
299
|
}
|
|
345
300
|
|
|
346
301
|
function activityBandSpeaks(state: HeaderDeckState): boolean {
|
|
347
|
-
|
|
348
|
-
const turn = state.header?.turn ?? null;
|
|
349
|
-
const costSpeaks = turn !== null && (turn.tokens > 0 || turn.elapsedMs > 0);
|
|
350
|
-
return activitySpeaks || costSpeaks;
|
|
302
|
+
return observedActivityLabel(state) !== '';
|
|
351
303
|
}
|
|
352
304
|
|
|
353
305
|
function activityTile(state: HeaderDeckState, theme: Theme): string {
|
|
354
|
-
// Line 3 left: the observed activity owns the slot
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
return label ? color(theme, 'text', label) : color(theme, 'dim', 'awaiting Human');
|
|
306
|
+
// Line 3 left: the observed activity owns the slot; the turn cluster
|
|
307
|
+
// rides its right side and retires with it.
|
|
308
|
+
return color(theme, 'text', observedActivityLabel(state));
|
|
358
309
|
}
|
|
359
310
|
|
|
360
311
|
function turnCostTile(state: HeaderDeckState, theme: Theme): string {
|
|
@@ -191,40 +191,6 @@ export const HEADER_PRESENTATION = {
|
|
|
191
191
|
locationColor: 'success',
|
|
192
192
|
} as const;
|
|
193
193
|
|
|
194
|
-
export const IDLE_HEADER_WORK: HeaderWork = {
|
|
195
|
-
lifecycle: 'listening',
|
|
196
|
-
titles: ['no focused task'],
|
|
197
|
-
color: 'accent',
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export function displayedHeaderWork(work: HeaderWork | null | undefined): {
|
|
201
|
-
work: HeaderWork;
|
|
202
|
-
idle: boolean;
|
|
203
|
-
} {
|
|
204
|
-
return work ? { work, idle: false } : { work: IDLE_HEADER_WORK, idle: true };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function runtimeHeaderWork(
|
|
208
|
-
work: HeaderWork | null | undefined,
|
|
209
|
-
agentBusy: boolean,
|
|
210
|
-
): { work: HeaderWork; idle: boolean } {
|
|
211
|
-
const displayed = displayedHeaderWork(work);
|
|
212
|
-
if (!agentBusy || !displayed.idle) return displayed;
|
|
213
|
-
return {
|
|
214
|
-
work: {
|
|
215
|
-
lifecycle: 'understanding',
|
|
216
|
-
titles: ['no focus'],
|
|
217
|
-
color: 'accent',
|
|
218
|
-
activity: { kind: 'understanding' },
|
|
219
|
-
},
|
|
220
|
-
idle: false,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function lifecycleStatusLabel(work: HeaderWork | null | undefined): string {
|
|
225
|
-
return work ? `⟩ ${work.lifecycle}` : '⟩';
|
|
226
|
-
}
|
|
227
|
-
|
|
228
194
|
export type ContextGauge = {
|
|
229
195
|
percent: number;
|
|
230
196
|
percentText: string;
|
package/packages/status/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
sessionSubject,
|
|
29
29
|
taskflowRunFocus,
|
|
30
30
|
taskFocus,
|
|
31
|
+
titleFromPrompt,
|
|
31
32
|
} from './src/active-work.ts';
|
|
32
33
|
import { collectDiagnostics } from './src/diagnostics.ts';
|
|
33
34
|
import { loadConfig } from './src/config.ts';
|
|
@@ -1197,8 +1198,23 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1197
1198
|
runtime?.settle();
|
|
1198
1199
|
});
|
|
1199
1200
|
|
|
1200
|
-
pi.on('before_agent_start', () => {
|
|
1201
|
+
pi.on('before_agent_start', (event) => {
|
|
1201
1202
|
runtime?.beginTurn();
|
|
1203
|
+
// Auto-title: the first real user prompt names the session while the
|
|
1204
|
+
// subject is still clear — the agent's `title` call stays optional
|
|
1205
|
+
// polish (every agent skipped the first-turn rule; verified
|
|
1206
|
+
// systematically in simplify-session-focus-surface). Human- or
|
|
1207
|
+
// agent-set titles and restored sessions are never overridden.
|
|
1208
|
+
// The event fields are narrowed structurally: the registry-pinned
|
|
1209
|
+
// @earendil-works/pi-coding-agent types predate messageType/prompt,
|
|
1210
|
+
// while the runtime event carries both (extensions.md, before_agent_start).
|
|
1211
|
+
const promptRun = event as { messageType?: unknown; prompt?: unknown };
|
|
1212
|
+
if (promptRun.messageType !== 'user' || typeof promptRun.prompt !== 'string') {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if ((runtime?.currentSubject() ?? NO_SESSION_SUBJECT).state !== 'clear') return;
|
|
1216
|
+
const next = titleFromPrompt(promptRun.prompt);
|
|
1217
|
+
if (next && next.state === 'set') persistAndApplySubject(next);
|
|
1202
1218
|
});
|
|
1203
1219
|
|
|
1204
1220
|
pi.on('tool_execution_start', (event) => {
|
|
@@ -1302,7 +1318,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1302
1318
|
name: 'title',
|
|
1303
1319
|
label: 'Session Title',
|
|
1304
1320
|
description:
|
|
1305
|
-
'Show or set the sanitized session title that keeps the focus row populated beneath OpenSpec and Session Work.
|
|
1321
|
+
'Show or set the sanitized session title that keeps the focus row populated beneath OpenSpec and Session Work. The session titles itself from the first real prompt; call this only to name or rename deliberately (at most 120 characters, never raw prompt text), revising when the topic clearly shifts; never require Human action for it',
|
|
1306
1322
|
promptSnippet: 'Manage the automatic session title',
|
|
1307
1323
|
parameters: Type.Object({
|
|
1308
1324
|
action: StringEnum(['status', 'set'] as const),
|
|
@@ -111,6 +111,18 @@ export function parseSessionSubject(value: unknown): SessionSubject | undefined
|
|
|
111
111
|
return sessionSubject(value.state, value.title);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Derive a session title from a raw user prompt — the extension-side
|
|
115
|
+
* fallback that makes the agent's first-turn `title` call optional
|
|
116
|
+
* polish instead of a load-bearing bet (every agent skipped it). The
|
|
117
|
+
* same sanitizer bounds it; slash commands are commands, not subjects:
|
|
118
|
+
* they never title. */
|
|
119
|
+
export function titleFromPrompt(prompt: unknown): SessionSubject | undefined {
|
|
120
|
+
if (typeof prompt !== 'string') return undefined;
|
|
121
|
+
const normalized = normalizeTitlePart(prompt);
|
|
122
|
+
if (!normalized || normalized.startsWith('/')) return undefined;
|
|
123
|
+
return sessionSubject('set', normalized);
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
export function restoreOpenSpecFocus(entries: readonly unknown[]): OpenSpecFocus {
|
|
115
127
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
116
128
|
const entry = entries[index];
|
|
@@ -408,14 +408,20 @@ export async function collectOpenSpec(options: {
|
|
|
408
408
|
// Progress from `list` remains valid even if apply instructions are unavailable.
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
411
|
+
// File truth first: when the artifact (or apply) task list parsed, its
|
|
412
|
+
// done flags are what the Human actually ticks — the CLI's done-counting
|
|
413
|
+
// does not track custom-schema execution artifacts and pinned the Work
|
|
414
|
+
// tile at 0/N (simplify-deck-defaults). CLI progress and list counts
|
|
415
|
+
// remain fallbacks; the blocked-apply zero-guard stays for the
|
|
416
|
+
// no-tasks case.
|
|
417
|
+
const completedTasks =
|
|
418
|
+
tasks.length > 0
|
|
419
|
+
? tasks.filter((task) => task.done).length
|
|
420
|
+
: Math.max(0, Math.trunc(instructionProgress?.complete ?? change.completedTasks));
|
|
421
|
+
const totalTasks =
|
|
422
|
+
tasks.length > 0
|
|
423
|
+
? tasks.length
|
|
424
|
+
: Math.max(0, Math.trunc(instructionProgress?.total ?? change.totalTasks));
|
|
419
425
|
const complete =
|
|
420
426
|
change.status === 'complete' || (totalTasks > 0 && completedTasks >= totalTasks);
|
|
421
427
|
|