@aemonge-dev/pi-stardeck 0.11.0 → 0.13.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/activity-age.ts +1 -1
- package/packages/pi-stardeck/src/deck.ts +9 -6
- package/packages/pi-stardeck/src/gauge.ts +1 -6
- package/packages/pi-stardeck/src/title-state.ts +124 -58
- package/packages/status/README.md +4 -4
- package/packages/status/index.ts +33 -338
- package/packages/status/src/active-work.ts +6 -71
- package/packages/status/src/ledger.ts +1 -3
- package/packages/status/src/openspec.ts +158 -18
- package/packages/status/src/publisher.ts +6 -76
- package/packages/status/src/reducer.ts +1 -19
- package/packages/status/src/title-state.ts +85 -52
- package/packages/status/src/types.ts +15 -2
- 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/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,12 @@ class GalacticaStatusRuntime {
|
|
|
187
171
|
private openSpecOverview: OpenSpecOverview | undefined;
|
|
188
172
|
private openSpecProjectDetected: boolean | undefined;
|
|
189
173
|
private focus: OpenSpecFocus;
|
|
190
|
-
|
|
174
|
+
/** Publisher-owned timestamp of the current focus fact — seeded at
|
|
175
|
+
* construction, refreshed on every accepted focus change; staleness
|
|
176
|
+
* compares it against the change directory's file truth. */
|
|
177
|
+
private focusAt: number | undefined;
|
|
191
178
|
private subject: SessionSubject;
|
|
192
179
|
private sessionName: string | undefined;
|
|
193
|
-
private lastTitle: string | undefined;
|
|
194
180
|
private diagnosticsPath: string | undefined;
|
|
195
181
|
private orchestrationPath: string | undefined;
|
|
196
182
|
private liveLifecycle: HeaderLifecycle;
|
|
@@ -218,7 +204,6 @@ class GalacticaStatusRuntime {
|
|
|
218
204
|
ctx: SessionContext,
|
|
219
205
|
publisher: FancyFooterPublisher,
|
|
220
206
|
focus: OpenSpecFocus,
|
|
221
|
-
workFocus: SessionWorkFocus,
|
|
222
207
|
subject: SessionSubject,
|
|
223
208
|
sessionName: string | undefined,
|
|
224
209
|
) {
|
|
@@ -226,12 +211,11 @@ class GalacticaStatusRuntime {
|
|
|
226
211
|
this.ctx = ctx;
|
|
227
212
|
this.publisher = publisher;
|
|
228
213
|
this.focus = focus;
|
|
229
|
-
this.workFocus = workFocus;
|
|
230
214
|
this.subject = subject;
|
|
231
215
|
this.sessionName = sessionName;
|
|
216
|
+
this.focusAt = Date.now();
|
|
232
217
|
const restoredAwaitingValidation =
|
|
233
|
-
|
|
234
|
-
workFocus.state === 'validation';
|
|
218
|
+
focus.mode === 'task' && focus.awaitingValidation;
|
|
235
219
|
this.liveLifecycle = restoredAwaitingValidation ? 'waiting' : 'listening';
|
|
236
220
|
this.liveActivity = restoredAwaitingValidation
|
|
237
221
|
? { kind: 'awaiting-validation' }
|
|
@@ -243,7 +227,6 @@ class GalacticaStatusRuntime {
|
|
|
243
227
|
});
|
|
244
228
|
const seededAt = Date.now();
|
|
245
229
|
this.recordEvent({ type: 'focus/openspec', at: seededAt, focus });
|
|
246
|
-
this.recordEvent({ type: 'focus/work', at: seededAt, work: workFocus });
|
|
247
230
|
this.recordEvent({ type: 'focus/subject', at: seededAt, subject });
|
|
248
231
|
}
|
|
249
232
|
|
|
@@ -608,28 +591,19 @@ class GalacticaStatusRuntime {
|
|
|
608
591
|
// the same object references), so output stays byte-identical while the
|
|
609
592
|
// ledger becomes the feeding seam.
|
|
610
593
|
const focus = this.cockpitFacts.openSpec;
|
|
611
|
-
const work = this.cockpitFacts.work;
|
|
612
594
|
const subject = this.cockpitFacts.subject;
|
|
613
595
|
const title = this.currentTitleState(snapshot);
|
|
614
596
|
const focusedOpenSpec =
|
|
615
597
|
focus.mode === 'task' && snapshot.openspec?.changeId === focus.change
|
|
616
598
|
? snapshot.openspec
|
|
617
599
|
: null;
|
|
618
|
-
const visibleWorkFocus =
|
|
619
|
-
focus.mode === 'task' || (work.state === 'clear' && this.openSpecFeedback)
|
|
620
|
-
? undefined
|
|
621
|
-
: work;
|
|
622
600
|
const widgets = buildWidgetSnapshots(
|
|
623
601
|
{ ...snapshot, openspec: focusedOpenSpec },
|
|
624
602
|
{
|
|
625
603
|
focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
|
|
626
|
-
openSpecFeedback:
|
|
627
|
-
focus.mode === 'none' && !visibleWorkFocus
|
|
628
|
-
? this.openSpecFeedback
|
|
629
|
-
: undefined,
|
|
604
|
+
openSpecFeedback: focus.mode === 'none' ? this.openSpecFeedback : undefined,
|
|
630
605
|
openSpecOverview: this.openSpecOverview,
|
|
631
606
|
openSpecProjectDetected: this.openSpecProjectDetected,
|
|
632
|
-
workFocus: visibleWorkFocus,
|
|
633
607
|
},
|
|
634
608
|
);
|
|
635
609
|
this.publisher.sync(widgets);
|
|
@@ -638,7 +612,6 @@ class GalacticaStatusRuntime {
|
|
|
638
612
|
buildHeaderStatusEvent(widgets, {
|
|
639
613
|
openSpec: focusedOpenSpec,
|
|
640
614
|
focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
|
|
641
|
-
workFocus: visibleWorkFocus,
|
|
642
615
|
subject,
|
|
643
616
|
orchestration: snapshot.orchestration,
|
|
644
617
|
lifecycle: this.liveLifecycle,
|
|
@@ -655,20 +628,10 @@ class GalacticaStatusRuntime {
|
|
|
655
628
|
diagnostics: snapshot.diagnostics,
|
|
656
629
|
}),
|
|
657
630
|
);
|
|
658
|
-
this.updateTitle(snapshot);
|
|
659
631
|
this.publishStatusHealth();
|
|
660
632
|
}
|
|
661
633
|
|
|
662
634
|
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
635
|
const facts = this.cockpitFacts;
|
|
673
636
|
const focusedTaskId =
|
|
674
637
|
facts.openSpec.mode === 'task' ? facts.openSpec.taskId : undefined;
|
|
@@ -680,18 +643,24 @@ class GalacticaStatusRuntime {
|
|
|
680
643
|
(task) => task.id === focusedTaskId,
|
|
681
644
|
)?.title
|
|
682
645
|
: undefined;
|
|
683
|
-
|
|
646
|
+
// The band chip renders the fallback chain — task focus, native session
|
|
647
|
+
// name, legacy subject, directory. The terminal titlebar is pi-title's
|
|
648
|
+
// alone; this package never paints it (pure-renderer-split Slice 2).
|
|
649
|
+
const subjectTitle = formatActiveWorkTitle({
|
|
684
650
|
focus: facts.openSpec,
|
|
685
|
-
workFocus: facts.work,
|
|
686
651
|
subject: facts.subject,
|
|
687
652
|
...(taskTitle ? { taskTitle } : {}),
|
|
688
653
|
...(this.sessionName ? { sessionName: this.sessionName } : {}),
|
|
689
654
|
projectRoot: snapshot.projectRoot,
|
|
690
655
|
cwd: snapshot.cwd,
|
|
691
656
|
});
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
657
|
+
return buildTitleState({
|
|
658
|
+
subject: { state: 'set', title: subjectTitle },
|
|
659
|
+
focus: facts.openSpec,
|
|
660
|
+
openspec: snapshot.openspec,
|
|
661
|
+
activity: this.titleActivity.current(),
|
|
662
|
+
focusAt: this.focusAt,
|
|
663
|
+
});
|
|
695
664
|
}
|
|
696
665
|
|
|
697
666
|
private recordEvent(event: LedgerEvent): void {
|
|
@@ -708,10 +677,6 @@ class GalacticaStatusRuntime {
|
|
|
708
677
|
return this.focus;
|
|
709
678
|
}
|
|
710
679
|
|
|
711
|
-
currentWorkFocus(): SessionWorkFocus {
|
|
712
|
-
return this.workFocus;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
680
|
currentSubject(): SessionSubject {
|
|
716
681
|
return this.subject;
|
|
717
682
|
}
|
|
@@ -731,31 +696,18 @@ class GalacticaStatusRuntime {
|
|
|
731
696
|
setFocus(focus: OpenSpecFocus): void {
|
|
732
697
|
if (sameOpenSpecFocus(this.focus, focus)) return;
|
|
733
698
|
this.focus = focus;
|
|
699
|
+
this.focusAt = Date.now();
|
|
734
700
|
this.recordEvent({ type: 'focus/openspec', at: Date.now(), focus });
|
|
735
701
|
this.store.clearOpenSpec();
|
|
736
702
|
if (focus.mode === 'task' && focus.awaitingValidation) {
|
|
737
703
|
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
738
|
-
} else if (focus.mode === 'none'
|
|
704
|
+
} else if (focus.mode === 'none') {
|
|
739
705
|
this.setLiveProjection('listening');
|
|
740
706
|
}
|
|
741
707
|
this.publish();
|
|
742
708
|
this.launch(this.refresh(false), 'openspec');
|
|
743
709
|
}
|
|
744
710
|
|
|
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
711
|
setSubject(subject: SessionSubject): void {
|
|
760
712
|
if (sameSessionSubject(this.subject, subject)) return;
|
|
761
713
|
this.subject = subject;
|
|
@@ -938,20 +890,16 @@ class GalacticaStatusRuntime {
|
|
|
938
890
|
this.publish();
|
|
939
891
|
return;
|
|
940
892
|
}
|
|
941
|
-
if (this.focus.mode === 'task'
|
|
893
|
+
if (this.focus.mode === 'task') {
|
|
942
894
|
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
943
895
|
return;
|
|
944
896
|
}
|
|
945
|
-
if (this.workFocus.state === 'active') {
|
|
946
|
-
this.setLiveProjection('waiting', { kind: 'awaiting-input' });
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
897
|
this.setLiveProjection('listening');
|
|
950
898
|
}
|
|
951
899
|
|
|
952
900
|
setSessionName(name: string | undefined): void {
|
|
953
901
|
this.sessionName = name;
|
|
954
|
-
this.
|
|
902
|
+
this.publish();
|
|
955
903
|
}
|
|
956
904
|
|
|
957
905
|
async forceRefresh(): Promise<void> {
|
|
@@ -978,11 +926,6 @@ class GalacticaStatusRuntime {
|
|
|
978
926
|
? `OpenSpec focus: ${this.openSpecFeedback}`
|
|
979
927
|
: 'OpenSpec focus: none',
|
|
980
928
|
);
|
|
981
|
-
lines.push(
|
|
982
|
-
this.workFocus.state === 'clear'
|
|
983
|
-
? 'Work: clear'
|
|
984
|
-
: `Work: ${this.workFocus.state} · ${this.workFocus.intent}`,
|
|
985
|
-
);
|
|
986
929
|
lines.push(
|
|
987
930
|
snapshot.diagnostics
|
|
988
931
|
? `Diagnostics: ${formatDiagnostics(snapshot.diagnostics)}`
|
|
@@ -1004,7 +947,6 @@ class GalacticaStatusRuntime {
|
|
|
1004
947
|
configPath: snapshot.configPath,
|
|
1005
948
|
configWarnings: snapshot.configWarnings,
|
|
1006
949
|
focus: this.focus,
|
|
1007
|
-
workFocus: this.workFocus,
|
|
1008
950
|
openSpecFeedback: this.openSpecFeedback ?? null,
|
|
1009
951
|
openSpecOverview: this.openSpecOverview ?? null,
|
|
1010
952
|
detected: {
|
|
@@ -1055,123 +997,20 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1055
997
|
}
|
|
1056
998
|
});
|
|
1057
999
|
|
|
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);
|
|
1000
|
+
// work-state owns focus writes and emits `focus-changed` after each
|
|
1001
|
+
// successful one (pi.events); the renderer subscribes for immediate
|
|
1002
|
+
// repaint (pure-renderer-split Slice 2).
|
|
1003
|
+
const stopFocusRepaint = pi.events.on('focus-changed', (data: unknown) => {
|
|
1004
|
+
const focus = parseOpenSpecFocus(data);
|
|
1005
|
+
if (!focus) return;
|
|
1076
1006
|
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
|
-
}
|
|
1007
|
+
runtime?.settle();
|
|
1008
|
+
});
|
|
1169
1009
|
|
|
1170
1010
|
pi.on('session_start', (_event, ctx) => {
|
|
1171
1011
|
runtime?.stop();
|
|
1172
1012
|
const branch = ctx.sessionManager.getBranch();
|
|
1173
1013
|
const focus = restoreOpenSpecFocus(branch);
|
|
1174
|
-
const workFocus = restoreSessionWorkFocus(branch);
|
|
1175
1014
|
const subject = restoreSessionSubject(branch);
|
|
1176
1015
|
const sessionName = pi.getSessionName() ?? ctx.sessionManager.getSessionName();
|
|
1177
1016
|
runtime = new GalacticaStatusRuntime(
|
|
@@ -1179,7 +1018,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1179
1018
|
ctx,
|
|
1180
1019
|
publisher,
|
|
1181
1020
|
focus,
|
|
1182
|
-
workFocus,
|
|
1183
1021
|
subject,
|
|
1184
1022
|
sessionName,
|
|
1185
1023
|
);
|
|
@@ -1193,28 +1031,12 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1193
1031
|
pi.on('session_tree', (_event, ctx) => {
|
|
1194
1032
|
const branch = ctx.sessionManager.getBranch();
|
|
1195
1033
|
runtime?.setFocus(restoreOpenSpecFocus(branch));
|
|
1196
|
-
runtime?.setWorkFocus(restoreSessionWorkFocus(branch));
|
|
1197
1034
|
runtime?.setSubject(restoreSessionSubject(branch));
|
|
1198
1035
|
runtime?.settle();
|
|
1199
1036
|
});
|
|
1200
1037
|
|
|
1201
|
-
pi.on('before_agent_start', (
|
|
1038
|
+
pi.on('before_agent_start', () => {
|
|
1202
1039
|
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
1040
|
});
|
|
1219
1041
|
|
|
1220
1042
|
pi.on('tool_execution_start', (event) => {
|
|
@@ -1230,23 +1052,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1230
1052
|
);
|
|
1231
1053
|
return;
|
|
1232
1054
|
}
|
|
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
1055
|
const initialPhase = taskflowInitialHeaderPhase(event.input);
|
|
1251
1056
|
if (initialPhase) {
|
|
1252
1057
|
runtime?.setTaskflowPhase(initialPhase);
|
|
@@ -1312,117 +1117,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1312
1117
|
runtime = undefined;
|
|
1313
1118
|
publisher.removeAll();
|
|
1314
1119
|
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
|
-
},
|
|
1120
|
+
stopFocusRepaint();
|
|
1426
1121
|
});
|
|
1427
1122
|
|
|
1428
1123
|
pi.registerCommand('galactica-status', {
|