@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.
@@ -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
- private workFocus: SessionWorkFocus;
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
- (focus.mode === 'task' && focus.awaitingValidation) ||
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
- const title = formatActiveWorkTitle({
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
- if (title === this.lastTitle) return;
693
- this.lastTitle = title;
694
- this.ctx.ui.setTitle(title);
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' && this.workFocus.state === 'clear') {
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' || this.workFocus.state === 'validation') {
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.updateTitle();
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
- function persistAndApplyWorkFocus(focus: SessionWorkFocus): void {
1059
- pi.appendEntry(SESSION_WORK_FOCUS_ENTRY_TYPE, focus);
1060
- runtime?.setWorkFocus(focus);
1061
- }
1062
-
1063
- function persistAndApplySubject(subject: SessionSubject): void {
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', (event) => {
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', {