@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aemonge-dev/pi-stardeck",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "A Vim- and tmux-inspired command deck for Pi",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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
- ( 3 › 2 · 00:05'12) working ⟩ checking Stabilize runtime and focus · step 3/5  task 1/2 › stps 3/5
9
- 󰓾 Stabilize runtime and focus in the cockpit
10
- ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
11
- [󰆧]  ~/projects/example  main ⟩  clean
12
- ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
13
- 󰚩 Opus › high ⟩ 󰾆 42% › 󰎞 2  10% › 󰜦 $0.10 ⟩  12% ›  375M ›  3/3
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 preferred editor-deck surface keeps two stable semantic rows. The first is a
18
- left-packed operational strip: runtime capsule, lifecycle, activity, controlled next
19
- action, and Task/Step progress. The second is a full-width explicit selection canvas
20
- that wraps to at most two lines. Every slot remains present; zero, idle, unavailable,
21
- and unselected states are dim instead of appearing or disappearing. Long content may
22
- wrap responsively, but runtime state never changes the deck topology. Decorative rails
23
- remain slim and contain no telemetry.
24
-
25
- The editor deck uses blue for the active runtime capsule, the bold lifecycle anchor,
26
- and regular-weight explicit selection. Ordinary active activity remains neutral, except
27
- that `recovering` and its ordinary suggestion are blue together while only the lifecycle
28
- is bold. Other ordinary suggestions remain green. Incomplete Task/Step progress is
29
- neutral until each counter completes and stays right-aligned without a major boundary.
30
- The lifecycle/activity boundary and rails remain purple; minor separators are dim.
31
- Human-input and validation cues stay yellow, blockers stay red, and every inactive value
32
- stays dim.
33
-
34
- The legacy above-editor widget owns only current work identity and preserves the
35
- ` ready ⟩ no focused task` idle row when no work is selected. The leading icon is
36
- runtime-authoritative: `` while Pi is busy in an active lifecycle and `` while
37
- settled, waiting, blocked, listening, or aborted. Internal lifecycle states render as
38
- short readable verbs. The Ramona loop remains exact: `understand`, `work`, `assure`,
39
- `learn`, and `answer`. Runtime boundaries are `wait`, `blocked`, `ready`, or `stopped`.
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?: 'legacy' | 'deck' | 'editor-deck';
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
- if (deckMode) requestRender?.();
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 publishRuntimeFooter = () => {
319
- if (deckMode) return;
320
- const activityAge = currentActivityAge();
321
- const status = formatRailChronometer(activityAge);
322
- const active = activityAge !== null;
323
- const signature = `${active ? 'active' : 'idle'}:${status}`;
324
- if (signature === lastActivityAgeSignature) return;
325
- lastActivityAgeSignature = signature;
326
- for (const message of buildRuntimeFooterWidgets(status, active)) {
327
- pi.events.emit(FANCY_FOOTER_WIDGET_CHANNEL, message);
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
- lastActivityAgeSignature = '';
286
+ startActivityTicker();
411
287
  requestRender?.();
412
- publishRuntimeFooter();
413
288
  };
414
289
 
415
290
  const clearObservedActivity = () => {
416
291
  lastObservedActivityAt = null;
417
- lastActivityAgeSignature = '';
292
+ stopActivityTicker();
418
293
  };
419
294
 
420
295
  const refreshActivityAge = () => {
421
296
  if (currentActivityAge() === null) return;
422
- if (deckMode) requestRender?.();
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
- if (deckMode) requestRender?.();
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
- if (activityAgeTimer) clearInterval(activityAgeTimer);
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
- if (deckMode) {
535
- clearFooter = installEmptyDeckFooter(
536
- ctx,
537
- (statuses) => {
538
- pi.events.emit(PI_NATIVE_STATUS_CHANNEL, {
539
- protocol: 1,
540
- type: 'snapshot',
541
- statuses,
542
- });
543
- },
544
- (available) => {
545
- nativeStatusBridgeAvailable = available;
546
- publishCapabilityHealth();
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
- if (deckMode) return renderDeck(width, theme);
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
- if (deckMode) requestRender?.();
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
- if (deckMode) requestRender?.();
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
- const ACTIVITY_LABELS: Record<HeaderActivity['kind'], string> = {
283
- understanding: 'interpreting',
284
- inspection: 'inspecting',
285
- synthesis: 'synthesizing',
286
- planning: 'planning',
287
- 'skill-proposal': 'proposing skill',
288
- mutation: 'updating',
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 line-3 band speaks when either side carries a real signal:
320
- * an observed activity (title-state or legacy) or a turn meter that
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 title = state.header?.title ?? null;
340
- if (title) {
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
- const activitySpeaks = observedActivityLabel(state) !== '';
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 — the live phase word
355
- // between tools, `awaiting Human` only when the session truly rests.
356
- const label = observedActivityLabel(state);
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;
@@ -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. Call this at the first conversational turn with a short sanitized title (at most 120 characters, never raw prompt text) and revise it only when the topic clearly shifts; never require Human action for it',
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
- const completedTasks = Math.max(
412
- 0,
413
- Math.trunc(instructionProgress?.complete ?? change.completedTasks),
414
- );
415
- const totalTasks = Math.max(
416
- 0,
417
- Math.trunc(instructionProgress?.total ?? change.totalTasks),
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