@aemonge-dev/pi-stardeck 0.9.3 → 0.10.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/src/deck.ts +9 -19
- package/packages/pi-stardeck/src/title-state.ts +4 -2
- package/packages/status/README.md +16 -10
- package/packages/status/index.ts +50 -203
- package/packages/status/src/active-work.ts +23 -3
- package/packages/status/src/title-state.ts +6 -3
package/package.json
CHANGED
|
@@ -357,32 +357,22 @@ function activityTile(state: HeaderDeckState, theme: Theme): string {
|
|
|
357
357
|
return label ? color(theme, 'text', label) : color(theme, 'dim', 'awaiting Human');
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
function formatTurnElapsed(ms: number): string {
|
|
361
|
-
// Matches the turn meter's span-idle ceiling: 99m59s999ms → `99:59`.
|
|
362
|
-
const bounded = Math.min(
|
|
363
|
-
(99 * 60 + 59) * 1_000 + 999,
|
|
364
|
-
Math.max(0, Math.floor(Number.isFinite(ms) ? ms : 0)),
|
|
365
|
-
);
|
|
366
|
-
const totalSeconds = Math.floor(bounded / 1_000);
|
|
367
|
-
return `${Math.floor(totalSeconds / 60)}:${String(totalSeconds % 60).padStart(2, '0')}`;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
360
|
function turnCostTile(state: HeaderDeckState, theme: Theme): string {
|
|
371
|
-
// Activity lane right: the turn cluster —
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
361
|
+
// Activity lane right: the turn cluster — turn time and tokens in one
|
|
362
|
+
// aligned group (Human amendment 2026-10-23: the deck's only timer;
|
|
363
|
+
// Human amendment 2026-10-24: the liveness icon is gone — the ticking
|
|
364
|
+
// chronometer `mmm:ss'cc` is the liveness). Idle keeps the accepted
|
|
365
|
+
// dim `— · 0 tok`; the quiet-activity-row contract depends on it.
|
|
375
366
|
const turn = state.header?.turn ?? null;
|
|
376
367
|
if (!turn) return '';
|
|
377
368
|
if (turn.tokens <= 0 && turn.elapsedMs <= 0) {
|
|
378
369
|
return color(theme, 'dim', '— · 0 tok');
|
|
379
370
|
}
|
|
380
|
-
return `${color(theme, '
|
|
371
|
+
return `${color(theme, 'warning', formatDeckElapsed(turn.elapsedMs), true)} ${color(
|
|
381
372
|
theme,
|
|
382
|
-
'
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
)} ${color(theme, 'dim', '·')} ${color(theme, 'warning', formatTokens(turn.tokens))}`;
|
|
373
|
+
'dim',
|
|
374
|
+
'·',
|
|
375
|
+
)} ${color(theme, 'warning', formatTokens(turn.tokens))}`;
|
|
386
376
|
}
|
|
387
377
|
|
|
388
378
|
function gitRight(state: HeaderDeckState, theme: Theme): string {
|
|
@@ -24,8 +24,10 @@ export const DECK_TITLE_ACTIVITY_CLASSES = [
|
|
|
24
24
|
|
|
25
25
|
export type DeckTitleActivityClass = (typeof DECK_TITLE_ACTIVITY_CLASSES)[number];
|
|
26
26
|
|
|
27
|
-
/** Short target label bound from the accepted schema
|
|
28
|
-
|
|
27
|
+
/** Short target label bound from the accepted schema (Human amendment
|
|
28
|
+
* 2026-10-24: 48 → 120, mirroring the status producer — the deck's
|
|
29
|
+
* render-time width crop owns the real limit now). */
|
|
30
|
+
export const DECK_TITLE_TARGET_MAX = 120;
|
|
29
31
|
|
|
30
32
|
/** Task/step totals bound from the accepted schema. */
|
|
31
33
|
export const DECK_TITLE_TOTAL_MAX = 999;
|
|
@@ -69,14 +69,16 @@ ephemeral Session Work state. Display priority is OpenSpec
|
|
|
69
69
|
Task → Session Work → safe direct fallback. OpenSpec focus clears active Session
|
|
70
70
|
Work when acquired, preventing hidden or stale work from resurfacing later.
|
|
71
71
|
|
|
72
|
-
Session Work is
|
|
73
|
-
|
|
72
|
+
Session Work is a legacy surface: its writer tool and `/work` command are retired, and
|
|
73
|
+
only old sessions' versioned custom entries remain, restored fail-soft on the active
|
|
74
|
+
Pi session branch (never a repository status file). OpenSpec focus is stored the same
|
|
75
|
+
way and supports this lifecycle:
|
|
74
76
|
|
|
75
77
|
```text
|
|
76
|
-
/
|
|
77
|
-
/
|
|
78
|
-
/
|
|
79
|
-
/
|
|
78
|
+
/openspec-focus set <change> <task>
|
|
79
|
+
/openspec-focus validate
|
|
80
|
+
/openspec-focus replace <change> <task>
|
|
81
|
+
/openspec-focus clear
|
|
80
82
|
```
|
|
81
83
|
|
|
82
84
|
When no OpenSpec portfolio is available, the matching row-one footer fallback is the
|
|
@@ -84,10 +86,14 @@ compact `▶ Focus` or `◇ Validate`; an entirely empty
|
|
|
84
86
|
project uses ` no OpenSpec`. The detailed Session Work intent remains in the header
|
|
85
87
|
instead of being duplicated in the footer. Active and validation states also replace the
|
|
86
88
|
fallback terminal title. Reload, resume, and tree navigation restore the newest valid
|
|
87
|
-
branch entry. The agent-callable `
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
`
|
|
89
|
+
branch entry. The agent-callable `openspec_focus` tool shares the command's `status`,
|
|
90
|
+
safeguarded `set`, deliberate `replace`, validating `validate`, and persistent `clear`
|
|
91
|
+
transitions; the `awaitingValidation` flag marks a focused task as awaiting Human
|
|
92
|
+
validation and auto-clears on the next `set`, `replace`, or `clear`. The
|
|
93
|
+
agent-callable `title` tool sets the sanitized session title shown when no OpenSpec
|
|
94
|
+
focus owns the row. The retired `work_focus` and `stage` instruments are gone: stages
|
|
95
|
+
derive from events and Taskflow phases, and legacy `session-work` entries from old
|
|
96
|
+
sessions still restore and render, fail-soft.
|
|
91
97
|
|
|
92
98
|
Global routing belongs to native Ramona policy rather than this status package. This
|
|
93
99
|
package does not inject parent routing prompts, block parent tools, create Session Work
|
package/packages/status/index.ts
CHANGED
|
@@ -20,12 +20,12 @@ import {
|
|
|
20
20
|
sameOpenSpecFocus,
|
|
21
21
|
sameSessionSubject,
|
|
22
22
|
sameSessionWorkFocus,
|
|
23
|
+
sameTaskFocus,
|
|
23
24
|
SESSION_SUBJECT_ENTRY_TYPE,
|
|
24
25
|
SESSION_WORK_FOCUS_ENTRY_TYPE,
|
|
25
26
|
type SessionSubject,
|
|
26
27
|
type SessionWorkFocus,
|
|
27
28
|
sessionSubject,
|
|
28
|
-
sessionWorkFocus,
|
|
29
29
|
taskflowRunFocus,
|
|
30
30
|
taskFocus,
|
|
31
31
|
} from './src/active-work.ts';
|
|
@@ -77,7 +77,6 @@ import {
|
|
|
77
77
|
} from './src/reducer.ts';
|
|
78
78
|
import type {
|
|
79
79
|
CockpitSnapshot,
|
|
80
|
-
StageValue,
|
|
81
80
|
CommandRunner,
|
|
82
81
|
DiagnosticsState,
|
|
83
82
|
LoadedConfig,
|
|
@@ -90,7 +89,7 @@ import type {
|
|
|
90
89
|
|
|
91
90
|
type SessionContext = ExtensionContext | ExtensionCommandContext;
|
|
92
91
|
type Domain = RuntimeErrorRecord['domain'];
|
|
93
|
-
type OpenSpecFocusAction = 'status' | 'set' | 'replace' | 'clear';
|
|
92
|
+
type OpenSpecFocusAction = 'status' | 'set' | 'replace' | 'validate' | 'clear';
|
|
94
93
|
|
|
95
94
|
type StatusHealthCondition = {
|
|
96
95
|
id: string;
|
|
@@ -229,9 +228,13 @@ class GalacticaStatusRuntime {
|
|
|
229
228
|
this.workFocus = workFocus;
|
|
230
229
|
this.subject = subject;
|
|
231
230
|
this.sessionName = sessionName;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
workFocus.state === 'validation'
|
|
231
|
+
const restoredAwaitingValidation =
|
|
232
|
+
(focus.mode === 'task' && focus.awaitingValidation) ||
|
|
233
|
+
workFocus.state === 'validation';
|
|
234
|
+
this.liveLifecycle = restoredAwaitingValidation ? 'waiting' : 'listening';
|
|
235
|
+
this.liveActivity = restoredAwaitingValidation
|
|
236
|
+
? { kind: 'awaiting-validation' }
|
|
237
|
+
: undefined;
|
|
235
238
|
this.store = new AtomicStatusStore({
|
|
236
239
|
cwd: ctx.cwd,
|
|
237
240
|
projectRoot: ctx.cwd,
|
|
@@ -729,6 +732,11 @@ class GalacticaStatusRuntime {
|
|
|
729
732
|
this.focus = focus;
|
|
730
733
|
this.recordEvent({ type: 'focus/openspec', at: Date.now(), focus });
|
|
731
734
|
this.store.clearOpenSpec();
|
|
735
|
+
if (focus.mode === 'task' && focus.awaitingValidation) {
|
|
736
|
+
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
737
|
+
} else if (focus.mode === 'none' && this.workFocus.state === 'clear') {
|
|
738
|
+
this.setLiveProjection('listening');
|
|
739
|
+
}
|
|
732
740
|
this.publish();
|
|
733
741
|
this.launch(this.refresh(false), 'openspec');
|
|
734
742
|
}
|
|
@@ -754,20 +762,6 @@ class GalacticaStatusRuntime {
|
|
|
754
762
|
this.publish();
|
|
755
763
|
}
|
|
756
764
|
|
|
757
|
-
declareStage(stage: StageValue): boolean {
|
|
758
|
-
if (!this.currentTurnId) return false;
|
|
759
|
-
const changed = this.runtimeSpans.setStage(this.currentTurnId, stage, true);
|
|
760
|
-
this.recordEvent({
|
|
761
|
-
type: 'run/stage',
|
|
762
|
-
at: Date.now(),
|
|
763
|
-
id: this.currentTurnId,
|
|
764
|
-
stage,
|
|
765
|
-
declared: true,
|
|
766
|
-
});
|
|
767
|
-
this.setLiveProjection(stage);
|
|
768
|
-
return changed;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
765
|
setLiveProjection(lifecycle: HeaderLifecycle, activity?: HeaderActivity): void {
|
|
772
766
|
if (
|
|
773
767
|
this.liveLifecycle === lifecycle &&
|
|
@@ -1081,19 +1075,11 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1081
1075
|
runtime?.setFocus(focus);
|
|
1082
1076
|
}
|
|
1083
1077
|
|
|
1084
|
-
function workStatus(focus: SessionWorkFocus): string {
|
|
1085
|
-
return focus.state === 'clear'
|
|
1086
|
-
? 'Work focus: clear'
|
|
1087
|
-
: `Work focus: ${focus.state} · ${focus.intent}`;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
1078
|
function subjectStatus(subject: SessionSubject): string {
|
|
1091
|
-
return subject.state === 'clear'
|
|
1092
|
-
? 'Session subject: clear'
|
|
1093
|
-
: `Session subject: ${subject.title}`;
|
|
1079
|
+
return subject.state === 'clear' ? 'Title: clear' : `Title: ${subject.title}`;
|
|
1094
1080
|
}
|
|
1095
1081
|
|
|
1096
|
-
function
|
|
1082
|
+
function applyTitleAction(
|
|
1097
1083
|
action: 'status' | 'set',
|
|
1098
1084
|
title?: string,
|
|
1099
1085
|
): { ok: boolean; message: string; subject: SessionSubject } {
|
|
@@ -1107,7 +1093,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1107
1093
|
if (!next || next.state !== 'set') {
|
|
1108
1094
|
return {
|
|
1109
1095
|
ok: false,
|
|
1110
|
-
message: '
|
|
1096
|
+
message: 'Title requires a non-empty title',
|
|
1111
1097
|
subject: current,
|
|
1112
1098
|
};
|
|
1113
1099
|
}
|
|
@@ -1115,79 +1101,20 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1115
1101
|
return { ok: true, message: subjectStatus(next), subject: next };
|
|
1116
1102
|
}
|
|
1117
1103
|
|
|
1118
|
-
function applyWorkAction(
|
|
1119
|
-
action: 'status' | 'set' | 'validate' | 'clear',
|
|
1120
|
-
intent?: string,
|
|
1121
|
-
): { ok: boolean; message: string; focus: SessionWorkFocus } {
|
|
1122
|
-
const current = runtime?.currentWorkFocus() ?? NO_SESSION_WORK_FOCUS;
|
|
1123
|
-
const openSpec = runtime?.currentFocus() ?? NO_OPEN_SPEC_FOCUS;
|
|
1124
|
-
|
|
1125
|
-
if (action === 'status') {
|
|
1126
|
-
return { ok: true, message: workStatus(current), focus: current };
|
|
1127
|
-
}
|
|
1128
|
-
if (action === 'clear') {
|
|
1129
|
-
if (current.state !== 'clear') persistAndApplyWorkFocus(NO_SESSION_WORK_FOCUS);
|
|
1130
|
-
return {
|
|
1131
|
-
ok: true,
|
|
1132
|
-
message: 'Work focus cleared',
|
|
1133
|
-
focus: NO_SESSION_WORK_FOCUS,
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
if (openSpec.mode === 'task') {
|
|
1137
|
-
return {
|
|
1138
|
-
ok: false,
|
|
1139
|
-
message:
|
|
1140
|
-
`OpenSpec focus is ${openSpec.change} › ${openSpec.taskId}; ` +
|
|
1141
|
-
'clear it before setting ephemeral work',
|
|
1142
|
-
focus: current,
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
if (action === 'validate') {
|
|
1146
|
-
if (current.state === 'clear') {
|
|
1147
|
-
return {
|
|
1148
|
-
ok: false,
|
|
1149
|
-
message: 'No active Work focus to validate',
|
|
1150
|
-
focus: current,
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
const validation = sessionWorkFocus('validation', current.intent);
|
|
1154
|
-
if (!validation) {
|
|
1155
|
-
return { ok: false, message: 'Invalid Work focus', focus: current };
|
|
1156
|
-
}
|
|
1157
|
-
if (!sameSessionWorkFocus(current, validation)) {
|
|
1158
|
-
persistAndApplyWorkFocus(validation);
|
|
1159
|
-
}
|
|
1160
|
-
return { ok: true, message: workStatus(validation), focus: validation };
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
const active = sessionWorkFocus('active', intent);
|
|
1164
|
-
if (!active) {
|
|
1165
|
-
return {
|
|
1166
|
-
ok: false,
|
|
1167
|
-
message: 'Work focus requires a non-empty intent',
|
|
1168
|
-
focus: current,
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
if (!sameSessionWorkFocus(current, active)) persistAndApplyWorkFocus(active);
|
|
1172
|
-
return { ok: true, message: workStatus(active), focus: active };
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
1104
|
function applyOpenSpecFocusAction(
|
|
1176
1105
|
action: OpenSpecFocusAction,
|
|
1177
1106
|
change?: string,
|
|
1178
1107
|
taskId?: string,
|
|
1179
1108
|
): { ok: boolean; message: string; focus: OpenSpecFocus } {
|
|
1180
1109
|
const current = runtime?.currentFocus() ?? NO_OPEN_SPEC_FOCUS;
|
|
1110
|
+
const statusMessage = (focus: OpenSpecFocus): string =>
|
|
1111
|
+
focus.mode === 'task'
|
|
1112
|
+
? `OpenSpec focus: ${focus.change} › ${focus.taskId}` +
|
|
1113
|
+
(focus.awaitingValidation ? ' — awaiting Human validation' : '')
|
|
1114
|
+
: 'OpenSpec focus: none';
|
|
1181
1115
|
|
|
1182
1116
|
if (action === 'status') {
|
|
1183
|
-
return {
|
|
1184
|
-
ok: true,
|
|
1185
|
-
message:
|
|
1186
|
-
current.mode === 'task'
|
|
1187
|
-
? `OpenSpec focus: ${current.change} › ${current.taskId}`
|
|
1188
|
-
: 'OpenSpec focus: none',
|
|
1189
|
-
focus: current,
|
|
1190
|
-
};
|
|
1117
|
+
return { ok: true, message: statusMessage(current), focus: current };
|
|
1191
1118
|
}
|
|
1192
1119
|
if (action === 'clear') {
|
|
1193
1120
|
persistAndApplyFocus(NO_OPEN_SPEC_FOCUS);
|
|
@@ -1197,6 +1124,18 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1197
1124
|
focus: NO_OPEN_SPEC_FOCUS,
|
|
1198
1125
|
};
|
|
1199
1126
|
}
|
|
1127
|
+
if (action === 'validate') {
|
|
1128
|
+
if (current.mode !== 'task') {
|
|
1129
|
+
return {
|
|
1130
|
+
ok: false,
|
|
1131
|
+
message: 'OpenSpec focus has no task to validate',
|
|
1132
|
+
focus: current,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const validated = { ...current, awaitingValidation: true };
|
|
1136
|
+
if (!sameOpenSpecFocus(current, validated)) persistAndApplyFocus(validated);
|
|
1137
|
+
return { ok: true, message: statusMessage(validated), focus: validated };
|
|
1138
|
+
}
|
|
1200
1139
|
|
|
1201
1140
|
const requested = taskFocus(change, taskId);
|
|
1202
1141
|
if (!requested) {
|
|
@@ -1209,7 +1148,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1209
1148
|
if (
|
|
1210
1149
|
action === 'set' &&
|
|
1211
1150
|
current.mode === 'task' &&
|
|
1212
|
-
!
|
|
1151
|
+
!sameTaskFocus(current, requested)
|
|
1213
1152
|
) {
|
|
1214
1153
|
return {
|
|
1215
1154
|
ok: false,
|
|
@@ -1222,7 +1161,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1222
1161
|
if (!sameOpenSpecFocus(current, requested)) persistAndApplyFocus(requested);
|
|
1223
1162
|
return {
|
|
1224
1163
|
ok: true,
|
|
1225
|
-
message:
|
|
1164
|
+
message: statusMessage(requested),
|
|
1226
1165
|
focus: requested,
|
|
1227
1166
|
};
|
|
1228
1167
|
}
|
|
@@ -1360,80 +1299,11 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1360
1299
|
});
|
|
1361
1300
|
|
|
1362
1301
|
pi.registerTool({
|
|
1363
|
-
name: '
|
|
1364
|
-
label: '
|
|
1365
|
-
description:
|
|
1366
|
-
'Show, set, mark for validation, or clear explicit ephemeral work metadata for this Pi session',
|
|
1367
|
-
promptSnippet: 'Manage explicit ephemeral Session Work focus metadata',
|
|
1368
|
-
parameters: Type.Object({
|
|
1369
|
-
action: StringEnum(['status', 'set', 'validate', 'clear'] as const),
|
|
1370
|
-
intent: Type.Optional(
|
|
1371
|
-
Type.String({
|
|
1372
|
-
description: 'Required only for set: the concise current intent',
|
|
1373
|
-
}),
|
|
1374
|
-
),
|
|
1375
|
-
}),
|
|
1376
|
-
async execute(_toolCallId, params) {
|
|
1377
|
-
const result = applyWorkAction(params.action, params.intent);
|
|
1378
|
-
return {
|
|
1379
|
-
content: [
|
|
1380
|
-
{
|
|
1381
|
-
type: 'text',
|
|
1382
|
-
text: result.ok ? result.message : `Error: ${result.message}`,
|
|
1383
|
-
},
|
|
1384
|
-
],
|
|
1385
|
-
details: {
|
|
1386
|
-
ok: result.ok,
|
|
1387
|
-
focus: result.focus,
|
|
1388
|
-
},
|
|
1389
|
-
};
|
|
1390
|
-
},
|
|
1391
|
-
});
|
|
1392
|
-
|
|
1393
|
-
pi.registerTool({
|
|
1394
|
-
name: 'stage',
|
|
1395
|
-
label: 'Stage',
|
|
1302
|
+
name: 'title',
|
|
1303
|
+
label: 'Session Title',
|
|
1396
1304
|
description:
|
|
1397
|
-
'
|
|
1398
|
-
promptSnippet: '
|
|
1399
|
-
parameters: Type.Object({
|
|
1400
|
-
stage: StringEnum([
|
|
1401
|
-
'understanding',
|
|
1402
|
-
'working',
|
|
1403
|
-
'assuring',
|
|
1404
|
-
'learning',
|
|
1405
|
-
'answering',
|
|
1406
|
-
'waiting',
|
|
1407
|
-
'blocked',
|
|
1408
|
-
'listening',
|
|
1409
|
-
'aborted',
|
|
1410
|
-
] as const),
|
|
1411
|
-
}),
|
|
1412
|
-
async execute(_toolCallId, params) {
|
|
1413
|
-
const declared = runtime?.declareStage(params.stage) ?? false;
|
|
1414
|
-
return {
|
|
1415
|
-
content: [
|
|
1416
|
-
{
|
|
1417
|
-
type: 'text' as const,
|
|
1418
|
-
text: declared
|
|
1419
|
-
? `Stage declared: ${params.stage}`
|
|
1420
|
-
: `Stage recorded: ${params.stage} (no active turn span)`,
|
|
1421
|
-
},
|
|
1422
|
-
],
|
|
1423
|
-
details: { kind: 'stage-declaration', stage: params.stage },
|
|
1424
|
-
};
|
|
1425
|
-
},
|
|
1426
|
-
});
|
|
1427
|
-
|
|
1428
|
-
pi.registerTool({
|
|
1429
|
-
name: 'subject',
|
|
1430
|
-
label: 'Session Subject',
|
|
1431
|
-
description:
|
|
1432
|
-
'Show or set the sanitized session subject that keeps the focus row populated beneath OpenSpec and Session Work',
|
|
1433
|
-
promptSnippet: 'Manage the automatic session subject title',
|
|
1434
|
-
promptGuidelines: [
|
|
1435
|
-
'Author a short sanitized subject (at most 120 characters, never raw prompt text) at the first conversational turn; revise it only when the topic clearly shifts. Never require Human action for it.',
|
|
1436
|
-
],
|
|
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',
|
|
1306
|
+
promptSnippet: 'Manage the automatic session title',
|
|
1437
1307
|
parameters: Type.Object({
|
|
1438
1308
|
action: StringEnum(['status', 'set'] as const),
|
|
1439
1309
|
title: Type.Optional(
|
|
@@ -1443,7 +1313,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1443
1313
|
),
|
|
1444
1314
|
}),
|
|
1445
1315
|
async execute(_toolCallId, params) {
|
|
1446
|
-
const result =
|
|
1316
|
+
const result = applyTitleAction(params.action, params.title);
|
|
1447
1317
|
return {
|
|
1448
1318
|
content: [
|
|
1449
1319
|
{
|
|
@@ -1463,14 +1333,11 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1463
1333
|
name: 'openspec_focus',
|
|
1464
1334
|
label: 'OpenSpec Focus',
|
|
1465
1335
|
description:
|
|
1466
|
-
'Show, safely set, deliberately replace, or clear branch-local OpenSpec Task focus',
|
|
1336
|
+
'Show, safely set, deliberately replace, validate, or clear branch-local OpenSpec Task focus. Use only for explicit Human direction or an exact declared OpenSpec Task handoff; never infer material Plan selection or Human validation',
|
|
1467
1337
|
promptSnippet:
|
|
1468
1338
|
'Manage exact branch-local OpenSpec Task focus with replacement safeguards',
|
|
1469
|
-
promptGuidelines: [
|
|
1470
|
-
'Use openspec_focus only for explicit Human direction or an exact declared OpenSpec Task handoff; never infer material Plan selection or Human validation.',
|
|
1471
|
-
],
|
|
1472
1339
|
parameters: Type.Object({
|
|
1473
|
-
action: StringEnum(['status', 'set', 'replace', 'clear'] as const),
|
|
1340
|
+
action: StringEnum(['status', 'set', 'replace', 'validate', 'clear'] as const),
|
|
1474
1341
|
change: Type.Optional(
|
|
1475
1342
|
Type.String({
|
|
1476
1343
|
description: 'Required for set and replace: the exact OpenSpec change ID',
|
|
@@ -1503,32 +1370,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1503
1370
|
},
|
|
1504
1371
|
});
|
|
1505
1372
|
|
|
1506
|
-
pi.registerCommand('work', {
|
|
1507
|
-
description: "Show, set, validate, or clear this session's ephemeral Work focus",
|
|
1508
|
-
handler: async (args, ctx) => {
|
|
1509
|
-
const trimmed = args.trim();
|
|
1510
|
-
if (!trimmed || trimmed.toLowerCase() === 'status') {
|
|
1511
|
-
ctx.ui.notify(applyWorkAction('status').message, 'info');
|
|
1512
|
-
return;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
const [rawAction, ...rest] = trimmed.split(/\s+/u);
|
|
1516
|
-
const action = rawAction?.toLowerCase();
|
|
1517
|
-
if (action === 'set') {
|
|
1518
|
-
const result = applyWorkAction('set', rest.join(' '));
|
|
1519
|
-
ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
if ((action === 'validate' || action === 'clear') && rest.length === 0) {
|
|
1523
|
-
const result = applyWorkAction(action);
|
|
1524
|
-
ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
ctx.ui.notify('Usage: /work [status|set <intent>|validate|clear]', 'warning');
|
|
1529
|
-
},
|
|
1530
|
-
});
|
|
1531
|
-
|
|
1532
1373
|
pi.registerCommand('openspec-focus', {
|
|
1533
1374
|
description: "Show, clear, or explicitly set this session's OpenSpec task focus",
|
|
1534
1375
|
handler: async (args, ctx) => {
|
|
@@ -1544,6 +1385,12 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1544
1385
|
return;
|
|
1545
1386
|
}
|
|
1546
1387
|
|
|
1388
|
+
if (parts.length === 1 && parts[0]?.toLowerCase() === 'validate') {
|
|
1389
|
+
const result = applyOpenSpecFocusAction('validate');
|
|
1390
|
+
ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1547
1394
|
const rawAction = parts[0]?.toLowerCase();
|
|
1548
1395
|
const action: 'set' | 'replace' = ['set', 'replace'].includes(rawAction ?? '')
|
|
1549
1396
|
? (rawAction as 'set' | 'replace')
|
|
@@ -8,6 +8,7 @@ export type TaskOpenSpecFocus = {
|
|
|
8
8
|
mode: 'task';
|
|
9
9
|
change: string;
|
|
10
10
|
taskId: string;
|
|
11
|
+
awaitingValidation: boolean;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export type OpenSpecFocus = { mode: 'none' } | TaskOpenSpecFocus;
|
|
@@ -52,18 +53,24 @@ function explicitId(value: unknown): string | undefined {
|
|
|
52
53
|
export function taskFocus(
|
|
53
54
|
change: unknown,
|
|
54
55
|
taskId: unknown,
|
|
56
|
+
awaitingValidation?: unknown,
|
|
55
57
|
): TaskOpenSpecFocus | undefined {
|
|
56
58
|
const normalizedChange = explicitId(change);
|
|
57
59
|
const normalizedTaskId = explicitId(taskId);
|
|
58
60
|
if (!normalizedChange || !normalizedTaskId) return undefined;
|
|
59
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
mode: 'task',
|
|
63
|
+
change: normalizedChange,
|
|
64
|
+
taskId: normalizedTaskId,
|
|
65
|
+
awaitingValidation: awaitingValidation === true,
|
|
66
|
+
};
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
export function parseOpenSpecFocus(value: unknown): OpenSpecFocus | undefined {
|
|
63
70
|
if (!isRecord(value)) return undefined;
|
|
64
71
|
if (value.mode === 'none') return NO_OPEN_SPEC_FOCUS;
|
|
65
72
|
if (value.mode !== 'task') return undefined;
|
|
66
|
-
return taskFocus(value.change, value.taskId);
|
|
73
|
+
return taskFocus(value.change, value.taskId, value.awaitingValidation);
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
export function sessionWorkFocus(
|
|
@@ -162,7 +169,9 @@ export function taskflowRunFocus(input: unknown): TaskOpenSpecFocus | undefined
|
|
|
162
169
|
: undefined;
|
|
163
170
|
}
|
|
164
171
|
|
|
165
|
-
|
|
172
|
+
/** Task identity (mode · change · taskId) — the replace-safeguard's
|
|
173
|
+
* question; the validation flag is state, not identity. */
|
|
174
|
+
export function sameTaskFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
|
|
166
175
|
if (left.mode !== right.mode) return false;
|
|
167
176
|
return (
|
|
168
177
|
left.mode === 'none' ||
|
|
@@ -172,6 +181,17 @@ export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): bo
|
|
|
172
181
|
);
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
|
|
185
|
+
if (left.mode !== right.mode) return false;
|
|
186
|
+
return (
|
|
187
|
+
left.mode === 'none' ||
|
|
188
|
+
(right.mode === 'task' &&
|
|
189
|
+
left.change === right.change &&
|
|
190
|
+
left.taskId === right.taskId &&
|
|
191
|
+
left.awaitingValidation === right.awaitingValidation)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
175
195
|
export function sameSessionWorkFocus(
|
|
176
196
|
left: SessionWorkFocus,
|
|
177
197
|
right: SessionWorkFocus,
|
|
@@ -22,8 +22,11 @@ export const TITLE_ACTIVITY_CLASSES = [
|
|
|
22
22
|
|
|
23
23
|
export type TitleActivityClass = (typeof TITLE_ACTIVITY_CLASSES)[number];
|
|
24
24
|
|
|
25
|
-
/** Short target label bound from the accepted schema
|
|
26
|
-
|
|
25
|
+
/** Short target label bound from the accepted schema (Human amendment
|
|
26
|
+
* 2026-10-24: 48 → 120 — the deck's activity lane now owns the full
|
|
27
|
+
* width beside the turn cluster, so the protocol bound yields to the
|
|
28
|
+
* render-time crop instead of pre-cutting subagent/bash labels). */
|
|
29
|
+
export const TITLE_TARGET_MAX = 120;
|
|
27
30
|
|
|
28
31
|
/** Task/step totals bound from the accepted schema. */
|
|
29
32
|
export const TITLE_TOTAL_MAX = 999;
|
|
@@ -165,7 +168,7 @@ function parseTitleWaiver(value: unknown): TitleWaiverState | null | undefined {
|
|
|
165
168
|
|
|
166
169
|
/**
|
|
167
170
|
* Defensive parser for incoming title-state payloads. Unknown versions,
|
|
168
|
-
* malformed shapes, and out-of-bounds values (target >
|
|
171
|
+
* malformed shapes, and out-of-bounds values (target > 120 characters,
|
|
169
172
|
* totals > 999) reject the whole object with `null`; it never throws.
|
|
170
173
|
*/
|
|
171
174
|
export function parseTitleState(value: unknown): TitleState | null {
|