pilotswarm 0.5.28 → 0.5.30
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/mcp/dist/bin/pilotswarm-mcp.js +4 -0
- package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -1
- package/mcp/dist/src/context.d.ts +43 -10
- package/mcp/dist/src/context.d.ts.map +1 -1
- package/mcp/dist/src/context.js +15 -7
- package/mcp/dist/src/context.js.map +1 -1
- package/mcp/dist/src/resources/artifacts.d.ts.map +1 -1
- package/mcp/dist/src/resources/artifacts.js +4 -3
- package/mcp/dist/src/resources/artifacts.js.map +1 -1
- package/mcp/dist/src/server.d.ts.map +1 -1
- package/mcp/dist/src/server.js +2 -0
- package/mcp/dist/src/server.js.map +1 -1
- package/mcp/dist/src/tools/agent-packages.d.ts +14 -0
- package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -0
- package/mcp/dist/src/tools/agent-packages.js +126 -0
- package/mcp/dist/src/tools/agent-packages.js.map +1 -0
- package/mcp/dist/src/tools/agents.d.ts.map +1 -1
- package/mcp/dist/src/tools/agents.js +18 -0
- package/mcp/dist/src/tools/agents.js.map +1 -1
- package/mcp/dist/src/tools/artifacts.d.ts.map +1 -1
- package/mcp/dist/src/tools/artifacts.js +12 -11
- package/mcp/dist/src/tools/artifacts.js.map +1 -1
- package/mcp/dist/src/tools/capabilities.js +1 -1
- package/mcp/dist/src/tools/capabilities.js.map +1 -1
- package/mcp/dist/src/tools/observability.js +1 -1
- package/mcp/dist/src/tools/observability.js.map +1 -1
- package/mcp/dist/src/tools/sessions.js +2 -2
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/mcp/dist/src/tools/system.d.ts.map +1 -1
- package/mcp/dist/src/tools/system.js +56 -5
- package/mcp/dist/src/tools/system.js.map +1 -1
- package/mcp/dist/src/tools/turn-control.js +1 -1
- package/mcp/dist/src/tools/turn-control.js.map +1 -1
- package/mcp/dist/src/util/command.d.ts +3 -3
- package/mcp/dist/src/util/command.d.ts.map +1 -1
- package/mcp/dist/src/util/command.js +1 -1
- package/mcp/dist/src/util/command.js.map +1 -1
- package/package.json +6 -6
- package/tui/bin/tui.js +6 -0
- package/tui/src/agents-cli.js +442 -0
- package/tui/src/app.js +44 -1
- package/tui/src/node-sdk-transport.js +386 -5
- package/tui/src/plugin-config.js +17 -0
- package/ui/core/src/commands.js +7 -0
- package/ui/core/src/controller.js +498 -33
- package/ui/core/src/formatting.js +60 -16
- package/ui/core/src/index.js +2 -0
- package/ui/core/src/reducer.js +421 -34
- package/ui/core/src/repo-import.js +298 -0
- package/ui/core/src/repo-links.js +54 -0
- package/ui/core/src/selectors.js +879 -91
- package/ui/core/src/session-tree.js +109 -8
- package/ui/core/src/state.js +108 -2
- package/ui/core/src/themes/helpers.js +4 -1
- package/ui/core/src/themes/index.js +4 -10
- package/ui/core/src/themes/workspace-dark-rich.js +65 -0
- package/ui/core/src/themes/workspace-dark.js +65 -0
- package/ui/react/src/components.js +181 -4
- package/ui/react/src/web-app.js +1410 -91
- package/web/api/router.js +20 -2
- package/web/authz.js +4 -10
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DLV45wH4.js → abnfDiagram-VRR7QNED-Cimc0f-u.js} +1 -1
- package/web/dist/assets/{arc-CE4xAYdE.js → arc-D8-_Dx5J.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DfzR8-9F.js → architectureDiagram-ZJ3FMSHR-lzMAuQHJ.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-C2pZdqvH.js → blockDiagram-677ZJIJ3-Dp2CWjVS.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-CKRspTan.js → c4Diagram-LMCZKHZV-DF93UkOy.js} +1 -1
- package/web/dist/assets/channel-OwWeiCox.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CNjLrfHT.js → chunk-2Q5K7J3B-pbsQRM-F.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-Bv5EwyaJ.js → chunk-32BRIVSS-B_f-gG1O.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-NDBesEbJ.js → chunk-5VM5RSS4-gqlpJDmt.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DHQht9ty.js → chunk-EX3LRPZG-Cms7hPmM.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-r0OQpJRF.js → chunk-JWPE2WC7-Boq5PtPI.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-fcwIDgtl.js → chunk-MOJQB5TN-DVn9i8zt.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-x7XQlDO1.js → chunk-RYQCIY6F-a1vKs3UX.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-U8ZNVy1m.js → chunk-V7JOEXUC-i60p5fHd.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DLEdPLcN.js → chunk-VR4S4FIN-a8p0o-8B.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-TeyAtB-i.js → chunk-XXDRQBXY-BwP1yBKd.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-CDaP7nuC.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CDaP7nuC.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-B3bAl1-Z.js → cose-bilkent-JH36ORCC-HW6kGgze.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DexP8nG-.js → cynefin-VYW2F7L2-YUFMn-vE.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Doa2XnSs.js → cynefinDiagram-TSTJHNR4-Brdy17i4.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DZMBKQcJ.js → dagre-VKFMJZFB-CAiSJJj-.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-BkzibmSq.js → diagram-FQU43EPY-B63Cqe0J.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CihyHR3a.js → diagram-G47NLZAW-ByktrDJ2.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-OMDUUxwW.js → diagram-NH7WQ7WH-BLSfRMoE.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-Lc0imJba.js → diagram-OA4YK3LP-DNVu-zpk.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Dlcy8hPe.js → diagram-WEI45ONY-CNf4Sz-I.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-Dh3u17Dg.js → ebnfDiagram-CCIWWBDH-9CT3ergx.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-C0OhlUC2.js → erDiagram-Q63AITRT-BPJHAMgl.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-CADKxT-x.js → flowDiagram-23GEKE2U-BVtpr6Iu.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-iVVdxWB7.js → ganttDiagram-NO4QXBWP-BTHnv9sV.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BwDUec_a.js → gitGraphDiagram-IHSO6WYX-BfmtqbL5.js} +1 -1
- package/web/dist/assets/index-B37T1c2s.css +1 -0
- package/web/dist/assets/index-Bmb8qK39.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CGPUif0s.js → infoDiagram-FWYZ7A6U-kU4i4HRu.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-QYnrl-KA.js → ishikawaDiagram-FXEZZL3T-Ch7i-E8O.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-CeKixxL0.js → journeyDiagram-5HDEW3XC-CPQxp0ps.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CeYokFBS.js → kanban-definition-HUTT4EX6-BBgMVqoJ.js} +1 -1
- package/web/dist/assets/{linear-BbA6_gnz.js → linear-rwksvzXJ.js} +1 -1
- package/web/dist/assets/{mermaid.core--xGRoQTa.js → mermaid.core-o7PszRUc.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-D53u8h0z.js → mindmap-definition-LN4V7U3C-wgOTdzlq.js} +1 -1
- package/web/dist/assets/{papaparse.min-BU0o7jXn.js → papaparse.min-BRXtjEI6.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-B7QSyG-3.js → pegDiagram-2B236MQR-BkhQqqTm.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-XtSGL7GG.js → pieDiagram-ENE6RG2P-CcMnvxUz.js} +1 -1
- package/web/dist/assets/pilotswarm-DYXtzuUN.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-LZoYSbTH.js → quadrantDiagram-ABIIQ3AL-_KD1QeEr.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-B5FREx2U.js → railroadDiagram-RFXS5EU6-CqI0olIe.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-1VRFkBQX.js → requirementDiagram-TGXJPOKE-1JxRwusF.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-J456gWvu.js → sankeyDiagram-HTMAVEWB-9IpFoKzi.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CaCcw6KO.js → sequenceDiagram-DBY2YBRQ-teyObSQ9.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-DClsP3_O.js → sizeCapture-X5ZJPWSS-DVhU2J_6.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-D6VyrKhd.js → stateDiagram-2N3HPSRC-DHPoWpIQ.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DJjZWYLS.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-q678iAuo.js → swimlanes-5IMT3BWC-BxTtDQoB.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CV12vDcK.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-WnPK_zhe.js → timeline-definition-FHXFAJF6-D0wXZx-4.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-WpumuLld.js → vennDiagram-L72KCM5P-BSFyI3iV.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CBrYLUm_.js → wardleyDiagram-EHGQE667-BN-S7hh0.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DbX1rnDW.js → xychartDiagram-FW5EYKEG-nDNuPs7o.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +36 -4
- package/ui/core/src/themes/daylight.js +0 -62
- package/ui/core/src/themes/light-high-contrast.js +0 -65
- package/ui/core/src/themes/paper-ink.js +0 -62
- package/ui/core/src/themes/solarized-ops.js +0 -59
- package/ui/core/src/themes/tokyo-night.js +0 -56
- package/web/dist/assets/channel-D7p-E4El.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-yFZ-1yMb.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-yFZ-1yMb.js +0 -1
- package/web/dist/assets/index-BGWTu1KK.css +0 -1
- package/web/dist/assets/index-Djfdr7ts.js +0 -24
- package/web/dist/assets/pilotswarm-CtO819QH.js +0 -94
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-D7eKFhHb.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DlVHQBQx.js +0 -8
package/ui/core/src/reducer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildSessionTree } from "./session-tree.js";
|
|
1
|
+
import { buildSessionTree, isManuallyOrderableSession } from "./session-tree.js";
|
|
2
2
|
import { FOCUS_REGIONS } from "./commands.js";
|
|
3
3
|
import { DEFAULT_HISTORY_EVENT_LIMIT, dedupeChatMessages } from "./history.js";
|
|
4
4
|
import { getPromptInputRows } from "./layout.js";
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
normalizeStoredCollapsedSessionIds,
|
|
12
12
|
normalizeStoredLayoutAdjustments,
|
|
13
13
|
normalizeStoredPinnedSessionIds,
|
|
14
|
+
normalizeStoredSessionOrder,
|
|
15
|
+
createInitialState,
|
|
14
16
|
} from "./state.js";
|
|
15
17
|
|
|
16
18
|
function cloneHistoryMap(historyMap) {
|
|
@@ -55,29 +57,43 @@ function prunePinnedIds(ids, byId) {
|
|
|
55
57
|
return pruneIdList(ids, byId).filter((sessionId) => isPinnableSession(byId[sessionId]));
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
function collectCollapsibleSessionIds(sessions = []) {
|
|
59
|
-
const byId = new Set((sessions || []).map((session) => session?.sessionId).filter(Boolean));
|
|
60
|
-
const collapsibleIds = new Set();
|
|
61
|
-
for (const session of sessions || []) {
|
|
62
|
-
if (!session?.sessionId) continue;
|
|
63
|
-
if (session.isGroup) collapsibleIds.add(session.sessionId);
|
|
64
|
-
const parentSessionId = session.parentSessionId;
|
|
65
|
-
if (parentSessionId && byId.has(parentSessionId)) {
|
|
66
|
-
collapsibleIds.add(parentSessionId);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return collapsibleIds;
|
|
70
|
-
}
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Keep a collapse flag unless the session is genuinely gone.
|
|
63
|
+
*
|
|
64
|
+
* "Collapsible" means the row is a folder OR has at least one LOADED child.
|
|
65
|
+
* Sub-agent children arrive lazily, so a parent is routinely not collapsible
|
|
66
|
+
* yet at the moment the profile is applied — dropping its flag there is why
|
|
67
|
+
* parent/sub-agent expansion never survived a reload, while folders (whose
|
|
68
|
+
* rows load with the listing) mostly did.
|
|
69
|
+
*
|
|
70
|
+
* So: a row we cannot currently see keeps its state. Not-loaded, filtered
|
|
71
|
+
* out, on another page, or simply slower to arrive are all indistinguishable
|
|
72
|
+
* from here, and none of them is the user changing their mind. The flag is a
|
|
73
|
+
* preference about a row, not a fact about the current listing.
|
|
74
|
+
*
|
|
75
|
+
* Nothing prunes on deletion either, and deliberately: there is no signal
|
|
76
|
+
* that means "deleted" as opposed to "not in this listing". A row missing
|
|
77
|
+
* from sessions/loaded may have been filtered, paged out, or simply not
|
|
78
|
+
* arrived yet, and treating that as deletion is precisely the bug above. A
|
|
79
|
+
* stale id costs one string and orders nothing; a wrongly dropped one costs
|
|
80
|
+
* the user's layout. If a session really is gone its id is inert, and if it
|
|
81
|
+
* comes back it returns exactly as the user left it.
|
|
82
|
+
*
|
|
83
|
+
* What protects you when something vanishes UNDER you is separate and
|
|
84
|
+
* already here: reconcileCollapsedIdsForActiveSession force-expands every
|
|
85
|
+
* ancestor of the active session, so the row you are actually looking at can
|
|
86
|
+
* never end up hidden inside a collapsed parent.
|
|
87
|
+
*/
|
|
88
|
+
function pruneCollapsedIds(collapsedIds) {
|
|
89
|
+
// Keeps everything. "Loaded but not collapsible yet" is the common case
|
|
90
|
+
// for a parent whose children have not arrived, so even that cannot be
|
|
91
|
+
// used to drop a flag — which is what made this function's original
|
|
92
|
+
// filter lose parent/sub-agent expansion on every reload.
|
|
93
|
+
return cloneCollapsedIds(collapsedIds);
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
|
|
81
97
|
function collectSessionAncestorIds(sessionId, byId) {
|
|
82
98
|
const ancestors = [];
|
|
83
99
|
const seen = new Set([sessionId]);
|
|
@@ -402,6 +418,14 @@ function resolveVisibleActiveSessionId(state, fallbackSessions = []) {
|
|
|
402
418
|
if (currentSessionId && state.sessions?.byId?.[currentSessionId] && !hasSessionVisibilityFilter(state.sessions)) {
|
|
403
419
|
return currentSessionId;
|
|
404
420
|
}
|
|
421
|
+
// A selection restored from the profile names a session the listing has
|
|
422
|
+
// not delivered yet — the folder listing routinely lands first, so the
|
|
423
|
+
// restored id was judged "not visible" and replaced by the default, which
|
|
424
|
+
// then got saved over it. Hold it until a sessions listing has actually
|
|
425
|
+
// arrived; only then is absence evidence the session is gone.
|
|
426
|
+
if (currentSessionId && !state.sessions?.listingSeen) {
|
|
427
|
+
return currentSessionId;
|
|
428
|
+
}
|
|
405
429
|
if (hasSessionVisibilityFilter(state.sessions)) {
|
|
406
430
|
return visibleRows[0]?.sessionId || null;
|
|
407
431
|
}
|
|
@@ -465,7 +489,7 @@ function applyVisibleSessionSelection(state, nextSessions) {
|
|
|
465
489
|
sessions = {
|
|
466
490
|
...sessions,
|
|
467
491
|
collapsedIds: expandedCollapsedIds,
|
|
468
|
-
flat: buildSessionTree(Object.values(sessions.byId || {}), expandedCollapsedIds, sessions.orderById, sessions.pinnedIds),
|
|
492
|
+
flat: buildSessionTree(Object.values(sessions.byId || {}), expandedCollapsedIds, sessions.orderById, sessions.pinnedIds, sessions.manualOrder),
|
|
469
493
|
};
|
|
470
494
|
}
|
|
471
495
|
}
|
|
@@ -494,7 +518,7 @@ function applyVisibleSessionSelection(state, nextSessions) {
|
|
|
494
518
|
? {
|
|
495
519
|
...sessions,
|
|
496
520
|
collapsedIds: nextCollapsedIds,
|
|
497
|
-
flat: buildSessionTree(Object.values(sessions.byId || {}), nextCollapsedIds, sessions.orderById, sessions.pinnedIds),
|
|
521
|
+
flat: buildSessionTree(Object.values(sessions.byId || {}), nextCollapsedIds, sessions.orderById, sessions.pinnedIds, sessions.manualOrder),
|
|
498
522
|
}
|
|
499
523
|
: sessions;
|
|
500
524
|
return {
|
|
@@ -636,11 +660,13 @@ export function appReducer(state, action) {
|
|
|
636
660
|
&& settings.themeId.trim();
|
|
637
661
|
const hasOwnerFilter = Object.prototype.hasOwnProperty.call(settings, "sessionOwnerFilter");
|
|
638
662
|
const hasLayout = Object.prototype.hasOwnProperty.call(settings, "layoutAdjustments");
|
|
663
|
+
// "rich" retired as a view mode (now theme.richChat) — stored
|
|
664
|
+
// profiles that still say "rich" simply keep the transcript.
|
|
639
665
|
const hasChatViewMode = Object.prototype.hasOwnProperty.call(settings, "chatViewMode")
|
|
640
|
-
&& (settings.chatViewMode === "summary" || settings.chatViewMode === "transcript"
|
|
641
|
-
|| settings.chatViewMode === "rich");
|
|
666
|
+
&& (settings.chatViewMode === "summary" || settings.chatViewMode === "transcript");
|
|
642
667
|
const hasPins = Object.prototype.hasOwnProperty.call(settings, "pinnedSessionIds");
|
|
643
668
|
const hasCollapsed = Object.prototype.hasOwnProperty.call(settings, "collapsedSessionIds");
|
|
669
|
+
const hasSessionOrder = Object.prototype.hasOwnProperty.call(settings, "sessionOrder");
|
|
644
670
|
// A pending/resolved deep-link intent outranks the profile's
|
|
645
671
|
// persisted activeSessionId — a remote profile poll must not
|
|
646
672
|
// yank selection away from the linked session.
|
|
@@ -667,6 +693,9 @@ export function appReducer(state, action) {
|
|
|
667
693
|
? pruneCollapsedIds(normalizeStoredCollapsedSessionIds(settings.collapsedSessionIds), state.sessions.byId)
|
|
668
694
|
: normalizeStoredCollapsedSessionIds(settings.collapsedSessionIds))
|
|
669
695
|
: state.sessions.collapsedIds;
|
|
696
|
+
const nextManualOrder = hasSessionOrder
|
|
697
|
+
? normalizeStoredSessionOrder(settings.sessionOrder)
|
|
698
|
+
: state.sessions.manualOrder;
|
|
670
699
|
const nextActiveSessionId = hasActive
|
|
671
700
|
? normalizeStoredActiveSessionId(settings.activeSessionId)
|
|
672
701
|
: state.sessions.activeSessionId;
|
|
@@ -679,11 +708,12 @@ export function appReducer(state, action) {
|
|
|
679
708
|
}
|
|
680
709
|
: {}),
|
|
681
710
|
pinnedIds: nextPinnedIds,
|
|
711
|
+
manualOrder: nextManualOrder,
|
|
682
712
|
collapsedIds: nextCollapsedIds,
|
|
683
713
|
collapsedIdsExplicit: hasCollapsed ? true : state.sessions.collapsedIdsExplicit,
|
|
684
714
|
activeSessionId: nextActiveSessionId,
|
|
685
|
-
flat: (hasPins || hasCollapsed)
|
|
686
|
-
? buildSessionTree(Object.values(state.sessions.byId), nextCollapsedIds, state.sessions.orderById, nextPinnedIds)
|
|
715
|
+
flat: (hasPins || hasCollapsed || hasSessionOrder)
|
|
716
|
+
? buildSessionTree(Object.values(state.sessions.byId), nextCollapsedIds, state.sessions.orderById, nextPinnedIds, nextManualOrder)
|
|
687
717
|
: state.sessions.flat,
|
|
688
718
|
};
|
|
689
719
|
const selection = hasLoadedSessions && (hasOwnerFilter || hasPins || hasCollapsed || hasActive)
|
|
@@ -702,7 +732,7 @@ export function appReducer(state, action) {
|
|
|
702
732
|
}
|
|
703
733
|
|
|
704
734
|
case "ui/chatViewMode":
|
|
705
|
-
if (action.mode !== "summary" && action.mode !== "transcript"
|
|
735
|
+
if (action.mode !== "summary" && action.mode !== "transcript") {
|
|
706
736
|
return state;
|
|
707
737
|
}
|
|
708
738
|
return {
|
|
@@ -954,6 +984,12 @@ export function appReducer(state, action) {
|
|
|
954
984
|
};
|
|
955
985
|
}
|
|
956
986
|
|
|
987
|
+
case "ui/nodeMapSelect": {
|
|
988
|
+
// Set, never toggle: a node is always selected while the Node Map
|
|
989
|
+
// is up, and the remembered choice survives leaving the tab.
|
|
990
|
+
if (!action.label) return state;
|
|
991
|
+
return { ...state, ui: { ...state.ui, nodeMapSelectedNode: String(action.label) } };
|
|
992
|
+
}
|
|
957
993
|
case "ui/inspectorTab":
|
|
958
994
|
return {
|
|
959
995
|
...state,
|
|
@@ -1058,6 +1094,14 @@ export function appReducer(state, action) {
|
|
|
1058
1094
|
const byId = {};
|
|
1059
1095
|
let anyChanged = false;
|
|
1060
1096
|
const nowMs = Date.now();
|
|
1097
|
+
// Folders first: this case rebuilds byId from the payload, so a
|
|
1098
|
+
// session refresh that carries no groups would otherwise delete
|
|
1099
|
+
// every folder until the next group fetch — the "flickers in and
|
|
1100
|
+
// out" bug. They are re-seeded from their own slice and can only
|
|
1101
|
+
// be removed by sessions/groupsLoaded.
|
|
1102
|
+
for (const groupRow of (state.sessions.groupRows || [])) {
|
|
1103
|
+
byId[groupRow.sessionId] = state.sessions.byId[groupRow.sessionId] || groupRow;
|
|
1104
|
+
}
|
|
1061
1105
|
for (const session of action.sessions) {
|
|
1062
1106
|
const previous = state.sessions.byId[session.sessionId];
|
|
1063
1107
|
const merged = mergeSessionRowVisualStatus(
|
|
@@ -1106,7 +1150,21 @@ export function appReducer(state, action) {
|
|
|
1106
1150
|
const collapsedIds = previousCollapsedIdsExplicit
|
|
1107
1151
|
? cloneCollapsedIds(state.sessions.collapsedIds)
|
|
1108
1152
|
: initialCollapsedIds;
|
|
1109
|
-
|
|
1153
|
+
// A row that ARRIVES while the app is open starts collapsed, even
|
|
1154
|
+
// for a user with explicit preferences — a new sub-tree should not
|
|
1155
|
+
// spill open on its own.
|
|
1156
|
+
//
|
|
1157
|
+
// "Arrives" is only meaningful against a baseline. On the FIRST
|
|
1158
|
+
// listing after a page load there is none (byId is empty), so
|
|
1159
|
+
// every row looked new and this force-collapsed the whole tree
|
|
1160
|
+
// over the profile that had just been restored. The save effect
|
|
1161
|
+
// then persisted that, and the next load read its own corruption
|
|
1162
|
+
// back as the user's preference — a one-way ratchet, which is why
|
|
1163
|
+
// one refresh looked fine and the one after it collapsed
|
|
1164
|
+
// everything. With no baseline we cannot tell new from
|
|
1165
|
+
// pre-existing, so we add nothing and trust the restored state.
|
|
1166
|
+
const hadBaseline = state.sessions.listingSeen === true;
|
|
1167
|
+
if (previousCollapsedIdsExplicit && hadBaseline) {
|
|
1110
1168
|
const previousDefaultCollapsedIds = collectDefaultCollapsedSessionIds(Object.values(state.sessions.byId));
|
|
1111
1169
|
for (const sessionId of initialCollapsedIds) {
|
|
1112
1170
|
if (!previousDefaultCollapsedIds.has(sessionId)) {
|
|
@@ -1119,12 +1177,13 @@ export function appReducer(state, action) {
|
|
|
1119
1177
|
// sessions). Sessions inside containers lose their pins.
|
|
1120
1178
|
const survivingPins = prunePinnedIds(state.sessions.pinnedIds, byId);
|
|
1121
1179
|
const survivingSelected = pruneIdList(state.sessions.selectedIds, byId);
|
|
1122
|
-
const flat = buildSessionTree(mergedSessions, collapsedIds, orderById, survivingPins);
|
|
1180
|
+
const flat = buildSessionTree(mergedSessions, collapsedIds, orderById, survivingPins, state.sessions.manualOrder);
|
|
1123
1181
|
const nextSessions = {
|
|
1124
1182
|
...state.sessions,
|
|
1125
1183
|
byId,
|
|
1126
1184
|
collapsedIds,
|
|
1127
1185
|
collapsedIdsExplicit: previousCollapsedIdsExplicit,
|
|
1186
|
+
listingSeen: true,
|
|
1128
1187
|
pinnedIds: survivingPins,
|
|
1129
1188
|
selectedIds: survivingSelected,
|
|
1130
1189
|
selectMode: state.sessions.selectMode && survivingSelected.length > 0,
|
|
@@ -1141,6 +1200,87 @@ export function appReducer(state, action) {
|
|
|
1141
1200
|
};
|
|
1142
1201
|
}
|
|
1143
1202
|
|
|
1203
|
+
case "sessions/groupsLoaded": {
|
|
1204
|
+
// The ONLY writer of folder rows. A successful fetch is the whole
|
|
1205
|
+
// truth: folders it omits are gone, folders it names are kept.
|
|
1206
|
+
const groups = Array.isArray(action.groups) ? action.groups : [];
|
|
1207
|
+
const byId = { ...state.sessions.byId };
|
|
1208
|
+
// A folder the fetch omits is gone — UNLESS a loaded session still
|
|
1209
|
+
// claims membership in it. Deleting one that is still referenced
|
|
1210
|
+
// was visibly destructive: the titled row was replaced by a
|
|
1211
|
+
// generic stand-in ("Group"), and because collapse state is keyed
|
|
1212
|
+
// by row id, the pruned entry took the group's COLLAPSED state
|
|
1213
|
+
// with it, so the whole group sprang open and dumped its members
|
|
1214
|
+
// into the list. One transient empty-but-successful listing did
|
|
1215
|
+
// all of that. Keep the row we already have; the stand-in stays
|
|
1216
|
+
// the last resort for a folder we have never seen.
|
|
1217
|
+
//
|
|
1218
|
+
// Judge that against the INCOMING catalog when the refresh sends
|
|
1219
|
+
// it: this action lands before sessions/loaded, so state still
|
|
1220
|
+
// holds the PREVIOUS membership, and a folder whose last member
|
|
1221
|
+
// had just moved out looked claimed by its own stale members and
|
|
1222
|
+
// survived its deletion forever.
|
|
1223
|
+
const claimSource = Array.isArray(action.sessions)
|
|
1224
|
+
? action.sessions
|
|
1225
|
+
: Object.values(state.sessions.byId);
|
|
1226
|
+
const claimedByLoadedSessions = new Set(
|
|
1227
|
+
claimSource
|
|
1228
|
+
.filter((session) => session && !session.isGroup && session.groupId && !session.parentSessionId)
|
|
1229
|
+
.map((session) => `group:${session.groupId}`),
|
|
1230
|
+
);
|
|
1231
|
+
const keptRows = [];
|
|
1232
|
+
for (const key of Object.keys(byId)) {
|
|
1233
|
+
if (!byId[key]?.isGroup) continue;
|
|
1234
|
+
if (groups.some((row) => row.sessionId === key)) continue;
|
|
1235
|
+
if (claimedByLoadedSessions.has(key)) { keptRows.push(byId[key]); continue; }
|
|
1236
|
+
delete byId[key];
|
|
1237
|
+
}
|
|
1238
|
+
for (const row of groups) {
|
|
1239
|
+
byId[row.sessionId] = { ...(byId[row.sessionId] || {}), ...row };
|
|
1240
|
+
}
|
|
1241
|
+
const survivingPins = prunePinnedIds(state.sessions.pinnedIds, byId);
|
|
1242
|
+
const { orderById, nextOrderOrdinal } = assignStableSessionOrder(
|
|
1243
|
+
state.sessions.orderById,
|
|
1244
|
+
state.sessions.nextOrderOrdinal,
|
|
1245
|
+
Object.values(byId),
|
|
1246
|
+
);
|
|
1247
|
+
// A folder the state has not seen before starts COLLAPSED, exactly
|
|
1248
|
+
// as it does on first load — otherwise a folder that round-trips
|
|
1249
|
+
// (deleted by one listing, restored by the next) comes back open
|
|
1250
|
+
// and spills its members into the list.
|
|
1251
|
+
//
|
|
1252
|
+
// "Not seen before" needs something to have been seen. On a fresh
|
|
1253
|
+
// page load byId is empty, so every folder qualified and this
|
|
1254
|
+
// re-collapsed everything the user had left open, on top of the
|
|
1255
|
+
// profile that had just been restored. Same baseline rule as
|
|
1256
|
+
// sessions/loaded: with nothing to compare against, add nothing.
|
|
1257
|
+
//
|
|
1258
|
+
// (An earlier attempt keyed this off collapsedIdsExplicit, which
|
|
1259
|
+
// fixed the reload but also stopped a genuinely new folder —
|
|
1260
|
+
// created while the app is open — from starting collapsed. The
|
|
1261
|
+
// baseline test gets both right.)
|
|
1262
|
+
const hadGroupBaseline = state.sessions.groupsListingSeen === true;
|
|
1263
|
+
const nextCollapsedIds = cloneCollapsedIds(state.sessions.collapsedIds);
|
|
1264
|
+
for (const row of groups) {
|
|
1265
|
+
if (!hadGroupBaseline) continue;
|
|
1266
|
+
if (!state.sessions.byId[row.sessionId]) nextCollapsedIds.add(row.sessionId);
|
|
1267
|
+
}
|
|
1268
|
+
const nextSessions = {
|
|
1269
|
+
...state.sessions,
|
|
1270
|
+
// sessions/loaded re-seeds folders from this slice, so a kept
|
|
1271
|
+
// row has to live here too or the next session refresh drops it.
|
|
1272
|
+
groupRows: [...groups, ...keptRows],
|
|
1273
|
+
byId,
|
|
1274
|
+
pinnedIds: survivingPins,
|
|
1275
|
+
orderById,
|
|
1276
|
+
nextOrderOrdinal,
|
|
1277
|
+
collapsedIds: nextCollapsedIds,
|
|
1278
|
+
groupsListingSeen: true,
|
|
1279
|
+
flat: buildSessionTree(Object.values(byId), nextCollapsedIds, orderById, survivingPins, state.sessions.manualOrder),
|
|
1280
|
+
};
|
|
1281
|
+
const groupSelection = applyVisibleSessionSelection(state, nextSessions);
|
|
1282
|
+
return { ...state, sessions: groupSelection.sessions, ui: groupSelection.ui };
|
|
1283
|
+
}
|
|
1144
1284
|
case "sessions/merged": {
|
|
1145
1285
|
if (!action.session?.sessionId) return state;
|
|
1146
1286
|
const previousSession = state.sessions.byId[action.session.sessionId];
|
|
@@ -1167,7 +1307,7 @@ export function appReducer(state, action) {
|
|
|
1167
1307
|
...state.sessions,
|
|
1168
1308
|
byId,
|
|
1169
1309
|
pinnedIds: survivingPins,
|
|
1170
|
-
flat: buildSessionTree(Object.values(byId), state.sessions.collapsedIds, orderById, survivingPins),
|
|
1310
|
+
flat: buildSessionTree(Object.values(byId), state.sessions.collapsedIds, orderById, survivingPins, state.sessions.manualOrder),
|
|
1171
1311
|
activeSessionId: state.sessions.activeSessionId,
|
|
1172
1312
|
orderById,
|
|
1173
1313
|
nextOrderOrdinal,
|
|
@@ -1181,6 +1321,7 @@ export function appReducer(state, action) {
|
|
|
1181
1321
|
}
|
|
1182
1322
|
|
|
1183
1323
|
case "sessions/selected": {
|
|
1324
|
+
state = { ...state, sessions: { ...state.sessions, listDeselected: false } };
|
|
1184
1325
|
// Per-session chat scroll memory: stash the outgoing session's
|
|
1185
1326
|
// offset and restore the incoming one's. If new chat arrives on
|
|
1186
1327
|
// re-entry, history/set's activeChatUpdated reset still snaps to
|
|
@@ -1254,7 +1395,7 @@ export function appReducer(state, action) {
|
|
|
1254
1395
|
...state.sessions,
|
|
1255
1396
|
collapsedIds,
|
|
1256
1397
|
collapsedIdsExplicit: true,
|
|
1257
|
-
flat: buildSessionTree(Object.values(state.sessions.byId), collapsedIds, state.sessions.orderById, state.sessions.pinnedIds),
|
|
1398
|
+
flat: buildSessionTree(Object.values(state.sessions.byId), collapsedIds, state.sessions.orderById, state.sessions.pinnedIds, state.sessions.manualOrder),
|
|
1258
1399
|
},
|
|
1259
1400
|
};
|
|
1260
1401
|
}
|
|
@@ -1268,7 +1409,7 @@ export function appReducer(state, action) {
|
|
|
1268
1409
|
...state.sessions,
|
|
1269
1410
|
collapsedIds,
|
|
1270
1411
|
collapsedIdsExplicit: true,
|
|
1271
|
-
flat: buildSessionTree(Object.values(state.sessions.byId), collapsedIds, state.sessions.orderById, state.sessions.pinnedIds),
|
|
1412
|
+
flat: buildSessionTree(Object.values(state.sessions.byId), collapsedIds, state.sessions.orderById, state.sessions.pinnedIds, state.sessions.manualOrder),
|
|
1272
1413
|
},
|
|
1273
1414
|
};
|
|
1274
1415
|
}
|
|
@@ -1290,7 +1431,64 @@ export function appReducer(state, action) {
|
|
|
1290
1431
|
sessions: {
|
|
1291
1432
|
...state.sessions,
|
|
1292
1433
|
pinnedIds: nextPinned,
|
|
1293
|
-
flat: buildSessionTree(Object.values(state.sessions.byId), state.sessions.collapsedIds, state.sessions.orderById, nextPinned),
|
|
1434
|
+
flat: buildSessionTree(Object.values(state.sessions.byId), state.sessions.collapsedIds, state.sessions.orderById, nextPinned, state.sessions.manualOrder),
|
|
1435
|
+
},
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// Drop `sessionId` immediately before `beforeSessionId` in the user's
|
|
1440
|
+
// explicit order (beforeSessionId null ⇒ last among its siblings).
|
|
1441
|
+
//
|
|
1442
|
+
// The stored list spans every level; ordering only ever compares
|
|
1443
|
+
// siblings, so writing a global list is safe and keeps one array to
|
|
1444
|
+
// persist. The reducer is deliberately permissive about ids it does
|
|
1445
|
+
// not recognise — the desktop is the only writer, but a phone or TUI
|
|
1446
|
+
// may hold a narrower slice of the fleet, and dropping unknown ids
|
|
1447
|
+
// here would let the smaller surface erase the larger one's placements.
|
|
1448
|
+
case "sessions/reorder": {
|
|
1449
|
+
const sessionId = String(action.sessionId || "").trim();
|
|
1450
|
+
if (!sessionId) return state;
|
|
1451
|
+
const session = state.sessions.byId[sessionId];
|
|
1452
|
+
// The tree ignores placements for immovable rows; refusing here
|
|
1453
|
+
// too keeps the stored list free of entries that can never apply.
|
|
1454
|
+
if (!isManuallyOrderableSession(session)) return state;
|
|
1455
|
+
|
|
1456
|
+
const beforeId = action.beforeSessionId == null
|
|
1457
|
+
? null
|
|
1458
|
+
: String(action.beforeSessionId).trim() || null;
|
|
1459
|
+
if (beforeId === sessionId) return state;
|
|
1460
|
+
|
|
1461
|
+
// Seed from the CURRENT rendered order so a first drag preserves
|
|
1462
|
+
// what the user is looking at. Without this, moving one row would
|
|
1463
|
+
// leave every other row unplaced and re-sorted underneath it.
|
|
1464
|
+
const seed = Array.isArray(state.sessions.manualOrder) && state.sessions.manualOrder.length > 0
|
|
1465
|
+
? state.sessions.manualOrder
|
|
1466
|
+
: (state.sessions.flat || [])
|
|
1467
|
+
.map((entry) => entry.sessionId)
|
|
1468
|
+
.filter((id) => isManuallyOrderableSession(state.sessions.byId[id]));
|
|
1469
|
+
|
|
1470
|
+
const without = seed.filter((id) => id !== sessionId);
|
|
1471
|
+
const insertAt = beforeId ? without.indexOf(beforeId) : -1;
|
|
1472
|
+
const nextManualOrder = insertAt >= 0
|
|
1473
|
+
? [...without.slice(0, insertAt), sessionId, ...without.slice(insertAt)]
|
|
1474
|
+
: [...without, sessionId];
|
|
1475
|
+
|
|
1476
|
+
const unchanged = nextManualOrder.length === (state.sessions.manualOrder || []).length
|
|
1477
|
+
&& nextManualOrder.every((id, index) => id === state.sessions.manualOrder[index]);
|
|
1478
|
+
if (unchanged) return state;
|
|
1479
|
+
|
|
1480
|
+
return {
|
|
1481
|
+
...state,
|
|
1482
|
+
sessions: {
|
|
1483
|
+
...state.sessions,
|
|
1484
|
+
manualOrder: nextManualOrder,
|
|
1485
|
+
flat: buildSessionTree(
|
|
1486
|
+
Object.values(state.sessions.byId),
|
|
1487
|
+
state.sessions.collapsedIds,
|
|
1488
|
+
state.sessions.orderById,
|
|
1489
|
+
state.sessions.pinnedIds,
|
|
1490
|
+
nextManualOrder,
|
|
1491
|
+
),
|
|
1294
1492
|
},
|
|
1295
1493
|
};
|
|
1296
1494
|
}
|
|
@@ -1348,6 +1546,23 @@ export function appReducer(state, action) {
|
|
|
1348
1546
|
};
|
|
1349
1547
|
}
|
|
1350
1548
|
|
|
1549
|
+
// Re-arm the list highlight WITHOUT re-running session selection. A
|
|
1550
|
+
// plain click on the still-active row after an empty-space deselect
|
|
1551
|
+
// cannot go through loadSession (it short-circuits on the active id),
|
|
1552
|
+
// so without this the row could never be selected again.
|
|
1553
|
+
case "sessions/listReselect": {
|
|
1554
|
+
if (!state.sessions.listDeselected) return state;
|
|
1555
|
+
return { ...state, sessions: { ...state.sessions, listDeselected: false } };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
case "sessions/listDeselect": {
|
|
1559
|
+
// Purely a list affordance: activeSessionId is untouched so the
|
|
1560
|
+
// chat, inspector and activity panes carry on unchanged.
|
|
1561
|
+
return {
|
|
1562
|
+
...state,
|
|
1563
|
+
sessions: { ...state.sessions, listDeselected: true, selectedIds: [], selectMode: false },
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1351
1566
|
case "sessions/selectClear": {
|
|
1352
1567
|
if (state.sessions.selectedIds.length === 0 && !state.sessions.selectMode) return state;
|
|
1353
1568
|
return {
|
|
@@ -1844,6 +2059,178 @@ export function appReducer(state, action) {
|
|
|
1844
2059
|
},
|
|
1845
2060
|
};
|
|
1846
2061
|
}
|
|
2062
|
+
case "admin/section": {
|
|
2063
|
+
const section = ["packages", "workers", "ghcp"].includes(action.section) ? action.section : "ghcp";
|
|
2064
|
+
return { ...state, admin: { ...state.admin, section } };
|
|
2065
|
+
}
|
|
2066
|
+
case "admin/workers/attempt": {
|
|
2067
|
+
const workers = state.admin.workers || {};
|
|
2068
|
+
return { ...state, admin: { ...state.admin, workers: { ...workers, attempts: (workers.attempts || 0) + 1, lastAttemptAt: Date.now(), lastSkip: action.skip || null } } };
|
|
2069
|
+
}
|
|
2070
|
+
case "admin/workers/loading": {
|
|
2071
|
+
return { ...state, admin: { ...state.admin, workers: { ...state.admin.workers, loading: true, error: null } } };
|
|
2072
|
+
}
|
|
2073
|
+
case "admin/workers/loaded": {
|
|
2074
|
+
return {
|
|
2075
|
+
...state,
|
|
2076
|
+
admin: {
|
|
2077
|
+
...state.admin,
|
|
2078
|
+
workers: {
|
|
2079
|
+
loading: false,
|
|
2080
|
+
error: null,
|
|
2081
|
+
list: Array.isArray(action.list) ? action.list : [],
|
|
2082
|
+
fetchedAt: Date.now(),
|
|
2083
|
+
},
|
|
2084
|
+
},
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
case "admin/workers/loadFailed": {
|
|
2088
|
+
return { ...state, admin: { ...state.admin, workers: { ...state.admin.workers, loading: false, error: action.error || "Failed to load workers" } } };
|
|
2089
|
+
}
|
|
2090
|
+
case "admin/packages/loading": {
|
|
2091
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, loading: true, error: null } } };
|
|
2092
|
+
}
|
|
2093
|
+
case "admin/packages/loaded": {
|
|
2094
|
+
const previous = state.admin.packages;
|
|
2095
|
+
const list = Array.isArray(action.list) ? action.list : [];
|
|
2096
|
+
// Keep the selection when the package still exists; otherwise clear
|
|
2097
|
+
// the dependent detail/workspace state with it.
|
|
2098
|
+
const stillThere = previous.selectedName && list.some((p) => p.name === previous.selectedName);
|
|
2099
|
+
return {
|
|
2100
|
+
...state,
|
|
2101
|
+
admin: {
|
|
2102
|
+
...state.admin,
|
|
2103
|
+
packages: {
|
|
2104
|
+
...previous,
|
|
2105
|
+
loading: false,
|
|
2106
|
+
error: null,
|
|
2107
|
+
list,
|
|
2108
|
+
workerState: Array.isArray(action.workerState) ? action.workerState : [],
|
|
2109
|
+
fetchedAt: Date.now(),
|
|
2110
|
+
selectedName: stillThere ? previous.selectedName : null,
|
|
2111
|
+
...(stillThere ? {} : { detail: null, workspace: { ...previous.workspace, tree: null, selectedPath: null, file: null } }),
|
|
2112
|
+
},
|
|
2113
|
+
},
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
case "admin/packages/loadFailed": {
|
|
2117
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, loading: false, error: action.error || "Failed to load agent packages" } } };
|
|
2118
|
+
}
|
|
2119
|
+
case "admin/packages/select": {
|
|
2120
|
+
return {
|
|
2121
|
+
...state,
|
|
2122
|
+
admin: {
|
|
2123
|
+
...state.admin,
|
|
2124
|
+
section: "packages",
|
|
2125
|
+
packages: {
|
|
2126
|
+
...state.admin.packages,
|
|
2127
|
+
selectedName: action.name || null,
|
|
2128
|
+
detail: null,
|
|
2129
|
+
detailLoading: Boolean(action.name),
|
|
2130
|
+
detailError: null,
|
|
2131
|
+
action: { pending: null, error: null },
|
|
2132
|
+
workspace: {
|
|
2133
|
+
tree: null, treeLoading: Boolean(action.name), treeError: null,
|
|
2134
|
+
expandedDirs: [], selectedPath: null,
|
|
2135
|
+
file: null, fileLoading: false, fileError: null,
|
|
2136
|
+
},
|
|
2137
|
+
},
|
|
2138
|
+
},
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
case "admin/packages/detail/loaded": {
|
|
2142
|
+
if (state.admin.packages.selectedName !== action.name) return state;
|
|
2143
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, detail: action.detail, detailLoading: false, detailError: null } } };
|
|
2144
|
+
}
|
|
2145
|
+
case "admin/packages/detail/loadFailed": {
|
|
2146
|
+
if (state.admin.packages.selectedName !== action.name) return state;
|
|
2147
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, detail: null, detailLoading: false, detailError: action.error || "Failed to load package" } } };
|
|
2148
|
+
}
|
|
2149
|
+
case "admin/packages/tree/loaded": {
|
|
2150
|
+
if (state.admin.packages.selectedName !== action.name) return state;
|
|
2151
|
+
const topDirs = Array.isArray(action.tree?.dirs)
|
|
2152
|
+
? action.tree.dirs.filter((dir) => !dir.includes("/"))
|
|
2153
|
+
: [];
|
|
2154
|
+
return {
|
|
2155
|
+
...state,
|
|
2156
|
+
admin: {
|
|
2157
|
+
...state.admin,
|
|
2158
|
+
packages: {
|
|
2159
|
+
...state.admin.packages,
|
|
2160
|
+
workspace: {
|
|
2161
|
+
...state.admin.packages.workspace,
|
|
2162
|
+
tree: action.tree,
|
|
2163
|
+
treeLoading: false,
|
|
2164
|
+
treeError: null,
|
|
2165
|
+
// Top-level folders start expanded; deeper levels open on click.
|
|
2166
|
+
expandedDirs: topDirs,
|
|
2167
|
+
},
|
|
2168
|
+
},
|
|
2169
|
+
},
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
case "admin/packages/tree/loadFailed": {
|
|
2173
|
+
if (state.admin.packages.selectedName !== action.name) return state;
|
|
2174
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...state.admin.packages.workspace, treeLoading: false, treeError: action.error || "Failed to load package files" } } } };
|
|
2175
|
+
}
|
|
2176
|
+
case "admin/packages/toggleDir": {
|
|
2177
|
+
const workspace = state.admin.packages.workspace;
|
|
2178
|
+
const expanded = new Set(workspace.expandedDirs);
|
|
2179
|
+
if (expanded.has(action.dir)) expanded.delete(action.dir);
|
|
2180
|
+
else expanded.add(action.dir);
|
|
2181
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...workspace, expandedDirs: [...expanded] } } } };
|
|
2182
|
+
}
|
|
2183
|
+
case "admin/packages/file/loading": {
|
|
2184
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...state.admin.packages.workspace, selectedPath: action.path, file: null, fileLoading: true, fileError: null } } } };
|
|
2185
|
+
}
|
|
2186
|
+
case "admin/packages/file/loaded": {
|
|
2187
|
+
if (state.admin.packages.workspace.selectedPath !== action.file?.path) return state;
|
|
2188
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...state.admin.packages.workspace, file: action.file, fileLoading: false, fileError: null } } } };
|
|
2189
|
+
}
|
|
2190
|
+
case "admin/packages/file/loadFailed": {
|
|
2191
|
+
if (state.admin.packages.workspace.selectedPath !== action.path) return state;
|
|
2192
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, workspace: { ...state.admin.packages.workspace, fileLoading: false, fileError: action.error || "Failed to load file" } } } };
|
|
2193
|
+
}
|
|
2194
|
+
case "admin/packages/action/pending": {
|
|
2195
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, action: { pending: action.action, error: null } } } };
|
|
2196
|
+
}
|
|
2197
|
+
case "admin/packages/action/done": {
|
|
2198
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, action: { pending: null, error: null } } } };
|
|
2199
|
+
}
|
|
2200
|
+
case "admin/packages/action/failed": {
|
|
2201
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, action: { pending: null, error: action.error || "Action failed" } } } };
|
|
2202
|
+
}
|
|
2203
|
+
case "admin/packages/addDialog/open": {
|
|
2204
|
+
// Update mode reuses this dialog wholesale — the work is identical
|
|
2205
|
+
// (point at a repo folder, import it here, publish) and only the
|
|
2206
|
+
// destination is already decided.
|
|
2207
|
+
const seeded = {
|
|
2208
|
+
...createInitialState().admin.packages.addDialog,
|
|
2209
|
+
open: true,
|
|
2210
|
+
updateName: action.updateName || null,
|
|
2211
|
+
scope: action.scope === "shared" ? "shared" : "user",
|
|
2212
|
+
};
|
|
2213
|
+
return { ...state, admin: { ...state.admin, section: "packages", packages: { ...state.admin.packages, addDialog: seeded } } };
|
|
2214
|
+
}
|
|
2215
|
+
case "admin/packages/addDialog/close": {
|
|
2216
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, addDialog: { ...createInitialState().admin.packages.addDialog, open: false } } } };
|
|
2217
|
+
}
|
|
2218
|
+
case "admin/packages/addDialog/setField": {
|
|
2219
|
+
const dialog = state.admin.packages.addDialog;
|
|
2220
|
+
if (!(action.field in dialog)) return state;
|
|
2221
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, addDialog: { ...dialog, [action.field]: action.value, error: null } } } };
|
|
2222
|
+
}
|
|
2223
|
+
case "admin/packages/addDialog/submitting": {
|
|
2224
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, addDialog: { ...state.admin.packages.addDialog, submitting: true, progress: null, error: null } } } };
|
|
2225
|
+
}
|
|
2226
|
+
case "admin/packages/addDialog/progress": {
|
|
2227
|
+
// Client-side import is a multi-request walk (tree, then blobs) —
|
|
2228
|
+
// the dialog narrates it so a slow repo never looks hung.
|
|
2229
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, addDialog: { ...state.admin.packages.addDialog, progress: action.message || null } } } };
|
|
2230
|
+
}
|
|
2231
|
+
case "admin/packages/addDialog/failed": {
|
|
2232
|
+
return { ...state, admin: { ...state.admin, packages: { ...state.admin.packages, addDialog: { ...state.admin.packages.addDialog, submitting: false, progress: null, error: action.error || "Failed to import package" } } } };
|
|
2233
|
+
}
|
|
1847
2234
|
case "admin/systemGhcpKey/saved": {
|
|
1848
2235
|
const status = action.status || {};
|
|
1849
2236
|
return {
|