pilotswarm 0.5.0 → 0.5.2

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.
Files changed (37) hide show
  1. package/mcp/dist/src/session-id.d.ts +13 -0
  2. package/mcp/dist/src/session-id.d.ts.map +1 -0
  3. package/mcp/dist/src/session-id.js +15 -0
  4. package/mcp/dist/src/session-id.js.map +1 -0
  5. package/mcp/dist/src/tools/artifacts.d.ts.map +1 -1
  6. package/mcp/dist/src/tools/artifacts.js +5 -4
  7. package/mcp/dist/src/tools/artifacts.js.map +1 -1
  8. package/mcp/dist/src/tools/observability.d.ts.map +1 -1
  9. package/mcp/dist/src/tools/observability.js +2 -1
  10. package/mcp/dist/src/tools/observability.js.map +1 -1
  11. package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
  12. package/mcp/dist/src/tools/sessions.js +9 -8
  13. package/mcp/dist/src/tools/sessions.js.map +1 -1
  14. package/mcp/dist/src/tools/turn-control.d.ts.map +1 -1
  15. package/mcp/dist/src/tools/turn-control.js +5 -4
  16. package/mcp/dist/src/tools/turn-control.js.map +1 -1
  17. package/package.json +3 -3
  18. package/tui/plugins/.mcp.json +1 -7
  19. package/tui/src/app.js +36 -5
  20. package/tui/src/node-sdk-transport.js +17 -0
  21. package/ui/core/src/commands.js +6 -0
  22. package/ui/core/src/controller.js +193 -10
  23. package/ui/core/src/reducer.js +118 -1
  24. package/ui/core/src/selectors.js +428 -21
  25. package/ui/core/src/state.js +17 -0
  26. package/ui/react/src/chat-status.js +22 -0
  27. package/ui/react/src/components.js +68 -6
  28. package/ui/react/src/web-app.js +67 -13
  29. package/web/dist/assets/index-BA1Gd5ZC.js +24 -0
  30. package/web/dist/assets/index-Jr_Wn0fg.css +1 -0
  31. package/web/dist/assets/pilotswarm-C6iSDexX.js +90 -0
  32. package/web/dist/assets/{react-IRUjK5Iz.js → react-CqzaLZob.js} +1 -1
  33. package/web/dist/index.html +4 -4
  34. package/web/runtime.js +23 -2
  35. package/web/dist/assets/index-B8ge3rzb.js +0 -24
  36. package/web/dist/assets/index-DXRIxmIv.css +0 -1
  37. package/web/dist/assets/pilotswarm-Dt1qY6hD.js +0 -90
@@ -18,6 +18,10 @@ import {
18
18
  getFocusRightTarget,
19
19
  getPromptInputRows,
20
20
  MIN_SESSION_PANE_HEIGHT,
21
+ MIN_CHAT_PANE_HEIGHT,
22
+ MIN_ACTIVITY_PANE_HEIGHT,
23
+ MIN_INSPECTOR_PANE_HEIGHT,
24
+ DEFAULT_ACTIVITY_PANE_RATIO,
21
25
  normalizeFocusRegion,
22
26
  } from "./layout.js";
23
27
  import { parseTerminalMarkupRuns } from "./formatting.js";
@@ -25,6 +29,7 @@ import {
25
29
  selectActiveArtifactLinks,
26
30
  selectActiveHttpLinks,
27
31
  selectActivityPane,
32
+ selectLiveActivityLines,
28
33
  selectChatLines,
29
34
  selectFileBrowserItems,
30
35
  selectFileSessionIdsForScope,
@@ -2155,6 +2160,7 @@ export class PilotSwarmUiController {
2155
2160
  try {
2156
2161
  const profile = await this.transport.getCurrentUserProfile();
2157
2162
  this.dispatch({ type: "admin/profile/loaded", profile });
2163
+ await this.refreshSystemGhcpKeyStatus(profile);
2158
2164
  return profile || null;
2159
2165
  } catch (error) {
2160
2166
  this.dispatch({ type: "admin/profile/loadFailed", error: error?.message || String(error) });
@@ -2162,6 +2168,35 @@ export class PilotSwarmUiController {
2162
2168
  }
2163
2169
  }
2164
2170
 
2171
+ /**
2172
+ * Load the SYSTEM user's Copilot key status (admins only). Quietly a
2173
+ * no-op when the caller is not admin or the transport predates the
2174
+ * feature, so the Admin Console degrades to the per-user-only view.
2175
+ */
2176
+ async refreshSystemGhcpKeyStatus(profile = null) {
2177
+ const effective = profile || this.getState().admin?.profile || null;
2178
+ if (!effective?.isAdmin) return null;
2179
+ if (typeof this.transport.getSystemGitHubCopilotKeyStatus !== "function") return null;
2180
+ this.dispatch({ type: "admin/systemGhcpKey/loading" });
2181
+ try {
2182
+ const status = await this.transport.getSystemGitHubCopilotKeyStatus();
2183
+ this.dispatch({ type: "admin/systemGhcpKey/loaded", status });
2184
+ return status || null;
2185
+ } catch (error) {
2186
+ this.dispatch({ type: "admin/systemGhcpKey/loadFailed", error: error?.message || String(error) });
2187
+ return null;
2188
+ }
2189
+ }
2190
+
2191
+ /**
2192
+ * Admin-only: switch the key editor between "your key" and
2193
+ * "System key" (the key ownerless system sessions run on).
2194
+ */
2195
+ setAdminGhcpKeyStoreAsSystem(value) {
2196
+ if (value && !this.getState().admin?.profile?.isAdmin) return;
2197
+ this.dispatch({ type: "admin/ghcpKey/setSystemTarget", value: Boolean(value) });
2198
+ }
2199
+
2165
2200
  beginAdminEditGhcpKey() {
2166
2201
  this.dispatch({ type: "admin/ghcpKey/beginEdit", draft: "" });
2167
2202
  }
@@ -2213,8 +2248,12 @@ export class PilotSwarmUiController {
2213
2248
  * use `clearAdminGhcpKey()` for that.
2214
2249
  */
2215
2250
  async saveAdminGhcpKey() {
2216
- if (typeof this.transport.setCurrentUserGitHubCopilotKey !== "function") {
2217
- this.dispatch({ type: "admin/ghcpKey/saveFailed", error: "Admin Console is not available on this transport." });
2251
+ const storeAsSystem = Boolean(this.getState().admin.ghcpKey.storeAsSystem);
2252
+ const setter = storeAsSystem ? "setSystemGitHubCopilotKey" : "setCurrentUserGitHubCopilotKey";
2253
+ if (typeof this.transport[setter] !== "function") {
2254
+ this.dispatch({ type: "admin/ghcpKey/saveFailed", error: storeAsSystem
2255
+ ? "This deployment does not support System keys yet."
2256
+ : "Admin Console is not available on this transport." });
2218
2257
  return;
2219
2258
  }
2220
2259
  const draft = String(this.getState().admin.ghcpKey.draft || "").trim();
@@ -2224,8 +2263,13 @@ export class PilotSwarmUiController {
2224
2263
  }
2225
2264
  this.dispatch({ type: "admin/ghcpKey/saving" });
2226
2265
  try {
2227
- const profile = await this.transport.setCurrentUserGitHubCopilotKey({ key: draft });
2228
- this.dispatch({ type: "admin/ghcpKey/saved", profile });
2266
+ if (storeAsSystem) {
2267
+ const status = await this.transport.setSystemGitHubCopilotKey({ key: draft });
2268
+ this.dispatch({ type: "admin/systemGhcpKey/saved", status });
2269
+ } else {
2270
+ const profile = await this.transport.setCurrentUserGitHubCopilotKey({ key: draft });
2271
+ this.dispatch({ type: "admin/ghcpKey/saved", profile });
2272
+ }
2229
2273
  } catch (error) {
2230
2274
  this.dispatch({ type: "admin/ghcpKey/saveFailed", error: error?.message || String(error) });
2231
2275
  }
@@ -2236,14 +2280,23 @@ export class PilotSwarmUiController {
2236
2280
  * worker's env-supplied default token.
2237
2281
  */
2238
2282
  async clearAdminGhcpKey() {
2239
- if (typeof this.transport.setCurrentUserGitHubCopilotKey !== "function") {
2240
- this.dispatch({ type: "admin/ghcpKey/saveFailed", error: "Admin Console is not available on this transport." });
2283
+ const storeAsSystem = Boolean(this.getState().admin.ghcpKey.storeAsSystem);
2284
+ const setter = storeAsSystem ? "setSystemGitHubCopilotKey" : "setCurrentUserGitHubCopilotKey";
2285
+ if (typeof this.transport[setter] !== "function") {
2286
+ this.dispatch({ type: "admin/ghcpKey/saveFailed", error: storeAsSystem
2287
+ ? "This deployment does not support System keys yet."
2288
+ : "Admin Console is not available on this transport." });
2241
2289
  return;
2242
2290
  }
2243
2291
  this.dispatch({ type: "admin/ghcpKey/saving" });
2244
2292
  try {
2245
- const profile = await this.transport.setCurrentUserGitHubCopilotKey({ key: null });
2246
- this.dispatch({ type: "admin/ghcpKey/saved", profile });
2293
+ if (storeAsSystem) {
2294
+ const status = await this.transport.setSystemGitHubCopilotKey({ key: null });
2295
+ this.dispatch({ type: "admin/systemGhcpKey/saved", status });
2296
+ } else {
2297
+ const profile = await this.transport.setCurrentUserGitHubCopilotKey({ key: null });
2298
+ this.dispatch({ type: "admin/ghcpKey/saved", profile });
2299
+ }
2247
2300
  } catch (error) {
2248
2301
  this.dispatch({ type: "admin/ghcpKey/saveFailed", error: error?.message || String(error) });
2249
2302
  }
@@ -2883,6 +2936,10 @@ export class PilotSwarmUiController {
2883
2936
  this.exitPendingPromptEdit({ restoreDraft: true });
2884
2937
  }
2885
2938
  this.dispatch({ type: "sessions/selected", sessionId });
2939
+ // Sequence-tab turn selection/expansion is per-session; clear it on
2940
+ // switch so a stale turn index doesn't carry into another session.
2941
+ this.dispatch({ type: "ui/sequenceExpandedTurns", turns: [] });
2942
+ this.dispatch({ type: "ui/sequenceSelectedTurn", turn: null });
2886
2943
  }
2887
2944
  if (this.getState().sessions.byId[sessionId]?.isGroup) {
2888
2945
  this.detachActiveSession();
@@ -3534,6 +3591,19 @@ export class PilotSwarmUiController {
3534
3591
  await this.refreshSessions();
3535
3592
  }
3536
3593
 
3594
+ openHelpModal() {
3595
+ this.dispatch({
3596
+ type: "ui/modal",
3597
+ modal: {
3598
+ type: "help",
3599
+ title: "Keybindings",
3600
+ selectedIndex: 0,
3601
+ previousFocus: this.getState().ui.focusRegion,
3602
+ },
3603
+ });
3604
+ this.dispatch({ type: "ui/status", text: "Keybindings — ? or Esc to close" });
3605
+ }
3606
+
3537
3607
  openThemePicker() {
3538
3608
  const themes = listThemes().map((theme) => ({
3539
3609
  id: theme.id,
@@ -4160,7 +4230,13 @@ export class PilotSwarmUiController {
4160
4230
 
4161
4231
  moveModalSelection(delta) {
4162
4232
  const modal = this.getState().ui.modal;
4163
- if (!modal || !Array.isArray(modal.items) || modal.items.length === 0) return;
4233
+ if (!modal) return;
4234
+ if (modal.type === "help") {
4235
+ const current = Math.max(0, Number(modal.selectedIndex) || 0);
4236
+ this.dispatch({ type: "ui/modalSelection", index: Math.max(0, current + delta) });
4237
+ return;
4238
+ }
4239
+ if (!Array.isArray(modal.items) || modal.items.length === 0) return;
4164
4240
  if (modal.type === "logFilter" || modal.type === "filesFilter" || modal.type === "historyFormat") {
4165
4241
  const currentPaneIndex = Math.max(0, Math.min(Number(modal.selectedIndex) || 0, modal.items.length - 1));
4166
4242
  const selected = modal.items[currentPaneIndex];
@@ -4216,6 +4292,10 @@ export class PilotSwarmUiController {
4216
4292
  async confirmModal() {
4217
4293
  const modal = this.getState().ui.modal;
4218
4294
  if (!modal) return;
4295
+ if (modal.type === "help") {
4296
+ this.closeModal();
4297
+ return;
4298
+ }
4219
4299
  if (modal.type === "confirm") {
4220
4300
  const previousFocus = modal.previousFocus;
4221
4301
  this.dispatch({ type: "ui/modal", modal: null });
@@ -4782,6 +4862,88 @@ export class PilotSwarmUiController {
4782
4862
  }
4783
4863
  }
4784
4864
 
4865
+ // Focus-aware vertical resize: [ / ] grow or shrink whichever pane is
4866
+ // focused. Sessions and chat share the left-column vertical split;
4867
+ // inspector and activity share the right-column vertical split. A positive
4868
+ // delta always grows the focused pane at the expense of its sibling.
4869
+ resizeFocusedPane(delta) {
4870
+ const focus = this.getState().ui.focusRegion;
4871
+ const layoutState = this.getState().ui.layout || {};
4872
+ const bodyHeight = this.getCurrentLayout().bodyHeight ?? (layoutState.viewportHeight ?? 40);
4873
+ const totalHeight = layoutState.viewportHeight ?? 40;
4874
+ // Bound the adjust so BOTH panes stay strictly ABOVE their collapse
4875
+ // thresholds (the collapse check is `<=`, hence the +1). A keyboard
4876
+ // resize then never hides a pane — which would drop it from the Tab
4877
+ // cycle — and never lands on a dead plateau. A positive delta grows the
4878
+ // focused pane.
4879
+ const clamp = (current, signed, min, max) => {
4880
+ const hi = Math.max(min, max);
4881
+ return Math.max(min, Math.min(hi, (current || 0) + signed));
4882
+ };
4883
+ if (focus === FOCUS_REGIONS.SESSIONS || focus === FOCUS_REGIONS.CHAT) {
4884
+ const base = getBaseSessionPaneHeight(bodyHeight);
4885
+ const minH = MIN_SESSION_PANE_HEIGHT + 1;
4886
+ const maxH = Math.min(getMaxSessionPaneHeight(totalHeight, bodyHeight), bodyHeight - MIN_CHAT_PANE_HEIGHT - 1);
4887
+ const signed = focus === FOCUS_REGIONS.CHAT ? -delta : delta;
4888
+ const next = clamp(layoutState.sessionPaneAdjust, signed, minH - base, maxH - base);
4889
+ this.dispatch({ type: "ui/sessionPaneAdjust", sessionPaneAdjust: next });
4890
+ } else if (focus === FOCUS_REGIONS.INSPECTOR || focus === FOCUS_REGIONS.ACTIVITY) {
4891
+ const base = Math.max(MIN_ACTIVITY_PANE_HEIGHT, Math.floor(bodyHeight * DEFAULT_ACTIVITY_PANE_RATIO));
4892
+ const minH = MIN_ACTIVITY_PANE_HEIGHT + 1;
4893
+ const maxH = bodyHeight - MIN_INSPECTOR_PANE_HEIGHT - 1;
4894
+ const signed = focus === FOCUS_REGIONS.INSPECTOR ? -delta : delta;
4895
+ const next = clamp(layoutState.activityPaneAdjust, signed, minH - base, maxH - base);
4896
+ this.dispatch({ type: "ui/activityPaneAdjust", activityPaneAdjust: next });
4897
+ }
4898
+ }
4899
+
4900
+ getActiveSequenceCompletedTurns() {
4901
+ const state = this.getState();
4902
+ const sid = state.sessions.activeSessionId;
4903
+ if (!sid) return [];
4904
+ const history = state.history.bySessionId?.get?.(sid);
4905
+ const turns = [];
4906
+ const seen = new Set();
4907
+ for (const event of history?.events || []) {
4908
+ if (event?.eventType !== "session.turn_completed") continue;
4909
+ const turn = Number(event?.data?.turnIndex ?? event?.data?.iteration);
4910
+ if (!Number.isFinite(turn) || seen.has(turn)) continue;
4911
+ seen.add(turn);
4912
+ turns.push(turn);
4913
+ }
4914
+ turns.sort((a, b) => a - b);
4915
+ return turns;
4916
+ }
4917
+
4918
+ // Move the sequence-tab turn cursor among completed turns. First press with
4919
+ // no selection lands on the latest turn; subsequent presses step.
4920
+ moveSequenceSelection(delta) {
4921
+ const turns = this.getActiveSequenceCompletedTurns();
4922
+ if (turns.length === 0) return;
4923
+ const at = turns.indexOf(Number(this.getState().ui.sequenceSelectedTurn));
4924
+ const index = at === -1
4925
+ ? turns.length - 1
4926
+ : Math.max(0, Math.min(turns.length - 1, at + delta));
4927
+ this.dispatch({ type: "ui/sequenceSelectedTurn", turn: turns[index] });
4928
+ }
4929
+
4930
+ toggleSequenceTurnExpanded() {
4931
+ const turns = this.getActiveSequenceCompletedTurns();
4932
+ if (turns.length === 0) return;
4933
+ let selected = Number(this.getState().ui.sequenceSelectedTurn);
4934
+ if (!turns.includes(selected)) {
4935
+ selected = turns[turns.length - 1];
4936
+ this.dispatch({ type: "ui/sequenceSelectedTurn", turn: selected });
4937
+ }
4938
+ const expanded = Array.isArray(this.getState().ui.sequenceExpandedTurns)
4939
+ ? this.getState().ui.sequenceExpandedTurns.map(Number)
4940
+ : [];
4941
+ const next = expanded.includes(selected)
4942
+ ? expanded.filter((turn) => turn !== selected)
4943
+ : [...expanded, selected];
4944
+ this.dispatch({ type: "ui/sequenceExpandedTurns", turns: next });
4945
+ }
4946
+
4785
4947
  nextInspectorTab() {
4786
4948
  const current = this.getState().ui.inspectorTab;
4787
4949
  const inspectorTab = cycleValue(INSPECTOR_TABS, current, 1);
@@ -5058,7 +5220,10 @@ export class PilotSwarmUiController {
5058
5220
 
5059
5221
  const contentWidth = Math.max(20, layout.leftWidth - 4);
5060
5222
  const contentHeight = Math.max(1, layout.chatPaneHeight - 2);
5061
- const lines = selectChatLines(state, contentWidth);
5223
+ const lines = [
5224
+ ...selectChatLines(state, contentWidth),
5225
+ ...selectLiveActivityLines(state),
5226
+ ];
5062
5227
  const bottomStickyLines = selectOutboxOverlayLines(state, contentWidth);
5063
5228
  const bottomStickyHeight = Math.min(
5064
5229
  Math.max(0, Math.floor(contentHeight * 0.34)),
@@ -6030,9 +6195,27 @@ export class PilotSwarmUiController {
6030
6195
  case UI_COMMANDS.SHRINK_SESSION_PANE:
6031
6196
  this.adjustSessionPaneSplit(-2);
6032
6197
  return;
6198
+ case UI_COMMANDS.GROW_FOCUSED_PANE:
6199
+ this.resizeFocusedPane(2);
6200
+ return;
6201
+ case UI_COMMANDS.SHRINK_FOCUSED_PANE:
6202
+ this.resizeFocusedPane(-2);
6203
+ return;
6033
6204
  case UI_COMMANDS.OPEN_ARTIFACT_PICKER:
6034
6205
  await this.openArtifactPicker();
6035
6206
  return;
6207
+ case UI_COMMANDS.OPEN_HELP:
6208
+ this.openHelpModal();
6209
+ return;
6210
+ case UI_COMMANDS.SEQUENCE_SELECT_PREV:
6211
+ this.moveSequenceSelection(-1);
6212
+ return;
6213
+ case UI_COMMANDS.SEQUENCE_SELECT_NEXT:
6214
+ this.moveSequenceSelection(1);
6215
+ return;
6216
+ case UI_COMMANDS.TOGGLE_SEQUENCE_TURN:
6217
+ this.toggleSequenceTurnExpanded();
6218
+ return;
6036
6219
  case UI_COMMANDS.TOGGLE_LOG_TAIL:
6037
6220
  this.toggleLogTail();
6038
6221
  return;
@@ -670,6 +670,24 @@ export function appReducer(state, action) {
670
670
  },
671
671
  };
672
672
 
673
+ case "ui/sequenceExpandedTurns":
674
+ return {
675
+ ...state,
676
+ ui: {
677
+ ...state.ui,
678
+ sequenceExpandedTurns: Array.isArray(action.turns) ? action.turns : [],
679
+ },
680
+ };
681
+
682
+ case "ui/sequenceSelectedTurn":
683
+ return {
684
+ ...state,
685
+ ui: {
686
+ ...state.ui,
687
+ sequenceSelectedTurn: action.turn == null ? null : Number(action.turn),
688
+ },
689
+ };
690
+
673
691
  case "sessions/filterQuery":
674
692
  {
675
693
  const nextSessions = {
@@ -701,7 +719,19 @@ export function appReducer(state, action) {
701
719
 
702
720
  case "ui/modalSelection": {
703
721
  const modal = state.ui.modal;
704
- if (!modal || !Array.isArray(modal.items) || modal.items.length === 0) {
722
+ if (!modal) return state;
723
+ if (modal.type === "help") {
724
+ // The help overlay has no `items`; the row-count upper bound is
725
+ // clamped by selectHelpModal, so just track the scroll anchor.
726
+ return {
727
+ ...state,
728
+ ui: {
729
+ ...state.ui,
730
+ modal: { ...modal, selectedIndex: Math.max(0, Number(action.index) || 0) },
731
+ },
732
+ };
733
+ }
734
+ if (!Array.isArray(modal.items) || modal.items.length === 0) {
705
735
  return state;
706
736
  }
707
737
  const nextIndex = Math.max(0, Math.min(action.index ?? 0, modal.items.length - 1));
@@ -1458,6 +1488,7 @@ export function appReducer(state, action) {
1458
1488
  saving: false,
1459
1489
  error: null,
1460
1490
  lastSavedAt: state.admin.ghcpKey.lastSavedAt,
1491
+ storeAsSystem: Boolean(state.admin.ghcpKey.storeAsSystem),
1461
1492
  },
1462
1493
  },
1463
1494
  };
@@ -1494,6 +1525,7 @@ export function appReducer(state, action) {
1494
1525
  saving: false,
1495
1526
  error: null,
1496
1527
  lastSavedAt: state.admin.ghcpKey.lastSavedAt,
1528
+ storeAsSystem: Boolean(state.admin.ghcpKey.storeAsSystem),
1497
1529
  },
1498
1530
  },
1499
1531
  };
@@ -1537,6 +1569,91 @@ export function appReducer(state, action) {
1537
1569
  saving: false,
1538
1570
  error: null,
1539
1571
  lastSavedAt: Date.now(),
1572
+ storeAsSystem: Boolean(state.admin.ghcpKey.storeAsSystem),
1573
+ },
1574
+ },
1575
+ };
1576
+ }
1577
+ case "admin/ghcpKey/setSystemTarget": {
1578
+ const storeAsSystem = Boolean(action.value);
1579
+ if (Boolean(state.admin.ghcpKey.storeAsSystem) === storeAsSystem) return state;
1580
+ return {
1581
+ ...state,
1582
+ admin: {
1583
+ ...state.admin,
1584
+ ghcpKey: {
1585
+ ...state.admin.ghcpKey,
1586
+ storeAsSystem,
1587
+ error: null,
1588
+ },
1589
+ },
1590
+ };
1591
+ }
1592
+ case "admin/systemGhcpKey/loading": {
1593
+ return {
1594
+ ...state,
1595
+ admin: {
1596
+ ...state.admin,
1597
+ systemGhcpKey: {
1598
+ ...state.admin.systemGhcpKey,
1599
+ loading: true,
1600
+ error: null,
1601
+ },
1602
+ },
1603
+ };
1604
+ }
1605
+ case "admin/systemGhcpKey/loaded": {
1606
+ const status = action.status || {};
1607
+ return {
1608
+ ...state,
1609
+ admin: {
1610
+ ...state.admin,
1611
+ systemGhcpKey: {
1612
+ supported: true,
1613
+ loading: false,
1614
+ configured: Boolean(status.configured),
1615
+ changedBy: status.changedBy || null,
1616
+ changedAt: status.changedAt || null,
1617
+ error: null,
1618
+ },
1619
+ },
1620
+ };
1621
+ }
1622
+ case "admin/systemGhcpKey/loadFailed": {
1623
+ return {
1624
+ ...state,
1625
+ admin: {
1626
+ ...state.admin,
1627
+ systemGhcpKey: {
1628
+ ...state.admin.systemGhcpKey,
1629
+ loading: false,
1630
+ error: action.error ? String(action.error) : "Failed to load System key status",
1631
+ },
1632
+ },
1633
+ };
1634
+ }
1635
+ case "admin/systemGhcpKey/saved": {
1636
+ const status = action.status || {};
1637
+ return {
1638
+ ...state,
1639
+ admin: {
1640
+ ...state.admin,
1641
+ ghcpKey: {
1642
+ editing: false,
1643
+ draft: "",
1644
+ cursorIndex: 0,
1645
+ saving: false,
1646
+ error: null,
1647
+ lastSavedAt: Date.now(),
1648
+ storeAsSystem: Boolean(state.admin.ghcpKey.storeAsSystem),
1649
+ },
1650
+ systemGhcpKey: {
1651
+ supported: true,
1652
+ loading: false,
1653
+ configured: Boolean(status.configured),
1654
+ changedBy: status.changedBy || null,
1655
+ changedAt: status.changedAt || null,
1656
+ error: null,
1540
1657
  },
1541
1658
  },
1542
1659
  };