@aemonge-dev/pi-stardeck 0.11.0 → 0.12.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 +3 -2
- package/packages/pi-stardeck/README.md +1 -1
- package/packages/pi-stardeck/src/gauge.ts +1 -6
- package/packages/status/README.md +4 -4
- package/packages/status/index.ts +26 -338
- package/packages/status/src/active-work.ts +2 -70
- package/packages/status/src/ledger.ts +1 -3
- package/packages/status/src/publisher.ts +6 -76
- package/packages/status/src/reducer.ts +1 -19
- package/packages/status/src/types.ts +0 -1
- package/packages/work-state/README.md +33 -0
- package/packages/work-state/index.ts +160 -0
- package/packages/work-state/package.json +33 -0
- package/packages/work-state/src/focus.ts +100 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aemonge-dev/pi-stardeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A Vim- and tmux-inspired command deck for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -48,10 +48,11 @@
|
|
|
48
48
|
"check:openspec": "node scripts/check-openspec.mjs",
|
|
49
49
|
"test:tooling": "node --import tsx/esm --test test/*.test.ts",
|
|
50
50
|
"test:status": "npm --prefix packages/status test",
|
|
51
|
+
"test:work-state": "npm --prefix packages/work-state test",
|
|
51
52
|
"test:stardeck": "npm --prefix packages/pi-stardeck test",
|
|
52
53
|
"test:footer": "npm --prefix packages/footer test",
|
|
53
54
|
"test:vim": "npm --prefix packages/pi-vim-top-border test",
|
|
54
|
-
"test:components": "npm run test:status && npm run test:stardeck && npm run test:footer && npm run test:vim"
|
|
55
|
+
"test:components": "npm run test:status && npm run test:work-state && npm run test:stardeck && npm run test:footer && npm run test:vim"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"@earendil-works/pi-ai": "*",
|
|
@@ -43,7 +43,7 @@ The additive header protocol projects selection independently from live work:
|
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
45
|
selection: {
|
|
46
|
-
source: 'openspec' | '
|
|
46
|
+
source: 'openspec' | 'subject' | 'legacy';
|
|
47
47
|
titles: string[]; // one or two bounded, normalized titles
|
|
48
48
|
color: HeaderColor;
|
|
49
49
|
} | null;
|
|
@@ -96,12 +96,7 @@ export type HeaderWork = {
|
|
|
96
96
|
activityPath?: HeaderActivitySegment[];
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
export const HEADER_SELECTION_SOURCES = [
|
|
100
|
-
'openspec',
|
|
101
|
-
'session-work',
|
|
102
|
-
'subject',
|
|
103
|
-
'legacy',
|
|
104
|
-
] as const;
|
|
99
|
+
export const HEADER_SELECTION_SOURCES = ['openspec', 'subject', 'legacy'] as const;
|
|
105
100
|
|
|
106
101
|
export type HeaderSelection = {
|
|
107
102
|
source: (typeof HEADER_SELECTION_SOURCES)[number];
|
|
@@ -90,10 +90,10 @@ branch entry. The agent-callable `openspec_focus` tool shares the command's `sta
|
|
|
90
90
|
safeguarded `set`, deliberate `replace`, validating `validate`, and persistent `clear`
|
|
91
91
|
transitions; the `awaitingValidation` flag marks a focused task as awaiting Human
|
|
92
92
|
validation and auto-clears on the next `set`, `replace`, or `clear`. The
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
sessions
|
|
93
|
+
retired `work_focus`, `stage`, and `title` instruments are gone: stages
|
|
94
|
+
derive from events and Taskflow phases, the focus row falls back to the
|
|
95
|
+
native session name (named by `pi-title`) and then to legacy `session-subject`
|
|
96
|
+
entries from old sessions, read defensively and fail-soft.
|
|
97
97
|
|
|
98
98
|
Global routing belongs to native Ramona policy rather than this status package. This
|
|
99
99
|
package does not inject parent routing prompts, block parent tools, create Session Work
|
package/packages/status/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { StringEnum } from '@earendil-works/pi-ai';
|
|
2
1
|
import type {
|
|
3
2
|
ExtensionAPI,
|
|
4
3
|
ExtensionCommandContext,
|
|
@@ -6,29 +5,15 @@ import type {
|
|
|
6
5
|
} from '@earendil-works/pi-coding-agent';
|
|
7
6
|
import { promises as fs, watch } from 'node:fs';
|
|
8
7
|
import { dirname, join, resolve } from 'node:path';
|
|
9
|
-
import { Type } from 'typebox';
|
|
10
8
|
import {
|
|
11
9
|
formatActiveWorkTitle,
|
|
12
|
-
NO_OPEN_SPEC_FOCUS,
|
|
13
|
-
NO_SESSION_SUBJECT,
|
|
14
|
-
NO_SESSION_WORK_FOCUS,
|
|
15
|
-
OPEN_SPEC_FOCUS_ENTRY_TYPE,
|
|
16
10
|
type OpenSpecFocus,
|
|
11
|
+
parseOpenSpecFocus,
|
|
17
12
|
restoreOpenSpecFocus,
|
|
18
13
|
restoreSessionSubject,
|
|
19
|
-
restoreSessionWorkFocus,
|
|
20
14
|
sameOpenSpecFocus,
|
|
21
15
|
sameSessionSubject,
|
|
22
|
-
sameSessionWorkFocus,
|
|
23
|
-
sameTaskFocus,
|
|
24
|
-
SESSION_SUBJECT_ENTRY_TYPE,
|
|
25
|
-
SESSION_WORK_FOCUS_ENTRY_TYPE,
|
|
26
16
|
type SessionSubject,
|
|
27
|
-
type SessionWorkFocus,
|
|
28
|
-
sessionSubject,
|
|
29
|
-
taskflowRunFocus,
|
|
30
|
-
taskFocus,
|
|
31
|
-
titleFromPrompt,
|
|
32
17
|
} from './src/active-work.ts';
|
|
33
18
|
import { collectDiagnostics } from './src/diagnostics.ts';
|
|
34
19
|
import { loadConfig } from './src/config.ts';
|
|
@@ -90,7 +75,6 @@ import type {
|
|
|
90
75
|
|
|
91
76
|
type SessionContext = ExtensionContext | ExtensionCommandContext;
|
|
92
77
|
type Domain = RuntimeErrorRecord['domain'];
|
|
93
|
-
type OpenSpecFocusAction = 'status' | 'set' | 'replace' | 'validate' | 'clear';
|
|
94
78
|
|
|
95
79
|
type StatusHealthCondition = {
|
|
96
80
|
id: string;
|
|
@@ -187,10 +171,8 @@ class GalacticaStatusRuntime {
|
|
|
187
171
|
private openSpecOverview: OpenSpecOverview | undefined;
|
|
188
172
|
private openSpecProjectDetected: boolean | undefined;
|
|
189
173
|
private focus: OpenSpecFocus;
|
|
190
|
-
private workFocus: SessionWorkFocus;
|
|
191
174
|
private subject: SessionSubject;
|
|
192
175
|
private sessionName: string | undefined;
|
|
193
|
-
private lastTitle: string | undefined;
|
|
194
176
|
private diagnosticsPath: string | undefined;
|
|
195
177
|
private orchestrationPath: string | undefined;
|
|
196
178
|
private liveLifecycle: HeaderLifecycle;
|
|
@@ -218,7 +200,6 @@ class GalacticaStatusRuntime {
|
|
|
218
200
|
ctx: SessionContext,
|
|
219
201
|
publisher: FancyFooterPublisher,
|
|
220
202
|
focus: OpenSpecFocus,
|
|
221
|
-
workFocus: SessionWorkFocus,
|
|
222
203
|
subject: SessionSubject,
|
|
223
204
|
sessionName: string | undefined,
|
|
224
205
|
) {
|
|
@@ -226,12 +207,10 @@ class GalacticaStatusRuntime {
|
|
|
226
207
|
this.ctx = ctx;
|
|
227
208
|
this.publisher = publisher;
|
|
228
209
|
this.focus = focus;
|
|
229
|
-
this.workFocus = workFocus;
|
|
230
210
|
this.subject = subject;
|
|
231
211
|
this.sessionName = sessionName;
|
|
232
212
|
const restoredAwaitingValidation =
|
|
233
|
-
|
|
234
|
-
workFocus.state === 'validation';
|
|
213
|
+
focus.mode === 'task' && focus.awaitingValidation;
|
|
235
214
|
this.liveLifecycle = restoredAwaitingValidation ? 'waiting' : 'listening';
|
|
236
215
|
this.liveActivity = restoredAwaitingValidation
|
|
237
216
|
? { kind: 'awaiting-validation' }
|
|
@@ -243,7 +222,6 @@ class GalacticaStatusRuntime {
|
|
|
243
222
|
});
|
|
244
223
|
const seededAt = Date.now();
|
|
245
224
|
this.recordEvent({ type: 'focus/openspec', at: seededAt, focus });
|
|
246
|
-
this.recordEvent({ type: 'focus/work', at: seededAt, work: workFocus });
|
|
247
225
|
this.recordEvent({ type: 'focus/subject', at: seededAt, subject });
|
|
248
226
|
}
|
|
249
227
|
|
|
@@ -608,28 +586,19 @@ class GalacticaStatusRuntime {
|
|
|
608
586
|
// the same object references), so output stays byte-identical while the
|
|
609
587
|
// ledger becomes the feeding seam.
|
|
610
588
|
const focus = this.cockpitFacts.openSpec;
|
|
611
|
-
const work = this.cockpitFacts.work;
|
|
612
589
|
const subject = this.cockpitFacts.subject;
|
|
613
590
|
const title = this.currentTitleState(snapshot);
|
|
614
591
|
const focusedOpenSpec =
|
|
615
592
|
focus.mode === 'task' && snapshot.openspec?.changeId === focus.change
|
|
616
593
|
? snapshot.openspec
|
|
617
594
|
: null;
|
|
618
|
-
const visibleWorkFocus =
|
|
619
|
-
focus.mode === 'task' || (work.state === 'clear' && this.openSpecFeedback)
|
|
620
|
-
? undefined
|
|
621
|
-
: work;
|
|
622
595
|
const widgets = buildWidgetSnapshots(
|
|
623
596
|
{ ...snapshot, openspec: focusedOpenSpec },
|
|
624
597
|
{
|
|
625
598
|
focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
|
|
626
|
-
openSpecFeedback:
|
|
627
|
-
focus.mode === 'none' && !visibleWorkFocus
|
|
628
|
-
? this.openSpecFeedback
|
|
629
|
-
: undefined,
|
|
599
|
+
openSpecFeedback: focus.mode === 'none' ? this.openSpecFeedback : undefined,
|
|
630
600
|
openSpecOverview: this.openSpecOverview,
|
|
631
601
|
openSpecProjectDetected: this.openSpecProjectDetected,
|
|
632
|
-
workFocus: visibleWorkFocus,
|
|
633
602
|
},
|
|
634
603
|
);
|
|
635
604
|
this.publisher.sync(widgets);
|
|
@@ -638,7 +607,6 @@ class GalacticaStatusRuntime {
|
|
|
638
607
|
buildHeaderStatusEvent(widgets, {
|
|
639
608
|
openSpec: focusedOpenSpec,
|
|
640
609
|
focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
|
|
641
|
-
workFocus: visibleWorkFocus,
|
|
642
610
|
subject,
|
|
643
611
|
orchestration: snapshot.orchestration,
|
|
644
612
|
lifecycle: this.liveLifecycle,
|
|
@@ -655,20 +623,10 @@ class GalacticaStatusRuntime {
|
|
|
655
623
|
diagnostics: snapshot.diagnostics,
|
|
656
624
|
}),
|
|
657
625
|
);
|
|
658
|
-
this.updateTitle(snapshot);
|
|
659
626
|
this.publishStatusHealth();
|
|
660
627
|
}
|
|
661
628
|
|
|
662
629
|
private currentTitleState(snapshot = this.store.get()): TitleState {
|
|
663
|
-
return buildTitleState({
|
|
664
|
-
subject: this.cockpitFacts.subject,
|
|
665
|
-
focus: this.cockpitFacts.openSpec,
|
|
666
|
-
openspec: snapshot.openspec,
|
|
667
|
-
activity: this.titleActivity.current(),
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
private updateTitle(snapshot = this.store.get()): void {
|
|
672
630
|
const facts = this.cockpitFacts;
|
|
673
631
|
const focusedTaskId =
|
|
674
632
|
facts.openSpec.mode === 'task' ? facts.openSpec.taskId : undefined;
|
|
@@ -680,18 +638,23 @@ class GalacticaStatusRuntime {
|
|
|
680
638
|
(task) => task.id === focusedTaskId,
|
|
681
639
|
)?.title
|
|
682
640
|
: undefined;
|
|
683
|
-
|
|
641
|
+
// The band chip renders the fallback chain — task focus, native session
|
|
642
|
+
// name, legacy subject, directory. The terminal titlebar is pi-title's
|
|
643
|
+
// alone; this package never paints it (pure-renderer-split Slice 2).
|
|
644
|
+
const subjectTitle = formatActiveWorkTitle({
|
|
684
645
|
focus: facts.openSpec,
|
|
685
|
-
workFocus: facts.work,
|
|
686
646
|
subject: facts.subject,
|
|
687
647
|
...(taskTitle ? { taskTitle } : {}),
|
|
688
648
|
...(this.sessionName ? { sessionName: this.sessionName } : {}),
|
|
689
649
|
projectRoot: snapshot.projectRoot,
|
|
690
650
|
cwd: snapshot.cwd,
|
|
691
651
|
});
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
652
|
+
return buildTitleState({
|
|
653
|
+
subject: { state: 'set', title: subjectTitle },
|
|
654
|
+
focus: facts.openSpec,
|
|
655
|
+
openspec: snapshot.openspec,
|
|
656
|
+
activity: this.titleActivity.current(),
|
|
657
|
+
});
|
|
695
658
|
}
|
|
696
659
|
|
|
697
660
|
private recordEvent(event: LedgerEvent): void {
|
|
@@ -708,10 +671,6 @@ class GalacticaStatusRuntime {
|
|
|
708
671
|
return this.focus;
|
|
709
672
|
}
|
|
710
673
|
|
|
711
|
-
currentWorkFocus(): SessionWorkFocus {
|
|
712
|
-
return this.workFocus;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
674
|
currentSubject(): SessionSubject {
|
|
716
675
|
return this.subject;
|
|
717
676
|
}
|
|
@@ -735,27 +694,13 @@ class GalacticaStatusRuntime {
|
|
|
735
694
|
this.store.clearOpenSpec();
|
|
736
695
|
if (focus.mode === 'task' && focus.awaitingValidation) {
|
|
737
696
|
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
738
|
-
} else if (focus.mode === 'none'
|
|
697
|
+
} else if (focus.mode === 'none') {
|
|
739
698
|
this.setLiveProjection('listening');
|
|
740
699
|
}
|
|
741
700
|
this.publish();
|
|
742
701
|
this.launch(this.refresh(false), 'openspec');
|
|
743
702
|
}
|
|
744
703
|
|
|
745
|
-
setWorkFocus(focus: SessionWorkFocus): void {
|
|
746
|
-
if (sameSessionWorkFocus(this.workFocus, focus)) return;
|
|
747
|
-
this.workFocus = focus;
|
|
748
|
-
this.recordEvent({ type: 'focus/work', at: Date.now(), work: focus });
|
|
749
|
-
if (focus.state === 'validation') {
|
|
750
|
-
this.liveLifecycle = 'waiting';
|
|
751
|
-
this.liveActivity = { kind: 'awaiting-validation' };
|
|
752
|
-
} else if (focus.state === 'clear' && this.focus.mode === 'none') {
|
|
753
|
-
this.liveLifecycle = 'listening';
|
|
754
|
-
this.liveActivity = undefined;
|
|
755
|
-
}
|
|
756
|
-
this.publish();
|
|
757
|
-
}
|
|
758
|
-
|
|
759
704
|
setSubject(subject: SessionSubject): void {
|
|
760
705
|
if (sameSessionSubject(this.subject, subject)) return;
|
|
761
706
|
this.subject = subject;
|
|
@@ -938,20 +883,16 @@ class GalacticaStatusRuntime {
|
|
|
938
883
|
this.publish();
|
|
939
884
|
return;
|
|
940
885
|
}
|
|
941
|
-
if (this.focus.mode === 'task'
|
|
886
|
+
if (this.focus.mode === 'task') {
|
|
942
887
|
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
943
888
|
return;
|
|
944
889
|
}
|
|
945
|
-
if (this.workFocus.state === 'active') {
|
|
946
|
-
this.setLiveProjection('waiting', { kind: 'awaiting-input' });
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
890
|
this.setLiveProjection('listening');
|
|
950
891
|
}
|
|
951
892
|
|
|
952
893
|
setSessionName(name: string | undefined): void {
|
|
953
894
|
this.sessionName = name;
|
|
954
|
-
this.
|
|
895
|
+
this.publish();
|
|
955
896
|
}
|
|
956
897
|
|
|
957
898
|
async forceRefresh(): Promise<void> {
|
|
@@ -978,11 +919,6 @@ class GalacticaStatusRuntime {
|
|
|
978
919
|
? `OpenSpec focus: ${this.openSpecFeedback}`
|
|
979
920
|
: 'OpenSpec focus: none',
|
|
980
921
|
);
|
|
981
|
-
lines.push(
|
|
982
|
-
this.workFocus.state === 'clear'
|
|
983
|
-
? 'Work: clear'
|
|
984
|
-
: `Work: ${this.workFocus.state} · ${this.workFocus.intent}`,
|
|
985
|
-
);
|
|
986
922
|
lines.push(
|
|
987
923
|
snapshot.diagnostics
|
|
988
924
|
? `Diagnostics: ${formatDiagnostics(snapshot.diagnostics)}`
|
|
@@ -1004,7 +940,6 @@ class GalacticaStatusRuntime {
|
|
|
1004
940
|
configPath: snapshot.configPath,
|
|
1005
941
|
configWarnings: snapshot.configWarnings,
|
|
1006
942
|
focus: this.focus,
|
|
1007
|
-
workFocus: this.workFocus,
|
|
1008
943
|
openSpecFeedback: this.openSpecFeedback ?? null,
|
|
1009
944
|
openSpecOverview: this.openSpecOverview ?? null,
|
|
1010
945
|
detected: {
|
|
@@ -1055,123 +990,20 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1055
990
|
}
|
|
1056
991
|
});
|
|
1057
992
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
pi.appendEntry(SESSION_SUBJECT_ENTRY_TYPE, subject);
|
|
1065
|
-
runtime?.setSubject(subject);
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
function persistAndApplyFocus(focus: OpenSpecFocus): void {
|
|
1069
|
-
if (
|
|
1070
|
-
focus.mode === 'task' &&
|
|
1071
|
-
(runtime?.currentWorkFocus() ?? NO_SESSION_WORK_FOCUS).state !== 'clear'
|
|
1072
|
-
) {
|
|
1073
|
-
persistAndApplyWorkFocus(NO_SESSION_WORK_FOCUS);
|
|
1074
|
-
}
|
|
1075
|
-
pi.appendEntry(OPEN_SPEC_FOCUS_ENTRY_TYPE, focus);
|
|
993
|
+
// work-state owns focus writes and emits `focus-changed` after each
|
|
994
|
+
// successful one (pi.events); the renderer subscribes for immediate
|
|
995
|
+
// repaint (pure-renderer-split Slice 2).
|
|
996
|
+
const stopFocusRepaint = pi.events.on('focus-changed', (data: unknown) => {
|
|
997
|
+
const focus = parseOpenSpecFocus(data);
|
|
998
|
+
if (!focus) return;
|
|
1076
999
|
runtime?.setFocus(focus);
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
function subjectStatus(subject: SessionSubject): string {
|
|
1080
|
-
return subject.state === 'clear' ? 'Title: clear' : `Title: ${subject.title}`;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function applyTitleAction(
|
|
1084
|
-
action: 'status' | 'set',
|
|
1085
|
-
title?: string,
|
|
1086
|
-
): { ok: boolean; message: string; subject: SessionSubject } {
|
|
1087
|
-
const current = runtime?.currentSubject() ?? NO_SESSION_SUBJECT;
|
|
1088
|
-
|
|
1089
|
-
if (action === 'status') {
|
|
1090
|
-
return { ok: true, message: subjectStatus(current), subject: current };
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const next = sessionSubject('set', title);
|
|
1094
|
-
if (!next || next.state !== 'set') {
|
|
1095
|
-
return {
|
|
1096
|
-
ok: false,
|
|
1097
|
-
message: 'Title requires a non-empty title',
|
|
1098
|
-
subject: current,
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
if (!sameSessionSubject(current, next)) persistAndApplySubject(next);
|
|
1102
|
-
return { ok: true, message: subjectStatus(next), subject: next };
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
function applyOpenSpecFocusAction(
|
|
1106
|
-
action: OpenSpecFocusAction,
|
|
1107
|
-
change?: string,
|
|
1108
|
-
taskId?: string,
|
|
1109
|
-
): { ok: boolean; message: string; focus: OpenSpecFocus } {
|
|
1110
|
-
const current = runtime?.currentFocus() ?? NO_OPEN_SPEC_FOCUS;
|
|
1111
|
-
const statusMessage = (focus: OpenSpecFocus): string =>
|
|
1112
|
-
focus.mode === 'task'
|
|
1113
|
-
? `OpenSpec focus: ${focus.change} › ${focus.taskId}` +
|
|
1114
|
-
(focus.awaitingValidation ? ' — awaiting Human validation' : '')
|
|
1115
|
-
: 'OpenSpec focus: none';
|
|
1116
|
-
|
|
1117
|
-
if (action === 'status') {
|
|
1118
|
-
return { ok: true, message: statusMessage(current), focus: current };
|
|
1119
|
-
}
|
|
1120
|
-
if (action === 'clear') {
|
|
1121
|
-
persistAndApplyFocus(NO_OPEN_SPEC_FOCUS);
|
|
1122
|
-
return {
|
|
1123
|
-
ok: true,
|
|
1124
|
-
message: 'OpenSpec focus cleared',
|
|
1125
|
-
focus: NO_OPEN_SPEC_FOCUS,
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
if (action === 'validate') {
|
|
1129
|
-
if (current.mode !== 'task') {
|
|
1130
|
-
return {
|
|
1131
|
-
ok: false,
|
|
1132
|
-
message: 'OpenSpec focus has no task to validate',
|
|
1133
|
-
focus: current,
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
const validated = { ...current, awaitingValidation: true };
|
|
1137
|
-
if (!sameOpenSpecFocus(current, validated)) persistAndApplyFocus(validated);
|
|
1138
|
-
return { ok: true, message: statusMessage(validated), focus: validated };
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
const requested = taskFocus(change, taskId);
|
|
1142
|
-
if (!requested) {
|
|
1143
|
-
return {
|
|
1144
|
-
ok: false,
|
|
1145
|
-
message: 'OpenSpec focus requires non-empty change and task identifiers',
|
|
1146
|
-
focus: current,
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
if (
|
|
1150
|
-
action === 'set' &&
|
|
1151
|
-
current.mode === 'task' &&
|
|
1152
|
-
!sameTaskFocus(current, requested)
|
|
1153
|
-
) {
|
|
1154
|
-
return {
|
|
1155
|
-
ok: false,
|
|
1156
|
-
message:
|
|
1157
|
-
`OpenSpec focus is ${current.change} › ${current.taskId}; ` +
|
|
1158
|
-
'use replace to change it deliberately',
|
|
1159
|
-
focus: current,
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
if (!sameOpenSpecFocus(current, requested)) persistAndApplyFocus(requested);
|
|
1163
|
-
return {
|
|
1164
|
-
ok: true,
|
|
1165
|
-
message: statusMessage(requested),
|
|
1166
|
-
focus: requested,
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1000
|
+
runtime?.settle();
|
|
1001
|
+
});
|
|
1169
1002
|
|
|
1170
1003
|
pi.on('session_start', (_event, ctx) => {
|
|
1171
1004
|
runtime?.stop();
|
|
1172
1005
|
const branch = ctx.sessionManager.getBranch();
|
|
1173
1006
|
const focus = restoreOpenSpecFocus(branch);
|
|
1174
|
-
const workFocus = restoreSessionWorkFocus(branch);
|
|
1175
1007
|
const subject = restoreSessionSubject(branch);
|
|
1176
1008
|
const sessionName = pi.getSessionName() ?? ctx.sessionManager.getSessionName();
|
|
1177
1009
|
runtime = new GalacticaStatusRuntime(
|
|
@@ -1179,7 +1011,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1179
1011
|
ctx,
|
|
1180
1012
|
publisher,
|
|
1181
1013
|
focus,
|
|
1182
|
-
workFocus,
|
|
1183
1014
|
subject,
|
|
1184
1015
|
sessionName,
|
|
1185
1016
|
);
|
|
@@ -1193,28 +1024,12 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1193
1024
|
pi.on('session_tree', (_event, ctx) => {
|
|
1194
1025
|
const branch = ctx.sessionManager.getBranch();
|
|
1195
1026
|
runtime?.setFocus(restoreOpenSpecFocus(branch));
|
|
1196
|
-
runtime?.setWorkFocus(restoreSessionWorkFocus(branch));
|
|
1197
1027
|
runtime?.setSubject(restoreSessionSubject(branch));
|
|
1198
1028
|
runtime?.settle();
|
|
1199
1029
|
});
|
|
1200
1030
|
|
|
1201
|
-
pi.on('before_agent_start', (
|
|
1031
|
+
pi.on('before_agent_start', () => {
|
|
1202
1032
|
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);
|
|
1218
1033
|
});
|
|
1219
1034
|
|
|
1220
1035
|
pi.on('tool_execution_start', (event) => {
|
|
@@ -1230,23 +1045,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1230
1045
|
);
|
|
1231
1046
|
return;
|
|
1232
1047
|
}
|
|
1233
|
-
const requested = taskflowRunFocus(event.input);
|
|
1234
|
-
|
|
1235
|
-
if (requested) {
|
|
1236
|
-
const current = runtime?.currentFocus() ?? NO_OPEN_SPEC_FOCUS;
|
|
1237
|
-
if (current.mode === 'task' && !sameOpenSpecFocus(current, requested)) {
|
|
1238
|
-
return {
|
|
1239
|
-
block: true,
|
|
1240
|
-
reason:
|
|
1241
|
-
`OpenSpec focus is ${current.change} › ${current.taskId}; ` +
|
|
1242
|
-
'use /openspec-focus clear or /openspec-focus replace <change> <task> before starting a conflicting run',
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
if (!sameOpenSpecFocus(current, requested)) {
|
|
1246
|
-
persistAndApplyFocus(requested);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
1048
|
const initialPhase = taskflowInitialHeaderPhase(event.input);
|
|
1251
1049
|
if (initialPhase) {
|
|
1252
1050
|
runtime?.setTaskflowPhase(initialPhase);
|
|
@@ -1312,117 +1110,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1312
1110
|
runtime = undefined;
|
|
1313
1111
|
publisher.removeAll();
|
|
1314
1112
|
stopReady();
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
pi.registerTool({
|
|
1318
|
-
name: 'title',
|
|
1319
|
-
label: 'Session Title',
|
|
1320
|
-
description:
|
|
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',
|
|
1322
|
-
promptSnippet: 'Manage the automatic session title',
|
|
1323
|
-
parameters: Type.Object({
|
|
1324
|
-
action: StringEnum(['status', 'set'] as const),
|
|
1325
|
-
title: Type.Optional(
|
|
1326
|
-
Type.String({
|
|
1327
|
-
description: 'Required only for set: the short sanitized subject title',
|
|
1328
|
-
}),
|
|
1329
|
-
),
|
|
1330
|
-
}),
|
|
1331
|
-
async execute(_toolCallId, params) {
|
|
1332
|
-
const result = applyTitleAction(params.action, params.title);
|
|
1333
|
-
return {
|
|
1334
|
-
content: [
|
|
1335
|
-
{
|
|
1336
|
-
type: 'text',
|
|
1337
|
-
text: result.ok ? result.message : `Error: ${result.message}`,
|
|
1338
|
-
},
|
|
1339
|
-
],
|
|
1340
|
-
details: {
|
|
1341
|
-
ok: result.ok,
|
|
1342
|
-
subject: result.subject,
|
|
1343
|
-
},
|
|
1344
|
-
};
|
|
1345
|
-
},
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
pi.registerTool({
|
|
1349
|
-
name: 'openspec_focus',
|
|
1350
|
-
label: 'OpenSpec Focus',
|
|
1351
|
-
description:
|
|
1352
|
-
'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',
|
|
1353
|
-
promptSnippet:
|
|
1354
|
-
'Manage exact branch-local OpenSpec Task focus with replacement safeguards',
|
|
1355
|
-
parameters: Type.Object({
|
|
1356
|
-
action: StringEnum(['status', 'set', 'replace', 'validate', 'clear'] as const),
|
|
1357
|
-
change: Type.Optional(
|
|
1358
|
-
Type.String({
|
|
1359
|
-
description: 'Required for set and replace: the exact OpenSpec change ID',
|
|
1360
|
-
}),
|
|
1361
|
-
),
|
|
1362
|
-
task: Type.Optional(
|
|
1363
|
-
Type.String({
|
|
1364
|
-
description: 'Required for set and replace: the exact OpenSpec Task ID',
|
|
1365
|
-
}),
|
|
1366
|
-
),
|
|
1367
|
-
}),
|
|
1368
|
-
async execute(_toolCallId, params) {
|
|
1369
|
-
const result = applyOpenSpecFocusAction(
|
|
1370
|
-
params.action,
|
|
1371
|
-
params.change,
|
|
1372
|
-
params.task,
|
|
1373
|
-
);
|
|
1374
|
-
return {
|
|
1375
|
-
content: [
|
|
1376
|
-
{
|
|
1377
|
-
type: 'text',
|
|
1378
|
-
text: result.ok ? result.message : `Error: ${result.message}`,
|
|
1379
|
-
},
|
|
1380
|
-
],
|
|
1381
|
-
details: {
|
|
1382
|
-
ok: result.ok,
|
|
1383
|
-
focus: result.focus,
|
|
1384
|
-
},
|
|
1385
|
-
};
|
|
1386
|
-
},
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
pi.registerCommand('openspec-focus', {
|
|
1390
|
-
description: "Show, clear, or explicitly set this session's OpenSpec task focus",
|
|
1391
|
-
handler: async (args, ctx) => {
|
|
1392
|
-
const parts = args.trim().split(/\s+/u).filter(Boolean);
|
|
1393
|
-
if (parts.length === 0) {
|
|
1394
|
-
ctx.ui.notify(applyOpenSpecFocusAction('status').message, 'info');
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
if (parts.length === 1 && parts[0]?.toLowerCase() === 'clear') {
|
|
1399
|
-
const result = applyOpenSpecFocusAction('clear');
|
|
1400
|
-
ctx.ui.notify(result.message, 'info');
|
|
1401
|
-
return;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
if (parts.length === 1 && parts[0]?.toLowerCase() === 'validate') {
|
|
1405
|
-
const result = applyOpenSpecFocusAction('validate');
|
|
1406
|
-
ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
const rawAction = parts[0]?.toLowerCase();
|
|
1411
|
-
const action: 'set' | 'replace' = ['set', 'replace'].includes(rawAction ?? '')
|
|
1412
|
-
? (rawAction as 'set' | 'replace')
|
|
1413
|
-
: 'set';
|
|
1414
|
-
const identifiers = rawAction === action ? parts.slice(1) : parts;
|
|
1415
|
-
if (identifiers.length !== 2) {
|
|
1416
|
-
ctx.ui.notify(
|
|
1417
|
-
'Usage: /openspec-focus [set|replace] <change> <task> | clear',
|
|
1418
|
-
'warning',
|
|
1419
|
-
);
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
const result = applyOpenSpecFocusAction(action, identifiers[0], identifiers[1]);
|
|
1424
|
-
ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
|
|
1425
|
-
},
|
|
1113
|
+
stopFocusRepaint();
|
|
1426
1114
|
});
|
|
1427
1115
|
|
|
1428
1116
|
pi.registerCommand('galactica-status', {
|