@aemonge-dev/pi-stardeck 0.10.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 +33 -66
- package/packages/pi-stardeck/index.ts +39 -202
- package/packages/pi-stardeck/src/deck.ts +15 -64
- package/packages/pi-stardeck/src/gauge.ts +1 -40
- package/packages/status/README.md +4 -4
- package/packages/status/index.ts +25 -321
- package/packages/status/src/active-work.ts +2 -58
- package/packages/status/src/ledger.ts +1 -3
- package/packages/status/src/openspec.ts +14 -8
- 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/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,28 +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
17
|
} from './src/active-work.ts';
|
|
32
18
|
import { collectDiagnostics } from './src/diagnostics.ts';
|
|
33
19
|
import { loadConfig } from './src/config.ts';
|
|
@@ -89,7 +75,6 @@ import type {
|
|
|
89
75
|
|
|
90
76
|
type SessionContext = ExtensionContext | ExtensionCommandContext;
|
|
91
77
|
type Domain = RuntimeErrorRecord['domain'];
|
|
92
|
-
type OpenSpecFocusAction = 'status' | 'set' | 'replace' | 'validate' | 'clear';
|
|
93
78
|
|
|
94
79
|
type StatusHealthCondition = {
|
|
95
80
|
id: string;
|
|
@@ -186,10 +171,8 @@ class GalacticaStatusRuntime {
|
|
|
186
171
|
private openSpecOverview: OpenSpecOverview | undefined;
|
|
187
172
|
private openSpecProjectDetected: boolean | undefined;
|
|
188
173
|
private focus: OpenSpecFocus;
|
|
189
|
-
private workFocus: SessionWorkFocus;
|
|
190
174
|
private subject: SessionSubject;
|
|
191
175
|
private sessionName: string | undefined;
|
|
192
|
-
private lastTitle: string | undefined;
|
|
193
176
|
private diagnosticsPath: string | undefined;
|
|
194
177
|
private orchestrationPath: string | undefined;
|
|
195
178
|
private liveLifecycle: HeaderLifecycle;
|
|
@@ -217,7 +200,6 @@ class GalacticaStatusRuntime {
|
|
|
217
200
|
ctx: SessionContext,
|
|
218
201
|
publisher: FancyFooterPublisher,
|
|
219
202
|
focus: OpenSpecFocus,
|
|
220
|
-
workFocus: SessionWorkFocus,
|
|
221
203
|
subject: SessionSubject,
|
|
222
204
|
sessionName: string | undefined,
|
|
223
205
|
) {
|
|
@@ -225,12 +207,10 @@ class GalacticaStatusRuntime {
|
|
|
225
207
|
this.ctx = ctx;
|
|
226
208
|
this.publisher = publisher;
|
|
227
209
|
this.focus = focus;
|
|
228
|
-
this.workFocus = workFocus;
|
|
229
210
|
this.subject = subject;
|
|
230
211
|
this.sessionName = sessionName;
|
|
231
212
|
const restoredAwaitingValidation =
|
|
232
|
-
|
|
233
|
-
workFocus.state === 'validation';
|
|
213
|
+
focus.mode === 'task' && focus.awaitingValidation;
|
|
234
214
|
this.liveLifecycle = restoredAwaitingValidation ? 'waiting' : 'listening';
|
|
235
215
|
this.liveActivity = restoredAwaitingValidation
|
|
236
216
|
? { kind: 'awaiting-validation' }
|
|
@@ -242,7 +222,6 @@ class GalacticaStatusRuntime {
|
|
|
242
222
|
});
|
|
243
223
|
const seededAt = Date.now();
|
|
244
224
|
this.recordEvent({ type: 'focus/openspec', at: seededAt, focus });
|
|
245
|
-
this.recordEvent({ type: 'focus/work', at: seededAt, work: workFocus });
|
|
246
225
|
this.recordEvent({ type: 'focus/subject', at: seededAt, subject });
|
|
247
226
|
}
|
|
248
227
|
|
|
@@ -607,28 +586,19 @@ class GalacticaStatusRuntime {
|
|
|
607
586
|
// the same object references), so output stays byte-identical while the
|
|
608
587
|
// ledger becomes the feeding seam.
|
|
609
588
|
const focus = this.cockpitFacts.openSpec;
|
|
610
|
-
const work = this.cockpitFacts.work;
|
|
611
589
|
const subject = this.cockpitFacts.subject;
|
|
612
590
|
const title = this.currentTitleState(snapshot);
|
|
613
591
|
const focusedOpenSpec =
|
|
614
592
|
focus.mode === 'task' && snapshot.openspec?.changeId === focus.change
|
|
615
593
|
? snapshot.openspec
|
|
616
594
|
: null;
|
|
617
|
-
const visibleWorkFocus =
|
|
618
|
-
focus.mode === 'task' || (work.state === 'clear' && this.openSpecFeedback)
|
|
619
|
-
? undefined
|
|
620
|
-
: work;
|
|
621
595
|
const widgets = buildWidgetSnapshots(
|
|
622
596
|
{ ...snapshot, openspec: focusedOpenSpec },
|
|
623
597
|
{
|
|
624
598
|
focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
|
|
625
|
-
openSpecFeedback:
|
|
626
|
-
focus.mode === 'none' && !visibleWorkFocus
|
|
627
|
-
? this.openSpecFeedback
|
|
628
|
-
: undefined,
|
|
599
|
+
openSpecFeedback: focus.mode === 'none' ? this.openSpecFeedback : undefined,
|
|
629
600
|
openSpecOverview: this.openSpecOverview,
|
|
630
601
|
openSpecProjectDetected: this.openSpecProjectDetected,
|
|
631
|
-
workFocus: visibleWorkFocus,
|
|
632
602
|
},
|
|
633
603
|
);
|
|
634
604
|
this.publisher.sync(widgets);
|
|
@@ -637,7 +607,6 @@ class GalacticaStatusRuntime {
|
|
|
637
607
|
buildHeaderStatusEvent(widgets, {
|
|
638
608
|
openSpec: focusedOpenSpec,
|
|
639
609
|
focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
|
|
640
|
-
workFocus: visibleWorkFocus,
|
|
641
610
|
subject,
|
|
642
611
|
orchestration: snapshot.orchestration,
|
|
643
612
|
lifecycle: this.liveLifecycle,
|
|
@@ -654,20 +623,10 @@ class GalacticaStatusRuntime {
|
|
|
654
623
|
diagnostics: snapshot.diagnostics,
|
|
655
624
|
}),
|
|
656
625
|
);
|
|
657
|
-
this.updateTitle(snapshot);
|
|
658
626
|
this.publishStatusHealth();
|
|
659
627
|
}
|
|
660
628
|
|
|
661
629
|
private currentTitleState(snapshot = this.store.get()): TitleState {
|
|
662
|
-
return buildTitleState({
|
|
663
|
-
subject: this.cockpitFacts.subject,
|
|
664
|
-
focus: this.cockpitFacts.openSpec,
|
|
665
|
-
openspec: snapshot.openspec,
|
|
666
|
-
activity: this.titleActivity.current(),
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
private updateTitle(snapshot = this.store.get()): void {
|
|
671
630
|
const facts = this.cockpitFacts;
|
|
672
631
|
const focusedTaskId =
|
|
673
632
|
facts.openSpec.mode === 'task' ? facts.openSpec.taskId : undefined;
|
|
@@ -679,18 +638,23 @@ class GalacticaStatusRuntime {
|
|
|
679
638
|
(task) => task.id === focusedTaskId,
|
|
680
639
|
)?.title
|
|
681
640
|
: undefined;
|
|
682
|
-
|
|
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({
|
|
683
645
|
focus: facts.openSpec,
|
|
684
|
-
workFocus: facts.work,
|
|
685
646
|
subject: facts.subject,
|
|
686
647
|
...(taskTitle ? { taskTitle } : {}),
|
|
687
648
|
...(this.sessionName ? { sessionName: this.sessionName } : {}),
|
|
688
649
|
projectRoot: snapshot.projectRoot,
|
|
689
650
|
cwd: snapshot.cwd,
|
|
690
651
|
});
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
652
|
+
return buildTitleState({
|
|
653
|
+
subject: { state: 'set', title: subjectTitle },
|
|
654
|
+
focus: facts.openSpec,
|
|
655
|
+
openspec: snapshot.openspec,
|
|
656
|
+
activity: this.titleActivity.current(),
|
|
657
|
+
});
|
|
694
658
|
}
|
|
695
659
|
|
|
696
660
|
private recordEvent(event: LedgerEvent): void {
|
|
@@ -707,10 +671,6 @@ class GalacticaStatusRuntime {
|
|
|
707
671
|
return this.focus;
|
|
708
672
|
}
|
|
709
673
|
|
|
710
|
-
currentWorkFocus(): SessionWorkFocus {
|
|
711
|
-
return this.workFocus;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
674
|
currentSubject(): SessionSubject {
|
|
715
675
|
return this.subject;
|
|
716
676
|
}
|
|
@@ -734,27 +694,13 @@ class GalacticaStatusRuntime {
|
|
|
734
694
|
this.store.clearOpenSpec();
|
|
735
695
|
if (focus.mode === 'task' && focus.awaitingValidation) {
|
|
736
696
|
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
737
|
-
} else if (focus.mode === 'none'
|
|
697
|
+
} else if (focus.mode === 'none') {
|
|
738
698
|
this.setLiveProjection('listening');
|
|
739
699
|
}
|
|
740
700
|
this.publish();
|
|
741
701
|
this.launch(this.refresh(false), 'openspec');
|
|
742
702
|
}
|
|
743
703
|
|
|
744
|
-
setWorkFocus(focus: SessionWorkFocus): void {
|
|
745
|
-
if (sameSessionWorkFocus(this.workFocus, focus)) return;
|
|
746
|
-
this.workFocus = focus;
|
|
747
|
-
this.recordEvent({ type: 'focus/work', at: Date.now(), work: focus });
|
|
748
|
-
if (focus.state === 'validation') {
|
|
749
|
-
this.liveLifecycle = 'waiting';
|
|
750
|
-
this.liveActivity = { kind: 'awaiting-validation' };
|
|
751
|
-
} else if (focus.state === 'clear' && this.focus.mode === 'none') {
|
|
752
|
-
this.liveLifecycle = 'listening';
|
|
753
|
-
this.liveActivity = undefined;
|
|
754
|
-
}
|
|
755
|
-
this.publish();
|
|
756
|
-
}
|
|
757
|
-
|
|
758
704
|
setSubject(subject: SessionSubject): void {
|
|
759
705
|
if (sameSessionSubject(this.subject, subject)) return;
|
|
760
706
|
this.subject = subject;
|
|
@@ -937,20 +883,16 @@ class GalacticaStatusRuntime {
|
|
|
937
883
|
this.publish();
|
|
938
884
|
return;
|
|
939
885
|
}
|
|
940
|
-
if (this.focus.mode === 'task'
|
|
886
|
+
if (this.focus.mode === 'task') {
|
|
941
887
|
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
942
888
|
return;
|
|
943
889
|
}
|
|
944
|
-
if (this.workFocus.state === 'active') {
|
|
945
|
-
this.setLiveProjection('waiting', { kind: 'awaiting-input' });
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
890
|
this.setLiveProjection('listening');
|
|
949
891
|
}
|
|
950
892
|
|
|
951
893
|
setSessionName(name: string | undefined): void {
|
|
952
894
|
this.sessionName = name;
|
|
953
|
-
this.
|
|
895
|
+
this.publish();
|
|
954
896
|
}
|
|
955
897
|
|
|
956
898
|
async forceRefresh(): Promise<void> {
|
|
@@ -977,11 +919,6 @@ class GalacticaStatusRuntime {
|
|
|
977
919
|
? `OpenSpec focus: ${this.openSpecFeedback}`
|
|
978
920
|
: 'OpenSpec focus: none',
|
|
979
921
|
);
|
|
980
|
-
lines.push(
|
|
981
|
-
this.workFocus.state === 'clear'
|
|
982
|
-
? 'Work: clear'
|
|
983
|
-
: `Work: ${this.workFocus.state} · ${this.workFocus.intent}`,
|
|
984
|
-
);
|
|
985
922
|
lines.push(
|
|
986
923
|
snapshot.diagnostics
|
|
987
924
|
? `Diagnostics: ${formatDiagnostics(snapshot.diagnostics)}`
|
|
@@ -1003,7 +940,6 @@ class GalacticaStatusRuntime {
|
|
|
1003
940
|
configPath: snapshot.configPath,
|
|
1004
941
|
configWarnings: snapshot.configWarnings,
|
|
1005
942
|
focus: this.focus,
|
|
1006
|
-
workFocus: this.workFocus,
|
|
1007
943
|
openSpecFeedback: this.openSpecFeedback ?? null,
|
|
1008
944
|
openSpecOverview: this.openSpecOverview ?? null,
|
|
1009
945
|
detected: {
|
|
@@ -1054,123 +990,20 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1054
990
|
}
|
|
1055
991
|
});
|
|
1056
992
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
pi.appendEntry(SESSION_SUBJECT_ENTRY_TYPE, subject);
|
|
1064
|
-
runtime?.setSubject(subject);
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function persistAndApplyFocus(focus: OpenSpecFocus): void {
|
|
1068
|
-
if (
|
|
1069
|
-
focus.mode === 'task' &&
|
|
1070
|
-
(runtime?.currentWorkFocus() ?? NO_SESSION_WORK_FOCUS).state !== 'clear'
|
|
1071
|
-
) {
|
|
1072
|
-
persistAndApplyWorkFocus(NO_SESSION_WORK_FOCUS);
|
|
1073
|
-
}
|
|
1074
|
-
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;
|
|
1075
999
|
runtime?.setFocus(focus);
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
function subjectStatus(subject: SessionSubject): string {
|
|
1079
|
-
return subject.state === 'clear' ? 'Title: clear' : `Title: ${subject.title}`;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function applyTitleAction(
|
|
1083
|
-
action: 'status' | 'set',
|
|
1084
|
-
title?: string,
|
|
1085
|
-
): { ok: boolean; message: string; subject: SessionSubject } {
|
|
1086
|
-
const current = runtime?.currentSubject() ?? NO_SESSION_SUBJECT;
|
|
1087
|
-
|
|
1088
|
-
if (action === 'status') {
|
|
1089
|
-
return { ok: true, message: subjectStatus(current), subject: current };
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
const next = sessionSubject('set', title);
|
|
1093
|
-
if (!next || next.state !== 'set') {
|
|
1094
|
-
return {
|
|
1095
|
-
ok: false,
|
|
1096
|
-
message: 'Title requires a non-empty title',
|
|
1097
|
-
subject: current,
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
if (!sameSessionSubject(current, next)) persistAndApplySubject(next);
|
|
1101
|
-
return { ok: true, message: subjectStatus(next), subject: next };
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
function applyOpenSpecFocusAction(
|
|
1105
|
-
action: OpenSpecFocusAction,
|
|
1106
|
-
change?: string,
|
|
1107
|
-
taskId?: string,
|
|
1108
|
-
): { ok: boolean; message: string; focus: OpenSpecFocus } {
|
|
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';
|
|
1115
|
-
|
|
1116
|
-
if (action === 'status') {
|
|
1117
|
-
return { ok: true, message: statusMessage(current), focus: current };
|
|
1118
|
-
}
|
|
1119
|
-
if (action === 'clear') {
|
|
1120
|
-
persistAndApplyFocus(NO_OPEN_SPEC_FOCUS);
|
|
1121
|
-
return {
|
|
1122
|
-
ok: true,
|
|
1123
|
-
message: 'OpenSpec focus cleared',
|
|
1124
|
-
focus: NO_OPEN_SPEC_FOCUS,
|
|
1125
|
-
};
|
|
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
|
-
}
|
|
1139
|
-
|
|
1140
|
-
const requested = taskFocus(change, taskId);
|
|
1141
|
-
if (!requested) {
|
|
1142
|
-
return {
|
|
1143
|
-
ok: false,
|
|
1144
|
-
message: 'OpenSpec focus requires non-empty change and task identifiers',
|
|
1145
|
-
focus: current,
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
if (
|
|
1149
|
-
action === 'set' &&
|
|
1150
|
-
current.mode === 'task' &&
|
|
1151
|
-
!sameTaskFocus(current, requested)
|
|
1152
|
-
) {
|
|
1153
|
-
return {
|
|
1154
|
-
ok: false,
|
|
1155
|
-
message:
|
|
1156
|
-
`OpenSpec focus is ${current.change} › ${current.taskId}; ` +
|
|
1157
|
-
'use replace to change it deliberately',
|
|
1158
|
-
focus: current,
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
if (!sameOpenSpecFocus(current, requested)) persistAndApplyFocus(requested);
|
|
1162
|
-
return {
|
|
1163
|
-
ok: true,
|
|
1164
|
-
message: statusMessage(requested),
|
|
1165
|
-
focus: requested,
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1000
|
+
runtime?.settle();
|
|
1001
|
+
});
|
|
1168
1002
|
|
|
1169
1003
|
pi.on('session_start', (_event, ctx) => {
|
|
1170
1004
|
runtime?.stop();
|
|
1171
1005
|
const branch = ctx.sessionManager.getBranch();
|
|
1172
1006
|
const focus = restoreOpenSpecFocus(branch);
|
|
1173
|
-
const workFocus = restoreSessionWorkFocus(branch);
|
|
1174
1007
|
const subject = restoreSessionSubject(branch);
|
|
1175
1008
|
const sessionName = pi.getSessionName() ?? ctx.sessionManager.getSessionName();
|
|
1176
1009
|
runtime = new GalacticaStatusRuntime(
|
|
@@ -1178,7 +1011,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1178
1011
|
ctx,
|
|
1179
1012
|
publisher,
|
|
1180
1013
|
focus,
|
|
1181
|
-
workFocus,
|
|
1182
1014
|
subject,
|
|
1183
1015
|
sessionName,
|
|
1184
1016
|
);
|
|
@@ -1192,7 +1024,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1192
1024
|
pi.on('session_tree', (_event, ctx) => {
|
|
1193
1025
|
const branch = ctx.sessionManager.getBranch();
|
|
1194
1026
|
runtime?.setFocus(restoreOpenSpecFocus(branch));
|
|
1195
|
-
runtime?.setWorkFocus(restoreSessionWorkFocus(branch));
|
|
1196
1027
|
runtime?.setSubject(restoreSessionSubject(branch));
|
|
1197
1028
|
runtime?.settle();
|
|
1198
1029
|
});
|
|
@@ -1214,23 +1045,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1214
1045
|
);
|
|
1215
1046
|
return;
|
|
1216
1047
|
}
|
|
1217
|
-
const requested = taskflowRunFocus(event.input);
|
|
1218
|
-
|
|
1219
|
-
if (requested) {
|
|
1220
|
-
const current = runtime?.currentFocus() ?? NO_OPEN_SPEC_FOCUS;
|
|
1221
|
-
if (current.mode === 'task' && !sameOpenSpecFocus(current, requested)) {
|
|
1222
|
-
return {
|
|
1223
|
-
block: true,
|
|
1224
|
-
reason:
|
|
1225
|
-
`OpenSpec focus is ${current.change} › ${current.taskId}; ` +
|
|
1226
|
-
'use /openspec-focus clear or /openspec-focus replace <change> <task> before starting a conflicting run',
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
if (!sameOpenSpecFocus(current, requested)) {
|
|
1230
|
-
persistAndApplyFocus(requested);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
1048
|
const initialPhase = taskflowInitialHeaderPhase(event.input);
|
|
1235
1049
|
if (initialPhase) {
|
|
1236
1050
|
runtime?.setTaskflowPhase(initialPhase);
|
|
@@ -1296,117 +1110,7 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1296
1110
|
runtime = undefined;
|
|
1297
1111
|
publisher.removeAll();
|
|
1298
1112
|
stopReady();
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
pi.registerTool({
|
|
1302
|
-
name: 'title',
|
|
1303
|
-
label: 'Session Title',
|
|
1304
|
-
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',
|
|
1306
|
-
promptSnippet: 'Manage the automatic session title',
|
|
1307
|
-
parameters: Type.Object({
|
|
1308
|
-
action: StringEnum(['status', 'set'] as const),
|
|
1309
|
-
title: Type.Optional(
|
|
1310
|
-
Type.String({
|
|
1311
|
-
description: 'Required only for set: the short sanitized subject title',
|
|
1312
|
-
}),
|
|
1313
|
-
),
|
|
1314
|
-
}),
|
|
1315
|
-
async execute(_toolCallId, params) {
|
|
1316
|
-
const result = applyTitleAction(params.action, params.title);
|
|
1317
|
-
return {
|
|
1318
|
-
content: [
|
|
1319
|
-
{
|
|
1320
|
-
type: 'text',
|
|
1321
|
-
text: result.ok ? result.message : `Error: ${result.message}`,
|
|
1322
|
-
},
|
|
1323
|
-
],
|
|
1324
|
-
details: {
|
|
1325
|
-
ok: result.ok,
|
|
1326
|
-
subject: result.subject,
|
|
1327
|
-
},
|
|
1328
|
-
};
|
|
1329
|
-
},
|
|
1330
|
-
});
|
|
1331
|
-
|
|
1332
|
-
pi.registerTool({
|
|
1333
|
-
name: 'openspec_focus',
|
|
1334
|
-
label: 'OpenSpec Focus',
|
|
1335
|
-
description:
|
|
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',
|
|
1337
|
-
promptSnippet:
|
|
1338
|
-
'Manage exact branch-local OpenSpec Task focus with replacement safeguards',
|
|
1339
|
-
parameters: Type.Object({
|
|
1340
|
-
action: StringEnum(['status', 'set', 'replace', 'validate', 'clear'] as const),
|
|
1341
|
-
change: Type.Optional(
|
|
1342
|
-
Type.String({
|
|
1343
|
-
description: 'Required for set and replace: the exact OpenSpec change ID',
|
|
1344
|
-
}),
|
|
1345
|
-
),
|
|
1346
|
-
task: Type.Optional(
|
|
1347
|
-
Type.String({
|
|
1348
|
-
description: 'Required for set and replace: the exact OpenSpec Task ID',
|
|
1349
|
-
}),
|
|
1350
|
-
),
|
|
1351
|
-
}),
|
|
1352
|
-
async execute(_toolCallId, params) {
|
|
1353
|
-
const result = applyOpenSpecFocusAction(
|
|
1354
|
-
params.action,
|
|
1355
|
-
params.change,
|
|
1356
|
-
params.task,
|
|
1357
|
-
);
|
|
1358
|
-
return {
|
|
1359
|
-
content: [
|
|
1360
|
-
{
|
|
1361
|
-
type: 'text',
|
|
1362
|
-
text: result.ok ? result.message : `Error: ${result.message}`,
|
|
1363
|
-
},
|
|
1364
|
-
],
|
|
1365
|
-
details: {
|
|
1366
|
-
ok: result.ok,
|
|
1367
|
-
focus: result.focus,
|
|
1368
|
-
},
|
|
1369
|
-
};
|
|
1370
|
-
},
|
|
1371
|
-
});
|
|
1372
|
-
|
|
1373
|
-
pi.registerCommand('openspec-focus', {
|
|
1374
|
-
description: "Show, clear, or explicitly set this session's OpenSpec task focus",
|
|
1375
|
-
handler: async (args, ctx) => {
|
|
1376
|
-
const parts = args.trim().split(/\s+/u).filter(Boolean);
|
|
1377
|
-
if (parts.length === 0) {
|
|
1378
|
-
ctx.ui.notify(applyOpenSpecFocusAction('status').message, 'info');
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
if (parts.length === 1 && parts[0]?.toLowerCase() === 'clear') {
|
|
1383
|
-
const result = applyOpenSpecFocusAction('clear');
|
|
1384
|
-
ctx.ui.notify(result.message, 'info');
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
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
|
-
|
|
1394
|
-
const rawAction = parts[0]?.toLowerCase();
|
|
1395
|
-
const action: 'set' | 'replace' = ['set', 'replace'].includes(rawAction ?? '')
|
|
1396
|
-
? (rawAction as 'set' | 'replace')
|
|
1397
|
-
: 'set';
|
|
1398
|
-
const identifiers = rawAction === action ? parts.slice(1) : parts;
|
|
1399
|
-
if (identifiers.length !== 2) {
|
|
1400
|
-
ctx.ui.notify(
|
|
1401
|
-
'Usage: /openspec-focus [set|replace] <change> <task> | clear',
|
|
1402
|
-
'warning',
|
|
1403
|
-
);
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
const result = applyOpenSpecFocusAction(action, identifiers[0], identifiers[1]);
|
|
1408
|
-
ctx.ui.notify(result.message, result.ok ? 'info' : 'warning');
|
|
1409
|
-
},
|
|
1113
|
+
stopFocusRepaint();
|
|
1410
1114
|
});
|
|
1411
1115
|
|
|
1412
1116
|
pi.registerCommand('galactica-status', {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
|
|
3
3
|
export const OPEN_SPEC_FOCUS_ENTRY_TYPE = 'galactica-status.openspec-focus.v1';
|
|
4
|
-
export const SESSION_WORK_FOCUS_ENTRY_TYPE = 'galactica-status.session-work.v1';
|
|
5
4
|
export const SESSION_SUBJECT_ENTRY_TYPE = 'galactica-status.session-subject.v1';
|
|
6
5
|
|
|
7
6
|
export type TaskOpenSpecFocus = {
|
|
@@ -15,11 +14,6 @@ export type OpenSpecFocus = { mode: 'none' } | TaskOpenSpecFocus;
|
|
|
15
14
|
|
|
16
15
|
export const NO_OPEN_SPEC_FOCUS: OpenSpecFocus = { mode: 'none' };
|
|
17
16
|
|
|
18
|
-
export type SessionWorkFocus =
|
|
19
|
-
{ state: 'clear' } | { state: 'active' | 'validation'; intent: string };
|
|
20
|
-
|
|
21
|
-
export const NO_SESSION_WORK_FOCUS: SessionWorkFocus = { state: 'clear' };
|
|
22
|
-
|
|
23
17
|
export type SessionSubject = { state: 'clear' } | { state: 'set'; title: string };
|
|
24
18
|
|
|
25
19
|
export const NO_SESSION_SUBJECT: SessionSubject = { state: 'clear' };
|
|
@@ -27,7 +21,6 @@ export const NO_SESSION_SUBJECT: SessionSubject = { state: 'clear' };
|
|
|
27
21
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/gu;
|
|
28
22
|
const MAX_TITLE_POINTS = 180;
|
|
29
23
|
const MAX_TASK_TITLE_POINTS = 72;
|
|
30
|
-
const MAX_WORK_INTENT_POINTS = 160;
|
|
31
24
|
const MAX_SUBJECT_POINTS = 120;
|
|
32
25
|
|
|
33
26
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -73,25 +66,6 @@ export function parseOpenSpecFocus(value: unknown): OpenSpecFocus | undefined {
|
|
|
73
66
|
return taskFocus(value.change, value.taskId, value.awaitingValidation);
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
export function sessionWorkFocus(
|
|
77
|
-
state: unknown,
|
|
78
|
-
intent?: unknown,
|
|
79
|
-
): SessionWorkFocus | undefined {
|
|
80
|
-
if (state === 'clear') return NO_SESSION_WORK_FOCUS;
|
|
81
|
-
if (state !== 'active' && state !== 'validation') return undefined;
|
|
82
|
-
const normalizedIntent = explicitId(intent);
|
|
83
|
-
if (!normalizedIntent) return undefined;
|
|
84
|
-
return {
|
|
85
|
-
state,
|
|
86
|
-
intent: shorten(normalizedIntent, MAX_WORK_INTENT_POINTS),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function parseSessionWorkFocus(value: unknown): SessionWorkFocus | undefined {
|
|
91
|
-
if (!isRecord(value)) return undefined;
|
|
92
|
-
return sessionWorkFocus(value.state, value.intent);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
69
|
export function sessionSubject(
|
|
96
70
|
state: unknown,
|
|
97
71
|
title?: unknown,
|
|
@@ -127,22 +101,6 @@ export function restoreOpenSpecFocus(entries: readonly unknown[]): OpenSpecFocus
|
|
|
127
101
|
return NO_OPEN_SPEC_FOCUS;
|
|
128
102
|
}
|
|
129
103
|
|
|
130
|
-
export function restoreSessionWorkFocus(entries: readonly unknown[]): SessionWorkFocus {
|
|
131
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
132
|
-
const entry = entries[index];
|
|
133
|
-
if (
|
|
134
|
-
!isRecord(entry) ||
|
|
135
|
-
entry.type !== 'custom' ||
|
|
136
|
-
entry.customType !== SESSION_WORK_FOCUS_ENTRY_TYPE
|
|
137
|
-
) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
const focus = parseSessionWorkFocus(entry.data);
|
|
141
|
-
if (focus) return focus;
|
|
142
|
-
}
|
|
143
|
-
return NO_SESSION_WORK_FOCUS;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
104
|
export function restoreSessionSubject(entries: readonly unknown[]): SessionSubject {
|
|
147
105
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
148
106
|
const entry = entries[index];
|
|
@@ -192,16 +150,6 @@ export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): bo
|
|
|
192
150
|
);
|
|
193
151
|
}
|
|
194
152
|
|
|
195
|
-
export function sameSessionWorkFocus(
|
|
196
|
-
left: SessionWorkFocus,
|
|
197
|
-
right: SessionWorkFocus,
|
|
198
|
-
): boolean {
|
|
199
|
-
if (left.state !== right.state) return false;
|
|
200
|
-
return (
|
|
201
|
-
left.state === 'clear' || (right.state !== 'clear' && left.intent === right.intent)
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
153
|
export function sameSessionSubject(
|
|
206
154
|
left: SessionSubject,
|
|
207
155
|
right: SessionSubject,
|
|
@@ -214,7 +162,6 @@ export function sameSessionSubject(
|
|
|
214
162
|
|
|
215
163
|
export function formatActiveWorkTitle(options: {
|
|
216
164
|
focus: OpenSpecFocus;
|
|
217
|
-
workFocus?: SessionWorkFocus;
|
|
218
165
|
subject?: SessionSubject;
|
|
219
166
|
taskTitle?: string;
|
|
220
167
|
sessionName?: string;
|
|
@@ -227,11 +174,8 @@ export function formatActiveWorkTitle(options: {
|
|
|
227
174
|
? shorten(normalizeTitlePart(options.taskTitle), MAX_TASK_TITLE_POINTS)
|
|
228
175
|
: '';
|
|
229
176
|
detail = `${options.focus.change} › ${options.focus.taskId}${taskTitle ? ` ${taskTitle}` : ''}`;
|
|
230
|
-
} else if (options.
|
|
231
|
-
detail =
|
|
232
|
-
options.workFocus.state === 'validation'
|
|
233
|
-
? `Validate › ${options.workFocus.intent}`
|
|
234
|
-
: options.workFocus.intent;
|
|
177
|
+
} else if (normalizeTitlePart(options.sessionName ?? '')) {
|
|
178
|
+
detail = normalizeTitlePart(options.sessionName ?? '');
|
|
235
179
|
} else if (options.subject && options.subject.state === 'set') {
|
|
236
180
|
detail = options.subject.title;
|
|
237
181
|
} else {
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
RunSpan,
|
|
6
6
|
StageValue,
|
|
7
7
|
} from './types.ts';
|
|
8
|
-
import type { OpenSpecFocus, SessionSubject
|
|
8
|
+
import type { OpenSpecFocus, SessionSubject } from './active-work.ts';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Append-only event ledger for the cockpit status core.
|
|
@@ -18,7 +18,6 @@ import type { OpenSpecFocus, SessionSubject, SessionWorkFocus } from './active-w
|
|
|
18
18
|
|
|
19
19
|
export const LEDGER_EVENT_TYPES = [
|
|
20
20
|
'focus/openspec',
|
|
21
|
-
'focus/work',
|
|
22
21
|
'focus/subject',
|
|
23
22
|
'run/start',
|
|
24
23
|
'run/stage',
|
|
@@ -37,7 +36,6 @@ export type LedgerEvent =
|
|
|
37
36
|
at: number;
|
|
38
37
|
focus: OpenSpecFocus;
|
|
39
38
|
}
|
|
40
|
-
| { type: 'focus/work'; at: number; work: SessionWorkFocus }
|
|
41
39
|
| { type: 'focus/subject'; at: number; subject: SessionSubject }
|
|
42
40
|
| { type: 'run/start'; at: number; span: RunSpan }
|
|
43
41
|
| { type: 'run/stage'; at: number; id: string; stage: StageValue; declared: boolean }
|