@parhelia/core 0.1.12762 → 0.1.12766
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/dist/agents-view/AgentsWorkspaceView.js +126 -15
- package/dist/agents-view/AgentsWorkspaceView.js.map +1 -1
- package/dist/editor/ai/AgentPanesGrid.d.ts +28 -0
- package/dist/editor/ai/AgentPanesGrid.js +62 -0
- package/dist/editor/ai/AgentPanesGrid.js.map +1 -0
- package/dist/editor/ai/AgentTerminal.d.ts +6 -1
- package/dist/editor/ai/AgentTerminal.js +142 -121
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/InlineAiDialog.js +20 -19
- package/dist/editor/ai/InlineAiDialog.js.map +1 -1
- package/dist/editor/ai/ToolCallDisplay.js +3 -0
- package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
- package/dist/editor/ai/agentDialogRegistry.d.ts +26 -0
- package/dist/editor/ai/agentDialogRegistry.js +221 -0
- package/dist/editor/ai/agentDialogRegistry.js.map +1 -0
- package/dist/editor/ai/agentPanesTypes.d.ts +27 -0
- package/dist/editor/ai/agentPanesTypes.js +16 -0
- package/dist/editor/ai/agentPanesTypes.js.map +1 -0
- package/dist/editor/ai/useAgentPanes.d.ts +33 -0
- package/dist/editor/ai/useAgentPanes.js +357 -0
- package/dist/editor/ai/useAgentPanes.js.map +1 -0
- package/dist/editor/ai/useAgentPanes.test.d.ts +1 -0
- package/dist/editor/ai/useAgentPanes.test.js +198 -0
- package/dist/editor/ai/useAgentPanes.test.js.map +1 -0
- package/dist/editor/client/EditorShell.js +3 -0
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/commands/itemCommands.js +9 -1
- package/dist/editor/commands/itemCommands.js.map +1 -1
- package/dist/editor/services/agentService.js +4 -16
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/agentSubscriptionRegistry.d.ts +7 -0
- package/dist/editor/services/agentSubscriptionRegistry.js +77 -0
- package/dist/editor/services/agentSubscriptionRegistry.js.map +1 -0
- package/dist/editor/services/agentSubscriptionRegistry.test.d.ts +1 -0
- package/dist/editor/services/agentSubscriptionRegistry.test.js +87 -0
- package/dist/editor/services/agentSubscriptionRegistry.test.js.map +1 -0
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/task-board/TaskBoardWorkspace.js +3 -3
- package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
- package/dist/task-board/components/ProjectDashboard.d.ts +3 -0
- package/dist/task-board/components/ProjectDashboard.js +26 -3
- package/dist/task-board/components/ProjectDashboard.js.map +1 -1
- package/dist/task-board/types.d.ts +4 -0
- package/dist/task-board/views/WizardView.js +48 -28
- package/dist/task-board/views/WizardView.js.map +1 -1
- package/dist/test/setup.d.ts +0 -0
- package/dist/test/setup.js +37 -0
- package/dist/test/setup.js.map +1 -0
- package/package.json +8 -3
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Per-instance dialog registries.
|
|
2
|
+
//
|
|
3
|
+
// Background: AgentDialogHandler dispatches "agent:dialog:show" as a broadcast
|
|
4
|
+
// CustomEvent. Every mounted AgentTerminal receives it and decides whether to
|
|
5
|
+
// accept based on agent-id match + focus. Pre-multi-pane there was at most
|
|
6
|
+
// one mount per agent, so the registries below were keyed only by agent id.
|
|
7
|
+
//
|
|
8
|
+
// With multi-pane, the same agent can be shown in two panes. We must:
|
|
9
|
+
// 1. Keep the agent "mounted" in the global registry as long as ANY pane
|
|
10
|
+
// mounts it. Removing one mount must not flip "mounted" to false.
|
|
11
|
+
// 2. When a dialog fires, exactly ONE pane accepts. Election order:
|
|
12
|
+
// focused pane first; otherwise the first visible (insertion-order) mount.
|
|
13
|
+
//
|
|
14
|
+
// Globals retain the same names for back-compat with consumer reads, but
|
|
15
|
+
// values are now Maps keyed by terminalInstanceId. Old consumers that read
|
|
16
|
+
// the legacy shapes (string[] / Record<id, entry>) get adapter views via
|
|
17
|
+
// the get* helpers below.
|
|
18
|
+
import { normalizeDialogAgentId } from "./agentMessageHelpers";
|
|
19
|
+
function globals() {
|
|
20
|
+
return globalThis;
|
|
21
|
+
}
|
|
22
|
+
function getMountedMap() {
|
|
23
|
+
const g = globals();
|
|
24
|
+
if (!g.__agentDialogMountedAgentsMap) {
|
|
25
|
+
g.__agentDialogMountedAgentsMap = new Map();
|
|
26
|
+
}
|
|
27
|
+
return g.__agentDialogMountedAgentsMap;
|
|
28
|
+
}
|
|
29
|
+
function getVisibleMap() {
|
|
30
|
+
const g = globals();
|
|
31
|
+
if (!g.__agentDialogVisibleCallbacksMap) {
|
|
32
|
+
g.__agentDialogVisibleCallbacksMap = new Map();
|
|
33
|
+
}
|
|
34
|
+
return g.__agentDialogVisibleCallbacksMap;
|
|
35
|
+
}
|
|
36
|
+
// Keep the legacy `__agentDialogMountedAgents: string[]` view in sync for any
|
|
37
|
+
// reader still using the old shape. The list contains every agent id with at
|
|
38
|
+
// least one mounted instance.
|
|
39
|
+
function syncMountedLegacy() {
|
|
40
|
+
const list = [];
|
|
41
|
+
for (const [agentId, set] of getMountedMap()) {
|
|
42
|
+
if (set.size > 0)
|
|
43
|
+
list.push(agentId);
|
|
44
|
+
}
|
|
45
|
+
globals().__agentDialogMountedAgents = list;
|
|
46
|
+
}
|
|
47
|
+
// Keep the legacy `__agentDialogVisibleCallbacks: Record<id, entry>` in sync.
|
|
48
|
+
// For each agent we expose the elected entry (focused first, else earliest
|
|
49
|
+
// visible).
|
|
50
|
+
function syncVisibleLegacy() {
|
|
51
|
+
const flat = {};
|
|
52
|
+
for (const [agentId, entries] of getVisibleMap()) {
|
|
53
|
+
const elected = electInstance(agentId, entries);
|
|
54
|
+
if (elected) {
|
|
55
|
+
flat[agentId] = {
|
|
56
|
+
callbackId: elected.entry.callbackId,
|
|
57
|
+
terminalInstanceId: elected.instanceId,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
globals().__agentDialogVisibleCallbacks = flat;
|
|
62
|
+
}
|
|
63
|
+
function electInstance(_agentId, entries) {
|
|
64
|
+
let chosen = null;
|
|
65
|
+
for (const [instanceId, entry] of entries) {
|
|
66
|
+
if (!chosen) {
|
|
67
|
+
chosen = { instanceId, entry };
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Focused beats unfocused.
|
|
71
|
+
if (entry.isFocused && !chosen.entry.isFocused) {
|
|
72
|
+
chosen = { instanceId, entry };
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!entry.isFocused && chosen.entry.isFocused) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Same focus state: earlier registration wins.
|
|
79
|
+
if (entry.registeredAt < chosen.entry.registeredAt) {
|
|
80
|
+
chosen = { instanceId, entry };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return chosen;
|
|
84
|
+
}
|
|
85
|
+
export function registerMountedInstance(agentId, instanceId) {
|
|
86
|
+
const normalized = normalizeDialogAgentId(agentId);
|
|
87
|
+
if (!normalized)
|
|
88
|
+
return;
|
|
89
|
+
const map = getMountedMap();
|
|
90
|
+
let set = map.get(normalized);
|
|
91
|
+
if (!set) {
|
|
92
|
+
set = new Set();
|
|
93
|
+
map.set(normalized, set);
|
|
94
|
+
}
|
|
95
|
+
set.add(instanceId);
|
|
96
|
+
syncMountedLegacy();
|
|
97
|
+
}
|
|
98
|
+
export function unregisterMountedInstance(agentId, instanceId) {
|
|
99
|
+
const normalized = normalizeDialogAgentId(agentId);
|
|
100
|
+
if (!normalized)
|
|
101
|
+
return;
|
|
102
|
+
const map = getMountedMap();
|
|
103
|
+
const set = map.get(normalized);
|
|
104
|
+
if (!set)
|
|
105
|
+
return;
|
|
106
|
+
set.delete(instanceId);
|
|
107
|
+
if (set.size === 0) {
|
|
108
|
+
map.delete(normalized);
|
|
109
|
+
}
|
|
110
|
+
syncMountedLegacy();
|
|
111
|
+
}
|
|
112
|
+
/** Returns true if at least one terminal is mounted for the given agent id. */
|
|
113
|
+
export function isAgentDialogMounted(agentId) {
|
|
114
|
+
const set = getMountedMap().get(normalizeDialogAgentId(agentId));
|
|
115
|
+
return !!set && set.size > 0;
|
|
116
|
+
}
|
|
117
|
+
export function getMountedAgentIds() {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const [agentId, set] of getMountedMap()) {
|
|
120
|
+
if (set.size > 0)
|
|
121
|
+
out.push(agentId);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
export function setVisibleDialogEntry(agentId, instanceId, callbackId, isFocused) {
|
|
126
|
+
const normalized = normalizeDialogAgentId(agentId);
|
|
127
|
+
if (!normalized)
|
|
128
|
+
return;
|
|
129
|
+
const map = getVisibleMap();
|
|
130
|
+
let entries = map.get(normalized);
|
|
131
|
+
if (!entries) {
|
|
132
|
+
entries = new Map();
|
|
133
|
+
map.set(normalized, entries);
|
|
134
|
+
}
|
|
135
|
+
if (callbackId) {
|
|
136
|
+
const existing = entries.get(instanceId);
|
|
137
|
+
entries.set(instanceId, {
|
|
138
|
+
callbackId,
|
|
139
|
+
isFocused,
|
|
140
|
+
registeredAt: existing?.registeredAt ?? Date.now(),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
entries.delete(instanceId);
|
|
145
|
+
if (entries.size === 0)
|
|
146
|
+
map.delete(normalized);
|
|
147
|
+
}
|
|
148
|
+
syncVisibleLegacy();
|
|
149
|
+
}
|
|
150
|
+
export function clearVisibleDialogEntriesForInstance(agentIds, instanceId) {
|
|
151
|
+
const map = getVisibleMap();
|
|
152
|
+
for (const id of agentIds) {
|
|
153
|
+
const normalized = normalizeDialogAgentId(id);
|
|
154
|
+
if (!normalized)
|
|
155
|
+
continue;
|
|
156
|
+
const entries = map.get(normalized);
|
|
157
|
+
if (!entries)
|
|
158
|
+
continue;
|
|
159
|
+
entries.delete(instanceId);
|
|
160
|
+
if (entries.size === 0)
|
|
161
|
+
map.delete(normalized);
|
|
162
|
+
}
|
|
163
|
+
syncVisibleLegacy();
|
|
164
|
+
}
|
|
165
|
+
export function updateInstanceFocus(agentIds, instanceId, isFocused) {
|
|
166
|
+
const map = getVisibleMap();
|
|
167
|
+
let changed = false;
|
|
168
|
+
for (const id of agentIds) {
|
|
169
|
+
const normalized = normalizeDialogAgentId(id);
|
|
170
|
+
if (!normalized)
|
|
171
|
+
continue;
|
|
172
|
+
const entries = map.get(normalized);
|
|
173
|
+
if (!entries)
|
|
174
|
+
continue;
|
|
175
|
+
const entry = entries.get(instanceId);
|
|
176
|
+
if (!entry)
|
|
177
|
+
continue;
|
|
178
|
+
if (entry.isFocused !== isFocused) {
|
|
179
|
+
entries.set(instanceId, { ...entry, isFocused });
|
|
180
|
+
changed = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (changed)
|
|
184
|
+
syncVisibleLegacy();
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Determines whether the given instance should accept an incoming
|
|
188
|
+
* `agent:dialog:show` event for the given agent id. Election rule:
|
|
189
|
+
* - Among all mounted instances for the agent, the focused one wins.
|
|
190
|
+
* - If none is focused, the earliest-registered one wins (first visible).
|
|
191
|
+
*
|
|
192
|
+
* Note: this consults the *mounted* registry (via the visibility entries
|
|
193
|
+
* we've registered alongside it). A terminal mount that has not yet called
|
|
194
|
+
* `setVisibleDialogEntry` won't participate in election; that's fine because
|
|
195
|
+
* those mounts also wouldn't have a callback attached.
|
|
196
|
+
*/
|
|
197
|
+
export function isElectedDialogReceiver(agentId, instanceId) {
|
|
198
|
+
const normalized = normalizeDialogAgentId(agentId);
|
|
199
|
+
if (!normalized)
|
|
200
|
+
return true;
|
|
201
|
+
// Prefer election over visibility entries (which only exist while a dialog
|
|
202
|
+
// is active). For incoming requests we elect over the *mounted* set.
|
|
203
|
+
const mountedSet = getMountedMap().get(normalized);
|
|
204
|
+
if (!mountedSet || mountedSet.size === 0)
|
|
205
|
+
return false;
|
|
206
|
+
if (mountedSet.size === 1)
|
|
207
|
+
return mountedSet.has(instanceId);
|
|
208
|
+
// Fall back to the visible map for focus / insertion order; if a dialog is
|
|
209
|
+
// already shown elsewhere, that one keeps owning subsequent events.
|
|
210
|
+
const entries = getVisibleMap().get(normalized);
|
|
211
|
+
if (entries && entries.size > 0) {
|
|
212
|
+
const chosen = electInstance(normalized, entries);
|
|
213
|
+
if (chosen)
|
|
214
|
+
return chosen.instanceId === instanceId;
|
|
215
|
+
}
|
|
216
|
+
// No active dialog: pick the first-mounted instance via insertion order.
|
|
217
|
+
// Set iteration preserves insertion order in JS, so the first iterated id wins.
|
|
218
|
+
const firstId = mountedSet.values().next().value;
|
|
219
|
+
return firstId === instanceId;
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=agentDialogRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentDialogRegistry.js","sourceRoot":"","sources":["../../../src/editor/ai/agentDialogRegistry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type PaneId = string;
|
|
2
|
+
export type PaneViewState = {
|
|
3
|
+
kind: "agent";
|
|
4
|
+
agentId: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "profile-selector";
|
|
7
|
+
} | {
|
|
8
|
+
kind: "overview";
|
|
9
|
+
};
|
|
10
|
+
export interface AgentPane {
|
|
11
|
+
id: PaneId;
|
|
12
|
+
view: PaneViewState;
|
|
13
|
+
}
|
|
14
|
+
export interface AgentPanesLayout {
|
|
15
|
+
orientation: "horizontal" | "vertical" | "grid";
|
|
16
|
+
/**
|
|
17
|
+
* Outer dimension is rows. For "horizontal" / "vertical", `rows.length === 1`.
|
|
18
|
+
* For "grid", multiple rows are stacked vertically and each row is a
|
|
19
|
+
* horizontal split.
|
|
20
|
+
*/
|
|
21
|
+
rows: AgentPane[][];
|
|
22
|
+
focusedPaneId: PaneId;
|
|
23
|
+
}
|
|
24
|
+
/** localStorageService key (will be prefixed with "parhelia."). */
|
|
25
|
+
export declare const AGENT_PANES_LAYOUT_STORAGE_KEY = "editor.agentPanesLayout";
|
|
26
|
+
/** Legacy single-active-id key. Read once for migration, then removed. */
|
|
27
|
+
export declare const LEGACY_ACTIVE_AGENT_STORAGE_KEY = "editor.activeAgentId";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Multi-pane layout for agent terminals.
|
|
2
|
+
//
|
|
3
|
+
// V1 ships orientation = "horizontal" | "vertical" (single row) via "Open in
|
|
4
|
+
// split right" / "Open in split down" context-menu actions on tabs. The
|
|
5
|
+
// "grid" orientation uses N rows of horizontal panes; the renderer nests
|
|
6
|
+
// two Splitters (outer vertical, inner horizontal per row).
|
|
7
|
+
//
|
|
8
|
+
// No `weight` field on AgentPane — Splitter (`editor/ui/Splitter.tsx`) owns
|
|
9
|
+
// panel-size persistence via its own localStorageKey. The layout model owns
|
|
10
|
+
// only which panes exist and which is focused; sizes round-trip through
|
|
11
|
+
// Splitter independently.
|
|
12
|
+
/** localStorageService key (will be prefixed with "parhelia."). */
|
|
13
|
+
export const AGENT_PANES_LAYOUT_STORAGE_KEY = "editor.agentPanesLayout";
|
|
14
|
+
/** Legacy single-active-id key. Read once for migration, then removed. */
|
|
15
|
+
export const LEGACY_ACTIVE_AGENT_STORAGE_KEY = "editor.activeAgentId";
|
|
16
|
+
//# sourceMappingURL=agentPanesTypes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentPanesTypes.js","sourceRoot":"","sources":["../../../src/editor/ai/agentPanesTypes.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,6EAA6E;AAC7E,wEAAwE;AACxE,yEAAyE;AACzE,4DAA4D;AAC5D,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,wEAAwE;AACxE,0BAA0B;AA2B1B,mEAAmE;AACnE,MAAM,CAAC,MAAM,8BAA8B,GAAG,yBAAyB,CAAC;AAExE,0EAA0E;AAC1E,MAAM,CAAC,MAAM,+BAA+B,GAAG,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Agent } from "../services/agentService";
|
|
2
|
+
import { type AgentPanesLayout, type PaneId } from "./agentPanesTypes";
|
|
3
|
+
export interface UseAgentPanesOptions {
|
|
4
|
+
/** localStorageService key (will be prefixed). Defaults to the tabbed
|
|
5
|
+
* Agents UI key. Pass a different key to keep view-scoped layouts
|
|
6
|
+
* separate (e.g. `${AGENT_PANES_LAYOUT_STORAGE_KEY}.workspace`). */
|
|
7
|
+
storageKey?: string;
|
|
8
|
+
/** When true, on first load (no stored layout) reads the legacy
|
|
9
|
+
* `editor.activeAgentId` key and synthesizes a single-pane layout from it.
|
|
10
|
+
* Default false: secondary views start with a single overview pane. */
|
|
11
|
+
migrateLegacy?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface UseAgentPanesResult {
|
|
14
|
+
layout: AgentPanesLayout;
|
|
15
|
+
focusedPaneId: PaneId;
|
|
16
|
+
/** Map of agentId -> ordered list of pane ids that show that agent. */
|
|
17
|
+
panesByAgentId: Map<string, PaneId[]>;
|
|
18
|
+
/** Set of agent ids visible in any pane. */
|
|
19
|
+
visibleAgentIds: Set<string>;
|
|
20
|
+
/** The agent currently shown in the focused pane (if any). */
|
|
21
|
+
focusedAgentId: string | null;
|
|
22
|
+
setFocusedPane: (paneId: PaneId) => void;
|
|
23
|
+
showAgentInFocusedPane: (agentId: string) => void;
|
|
24
|
+
showProfileSelectorInFocusedPane: () => void;
|
|
25
|
+
showOverviewInFocusedPane: () => void;
|
|
26
|
+
splitPane: (sourcePaneId: PaneId,
|
|
27
|
+
/** Agent to put in the new pane. Pass `null` for an empty (overview) pane. */
|
|
28
|
+
agentId: string | null, direction: "right" | "down") => void;
|
|
29
|
+
closePane: (paneId: PaneId) => void;
|
|
30
|
+
closeAgentEverywhere: (agentId: string) => void;
|
|
31
|
+
pruneMissingAgents: (existingAgentIds: Iterable<string>) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function useAgentPanes(_agents: Agent[], options?: UseAgentPanesOptions): UseAgentPanesResult;
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Hook owning the multi-pane agent layout. Replaces the single
|
|
2
|
+
// `activeAgentId` state and `setActiveAgentIdWithStorage` plumbing in
|
|
3
|
+
// Agents.tsx with a layout of N panes plus a focused pane id.
|
|
4
|
+
//
|
|
5
|
+
// Persistence: layout JSON in localStorageService under
|
|
6
|
+
// `editor.agentPanesLayout`. On first run, migrates from legacy
|
|
7
|
+
// `editor.activeAgentId` (a single agent id) into a single-pane layout.
|
|
8
|
+
//
|
|
9
|
+
// Splitter sizes are NOT persisted here — Splitter.tsx owns its own
|
|
10
|
+
// localStorage. This hook only owns which panes exist and which is focused.
|
|
11
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
+
import { localStorageService } from "../services/localStorageService";
|
|
13
|
+
import { AGENT_PANES_LAYOUT_STORAGE_KEY, LEGACY_ACTIVE_AGENT_STORAGE_KEY, } from "./agentPanesTypes";
|
|
14
|
+
let paneCounter = 0;
|
|
15
|
+
function newPaneId() {
|
|
16
|
+
paneCounter += 1;
|
|
17
|
+
return `pane-${Date.now().toString(36)}-${paneCounter}`;
|
|
18
|
+
}
|
|
19
|
+
function makeOverviewLayout() {
|
|
20
|
+
const id = newPaneId();
|
|
21
|
+
return {
|
|
22
|
+
orientation: "horizontal",
|
|
23
|
+
rows: [[{ id, view: { kind: "overview" } }]],
|
|
24
|
+
focusedPaneId: id,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function makeAgentLayout(agentId) {
|
|
28
|
+
const id = newPaneId();
|
|
29
|
+
return {
|
|
30
|
+
orientation: "horizontal",
|
|
31
|
+
rows: [[{ id, view: { kind: "agent", agentId } }]],
|
|
32
|
+
focusedPaneId: id,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function makeProfileSelectorLayout() {
|
|
36
|
+
const id = newPaneId();
|
|
37
|
+
return {
|
|
38
|
+
orientation: "horizontal",
|
|
39
|
+
rows: [[{ id, view: { kind: "profile-selector" } }]],
|
|
40
|
+
focusedPaneId: id,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate a layout that was just loaded from localStorage. Returns null if
|
|
45
|
+
* it's structurally invalid, or a sanitized copy if it's mostly fine.
|
|
46
|
+
*/
|
|
47
|
+
function sanitizeStoredLayout(raw) {
|
|
48
|
+
if (!raw || typeof raw !== "object")
|
|
49
|
+
return null;
|
|
50
|
+
const candidate = raw;
|
|
51
|
+
if (candidate.orientation !== "horizontal" &&
|
|
52
|
+
candidate.orientation !== "vertical" &&
|
|
53
|
+
candidate.orientation !== "grid") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(candidate.rows) || candidate.rows.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const cleanedRows = [];
|
|
60
|
+
for (const row of candidate.rows) {
|
|
61
|
+
if (!Array.isArray(row) || row.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
const cleanedRow = [];
|
|
64
|
+
for (const pane of row) {
|
|
65
|
+
if (!pane || typeof pane !== "object")
|
|
66
|
+
continue;
|
|
67
|
+
const p = pane;
|
|
68
|
+
if (typeof p.id !== "string" || !p.id)
|
|
69
|
+
continue;
|
|
70
|
+
const view = p.view;
|
|
71
|
+
if (!view || typeof view !== "object")
|
|
72
|
+
continue;
|
|
73
|
+
if (view.kind !== "agent" &&
|
|
74
|
+
view.kind !== "profile-selector" &&
|
|
75
|
+
view.kind !== "overview") {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (view.kind === "agent" && typeof view.agentId !== "string")
|
|
79
|
+
continue;
|
|
80
|
+
cleanedRow.push({ id: p.id, view });
|
|
81
|
+
}
|
|
82
|
+
if (cleanedRow.length > 0)
|
|
83
|
+
cleanedRows.push(cleanedRow);
|
|
84
|
+
}
|
|
85
|
+
if (cleanedRows.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
const allIds = new Set();
|
|
88
|
+
for (const row of cleanedRows)
|
|
89
|
+
for (const pane of row)
|
|
90
|
+
allIds.add(pane.id);
|
|
91
|
+
const firstPane = cleanedRows[0]?.[0];
|
|
92
|
+
if (!firstPane)
|
|
93
|
+
return null;
|
|
94
|
+
const focusedPaneId = typeof candidate.focusedPaneId === "string" &&
|
|
95
|
+
allIds.has(candidate.focusedPaneId)
|
|
96
|
+
? candidate.focusedPaneId
|
|
97
|
+
: firstPane.id;
|
|
98
|
+
return {
|
|
99
|
+
orientation: candidate.orientation,
|
|
100
|
+
rows: cleanedRows,
|
|
101
|
+
focusedPaneId,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function loadInitialLayout(options) {
|
|
105
|
+
const stored = localStorageService.getItem(options.storageKey);
|
|
106
|
+
const sanitized = sanitizeStoredLayout(stored);
|
|
107
|
+
if (sanitized)
|
|
108
|
+
return sanitized;
|
|
109
|
+
if (options.migrateLegacy) {
|
|
110
|
+
// Migrate from the legacy single-active-id key. Possible values:
|
|
111
|
+
// - missing: show overview
|
|
112
|
+
// - "" (sentinel): profile selector
|
|
113
|
+
// - <agent id>: single-pane agent
|
|
114
|
+
const legacy = localStorageService.getString(LEGACY_ACTIVE_AGENT_STORAGE_KEY);
|
|
115
|
+
if (legacy === null || legacy === undefined) {
|
|
116
|
+
return makeOverviewLayout();
|
|
117
|
+
}
|
|
118
|
+
if (legacy === "") {
|
|
119
|
+
return makeProfileSelectorLayout();
|
|
120
|
+
}
|
|
121
|
+
return makeAgentLayout(legacy);
|
|
122
|
+
}
|
|
123
|
+
return makeOverviewLayout();
|
|
124
|
+
}
|
|
125
|
+
function findPane(layout, paneId) {
|
|
126
|
+
for (const [r, row] of layout.rows.entries()) {
|
|
127
|
+
for (const [c, pane] of row.entries()) {
|
|
128
|
+
if (pane.id === paneId)
|
|
129
|
+
return { rowIndex: r, colIndex: c, pane };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
function flattenPanes(layout) {
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const row of layout.rows)
|
|
137
|
+
for (const pane of row)
|
|
138
|
+
out.push(pane);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function paneCount(layout) {
|
|
142
|
+
return flattenPanes(layout).length;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* After mutating rows, ensure focus still points to a real pane. If not,
|
|
146
|
+
* fall back to the first pane in row-major order.
|
|
147
|
+
*/
|
|
148
|
+
function fixFocus(rows, previous) {
|
|
149
|
+
for (const row of rows)
|
|
150
|
+
for (const pane of row)
|
|
151
|
+
if (pane.id === previous)
|
|
152
|
+
return previous;
|
|
153
|
+
return rows[0]?.[0]?.id ?? previous;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Collapse to a sensible empty state: a single overview pane.
|
|
157
|
+
*/
|
|
158
|
+
function ensureNonEmpty(rows) {
|
|
159
|
+
if (rows.length === 0) {
|
|
160
|
+
return [[{ id: newPaneId(), view: { kind: "overview" } }]];
|
|
161
|
+
}
|
|
162
|
+
return rows;
|
|
163
|
+
}
|
|
164
|
+
export function useAgentPanes(_agents, options = {}) {
|
|
165
|
+
const storageKey = options.storageKey ?? AGENT_PANES_LAYOUT_STORAGE_KEY;
|
|
166
|
+
const migrateLegacy = options.migrateLegacy ?? false;
|
|
167
|
+
const [layout, setLayout] = useState(() => loadInitialLayout({ storageKey, migrateLegacy }));
|
|
168
|
+
const layoutRef = useRef(layout);
|
|
169
|
+
layoutRef.current = layout;
|
|
170
|
+
// Persist the layout. Splitter sizes round-trip through Splitter's own
|
|
171
|
+
// localStorage; this only persists pane structure + focus.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
localStorageService.setItem(storageKey, layout);
|
|
174
|
+
// Drop the legacy single-active-id key on first save of the new layout
|
|
175
|
+
// (only applicable for the primary tabbed view).
|
|
176
|
+
if (migrateLegacy) {
|
|
177
|
+
localStorageService.removeItem(LEGACY_ACTIVE_AGENT_STORAGE_KEY);
|
|
178
|
+
}
|
|
179
|
+
}, [layout, storageKey, migrateLegacy]);
|
|
180
|
+
const panesByAgentId = useMemo(() => {
|
|
181
|
+
const map = new Map();
|
|
182
|
+
for (const row of layout.rows) {
|
|
183
|
+
for (const pane of row) {
|
|
184
|
+
if (pane.view.kind === "agent") {
|
|
185
|
+
const existing = map.get(pane.view.agentId);
|
|
186
|
+
if (existing)
|
|
187
|
+
existing.push(pane.id);
|
|
188
|
+
else
|
|
189
|
+
map.set(pane.view.agentId, [pane.id]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return map;
|
|
194
|
+
}, [layout]);
|
|
195
|
+
const visibleAgentIds = useMemo(() => new Set(panesByAgentId.keys()), [panesByAgentId]);
|
|
196
|
+
const focusedAgentId = useMemo(() => {
|
|
197
|
+
const found = findPane(layout, layout.focusedPaneId);
|
|
198
|
+
if (found && found.pane.view.kind === "agent") {
|
|
199
|
+
return found.pane.view.agentId;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}, [layout]);
|
|
203
|
+
const setFocusedPane = useCallback((paneId) => {
|
|
204
|
+
setLayout((prev) => {
|
|
205
|
+
if (prev.focusedPaneId === paneId)
|
|
206
|
+
return prev;
|
|
207
|
+
// Confirm the pane exists before switching.
|
|
208
|
+
if (!findPane(prev, paneId))
|
|
209
|
+
return prev;
|
|
210
|
+
return { ...prev, focusedPaneId: paneId };
|
|
211
|
+
});
|
|
212
|
+
}, []);
|
|
213
|
+
const replaceFocusedPaneView = useCallback((view) => {
|
|
214
|
+
setLayout((prev) => {
|
|
215
|
+
const found = findPane(prev, prev.focusedPaneId);
|
|
216
|
+
if (!found)
|
|
217
|
+
return prev;
|
|
218
|
+
const newRows = prev.rows.map((row) => row.map((pane) => pane.id === prev.focusedPaneId ? { ...pane, view } : pane));
|
|
219
|
+
return { ...prev, rows: newRows };
|
|
220
|
+
});
|
|
221
|
+
}, []);
|
|
222
|
+
const showAgentInFocusedPane = useCallback((agentId) => {
|
|
223
|
+
replaceFocusedPaneView({ kind: "agent", agentId });
|
|
224
|
+
}, [replaceFocusedPaneView]);
|
|
225
|
+
const showProfileSelectorInFocusedPane = useCallback(() => {
|
|
226
|
+
replaceFocusedPaneView({ kind: "profile-selector" });
|
|
227
|
+
}, [replaceFocusedPaneView]);
|
|
228
|
+
const showOverviewInFocusedPane = useCallback(() => {
|
|
229
|
+
replaceFocusedPaneView({ kind: "overview" });
|
|
230
|
+
}, [replaceFocusedPaneView]);
|
|
231
|
+
const splitPane = useCallback((sourcePaneId, agentId, direction) => {
|
|
232
|
+
setLayout((prev) => {
|
|
233
|
+
const found = findPane(prev, sourcePaneId);
|
|
234
|
+
if (!found)
|
|
235
|
+
return prev;
|
|
236
|
+
const newPane = {
|
|
237
|
+
id: newPaneId(),
|
|
238
|
+
view: agentId === null
|
|
239
|
+
? { kind: "overview" }
|
|
240
|
+
: { kind: "agent", agentId },
|
|
241
|
+
};
|
|
242
|
+
if (direction === "right") {
|
|
243
|
+
const rows = prev.rows.map((row, r) => r === found.rowIndex
|
|
244
|
+
? [
|
|
245
|
+
...row.slice(0, found.colIndex + 1),
|
|
246
|
+
newPane,
|
|
247
|
+
...row.slice(found.colIndex + 1),
|
|
248
|
+
]
|
|
249
|
+
: row);
|
|
250
|
+
// Single-row + horizontal split = "horizontal" orientation. With
|
|
251
|
+
// multiple rows we must be in grid mode.
|
|
252
|
+
const orientation = rows.length > 1 ? "grid" : "horizontal";
|
|
253
|
+
return {
|
|
254
|
+
...prev,
|
|
255
|
+
rows,
|
|
256
|
+
orientation,
|
|
257
|
+
focusedPaneId: newPane.id,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// direction === "down": insert a new row below the source's row,
|
|
261
|
+
// containing only the new pane. This becomes a 2-D grid.
|
|
262
|
+
const rows = [
|
|
263
|
+
...prev.rows.slice(0, found.rowIndex + 1),
|
|
264
|
+
[newPane],
|
|
265
|
+
...prev.rows.slice(found.rowIndex + 1),
|
|
266
|
+
];
|
|
267
|
+
const orientation = rows.length > 1
|
|
268
|
+
? rows.some((r) => r.length > 1)
|
|
269
|
+
? "grid"
|
|
270
|
+
: "vertical"
|
|
271
|
+
: "horizontal";
|
|
272
|
+
return {
|
|
273
|
+
...prev,
|
|
274
|
+
rows,
|
|
275
|
+
orientation,
|
|
276
|
+
focusedPaneId: newPane.id,
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
}, []);
|
|
280
|
+
const closePane = useCallback((paneId) => {
|
|
281
|
+
setLayout((prev) => {
|
|
282
|
+
if (paneCount(prev) <= 1) {
|
|
283
|
+
// Last pane: never remove it; reset its view to overview instead.
|
|
284
|
+
const rows = prev.rows.map((row) => row.map((pane) => pane.id === paneId ? { ...pane, view: { kind: "overview" } } : pane));
|
|
285
|
+
return { ...prev, rows, focusedPaneId: paneId };
|
|
286
|
+
}
|
|
287
|
+
const filteredRows = prev.rows
|
|
288
|
+
.map((row) => row.filter((pane) => pane.id !== paneId))
|
|
289
|
+
.filter((row) => row.length > 0);
|
|
290
|
+
const rows = ensureNonEmpty(filteredRows);
|
|
291
|
+
const orientation = rows.length > 1
|
|
292
|
+
? rows.some((r) => r.length > 1)
|
|
293
|
+
? "grid"
|
|
294
|
+
: "vertical"
|
|
295
|
+
: "horizontal";
|
|
296
|
+
return {
|
|
297
|
+
...prev,
|
|
298
|
+
rows,
|
|
299
|
+
orientation,
|
|
300
|
+
focusedPaneId: fixFocus(rows, prev.focusedPaneId),
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
}, []);
|
|
304
|
+
const closeAgentEverywhere = useCallback((agentId) => {
|
|
305
|
+
setLayout((prev) => {
|
|
306
|
+
let changed = false;
|
|
307
|
+
const rows = prev.rows.map((row) => row.map((pane) => {
|
|
308
|
+
if (pane.view.kind === "agent" && pane.view.agentId === agentId) {
|
|
309
|
+
changed = true;
|
|
310
|
+
return {
|
|
311
|
+
...pane,
|
|
312
|
+
view: { kind: "profile-selector" },
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return pane;
|
|
316
|
+
}));
|
|
317
|
+
return changed ? { ...prev, rows } : prev;
|
|
318
|
+
});
|
|
319
|
+
}, []);
|
|
320
|
+
const pruneMissingAgents = useCallback((existingAgentIds) => {
|
|
321
|
+
const existingSet = new Set(existingAgentIds);
|
|
322
|
+
setLayout((prev) => {
|
|
323
|
+
let changed = false;
|
|
324
|
+
const rows = prev.rows.map((row) => row.map((pane) => {
|
|
325
|
+
if (pane.view.kind === "agent" &&
|
|
326
|
+
!existingSet.has(pane.view.agentId)) {
|
|
327
|
+
changed = true;
|
|
328
|
+
return { ...pane, view: { kind: "profile-selector" } };
|
|
329
|
+
}
|
|
330
|
+
return pane;
|
|
331
|
+
}));
|
|
332
|
+
if (!changed)
|
|
333
|
+
return prev;
|
|
334
|
+
return { ...prev, rows, focusedPaneId: fixFocus(rows, prev.focusedPaneId) };
|
|
335
|
+
});
|
|
336
|
+
}, []);
|
|
337
|
+
// Test-only escape hatch. Note: paneCounter resets between page loads,
|
|
338
|
+
// not between tests; tests should rely on stable-shaped IDs rather than
|
|
339
|
+
// exact matching.
|
|
340
|
+
void layoutRef;
|
|
341
|
+
return {
|
|
342
|
+
layout,
|
|
343
|
+
focusedPaneId: layout.focusedPaneId,
|
|
344
|
+
panesByAgentId,
|
|
345
|
+
visibleAgentIds,
|
|
346
|
+
focusedAgentId,
|
|
347
|
+
setFocusedPane,
|
|
348
|
+
showAgentInFocusedPane,
|
|
349
|
+
showProfileSelectorInFocusedPane,
|
|
350
|
+
showOverviewInFocusedPane,
|
|
351
|
+
splitPane,
|
|
352
|
+
closePane,
|
|
353
|
+
closeAgentEverywhere,
|
|
354
|
+
pruneMissingAgents,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
//# sourceMappingURL=useAgentPanes.js.map
|