pilotswarm 0.0.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/mcp/README.md +484 -0
- package/mcp/dist/bin/pilotswarm-mcp.d.ts +3 -0
- package/mcp/dist/bin/pilotswarm-mcp.d.ts.map +1 -0
- package/mcp/dist/bin/pilotswarm-mcp.js +367 -0
- package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -0
- package/mcp/dist/src/auth.d.ts +7 -0
- package/mcp/dist/src/auth.d.ts.map +1 -0
- package/mcp/dist/src/auth.js +99 -0
- package/mcp/dist/src/auth.js.map +1 -0
- package/mcp/dist/src/context.d.ts +48 -0
- package/mcp/dist/src/context.d.ts.map +1 -0
- package/mcp/dist/src/context.js +83 -0
- package/mcp/dist/src/context.js.map +1 -0
- package/mcp/dist/src/index.d.ts +4 -0
- package/mcp/dist/src/index.d.ts.map +1 -0
- package/mcp/dist/src/index.js +3 -0
- package/mcp/dist/src/index.js.map +1 -0
- package/mcp/dist/src/prompts/skills.d.ts +4 -0
- package/mcp/dist/src/prompts/skills.d.ts.map +1 -0
- package/mcp/dist/src/prompts/skills.js +11 -0
- package/mcp/dist/src/prompts/skills.js.map +1 -0
- package/mcp/dist/src/resources/agents.d.ts +4 -0
- package/mcp/dist/src/resources/agents.d.ts.map +1 -0
- package/mcp/dist/src/resources/agents.js +64 -0
- package/mcp/dist/src/resources/agents.js.map +1 -0
- package/mcp/dist/src/resources/facts.d.ts +4 -0
- package/mcp/dist/src/resources/facts.d.ts.map +1 -0
- package/mcp/dist/src/resources/facts.js +125 -0
- package/mcp/dist/src/resources/facts.js.map +1 -0
- package/mcp/dist/src/resources/models.d.ts +4 -0
- package/mcp/dist/src/resources/models.d.ts.map +1 -0
- package/mcp/dist/src/resources/models.js +43 -0
- package/mcp/dist/src/resources/models.js.map +1 -0
- package/mcp/dist/src/resources/sessions.d.ts +4 -0
- package/mcp/dist/src/resources/sessions.d.ts.map +1 -0
- package/mcp/dist/src/resources/sessions.js +190 -0
- package/mcp/dist/src/resources/sessions.js.map +1 -0
- package/mcp/dist/src/resources/subscriptions.d.ts +9 -0
- package/mcp/dist/src/resources/subscriptions.d.ts.map +1 -0
- package/mcp/dist/src/resources/subscriptions.js +157 -0
- package/mcp/dist/src/resources/subscriptions.js.map +1 -0
- package/mcp/dist/src/server.d.ts +4 -0
- package/mcp/dist/src/server.d.ts.map +1 -0
- package/mcp/dist/src/server.js +59 -0
- package/mcp/dist/src/server.js.map +1 -0
- package/mcp/dist/src/tools/agents.d.ts +4 -0
- package/mcp/dist/src/tools/agents.d.ts.map +1 -0
- package/mcp/dist/src/tools/agents.js +317 -0
- package/mcp/dist/src/tools/agents.js.map +1 -0
- package/mcp/dist/src/tools/facts.d.ts +4 -0
- package/mcp/dist/src/tools/facts.d.ts.map +1 -0
- package/mcp/dist/src/tools/facts.js +151 -0
- package/mcp/dist/src/tools/facts.js.map +1 -0
- package/mcp/dist/src/tools/models.d.ts +4 -0
- package/mcp/dist/src/tools/models.d.ts.map +1 -0
- package/mcp/dist/src/tools/models.js +256 -0
- package/mcp/dist/src/tools/models.js.map +1 -0
- package/mcp/dist/src/tools/sessions.d.ts +4 -0
- package/mcp/dist/src/tools/sessions.d.ts.map +1 -0
- package/mcp/dist/src/tools/sessions.js +606 -0
- package/mcp/dist/src/tools/sessions.js.map +1 -0
- package/mcp/dist/src/util/command.d.ts +52 -0
- package/mcp/dist/src/util/command.d.ts.map +1 -0
- package/mcp/dist/src/util/command.js +78 -0
- package/mcp/dist/src/util/command.js.map +1 -0
- package/package.json +81 -6
- package/tui/README.md +35 -0
- package/tui/bin/tui.js +30 -0
- package/tui/plugins/.mcp.json +7 -0
- package/tui/plugins/plugin.json +13 -0
- package/tui/plugins/session-policy.json +8 -0
- package/tui/src/app.js +850 -0
- package/tui/src/auth/cli.js +111 -0
- package/tui/src/auth/entra-auth.js +218 -0
- package/tui/src/bootstrap-env.js +176 -0
- package/tui/src/embedded-workers.js +79 -0
- package/tui/src/http-transport-host.js +106 -0
- package/tui/src/index.js +340 -0
- package/tui/src/node-sdk-transport.js +1793 -0
- package/tui/src/platform.js +984 -0
- package/tui/src/plugin-config.js +239 -0
- package/tui/src/portal.js +7 -0
- package/tui/src/version.js +7 -0
- package/tui/tui-splash.txt +11 -0
- package/ui/core/README.md +6 -0
- package/ui/core/src/commands.js +93 -0
- package/ui/core/src/context-usage.js +212 -0
- package/ui/core/src/controller.js +6104 -0
- package/ui/core/src/formatting.js +1036 -0
- package/ui/core/src/history.js +932 -0
- package/ui/core/src/index.js +13 -0
- package/ui/core/src/layout.js +332 -0
- package/ui/core/src/reducer.js +1935 -0
- package/ui/core/src/selectors.js +5409 -0
- package/ui/core/src/session-errors.js +14 -0
- package/ui/core/src/session-tree.js +151 -0
- package/ui/core/src/state.js +248 -0
- package/ui/core/src/store.js +23 -0
- package/ui/core/src/system-titles.js +24 -0
- package/ui/core/src/themes/catppuccin-mocha.js +56 -0
- package/ui/core/src/themes/cobalt2.js +56 -0
- package/ui/core/src/themes/dark-high-contrast.js +56 -0
- package/ui/core/src/themes/daylight.js +62 -0
- package/ui/core/src/themes/dracula.js +56 -0
- package/ui/core/src/themes/github-dark.js +56 -0
- package/ui/core/src/themes/github-light.js +59 -0
- package/ui/core/src/themes/gruvbox-dark.js +56 -0
- package/ui/core/src/themes/hacker-x-matrix.js +56 -0
- package/ui/core/src/themes/hacker-x-orion-prime.js +56 -0
- package/ui/core/src/themes/helpers.js +79 -0
- package/ui/core/src/themes/high-contrast-mono.js +59 -0
- package/ui/core/src/themes/index.js +52 -0
- package/ui/core/src/themes/light-high-contrast.js +62 -0
- package/ui/core/src/themes/noctis-obscuro.js +56 -0
- package/ui/core/src/themes/noctis.js +56 -0
- package/ui/core/src/themes/paper-ink.js +62 -0
- package/ui/core/src/themes/solarized-ops.js +59 -0
- package/ui/core/src/themes/terminal-green.js +59 -0
- package/ui/core/src/themes/tokyo-night.js +56 -0
- package/ui/react/README.md +5 -0
- package/ui/react/src/chat-status.js +39 -0
- package/ui/react/src/components.js +1989 -0
- package/ui/react/src/index.js +4 -0
- package/ui/react/src/platform.js +15 -0
- package/ui/react/src/use-controller-state.js +38 -0
- package/ui/react/src/web-app.js +4390 -0
- package/web/README.md +198 -0
- package/web/api/router.js +196 -0
- package/web/api/ws.js +152 -0
- package/web/auth/authz/engine.js +204 -0
- package/web/auth/config.js +115 -0
- package/web/auth/index.js +175 -0
- package/web/auth/normalize/entra.js +22 -0
- package/web/auth/providers/entra.js +76 -0
- package/web/auth/providers/none.js +24 -0
- package/web/auth.js +10 -0
- package/web/bin/serve.js +53 -0
- package/web/config.js +20 -0
- package/web/dist/app.js +469 -0
- package/web/dist/assets/index-CBgQQk-j.css +1 -0
- package/web/dist/assets/index-DMefB7Wb.js +24 -0
- package/web/dist/assets/msal-CytV9RFv.js +7 -0
- package/web/dist/assets/pilotswarm-DyBlqVTY.js +87 -0
- package/web/dist/assets/react-BZwsW8pe.js +1 -0
- package/web/dist/index.html +16 -0
- package/web/runtime.js +454 -0
- package/web/server.js +276 -0
- package/index.js +0 -1
|
@@ -0,0 +1,4390 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { appendAnimatedDotsToRuns, useAnimatedDots } from "./chat-status.js";
|
|
3
|
+
import {
|
|
4
|
+
UI_COMMANDS,
|
|
5
|
+
INSPECTOR_TABS,
|
|
6
|
+
appReducer,
|
|
7
|
+
canStopSessionTurn,
|
|
8
|
+
computeLegacyLayout,
|
|
9
|
+
createInitialState,
|
|
10
|
+
createStore,
|
|
11
|
+
getPromptInputRows,
|
|
12
|
+
getTheme,
|
|
13
|
+
parseTerminalMarkupRuns,
|
|
14
|
+
PilotSwarmUiController,
|
|
15
|
+
tokenizeInlineMarkdown,
|
|
16
|
+
selectActivityPane,
|
|
17
|
+
selectAdminConsole,
|
|
18
|
+
selectArtifactPickerModal,
|
|
19
|
+
selectArtifactUploadModal,
|
|
20
|
+
selectChatLines,
|
|
21
|
+
selectChatPaneChrome,
|
|
22
|
+
selectOutboxOverlayLines,
|
|
23
|
+
selectFileBrowserItems,
|
|
24
|
+
selectFilesFilterModal,
|
|
25
|
+
selectFilesScope,
|
|
26
|
+
selectFilesView,
|
|
27
|
+
selectHistoryFormatModal,
|
|
28
|
+
selectInspector,
|
|
29
|
+
selectLogFilterModal,
|
|
30
|
+
selectModelPickerModal,
|
|
31
|
+
selectReasoningEffortPickerModal,
|
|
32
|
+
selectRenameSessionModal,
|
|
33
|
+
selectSessionAgentPickerModal,
|
|
34
|
+
selectSessionGroupNameModal,
|
|
35
|
+
selectSessionGroupPickerModal,
|
|
36
|
+
selectSessionOwnerFilterModal,
|
|
37
|
+
selectSessionRows,
|
|
38
|
+
selectStatusBar,
|
|
39
|
+
selectThemePickerModal,
|
|
40
|
+
selectConfirmModal,
|
|
41
|
+
normalizeStoredLayoutAdjustments,
|
|
42
|
+
normalizeStoredPinnedSessionIds,
|
|
43
|
+
} from "pilotswarm/ui-core";
|
|
44
|
+
import { useControllerSelector } from "./use-controller-state.js";
|
|
45
|
+
|
|
46
|
+
const MOBILE_BREAKPOINT = 920;
|
|
47
|
+
const GRID_CELL_WIDTH = 7;
|
|
48
|
+
const GRID_CELL_HEIGHT = 19;
|
|
49
|
+
const PORTAL_SESSION_COLUMN_RATIO = 0.24;
|
|
50
|
+
const PORTAL_SESSION_CHAT_DIVIDER_PX = 16;
|
|
51
|
+
const PORTAL_WORKSPACE_GAP_PX = 8;
|
|
52
|
+
const PORTAL_MIN_CHAT_COLUMN_PX = 224;
|
|
53
|
+
const PORTAL_SESSION_COMPACT_COLUMN_PX = 190;
|
|
54
|
+
const PORTAL_SESSION_HIDDEN_COLUMN_PX = 48;
|
|
55
|
+
const SCROLL_ROW_HEIGHT = 16;
|
|
56
|
+
const SCROLL_BOTTOM_EPSILON_PX = 0.5;
|
|
57
|
+
const PROGRAMMATIC_SCROLL_TOLERANCE_PX = SCROLL_BOTTOM_EPSILON_PX;
|
|
58
|
+
const PROFILE_SETTINGS_POLL_MS = 5000;
|
|
59
|
+
const REASONING_EFFORT_LABELS = new Set(["low", "medium", "high", "xhigh"]);
|
|
60
|
+
const LEGACY_BROWSER_PREFERENCE_STORAGE_KEYS = [
|
|
61
|
+
"pilotswarm.theme",
|
|
62
|
+
"pilotswarm.sessionOwnerFilter",
|
|
63
|
+
"pilotswarm.chatFocus",
|
|
64
|
+
"pilotswarm.layoutAdjustments",
|
|
65
|
+
"pilotswarm.pinnedSessions",
|
|
66
|
+
];
|
|
67
|
+
const LEGACY_BROWSER_PREFERENCE_COOKIE_NAMES = [
|
|
68
|
+
"pilotswarm_theme",
|
|
69
|
+
"pilotswarm_session_owner_filter",
|
|
70
|
+
];
|
|
71
|
+
const INSPECTOR_TAB_LABELS = {
|
|
72
|
+
sequence: "Sequence",
|
|
73
|
+
logs: "Logs",
|
|
74
|
+
nodes: "Node Map",
|
|
75
|
+
history: "History",
|
|
76
|
+
files: "Files",
|
|
77
|
+
stats: "Stats",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function cycleTabs(tabs, current, delta) {
|
|
81
|
+
const values = Array.isArray(tabs) ? tabs.filter(Boolean) : [];
|
|
82
|
+
if (values.length === 0) return current;
|
|
83
|
+
const index = values.indexOf(current);
|
|
84
|
+
const safeIndex = index === -1 ? 0 : index;
|
|
85
|
+
const nextIndex = (safeIndex + delta + values.length) % values.length;
|
|
86
|
+
return values[nextIndex];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function supportsBrowserFileUploads(controller) {
|
|
90
|
+
return typeof controller?.transport?.uploadArtifactFromFile === "function";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function supportsPathArtifactUploads(controller) {
|
|
94
|
+
return typeof controller?.transport?.uploadArtifactFromPath === "function";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function supportsArtifactBrowser(controller) {
|
|
98
|
+
return typeof controller?.transport?.listArtifacts === "function";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function supportsArtifactDelete(controller) {
|
|
102
|
+
return typeof controller?.transport?.deleteArtifact === "function";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function supportsLocalFileOpen(controller) {
|
|
106
|
+
return typeof controller?.transport?.openPathInDefaultApp === "function";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function clearBrowserPreferenceCache() {
|
|
110
|
+
if (typeof window === "undefined") return;
|
|
111
|
+
try {
|
|
112
|
+
for (const key of LEGACY_BROWSER_PREFERENCE_STORAGE_KEYS) {
|
|
113
|
+
window.localStorage.removeItem(key);
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
try {
|
|
117
|
+
for (const name of LEGACY_BROWSER_PREFERENCE_COOKIE_NAMES) {
|
|
118
|
+
document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`;
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeProfileSettings(settings) {
|
|
124
|
+
const candidate = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
|
125
|
+
const normalized = {};
|
|
126
|
+
if (typeof candidate.themeId === "string" && candidate.themeId.trim()) {
|
|
127
|
+
normalized.themeId = candidate.themeId.trim();
|
|
128
|
+
}
|
|
129
|
+
if (hasOwn(candidate, "sessionOwnerFilter") && candidate.sessionOwnerFilter && typeof candidate.sessionOwnerFilter === "object") {
|
|
130
|
+
normalized.sessionOwnerFilter = candidate.sessionOwnerFilter;
|
|
131
|
+
}
|
|
132
|
+
if (hasOwn(candidate, "layoutAdjustments")) {
|
|
133
|
+
normalized.layoutAdjustments = normalizeStoredLayoutAdjustments(candidate.layoutAdjustments);
|
|
134
|
+
}
|
|
135
|
+
if (hasOwn(candidate, "pinnedSessionIds")) {
|
|
136
|
+
normalized.pinnedSessionIds = normalizeStoredPinnedSessionIds(candidate.pinnedSessionIds);
|
|
137
|
+
}
|
|
138
|
+
if (hasOwn(candidate, "collapsedSessionIds")) {
|
|
139
|
+
normalized.collapsedSessionIds = normalizeStoredCollapsedSessionIdsToArray(candidate.collapsedSessionIds);
|
|
140
|
+
}
|
|
141
|
+
if (hasOwn(candidate, "activeSessionId")) {
|
|
142
|
+
const id = candidate.activeSessionId == null ? null : String(candidate.activeSessionId).trim();
|
|
143
|
+
normalized.activeSessionId = id || null;
|
|
144
|
+
}
|
|
145
|
+
if (candidate.chatViewMode === "summary" || candidate.chatViewMode === "transcript") {
|
|
146
|
+
normalized.chatViewMode = candidate.chatViewMode;
|
|
147
|
+
}
|
|
148
|
+
return normalized;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeStoredCollapsedSessionIdsToArray(value) {
|
|
152
|
+
if (value instanceof Set) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const entry of value) {
|
|
155
|
+
const id = String(entry || "").trim();
|
|
156
|
+
if (id) out.push(id);
|
|
157
|
+
}
|
|
158
|
+
out.sort();
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
if (!Array.isArray(value)) return [];
|
|
162
|
+
const seen = new Set();
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const entry of value) {
|
|
165
|
+
const id = String(entry || "").trim();
|
|
166
|
+
if (!id || seen.has(id)) continue;
|
|
167
|
+
seen.add(id);
|
|
168
|
+
out.push(id);
|
|
169
|
+
}
|
|
170
|
+
out.sort();
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function hasOwn(value, key) {
|
|
175
|
+
return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function profileSettingsFromViewState(state) {
|
|
179
|
+
return normalizeProfileSettings({
|
|
180
|
+
themeId: state.themeId,
|
|
181
|
+
sessionOwnerFilter: state.ownerFilter,
|
|
182
|
+
layoutAdjustments: {
|
|
183
|
+
paneAdjust: state.paneAdjust,
|
|
184
|
+
sessionPaneAdjust: state.sessionPaneAdjust,
|
|
185
|
+
portalSessionColumnAdjust: state.portalSessionColumnAdjust,
|
|
186
|
+
activityPaneAdjust: state.activityPaneAdjust,
|
|
187
|
+
},
|
|
188
|
+
pinnedSessionIds: state.pinnedIds,
|
|
189
|
+
collapsedSessionIds: state.collapsedSessionIds,
|
|
190
|
+
activeSessionId: state.activeSessionId,
|
|
191
|
+
chatViewMode: state.chatViewMode,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildDefaultProfileSettingsFromState(state) {
|
|
196
|
+
return normalizeProfileSettings({
|
|
197
|
+
themeId: state?.ui?.themeId,
|
|
198
|
+
sessionOwnerFilter: state?.sessions?.ownerFilter,
|
|
199
|
+
layoutAdjustments: state?.ui?.layout,
|
|
200
|
+
pinnedSessionIds: state?.sessions?.pinnedIds,
|
|
201
|
+
collapsedSessionIds: state?.sessions?.collapsedIds,
|
|
202
|
+
activeSessionId: state?.sessions?.activeSessionId,
|
|
203
|
+
chatViewMode: state?.ui?.chatViewMode,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function materializeProfileSettings(remoteSettings, defaults) {
|
|
208
|
+
const normalizedRemote = normalizeProfileSettings(remoteSettings);
|
|
209
|
+
const normalizedDefaults = normalizeProfileSettings(defaults);
|
|
210
|
+
// For chatViewMode specifically, do NOT fall back to defaults when the
|
|
211
|
+
// remote profile lacks the field. The poll runs every few seconds; if
|
|
212
|
+
// we synthesized a default here, every poll where the server hasn't
|
|
213
|
+
// yet persisted the user's toggle would clobber the local choice and
|
|
214
|
+
// snap the chat pane back from Summary to Chat. Leaving chatViewMode
|
|
215
|
+
// off the materialized settings causes the `profileSettings/apply`
|
|
216
|
+
// reducer's `hasChatViewMode` guard to preserve the current value.
|
|
217
|
+
return normalizeProfileSettings({
|
|
218
|
+
themeId: hasOwn(normalizedRemote, "themeId")
|
|
219
|
+
? normalizedRemote.themeId
|
|
220
|
+
: normalizedDefaults.themeId,
|
|
221
|
+
sessionOwnerFilter: hasOwn(normalizedRemote, "sessionOwnerFilter")
|
|
222
|
+
? normalizedRemote.sessionOwnerFilter
|
|
223
|
+
: normalizedDefaults.sessionOwnerFilter,
|
|
224
|
+
layoutAdjustments: hasOwn(normalizedRemote, "layoutAdjustments")
|
|
225
|
+
? normalizedRemote.layoutAdjustments
|
|
226
|
+
: normalizedDefaults.layoutAdjustments,
|
|
227
|
+
pinnedSessionIds: hasOwn(normalizedRemote, "pinnedSessionIds")
|
|
228
|
+
? normalizedRemote.pinnedSessionIds
|
|
229
|
+
: normalizedDefaults.pinnedSessionIds,
|
|
230
|
+
...(hasOwn(normalizedRemote, "collapsedSessionIds")
|
|
231
|
+
? { collapsedSessionIds: normalizedRemote.collapsedSessionIds }
|
|
232
|
+
: {}),
|
|
233
|
+
...(hasOwn(normalizedRemote, "activeSessionId")
|
|
234
|
+
? { activeSessionId: normalizedRemote.activeSessionId }
|
|
235
|
+
: {}),
|
|
236
|
+
...(hasOwn(normalizedRemote, "chatViewMode")
|
|
237
|
+
? { chatViewMode: normalizedRemote.chatViewMode }
|
|
238
|
+
: {}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function saveProfileSettings(controller, settings) {
|
|
243
|
+
if (typeof controller?.transport?.setCurrentUserProfileSettings !== "function") return null;
|
|
244
|
+
return controller.transport.setCurrentUserProfileSettings({
|
|
245
|
+
settings: normalizeProfileSettings(settings),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getVisibleInspectorTabs(controller) {
|
|
250
|
+
return supportsArtifactBrowser(controller)
|
|
251
|
+
? INSPECTOR_TABS
|
|
252
|
+
: INSPECTOR_TABS.filter((tab) => tab !== "files");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function shallowEqualObject(left, right) {
|
|
256
|
+
if (Object.is(left, right)) return true;
|
|
257
|
+
if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
|
|
258
|
+
const leftKeys = Object.keys(left);
|
|
259
|
+
const rightKeys = Object.keys(right);
|
|
260
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
261
|
+
for (const key of leftKeys) {
|
|
262
|
+
if (!Object.prototype.hasOwnProperty.call(right, key)) return false;
|
|
263
|
+
if (!Object.is(left[key], right[key])) return false;
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function clampNumber(value, min, max) {
|
|
269
|
+
return Math.max(min, Math.min(max, value));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function portalSessionColumnBounds(width) {
|
|
273
|
+
const safeWidth = Math.max(0, Number(width) || 0);
|
|
274
|
+
const availableWidth = Math.max(
|
|
275
|
+
0,
|
|
276
|
+
safeWidth - PORTAL_SESSION_CHAT_DIVIDER_PX - (PORTAL_WORKSPACE_GAP_PX * 2),
|
|
277
|
+
);
|
|
278
|
+
const baseWidth = availableWidth * PORTAL_SESSION_COLUMN_RATIO;
|
|
279
|
+
const maxSessionWidth = Math.max(0, availableWidth - PORTAL_MIN_CHAT_COLUMN_PX);
|
|
280
|
+
return {
|
|
281
|
+
minAdjust: -baseWidth,
|
|
282
|
+
maxAdjust: maxSessionWidth - baseWidth,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function portalSessionColumnWidth(width, adjust) {
|
|
287
|
+
const bounds = portalSessionColumnBounds(width);
|
|
288
|
+
const safeWidth = Math.max(0, Number(width) || 0);
|
|
289
|
+
const availableWidth = Math.max(
|
|
290
|
+
0,
|
|
291
|
+
safeWidth - PORTAL_SESSION_CHAT_DIVIDER_PX - (PORTAL_WORKSPACE_GAP_PX * 2),
|
|
292
|
+
);
|
|
293
|
+
const baseWidth = availableWidth * PORTAL_SESSION_COLUMN_RATIO;
|
|
294
|
+
return clampNumber(baseWidth + (Number(adjust) || 0), 0, baseWidth + bounds.maxAdjust);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function portalSessionColumnMode(width) {
|
|
298
|
+
if (width <= PORTAL_SESSION_HIDDEN_COLUMN_PX) return "hidden";
|
|
299
|
+
if (width <= PORTAL_SESSION_COMPACT_COLUMN_PX) return "compact";
|
|
300
|
+
return "wrap";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function getStatePromptRows(state) {
|
|
304
|
+
const promptRows = Number(state?.ui?.promptRows);
|
|
305
|
+
return Number.isFinite(promptRows) && promptRows > 0
|
|
306
|
+
? promptRows
|
|
307
|
+
: getPromptInputRows(state?.ui?.prompt || "");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function computeStateLayout(state) {
|
|
311
|
+
return computeLegacyLayout({
|
|
312
|
+
width: state.ui.layout?.viewportWidth ?? 120,
|
|
313
|
+
height: state.ui.layout?.viewportHeight ?? 40,
|
|
314
|
+
}, state.ui.layout?.paneAdjust ?? 0, getStatePromptRows(state), state.ui.layout?.sessionPaneAdjust ?? 0);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function getPortalInspectorContentWidth(paneWidth, inspectorTab) {
|
|
318
|
+
// Sequence uses preserved-width lines inside padded, scroll-synced containers.
|
|
319
|
+
// Reserve a few extra columns so content that fits visually does not trip a cosmetic x-scrollbar.
|
|
320
|
+
const overflowGuardColumns = inspectorTab === "sequence" ? 3 : 0;
|
|
321
|
+
return Math.max(20, paneWidth - 4 - overflowGuardColumns);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function normalizeLines(lines) {
|
|
325
|
+
const normalized = [];
|
|
326
|
+
|
|
327
|
+
const decodeEscapedNewlines = (value) => {
|
|
328
|
+
const text = String(value || "");
|
|
329
|
+
if (!text.includes("\\n") || text.includes("\n")) return text;
|
|
330
|
+
return text.replace(/\\n/g, "\n");
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
for (const line of lines || []) {
|
|
334
|
+
if (line?.kind === "markup") {
|
|
335
|
+
// Markup lines carry preformatted art (splash screens). Tag them
|
|
336
|
+
// so the chat renderer keeps each line intact: the pane's
|
|
337
|
+
// pre-wrap/overflow-wrap rules would rewrap wide art lines at
|
|
338
|
+
// arbitrary points and scramble the image.
|
|
339
|
+
for (const parsedLine of parseTerminalMarkupRuns(line.value || "")) {
|
|
340
|
+
normalized.push({ kind: "runs", runs: parsedLine, preserve: true });
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
// Sentinel kinds preserved as-is so parseStructuredChatBlocks can
|
|
345
|
+
// recognize and render them (e.g. markdownTable → HTML <table>).
|
|
346
|
+
if (line?.kind === "markdownTable") {
|
|
347
|
+
normalized.push(line);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (line?.kind === "runs") {
|
|
351
|
+
const runs = Array.isArray(line.runs) ? line.runs : [];
|
|
352
|
+
if (runs.length === 1) {
|
|
353
|
+
const onlyRun = runs[0] || {};
|
|
354
|
+
const decodedText = decodeEscapedNewlines(onlyRun.text || "");
|
|
355
|
+
if (decodedText.includes("\n")) {
|
|
356
|
+
const fragments = decodedText.split("\n");
|
|
357
|
+
for (const fragment of fragments) {
|
|
358
|
+
normalized.push({
|
|
359
|
+
kind: "runs",
|
|
360
|
+
runs: [{ ...onlyRun, text: fragment }],
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
normalized.push({ kind: "runs", runs: [{ ...onlyRun, text: decodedText }] });
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
normalized.push({
|
|
369
|
+
kind: "runs",
|
|
370
|
+
runs: runs.map((run) => ({
|
|
371
|
+
...run,
|
|
372
|
+
text: decodeEscapedNewlines(run?.text || ""),
|
|
373
|
+
})),
|
|
374
|
+
});
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (Array.isArray(line)) {
|
|
378
|
+
normalized.push({ kind: "runs", runs: line });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (line && typeof line === "object" && typeof line.text === "string") {
|
|
382
|
+
const decodedText = decodeEscapedNewlines(line.text);
|
|
383
|
+
if (decodedText.includes("\n")) {
|
|
384
|
+
const fragments = decodedText.split("\n");
|
|
385
|
+
for (const fragment of fragments) {
|
|
386
|
+
normalized.push({ kind: "text", ...line, text: fragment });
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
normalized.push({ kind: "text", ...line, text: decodedText });
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
normalized.push({ kind: "text", ...line });
|
|
394
|
+
}
|
|
395
|
+
return normalized;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function resolveColor(theme, token) {
|
|
399
|
+
if (!token) return undefined;
|
|
400
|
+
return theme?.tui?.[token] || theme?.terminal?.[token] || theme?.page?.[token] || token;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function runsToText(runs = []) {
|
|
404
|
+
return runs.map((run) => String(run?.text || "")).join("");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function flattenTitleText(title) {
|
|
408
|
+
if (Array.isArray(title)) return runsToText(title);
|
|
409
|
+
return String(title || "");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function compactTitleRuns(title, maxWidth = 40) {
|
|
413
|
+
if (!Array.isArray(title)) {
|
|
414
|
+
const text = String(title || "");
|
|
415
|
+
return [{ text: text.length > maxWidth ? `${text.slice(0, maxWidth - 1)}…` : text, color: "white", bold: true }];
|
|
416
|
+
}
|
|
417
|
+
const compactRuns = [];
|
|
418
|
+
let remaining = Math.max(8, maxWidth);
|
|
419
|
+
for (const run of title) {
|
|
420
|
+
if (remaining <= 0) break;
|
|
421
|
+
const color = run?.color;
|
|
422
|
+
if (color === "gray" && compactRuns.length > 0) continue;
|
|
423
|
+
const text = String(run?.text || "");
|
|
424
|
+
if (!text) continue;
|
|
425
|
+
const chunk = text.length > remaining && remaining > 1
|
|
426
|
+
? `${text.slice(0, remaining - 1)}…`
|
|
427
|
+
: text.slice(0, remaining);
|
|
428
|
+
if (!chunk) continue;
|
|
429
|
+
compactRuns.push({ ...run, text: chunk });
|
|
430
|
+
remaining -= chunk.length;
|
|
431
|
+
}
|
|
432
|
+
return compactRuns.length > 0 ? compactRuns : title;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function applyDocumentTheme(themeId) {
|
|
436
|
+
const theme = getTheme(themeId);
|
|
437
|
+
if (!theme || typeof document === "undefined") return;
|
|
438
|
+
const root = document.documentElement;
|
|
439
|
+
root.style.setProperty("--ps-page-background", theme.page.background);
|
|
440
|
+
root.style.setProperty("--ps-page-foreground", theme.page.foreground);
|
|
441
|
+
root.style.setProperty("--ps-surface", theme.tui.surface);
|
|
442
|
+
root.style.setProperty("--ps-background", theme.tui.background);
|
|
443
|
+
root.style.setProperty("--ps-foreground", theme.tui.foreground);
|
|
444
|
+
root.style.setProperty("--ps-muted", theme.tui.gray);
|
|
445
|
+
root.style.setProperty("--ps-border", theme.tui.border || theme.tui.gray);
|
|
446
|
+
root.style.setProperty("--ps-selection-background", theme.tui.selectionBackground);
|
|
447
|
+
root.style.setProperty("--ps-selection-foreground", theme.tui.selectionForeground);
|
|
448
|
+
root.style.setProperty("--ps-highlight-background", theme.tui.activeHighlightBackground);
|
|
449
|
+
root.style.setProperty("--ps-highlight-foreground", theme.tui.activeHighlightForeground);
|
|
450
|
+
root.style.setProperty("--ps-modal-backdrop", theme.page.modalBackdrop);
|
|
451
|
+
root.style.setProperty("--ps-modal-background", theme.page.modalBackground);
|
|
452
|
+
root.style.setProperty("--ps-modal-border", theme.page.modalBorder);
|
|
453
|
+
root.style.setProperty("--ps-modal-foreground", theme.page.modalForeground);
|
|
454
|
+
root.style.setProperty("--ps-modal-muted", theme.page.modalMuted);
|
|
455
|
+
root.style.setProperty("--ps-modal-selected-background", theme.page.modalSelectedBackground);
|
|
456
|
+
root.style.setProperty("--ps-modal-selected-border", theme.page.modalSelectedBorder);
|
|
457
|
+
root.style.setProperty("--ps-modal-selected-foreground", theme.page.modalSelectedForeground);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function useMeasuredViewport(ref) {
|
|
461
|
+
const [viewport, setViewport] = React.useState({ width: 0, height: 0 });
|
|
462
|
+
|
|
463
|
+
React.useLayoutEffect(() => {
|
|
464
|
+
const element = ref.current;
|
|
465
|
+
if (!element) return undefined;
|
|
466
|
+
|
|
467
|
+
const update = () => {
|
|
468
|
+
setViewport({
|
|
469
|
+
width: element.clientWidth,
|
|
470
|
+
height: element.clientHeight,
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
update();
|
|
475
|
+
const observer = new ResizeObserver(update);
|
|
476
|
+
observer.observe(element);
|
|
477
|
+
window.addEventListener("resize", update);
|
|
478
|
+
return () => {
|
|
479
|
+
observer.disconnect();
|
|
480
|
+
window.removeEventListener("resize", update);
|
|
481
|
+
};
|
|
482
|
+
}, [ref]);
|
|
483
|
+
|
|
484
|
+
return viewport;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function computeGridViewport(viewport) {
|
|
488
|
+
const width = Math.max(320, viewport.width || window.innerWidth || 1280);
|
|
489
|
+
const height = Math.max(320, viewport.height || window.innerHeight || 800);
|
|
490
|
+
return {
|
|
491
|
+
width: Math.max(40, Math.floor(width / GRID_CELL_WIDTH)),
|
|
492
|
+
height: Math.max(18, Math.floor(height / GRID_CELL_HEIGHT)),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function getScrollDistanceToBottom(node) {
|
|
497
|
+
if (!node) return 0;
|
|
498
|
+
const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
|
|
499
|
+
return Math.max(0, maxScroll - node.scrollTop);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function isScrollViewportAtBottom(node) {
|
|
503
|
+
return getScrollDistanceToBottom(node) <= SCROLL_BOTTOM_EPSILON_PX;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function useScrollSync(ref, lines, scrollOffset, scrollMode, paneKey, controller, { stickyBottom = false } = {}) {
|
|
507
|
+
const normalizedLines = React.useMemo(() => normalizeLines(lines), [lines]);
|
|
508
|
+
// Programmatic scrollTop assignments fire a 'scroll' event that would
|
|
509
|
+
// otherwise call onScroll → dispatch ui/scroll → clobber the user's
|
|
510
|
+
// offset (especially when content briefly shrinks during a refresh
|
|
511
|
+
// and clamps nextScrollTop downward). The flag tells onScroll to
|
|
512
|
+
// ignore the matching scroll event after we set scrollTop ourselves.
|
|
513
|
+
const programmaticScrollRef = React.useRef(null);
|
|
514
|
+
const previousViewportStateRef = React.useRef({
|
|
515
|
+
scrollMode,
|
|
516
|
+
scrollOffset,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
React.useLayoutEffect(() => {
|
|
520
|
+
const node = ref.current;
|
|
521
|
+
if (!node) return;
|
|
522
|
+
const previousViewportState = previousViewportStateRef.current;
|
|
523
|
+
const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
|
|
524
|
+
const preservePausedStickyScroll = stickyBottom
|
|
525
|
+
&& scrollMode === "top"
|
|
526
|
+
&& previousViewportState?.scrollMode === "top"
|
|
527
|
+
&& previousViewportState?.scrollOffset === scrollOffset;
|
|
528
|
+
const offsetPixels = Math.max(0, Number(scrollOffset) || 0) * SCROLL_ROW_HEIGHT;
|
|
529
|
+
const nextScrollTop = preservePausedStickyScroll
|
|
530
|
+
? Math.max(0, Math.min(node.scrollTop, maxScroll))
|
|
531
|
+
: scrollMode === "bottom"
|
|
532
|
+
? Math.max(0, maxScroll - offsetPixels)
|
|
533
|
+
: Math.min(maxScroll, offsetPixels);
|
|
534
|
+
if (Math.abs(node.scrollTop - nextScrollTop) > PROGRAMMATIC_SCROLL_TOLERANCE_PX) {
|
|
535
|
+
const pendingProgrammaticScroll = { target: nextScrollTop };
|
|
536
|
+
programmaticScrollRef.current = pendingProgrammaticScroll;
|
|
537
|
+
node.scrollTop = nextScrollTop;
|
|
538
|
+
// Clear on the next frame — the scroll event from the assignment
|
|
539
|
+
// above is queued synchronously and handled in the same task.
|
|
540
|
+
window.requestAnimationFrame(() => {
|
|
541
|
+
if (programmaticScrollRef.current === pendingProgrammaticScroll) {
|
|
542
|
+
programmaticScrollRef.current = null;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
previousViewportStateRef.current = {
|
|
547
|
+
scrollMode,
|
|
548
|
+
scrollOffset,
|
|
549
|
+
};
|
|
550
|
+
}, [normalizedLines, ref, scrollMode, scrollOffset, stickyBottom]);
|
|
551
|
+
|
|
552
|
+
const onScroll = React.useCallback(() => {
|
|
553
|
+
const node = ref.current;
|
|
554
|
+
if (!node || !paneKey) return;
|
|
555
|
+
const pendingProgrammatic = programmaticScrollRef.current;
|
|
556
|
+
if (pendingProgrammatic) {
|
|
557
|
+
if (Math.abs(node.scrollTop - pendingProgrammatic.target) <= PROGRAMMATIC_SCROLL_TOLERANCE_PX) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
programmaticScrollRef.current = null;
|
|
561
|
+
}
|
|
562
|
+
const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
|
|
563
|
+
// When the pane has no scrollable content (transient loading state
|
|
564
|
+
// that briefly collapses the body to one line), the browser auto-
|
|
565
|
+
// clamps scrollTop to 0 and fires a scroll event we did NOT trigger.
|
|
566
|
+
// Writing that into state would clobber the user's saved offset.
|
|
567
|
+
// Skip the dispatch — next render with real content will restore
|
|
568
|
+
// the desired scroll position from preserved state.
|
|
569
|
+
if (maxScroll <= 0) return;
|
|
570
|
+
if (stickyBottom && typeof controller.updatePaneScrollFromViewport === "function") {
|
|
571
|
+
controller.updatePaneScrollFromViewport(
|
|
572
|
+
paneKey,
|
|
573
|
+
Math.max(0, node.scrollTop) / SCROLL_ROW_HEIGHT,
|
|
574
|
+
{ atBottom: isScrollViewportAtBottom(node) },
|
|
575
|
+
);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const pixels = scrollMode === "bottom"
|
|
580
|
+
? Math.max(0, maxScroll - node.scrollTop)
|
|
581
|
+
: Math.max(0, node.scrollTop);
|
|
582
|
+
controller.dispatch({
|
|
583
|
+
type: "ui/scroll",
|
|
584
|
+
pane: paneKey,
|
|
585
|
+
offset: pixels / SCROLL_ROW_HEIGHT,
|
|
586
|
+
});
|
|
587
|
+
if (paneKey === "chat" && scrollMode === "bottom" && node.scrollTop <= PROGRAMMATIC_SCROLL_TOLERANCE_PX) {
|
|
588
|
+
controller.armChatTopHistoryLoad?.();
|
|
589
|
+
}
|
|
590
|
+
}, [controller, paneKey, ref, scrollMode, stickyBottom]);
|
|
591
|
+
|
|
592
|
+
const onWheel = React.useCallback((event) => {
|
|
593
|
+
const node = ref.current;
|
|
594
|
+
if (!node || paneKey !== "chat" || scrollMode !== "bottom" || event.deltaY >= 0) return;
|
|
595
|
+
if (node.scrollTop > PROGRAMMATIC_SCROLL_TOLERANCE_PX) return;
|
|
596
|
+
const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
|
|
597
|
+
controller.handleChatTopHistoryScrollIntent?.(maxScroll / SCROLL_ROW_HEIGHT);
|
|
598
|
+
}, [controller, paneKey, ref, scrollMode]);
|
|
599
|
+
|
|
600
|
+
return { normalizedLines, onScroll, onWheel };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function Runs({ runs, theme }) {
|
|
604
|
+
return React.createElement(React.Fragment, null,
|
|
605
|
+
(runs || []).map((run, index) => {
|
|
606
|
+
const style = {
|
|
607
|
+
color: resolveColor(theme, run.color),
|
|
608
|
+
backgroundColor: resolveColor(theme, run.backgroundColor),
|
|
609
|
+
fontWeight: run.bold ? 700 : 400,
|
|
610
|
+
textDecoration: run.underline ? "underline" : "none",
|
|
611
|
+
};
|
|
612
|
+
const href = String(run?.href || "").trim();
|
|
613
|
+
const isExternalHref = /^https?:\/\//i.test(href);
|
|
614
|
+
|
|
615
|
+
return isExternalHref
|
|
616
|
+
? React.createElement("a", {
|
|
617
|
+
key: `${index}:${run.text || ""}`,
|
|
618
|
+
className: "ps-md-link",
|
|
619
|
+
href,
|
|
620
|
+
target: "_blank",
|
|
621
|
+
rel: "noreferrer",
|
|
622
|
+
style,
|
|
623
|
+
}, run.text || "")
|
|
624
|
+
: React.createElement("span", {
|
|
625
|
+
key: `${index}:${run.text || ""}`,
|
|
626
|
+
style,
|
|
627
|
+
}, run.text || "");
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function SystemNoticeLine({ line, theme }) {
|
|
633
|
+
const body = String(line?.body || "").trim();
|
|
634
|
+
return React.createElement("details", { className: "ps-system-notice" },
|
|
635
|
+
React.createElement("summary", {
|
|
636
|
+
className: "ps-system-notice-summary",
|
|
637
|
+
style: { color: resolveColor(theme, line?.color) || "var(--ps-muted)" },
|
|
638
|
+
},
|
|
639
|
+
React.createElement("span", { className: "ps-system-notice-summary-text" }, line?.text || "System notice")),
|
|
640
|
+
body
|
|
641
|
+
? React.createElement("div", { className: "ps-system-notice-body" },
|
|
642
|
+
React.createElement(MarkdownPreviewContent, { content: body, theme }))
|
|
643
|
+
: null);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function Line({ line, theme, className = "" }) {
|
|
647
|
+
const lineClassName = className ? `ps-line ${className}` : "ps-line";
|
|
648
|
+
if (!line) {
|
|
649
|
+
return React.createElement("div", { className: lineClassName }, " ");
|
|
650
|
+
}
|
|
651
|
+
if (line.kind === "systemNotice") {
|
|
652
|
+
return React.createElement(SystemNoticeLine, { line, theme });
|
|
653
|
+
}
|
|
654
|
+
if (line.kind === "runs") {
|
|
655
|
+
return React.createElement("div", { className: lineClassName },
|
|
656
|
+
React.createElement(Runs, { runs: line.runs, theme }));
|
|
657
|
+
}
|
|
658
|
+
return React.createElement("div", {
|
|
659
|
+
className: lineClassName,
|
|
660
|
+
style: {
|
|
661
|
+
color: resolveColor(theme, line.color),
|
|
662
|
+
backgroundColor: resolveColor(theme, line.backgroundColor),
|
|
663
|
+
fontWeight: line.bold ? 700 : 400,
|
|
664
|
+
textDecoration: line.underline ? "underline" : "none",
|
|
665
|
+
},
|
|
666
|
+
}, line.text || " ");
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function lineText(line) {
|
|
670
|
+
if (!line) return "";
|
|
671
|
+
if (line.kind === "runs") return runsToText(line.runs);
|
|
672
|
+
return String(line.text || "");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function usePanePixelScroll(ref, scrollOffset, paneKey, controller) {
|
|
676
|
+
// See useScrollSync for rationale on the programmatic-scroll guard.
|
|
677
|
+
const programmaticScrollRef = React.useRef(false);
|
|
678
|
+
|
|
679
|
+
React.useLayoutEffect(() => {
|
|
680
|
+
const node = ref.current;
|
|
681
|
+
if (!node) return;
|
|
682
|
+
const maxScroll = Math.max(0, node.scrollHeight - node.clientHeight);
|
|
683
|
+
const nextScrollTop = Math.min(maxScroll, Math.max(0, Number(scrollOffset) || 0) * SCROLL_ROW_HEIGHT);
|
|
684
|
+
if (Math.abs(node.scrollTop - nextScrollTop) > 2) {
|
|
685
|
+
programmaticScrollRef.current = true;
|
|
686
|
+
node.scrollTop = nextScrollTop;
|
|
687
|
+
window.requestAnimationFrame(() => {
|
|
688
|
+
programmaticScrollRef.current = false;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}, [ref, scrollOffset]);
|
|
692
|
+
|
|
693
|
+
return React.useCallback(() => {
|
|
694
|
+
if (programmaticScrollRef.current) return;
|
|
695
|
+
const node = ref.current;
|
|
696
|
+
if (!node || !paneKey) return;
|
|
697
|
+
// See useScrollSync — ignore browser auto-clamp during a
|
|
698
|
+
// transient empty/loading body.
|
|
699
|
+
if (node.scrollHeight <= node.clientHeight) return;
|
|
700
|
+
controller.dispatch({
|
|
701
|
+
type: "ui/scroll",
|
|
702
|
+
pane: paneKey,
|
|
703
|
+
offset: Math.max(0, node.scrollTop) / SCROLL_ROW_HEIGHT,
|
|
704
|
+
});
|
|
705
|
+
}, [controller, paneKey, ref]);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function renderInlineMarkdown(source, theme, keyPrefix = "md") {
|
|
709
|
+
return tokenizeInlineMarkdown(source).map((token, index) => {
|
|
710
|
+
const key = `${keyPrefix}:${index}`;
|
|
711
|
+
if (token.type === "code") {
|
|
712
|
+
return React.createElement("code", { key, className: "ps-md-inline-code" }, token.text);
|
|
713
|
+
}
|
|
714
|
+
if (token.type === "strong") {
|
|
715
|
+
return React.createElement("strong", { key, className: "ps-md-strong" }, renderInlineMarkdown(token.text, theme, `${key}:strong`));
|
|
716
|
+
}
|
|
717
|
+
if (token.type === "em") {
|
|
718
|
+
return React.createElement("em", { key, className: "ps-md-em" }, renderInlineMarkdown(token.text, theme, `${key}:em`));
|
|
719
|
+
}
|
|
720
|
+
if (token.type === "link") {
|
|
721
|
+
return React.createElement("a", {
|
|
722
|
+
key,
|
|
723
|
+
className: "ps-md-link",
|
|
724
|
+
href: token.href,
|
|
725
|
+
target: "_blank",
|
|
726
|
+
rel: "noreferrer",
|
|
727
|
+
style: { color: resolveColor(theme, "cyan") },
|
|
728
|
+
}, token.text);
|
|
729
|
+
}
|
|
730
|
+
return React.createElement(React.Fragment, { key }, token.text);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function normalizeTableCellText(value = "") {
|
|
735
|
+
return String(value || "")
|
|
736
|
+
.replace(/\s+/g, " ")
|
|
737
|
+
.trim();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const FIT_WIDTH_FLEX_HEADER_KEYWORDS = [
|
|
741
|
+
"description",
|
|
742
|
+
"mechanism",
|
|
743
|
+
"details",
|
|
744
|
+
"notes",
|
|
745
|
+
"summary",
|
|
746
|
+
"message",
|
|
747
|
+
"comment",
|
|
748
|
+
"reason",
|
|
749
|
+
"body",
|
|
750
|
+
"content",
|
|
751
|
+
"explanation",
|
|
752
|
+
"rationale",
|
|
753
|
+
"one-liner",
|
|
754
|
+
];
|
|
755
|
+
|
|
756
|
+
const FIT_WIDTH_FLEX_MIN_MAX_LEN = 24;
|
|
757
|
+
const FIT_WIDTH_RIGID_CHAR_CAP = 32;
|
|
758
|
+
const FIT_WIDTH_MIN_RIGID_CHARS = 6;
|
|
759
|
+
const FIT_WIDTH_FLEX_MIN_CHARS = 30;
|
|
760
|
+
const FIT_WIDTH_FLEX_MAX_CHARS = 56;
|
|
761
|
+
const FIT_WIDTH_FLEX_MIN_FRACTION = 0.4;
|
|
762
|
+
const FIT_WIDTH_FLEX_TO_RIGID_RATIO = 1.4;
|
|
763
|
+
const FIT_WIDTH_MIN_EXTRA_CHARS = 2;
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Compute per-column layout for a fit-width markdown / chat table.
|
|
767
|
+
*
|
|
768
|
+
* Strategy:
|
|
769
|
+
* 1. Measure max cell length and "wrappability" (cells with whitespace) per column.
|
|
770
|
+
* 2. Identify a single "flex" column — long, prose-like, ideally with a
|
|
771
|
+
* header keyword like Description / Mechanism / Notes. This column
|
|
772
|
+
* absorbs overflow by wrapping aggressively.
|
|
773
|
+
* 3. Give every other column a budget = clamp(maxLen + padding, MIN, RIGID_CAP),
|
|
774
|
+
* so rigid columns stay at their content-fit width even when one
|
|
775
|
+
* sibling column has hundreds of characters of prose.
|
|
776
|
+
* 4. Give the flex column a bounded share of the rigid-column budget. A
|
|
777
|
+
* very long cell should wrap; it should not steal so much percentage
|
|
778
|
+
* width that compact columns like Count / Status collapse on phones.
|
|
779
|
+
* 5. Return a minimum table width in ch so the wrapper can scroll
|
|
780
|
+
* horizontally rather than forcing all columns below readable size.
|
|
781
|
+
*
|
|
782
|
+
* Returns { widths: ["12.34%", ...], flexIndex: number, minWidth: "64ch" }
|
|
783
|
+
* when a flex column is identified — the table renderer then forces
|
|
784
|
+
* table-layout: fixed, adds an `is-flex-column` class to the chosen column,
|
|
785
|
+
* and gives the table a readable minimum width. Returns null when no flex
|
|
786
|
+
* column is found, in which case the renderer falls back to the browser's
|
|
787
|
+
* auto-table-layout (which is already good for short / uniform tables).
|
|
788
|
+
*
|
|
789
|
+
* Background: the previous behavior used the browser's auto-table-layout
|
|
790
|
+
* unconditionally. That works well when columns are uniform but is biased
|
|
791
|
+
* toward wide columns when one column has prose hundreds of characters
|
|
792
|
+
* long (e.g. a Mechanism / Description column) — auto-layout distributes
|
|
793
|
+
* width proportional to (max-content − min-content), which lets the prose
|
|
794
|
+
* column squeeze the rigid columns down to a few characters each.
|
|
795
|
+
*/
|
|
796
|
+
function computeFitWidthColumnLayout(rows = []) {
|
|
797
|
+
const columnCount = Math.max(0, ...rows.map((row) => row.length));
|
|
798
|
+
if (columnCount <= 0) return null;
|
|
799
|
+
|
|
800
|
+
const headerRow = rows[0] || [];
|
|
801
|
+
const dataRowCount = Math.max(1, rows.length - 1);
|
|
802
|
+
|
|
803
|
+
const stats = Array.from({ length: columnCount }, () => ({
|
|
804
|
+
max: 0,
|
|
805
|
+
spaceCells: 0,
|
|
806
|
+
}));
|
|
807
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
808
|
+
const row = rows[rowIndex];
|
|
809
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
810
|
+
const text = normalizeTableCellText(row[columnIndex] || "");
|
|
811
|
+
if (text.length > stats[columnIndex].max) stats[columnIndex].max = text.length;
|
|
812
|
+
if (rowIndex > 0 && /\s/.test(text)) stats[columnIndex].spaceCells += 1;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
let flexIndex = -1;
|
|
817
|
+
let flexScore = 0;
|
|
818
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
819
|
+
const stat = stats[columnIndex];
|
|
820
|
+
if (stat.max < FIT_WIDTH_FLEX_MIN_MAX_LEN) continue;
|
|
821
|
+
const spaceRatio = stat.spaceCells / dataRowCount;
|
|
822
|
+
if (spaceRatio < 0.4) continue;
|
|
823
|
+
const headerText = normalizeTableCellText(headerRow[columnIndex] || "").toLowerCase();
|
|
824
|
+
const headerBonus = FIT_WIDTH_FLEX_HEADER_KEYWORDS.some((keyword) => headerText.includes(keyword)) ? 60 : 0;
|
|
825
|
+
const score = stat.max + headerBonus;
|
|
826
|
+
if (score > flexScore) {
|
|
827
|
+
flexScore = score;
|
|
828
|
+
flexIndex = columnIndex;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (flexIndex < 0) return null;
|
|
833
|
+
|
|
834
|
+
const rigidBudgets = stats.map((stat, columnIndex) => {
|
|
835
|
+
if (columnIndex === flexIndex) return 0;
|
|
836
|
+
return Math.max(FIT_WIDTH_MIN_RIGID_CHARS, Math.min(stat.max + 2, FIT_WIDTH_RIGID_CHAR_CAP));
|
|
837
|
+
});
|
|
838
|
+
const sumRigid = rigidBudgets.reduce((sum, value) => sum + value, 0);
|
|
839
|
+
const flexStat = stats[flexIndex];
|
|
840
|
+
const flexBudget = Math.max(
|
|
841
|
+
FIT_WIDTH_FLEX_MIN_CHARS,
|
|
842
|
+
Math.min(
|
|
843
|
+
flexStat.max * 0.45,
|
|
844
|
+
FIT_WIDTH_FLEX_MAX_CHARS,
|
|
845
|
+
Math.max(FIT_WIDTH_FLEX_MIN_CHARS, sumRigid * FIT_WIDTH_FLEX_TO_RIGID_RATIO),
|
|
846
|
+
),
|
|
847
|
+
);
|
|
848
|
+
const budgets = rigidBudgets.map((value, columnIndex) => (
|
|
849
|
+
columnIndex === flexIndex ? flexBudget : value
|
|
850
|
+
));
|
|
851
|
+
|
|
852
|
+
const totalBudget = budgets.reduce((sum, value) => sum + value, 0);
|
|
853
|
+
if (totalBudget > 0 && budgets[flexIndex] / totalBudget < FIT_WIDTH_FLEX_MIN_FRACTION) {
|
|
854
|
+
const sumRigid = totalBudget - budgets[flexIndex];
|
|
855
|
+
budgets[flexIndex] = sumRigid * (FIT_WIDTH_FLEX_MIN_FRACTION / (1 - FIT_WIDTH_FLEX_MIN_FRACTION));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const finalTotal = budgets.reduce((sum, value) => sum + value, 0);
|
|
859
|
+
if (!(finalTotal > 0)) return null;
|
|
860
|
+
return {
|
|
861
|
+
widths: budgets.map((value) => `${((value / finalTotal) * 100).toFixed(2)}%`),
|
|
862
|
+
flexIndex,
|
|
863
|
+
minWidth: `${Math.ceil(finalTotal + FIT_WIDTH_MIN_EXTRA_CHARS)}ch`,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function buildTableColumnLabels(headerRows = [], columnCount = 0) {
|
|
868
|
+
return Array.from({ length: columnCount }, (_, columnIndex) => {
|
|
869
|
+
const label = (Array.isArray(headerRows) ? headerRows : [])
|
|
870
|
+
.map((row) => normalizeTableCellText(row?.[columnIndex] || ""))
|
|
871
|
+
.filter(Boolean)
|
|
872
|
+
.join(" / ");
|
|
873
|
+
return label || null;
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function isMarkdownSpecialLine(line = "", nextLine = "") {
|
|
878
|
+
const value = String(line || "");
|
|
879
|
+
return /^\s*#{1,6}\s+/.test(value)
|
|
880
|
+
|| /^\s*>/.test(value)
|
|
881
|
+
|| /^\s*([-*]|\d+\.)\s+/.test(value)
|
|
882
|
+
|| /^\s*```/.test(value)
|
|
883
|
+
|| (value.includes("|") && /^\s*\|?[\s:-]+(?:\|[\s:-]+)+\|?\s*$/.test(String(nextLine || "")));
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function splitMarkdownTableRow(line = "") {
|
|
887
|
+
const trimmed = String(line || "").trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
888
|
+
return trimmed.split("|").map((cell) => cell.trim());
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function parseMarkdownBlocks(source = "") {
|
|
892
|
+
const text = String(source || "").replace(/\r\n?/g, "\n");
|
|
893
|
+
const lines = text.split("\n");
|
|
894
|
+
const blocks = [];
|
|
895
|
+
|
|
896
|
+
for (let index = 0; index < lines.length;) {
|
|
897
|
+
const line = lines[index];
|
|
898
|
+
const trimmed = line.trim();
|
|
899
|
+
|
|
900
|
+
if (!trimmed) {
|
|
901
|
+
index += 1;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const fenceMatch = /^```(\S*)\s*$/.exec(trimmed);
|
|
906
|
+
if (fenceMatch) {
|
|
907
|
+
const language = fenceMatch[1] || "";
|
|
908
|
+
const codeLines = [];
|
|
909
|
+
index += 1;
|
|
910
|
+
while (index < lines.length && !/^```/.test(lines[index].trim())) {
|
|
911
|
+
codeLines.push(lines[index]);
|
|
912
|
+
index += 1;
|
|
913
|
+
}
|
|
914
|
+
if (index < lines.length) index += 1;
|
|
915
|
+
blocks.push({ type: "code", language, content: codeLines.join("\n") });
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const headingMatch = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
920
|
+
if (headingMatch) {
|
|
921
|
+
blocks.push({ type: "heading", level: headingMatch[1].length, text: headingMatch[2].trim() });
|
|
922
|
+
index += 1;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (line.includes("|") && index + 1 < lines.length && /^\s*\|?[\s:-]+(?:\|[\s:-]+)+\|?\s*$/.test(lines[index + 1].trim())) {
|
|
927
|
+
const header = splitMarkdownTableRow(line);
|
|
928
|
+
index += 2;
|
|
929
|
+
const rows = [];
|
|
930
|
+
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
931
|
+
rows.push(splitMarkdownTableRow(lines[index]));
|
|
932
|
+
index += 1;
|
|
933
|
+
}
|
|
934
|
+
blocks.push({ type: "table", header, rows });
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (/^\s*>/.test(line)) {
|
|
939
|
+
const quoteLines = [];
|
|
940
|
+
while (index < lines.length && /^\s*>/.test(lines[index])) {
|
|
941
|
+
quoteLines.push(lines[index].replace(/^\s*>\s?/, ""));
|
|
942
|
+
index += 1;
|
|
943
|
+
}
|
|
944
|
+
blocks.push({ type: "blockquote", text: quoteLines.join("\n").trim() });
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const listMatch = /^(\s*)([-*]|\d+\.)\s+(.*)$/.exec(line);
|
|
949
|
+
if (listMatch) {
|
|
950
|
+
const ordered = /\d+\./.test(listMatch[2]);
|
|
951
|
+
const items = [];
|
|
952
|
+
while (index < lines.length) {
|
|
953
|
+
const current = lines[index];
|
|
954
|
+
const itemMatch = /^(\s*)([-*]|\d+\.)\s+(.*)$/.exec(current);
|
|
955
|
+
if (!itemMatch || /\d+\./.test(itemMatch[2]) !== ordered) break;
|
|
956
|
+
const itemLines = [itemMatch[3].trim()];
|
|
957
|
+
index += 1;
|
|
958
|
+
while (
|
|
959
|
+
index < lines.length
|
|
960
|
+
&& lines[index].trim()
|
|
961
|
+
&& !/^(\s*)([-*]|\d+\.)\s+/.test(lines[index])
|
|
962
|
+
&& !isMarkdownSpecialLine(lines[index], lines[index + 1] || "")
|
|
963
|
+
) {
|
|
964
|
+
itemLines.push(lines[index].trim());
|
|
965
|
+
index += 1;
|
|
966
|
+
}
|
|
967
|
+
items.push(itemLines.join(" "));
|
|
968
|
+
if (!lines[index]?.trim()) break;
|
|
969
|
+
}
|
|
970
|
+
blocks.push({ type: "list", ordered, items });
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const paragraphLines = [line.trim()];
|
|
975
|
+
index += 1;
|
|
976
|
+
while (
|
|
977
|
+
index < lines.length
|
|
978
|
+
&& lines[index].trim()
|
|
979
|
+
&& !isMarkdownSpecialLine(lines[index], lines[index + 1] || "")
|
|
980
|
+
) {
|
|
981
|
+
paragraphLines.push(lines[index].trim());
|
|
982
|
+
index += 1;
|
|
983
|
+
}
|
|
984
|
+
blocks.push({ type: "paragraph", text: paragraphLines.join(" ") });
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return blocks;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function MarkdownPreviewContent({ content, theme }) {
|
|
991
|
+
const blocks = React.useMemo(() => parseMarkdownBlocks(content), [content]);
|
|
992
|
+
if (blocks.length === 0) {
|
|
993
|
+
return React.createElement("div", { className: "ps-empty-state" }, "No preview content.");
|
|
994
|
+
}
|
|
995
|
+
return React.createElement("div", { className: "ps-markdown-preview" },
|
|
996
|
+
blocks.map((block, index) => {
|
|
997
|
+
if (block.type === "heading") {
|
|
998
|
+
return React.createElement("div", {
|
|
999
|
+
key: `block:${index}`,
|
|
1000
|
+
className: `ps-md-heading is-h${block.level}`,
|
|
1001
|
+
}, renderInlineMarkdown(block.text, theme, `heading:${index}`));
|
|
1002
|
+
}
|
|
1003
|
+
if (block.type === "code") {
|
|
1004
|
+
return React.createElement("section", { key: `block:${index}`, className: "ps-md-code-block" },
|
|
1005
|
+
React.createElement("div", { className: "ps-md-code-header" }, block.language || "text"),
|
|
1006
|
+
React.createElement("pre", { className: "ps-md-code-pre" },
|
|
1007
|
+
React.createElement("code", null, block.content)));
|
|
1008
|
+
}
|
|
1009
|
+
if (block.type === "blockquote") {
|
|
1010
|
+
return React.createElement("blockquote", { key: `block:${index}`, className: "ps-md-quote" },
|
|
1011
|
+
renderInlineMarkdown(block.text, theme, `quote:${index}`));
|
|
1012
|
+
}
|
|
1013
|
+
if (block.type === "list") {
|
|
1014
|
+
const ListTag = block.ordered ? "ol" : "ul";
|
|
1015
|
+
return React.createElement(ListTag, {
|
|
1016
|
+
key: `block:${index}`,
|
|
1017
|
+
className: `ps-md-list${block.ordered ? " is-ordered" : ""}`,
|
|
1018
|
+
}, block.items.map((item, itemIndex) => React.createElement("li", {
|
|
1019
|
+
key: `item:${itemIndex}`,
|
|
1020
|
+
className: "ps-md-list-item",
|
|
1021
|
+
}, renderInlineMarkdown(item, theme, `list:${index}:${itemIndex}`))));
|
|
1022
|
+
}
|
|
1023
|
+
if (block.type === "table") {
|
|
1024
|
+
const columnCount = Math.max(block.header.length || 0, ...block.rows.map((row) => row.length));
|
|
1025
|
+
// Wrap cells by default. Only fall back to horizontal scroll
|
|
1026
|
+
// when there are so many columns that wrapping would crush
|
|
1027
|
+
// every cell (>6 columns is the practical readability cliff
|
|
1028
|
+
// on phones).
|
|
1029
|
+
const fitToWidth = columnCount > 0 && columnCount <= 6;
|
|
1030
|
+
// Default: let the browser's auto-table-layout do column
|
|
1031
|
+
// sizing under the fit-content constraint — that handles
|
|
1032
|
+
// short / uniform tables well. When one column has long
|
|
1033
|
+
// prose (e.g. a Mechanism / Description column), auto-layout
|
|
1034
|
+
// squeezes the rigid columns; computeFitWidthColumnLayout
|
|
1035
|
+
// detects that case, picks a flex column, and assigns
|
|
1036
|
+
// explicit widths so the rigid columns hold their
|
|
1037
|
+
// content-fit width and the flex column wraps to absorb the
|
|
1038
|
+
// remainder.
|
|
1039
|
+
const layout = fitToWidth
|
|
1040
|
+
? computeFitWidthColumnLayout([block.header, ...block.rows])
|
|
1041
|
+
: null;
|
|
1042
|
+
const hasFlexColumn = !!layout;
|
|
1043
|
+
const flexIndex = layout ? layout.flexIndex : -1;
|
|
1044
|
+
const cellClass = (cellIndex) => (cellIndex === flexIndex ? "is-flex-column" : null);
|
|
1045
|
+
const columnLabels = buildTableColumnLabels([block.header], columnCount);
|
|
1046
|
+
const tableClass = `ps-md-table${fitToWidth ? " is-fit-width" : ""}${hasFlexColumn ? " has-flex-column" : ""}`;
|
|
1047
|
+
const wrapClass = `ps-md-table-wrap${fitToWidth ? " is-fit-width" : ""}${hasFlexColumn ? " has-flex-column" : ""}`;
|
|
1048
|
+
return React.createElement("div", {
|
|
1049
|
+
key: `block:${index}`,
|
|
1050
|
+
className: wrapClass,
|
|
1051
|
+
},
|
|
1052
|
+
React.createElement("table", {
|
|
1053
|
+
className: tableClass,
|
|
1054
|
+
style: layout?.minWidth ? { "--ps-table-min-width": layout.minWidth } : undefined,
|
|
1055
|
+
},
|
|
1056
|
+
layout
|
|
1057
|
+
? React.createElement("colgroup", null,
|
|
1058
|
+
layout.widths.map((width, columnIndex) => React.createElement("col", {
|
|
1059
|
+
key: `col:${columnIndex}`,
|
|
1060
|
+
className: cellClass(columnIndex) || undefined,
|
|
1061
|
+
style: { width },
|
|
1062
|
+
})))
|
|
1063
|
+
: null,
|
|
1064
|
+
React.createElement("thead", null,
|
|
1065
|
+
React.createElement("tr", null,
|
|
1066
|
+
block.header.map((cell, cellIndex) => React.createElement("th", {
|
|
1067
|
+
key: `head:${cellIndex}`,
|
|
1068
|
+
className: cellClass(cellIndex) || undefined,
|
|
1069
|
+
},
|
|
1070
|
+
renderInlineMarkdown(cell, theme, `table:${index}:head:${cellIndex}`))))),
|
|
1071
|
+
React.createElement("tbody", null,
|
|
1072
|
+
block.rows.map((row, rowIndex) => React.createElement("tr", { key: `row:${rowIndex}` },
|
|
1073
|
+
row.map((cell, cellIndex) => React.createElement("td", {
|
|
1074
|
+
key: `cell:${rowIndex}:${cellIndex}`,
|
|
1075
|
+
className: cellClass(cellIndex) || undefined,
|
|
1076
|
+
"data-label": columnLabels[cellIndex] || undefined,
|
|
1077
|
+
},
|
|
1078
|
+
React.createElement("span", { className: "ps-table-cell-value" },
|
|
1079
|
+
renderInlineMarkdown(cell, theme, `table:${index}:${rowIndex}:${cellIndex}`)))))))));
|
|
1080
|
+
}
|
|
1081
|
+
return React.createElement("p", { key: `block:${index}`, className: "ps-md-paragraph" },
|
|
1082
|
+
renderInlineMarkdown(block.text, theme, `para:${index}`));
|
|
1083
|
+
}));
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function MarkdownPreviewPanel({ controller, title, color, focused, scrollOffset = 0, paneKey, theme, content }) {
|
|
1087
|
+
const ref = React.useRef(null);
|
|
1088
|
+
const onScroll = usePanePixelScroll(ref, scrollOffset, paneKey, controller);
|
|
1089
|
+
|
|
1090
|
+
return React.createElement(Panel, { title, color, focused, theme },
|
|
1091
|
+
React.createElement("div", {
|
|
1092
|
+
ref,
|
|
1093
|
+
className: "ps-scroll-panel ps-markdown-scroll",
|
|
1094
|
+
onScroll,
|
|
1095
|
+
}, React.createElement(MarkdownPreviewContent, { content, theme })));
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function formatArtifactPreviewBytes(value) {
|
|
1099
|
+
const bytes = Number(value);
|
|
1100
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "Unknown size";
|
|
1101
|
+
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
|
1102
|
+
if (bytes < 1024 * 1024) {
|
|
1103
|
+
const kb = bytes / 1024;
|
|
1104
|
+
return `${kb >= 10 ? Math.round(kb) : kb.toFixed(1)} KB`;
|
|
1105
|
+
}
|
|
1106
|
+
const mb = bytes / (1024 * 1024);
|
|
1107
|
+
return `${mb >= 10 ? Math.round(mb) : mb.toFixed(1)} MB`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function BinaryArtifactPreviewPanel({ title, color, focused, theme, filename, contentType, sizeBytes, source, uploadedAt }) {
|
|
1111
|
+
const meta = [];
|
|
1112
|
+
if (contentType) meta.push(contentType);
|
|
1113
|
+
if (sizeBytes != null) meta.push(formatArtifactPreviewBytes(sizeBytes));
|
|
1114
|
+
if (source) meta.push(source);
|
|
1115
|
+
const uploadedLabel = uploadedAt
|
|
1116
|
+
? new Date(uploadedAt).toLocaleString(undefined, {
|
|
1117
|
+
year: "numeric",
|
|
1118
|
+
month: "short",
|
|
1119
|
+
day: "numeric",
|
|
1120
|
+
hour: "numeric",
|
|
1121
|
+
minute: "2-digit",
|
|
1122
|
+
})
|
|
1123
|
+
: "";
|
|
1124
|
+
|
|
1125
|
+
return React.createElement(Panel, { title, color, focused, theme },
|
|
1126
|
+
React.createElement("div", { className: "ps-binary-preview-card" },
|
|
1127
|
+
React.createElement("div", { className: "ps-binary-preview-kicker" }, "Binary artifact"),
|
|
1128
|
+
React.createElement("h3", { className: "ps-binary-preview-title" }, filename || "Artifact"),
|
|
1129
|
+
meta.length > 0
|
|
1130
|
+
? React.createElement("div", { className: "ps-binary-preview-meta" }, meta.join(" • "))
|
|
1131
|
+
: null,
|
|
1132
|
+
uploadedLabel
|
|
1133
|
+
? React.createElement("div", { className: "ps-binary-preview-time" }, `Uploaded ${uploadedLabel}`)
|
|
1134
|
+
: null,
|
|
1135
|
+
React.createElement("p", { className: "ps-binary-preview-copy" },
|
|
1136
|
+
"Preview is intentionally disabled for non-text artifacts in the browser workspace. Use Download to save the file and open it in the default app.")));
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function isBoxTopLine(text) {
|
|
1140
|
+
const value = String(text || "").trim();
|
|
1141
|
+
return value.startsWith("┌") && value.endsWith("┐");
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function isBoxBottomLine(text) {
|
|
1145
|
+
const value = String(text || "").trim();
|
|
1146
|
+
return value.startsWith("└") && value.endsWith("┘");
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function isBoxDividerLine(text) {
|
|
1150
|
+
const value = String(text || "").trim();
|
|
1151
|
+
return value.startsWith("├") && value.endsWith("┤");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function isBoxContentLine(text) {
|
|
1155
|
+
const value = String(text || "").trim();
|
|
1156
|
+
return value.startsWith("│") && value.endsWith("│");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function extractCodeFenceLanguage(line) {
|
|
1160
|
+
const value = String(lineText(line) || "").trim();
|
|
1161
|
+
if (!isBoxTopLine(value)) return "";
|
|
1162
|
+
return value
|
|
1163
|
+
.slice(1, -1)
|
|
1164
|
+
.replace(/^─+/u, "")
|
|
1165
|
+
.replace(/─+$/u, "")
|
|
1166
|
+
.trim();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function extractCodeFenceLine(line) {
|
|
1170
|
+
if (line?.kind === "runs" && Array.isArray(line.runs) && line.runs.length >= 3) {
|
|
1171
|
+
return String(line.runs[1]?.text || "").replace(/\s+$/u, "");
|
|
1172
|
+
}
|
|
1173
|
+
const value = lineText(line);
|
|
1174
|
+
if (!isBoxContentLine(value)) return String(value || "");
|
|
1175
|
+
return String(value)
|
|
1176
|
+
.slice(1, -1)
|
|
1177
|
+
.replace(/\s+$/u, "");
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function trimRunsEdgeWhitespace(runs = []) {
|
|
1181
|
+
const nextRuns = (runs || []).map((run) => ({ ...run }));
|
|
1182
|
+
if (nextRuns.length === 0) return nextRuns;
|
|
1183
|
+
nextRuns[0].text = String(nextRuns[0].text || "").replace(/^\s+/, "");
|
|
1184
|
+
nextRuns[nextRuns.length - 1].text = String(nextRuns[nextRuns.length - 1].text || "").replace(/\s+$/, "");
|
|
1185
|
+
return nextRuns.filter((run, index) => String(run.text || "").length > 0 || nextRuns.length === 1 || index === 0);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function extractFramedRuns(line, { fallbackColor = null } = {}) {
|
|
1189
|
+
if (line?.kind === "runs" && Array.isArray(line.runs) && line.runs.length >= 3) {
|
|
1190
|
+
return trimRunsEdgeWhitespace(line.runs.slice(1, -1));
|
|
1191
|
+
}
|
|
1192
|
+
const text = lineText(line)
|
|
1193
|
+
.replace(/^\s*[┌│]\s?/, "")
|
|
1194
|
+
.replace(/\s?[┐│]\s*$/, "")
|
|
1195
|
+
.replace(/^─+/, "")
|
|
1196
|
+
.replace(/─+$/, "")
|
|
1197
|
+
.trim();
|
|
1198
|
+
return [{ text, color: fallbackColor }];
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function splitBoxTableCells(text) {
|
|
1202
|
+
const value = String(text || "").trim();
|
|
1203
|
+
if (!isBoxContentLine(value)) return [];
|
|
1204
|
+
return value
|
|
1205
|
+
.slice(1, -1)
|
|
1206
|
+
.split("│")
|
|
1207
|
+
.map((cell) => cell.trim());
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function shouldJoinBoxTableFragmentsWithoutSpace(left = "", right = "") {
|
|
1211
|
+
const previous = String(left || "").trimEnd();
|
|
1212
|
+
const next = String(right || "").trimStart();
|
|
1213
|
+
if (!previous || !next) return false;
|
|
1214
|
+
|
|
1215
|
+
const previousChar = previous.slice(-1);
|
|
1216
|
+
const nextChar = next[0];
|
|
1217
|
+
|
|
1218
|
+
if (/^[,.;:!?%)\]}>]/u.test(nextChar)) return true;
|
|
1219
|
+
if (/^[._/\\-]/u.test(nextChar)) return true;
|
|
1220
|
+
if (/[([{<._/\\-]$/u.test(previousChar)) return true;
|
|
1221
|
+
if (/\.[A-Za-z0-9]{0,4}$/u.test(previous) && /^[A-Za-z0-9]/u.test(nextChar)) return true;
|
|
1222
|
+
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export function mergeBoxTableCellFragments(fragments = []) {
|
|
1227
|
+
const parts = (Array.isArray(fragments) ? fragments : [fragments])
|
|
1228
|
+
.map((fragment) => String(fragment || "").trim())
|
|
1229
|
+
.filter(Boolean);
|
|
1230
|
+
if (parts.length === 0) return "";
|
|
1231
|
+
|
|
1232
|
+
return parts.slice(1).reduce((merged, fragment) => (
|
|
1233
|
+
shouldJoinBoxTableFragmentsWithoutSpace(merged, fragment)
|
|
1234
|
+
? `${merged}${fragment}`
|
|
1235
|
+
: `${merged} ${fragment}`
|
|
1236
|
+
), parts[0]);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function mergeBoxTableRowGroup(rowGroup = []) {
|
|
1240
|
+
const columnCount = Math.max(0, ...rowGroup.map((row) => row.length));
|
|
1241
|
+
return Array.from({ length: columnCount }, (_, columnIndex) => mergeBoxTableCellFragments(
|
|
1242
|
+
rowGroup
|
|
1243
|
+
.map((row) => String(row[columnIndex] || "").trim())
|
|
1244
|
+
.filter(Boolean),
|
|
1245
|
+
));
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function parseStructuredChatBlocks(lines = []) {
|
|
1249
|
+
const blocks = [];
|
|
1250
|
+
|
|
1251
|
+
for (let index = 0; index < lines.length;) {
|
|
1252
|
+
const currentLine = lines[index];
|
|
1253
|
+
|
|
1254
|
+
// Preformatted markup lines (splash art) bypass box/table/card
|
|
1255
|
+
// detection entirely and render as one preserve block, so no line
|
|
1256
|
+
// ever rewraps or gets reinterpreted as a structured element.
|
|
1257
|
+
if (currentLine?.preserve) {
|
|
1258
|
+
const preserveLines = [];
|
|
1259
|
+
while (index < lines.length && lines[index]?.preserve) {
|
|
1260
|
+
preserveLines.push(lines[index]);
|
|
1261
|
+
index += 1;
|
|
1262
|
+
}
|
|
1263
|
+
blocks.push({ type: "preserve", lines: preserveLines });
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Sentinel markdown-table line emitted by parseMarkdownLines when
|
|
1268
|
+
// tableMode === "sentinel". Carries the raw header + rows so the
|
|
1269
|
+
// portal renders a real HTML table with markdown cell content (so
|
|
1270
|
+
// [label](url) inside cells stays clickable, instead of being flattened
|
|
1271
|
+
// into plain text by the box-art width-fitter).
|
|
1272
|
+
if (currentLine?.kind === "markdownTable") {
|
|
1273
|
+
blocks.push({
|
|
1274
|
+
type: "table",
|
|
1275
|
+
headerRows: Array.isArray(currentLine.header) && currentLine.header.length > 0
|
|
1276
|
+
? [currentLine.header]
|
|
1277
|
+
: [],
|
|
1278
|
+
bodyRows: Array.isArray(currentLine.rows) ? currentLine.rows : [],
|
|
1279
|
+
});
|
|
1280
|
+
index += 1;
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const currentText = lineText(currentLine);
|
|
1285
|
+
|
|
1286
|
+
if (isBoxTopLine(currentText) && currentText.includes("┬")) {
|
|
1287
|
+
const headerRows = [];
|
|
1288
|
+
const bodyRows = [];
|
|
1289
|
+
let currentRowGroup = [];
|
|
1290
|
+
let inHeader = true;
|
|
1291
|
+
index += 1;
|
|
1292
|
+
|
|
1293
|
+
while (index < lines.length) {
|
|
1294
|
+
const nextLine = lines[index];
|
|
1295
|
+
const nextText = lineText(nextLine);
|
|
1296
|
+
if (isBoxBottomLine(nextText)) {
|
|
1297
|
+
if (currentRowGroup.length > 0) {
|
|
1298
|
+
const mergedRow = mergeBoxTableRowGroup(currentRowGroup);
|
|
1299
|
+
if (mergedRow.length > 0) {
|
|
1300
|
+
if (inHeader) headerRows.push(mergedRow);
|
|
1301
|
+
else bodyRows.push(mergedRow);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
index += 1;
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
if (isBoxDividerLine(nextText)) {
|
|
1308
|
+
if (currentRowGroup.length > 0) {
|
|
1309
|
+
const mergedRow = mergeBoxTableRowGroup(currentRowGroup);
|
|
1310
|
+
if (mergedRow.length > 0) {
|
|
1311
|
+
if (inHeader) headerRows.push(mergedRow);
|
|
1312
|
+
else bodyRows.push(mergedRow);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
currentRowGroup = [];
|
|
1316
|
+
inHeader = false;
|
|
1317
|
+
index += 1;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (isBoxContentLine(nextText)) {
|
|
1321
|
+
const cells = splitBoxTableCells(nextText);
|
|
1322
|
+
if (cells.length > 0) {
|
|
1323
|
+
currentRowGroup.push(cells);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
index += 1;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
blocks.push({ type: "table", headerRows, bodyRows });
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (
|
|
1334
|
+
currentLine?.kind === "runs"
|
|
1335
|
+
&& Array.isArray(currentLine.runs)
|
|
1336
|
+
&& currentLine.runs.length === 1
|
|
1337
|
+
&& isBoxTopLine(currentText)
|
|
1338
|
+
) {
|
|
1339
|
+
const language = extractCodeFenceLanguage(currentLine);
|
|
1340
|
+
const codeLines = [];
|
|
1341
|
+
index += 1;
|
|
1342
|
+
|
|
1343
|
+
while (index < lines.length) {
|
|
1344
|
+
const nextLine = lines[index];
|
|
1345
|
+
const nextText = lineText(nextLine);
|
|
1346
|
+
if (isBoxBottomLine(nextText)) {
|
|
1347
|
+
index += 1;
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
if (isBoxContentLine(nextText)) {
|
|
1351
|
+
codeLines.push(extractCodeFenceLine(nextLine));
|
|
1352
|
+
} else {
|
|
1353
|
+
codeLines.push(lineText(nextLine));
|
|
1354
|
+
}
|
|
1355
|
+
index += 1;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (index < lines.length && lineText(lines[index]).trim().length === 0) {
|
|
1359
|
+
index += 1;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
blocks.push({
|
|
1363
|
+
type: "code",
|
|
1364
|
+
language: language || "text",
|
|
1365
|
+
content: codeLines.join("\n"),
|
|
1366
|
+
});
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (
|
|
1371
|
+
currentLine?.kind === "runs"
|
|
1372
|
+
&& Array.isArray(currentLine.runs)
|
|
1373
|
+
&& currentLine.runs.length > 2
|
|
1374
|
+
&& isBoxTopLine(currentText)
|
|
1375
|
+
) {
|
|
1376
|
+
const headerRuns = extractFramedRuns(currentLine);
|
|
1377
|
+
const borderColor = currentLine.runs[0]?.color || "gray";
|
|
1378
|
+
const bodyLines = [];
|
|
1379
|
+
index += 1;
|
|
1380
|
+
|
|
1381
|
+
while (index < lines.length) {
|
|
1382
|
+
const nextLine = lines[index];
|
|
1383
|
+
const nextText = lineText(nextLine);
|
|
1384
|
+
if (isBoxBottomLine(nextText)) {
|
|
1385
|
+
index += 1;
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
if (isBoxContentLine(nextText)) {
|
|
1389
|
+
bodyLines.push(extractFramedRuns(nextLine));
|
|
1390
|
+
} else {
|
|
1391
|
+
bodyLines.push(nextLine?.kind === "runs"
|
|
1392
|
+
? nextLine.runs
|
|
1393
|
+
: [{ text: lineText(nextLine), color: nextLine?.color || null }]);
|
|
1394
|
+
}
|
|
1395
|
+
index += 1;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (index < lines.length && lineText(lines[index]).trim().length === 0) {
|
|
1399
|
+
index += 1;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
blocks.push({ type: "card", headerRuns, bodyLines, borderColor });
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
blocks.push({ type: "line", line: currentLine });
|
|
1407
|
+
index += 1;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
return blocks;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function StructuredChatBlocks({ lines, theme }) {
|
|
1414
|
+
const blocks = React.useMemo(() => parseStructuredChatBlocks(lines), [lines]);
|
|
1415
|
+
|
|
1416
|
+
return React.createElement(React.Fragment, null,
|
|
1417
|
+
blocks.map((block, index) => {
|
|
1418
|
+
if (block.type === "preserve") {
|
|
1419
|
+
return React.createElement("div", { key: `preserve:${index}`, className: "ps-chat-preserve-block" },
|
|
1420
|
+
block.lines.map((line, lineIndex) => React.createElement(Line, {
|
|
1421
|
+
key: `preserve:${index}:${lineIndex}`,
|
|
1422
|
+
line,
|
|
1423
|
+
theme,
|
|
1424
|
+
className: "ps-line-preserve",
|
|
1425
|
+
})));
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (block.type === "code") {
|
|
1429
|
+
return React.createElement("section", { key: `code:${index}`, className: "ps-md-code-block ps-chat-code-block" },
|
|
1430
|
+
React.createElement("div", { className: "ps-md-code-header" }, block.language || "text"),
|
|
1431
|
+
React.createElement("pre", { className: "ps-md-code-pre" },
|
|
1432
|
+
React.createElement("code", null, block.content || "")));
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (block.type === "card") {
|
|
1436
|
+
return React.createElement("section", {
|
|
1437
|
+
key: `card:${index}`,
|
|
1438
|
+
className: "ps-chat-card",
|
|
1439
|
+
style: { "--ps-chat-card-accent": resolveColor(theme, block.borderColor) || "var(--ps-border)" },
|
|
1440
|
+
},
|
|
1441
|
+
React.createElement("header", { className: "ps-chat-card-header" },
|
|
1442
|
+
React.createElement(Runs, { runs: block.headerRuns, theme })),
|
|
1443
|
+
React.createElement("div", { className: "ps-chat-card-body" },
|
|
1444
|
+
(block.bodyLines || []).map((bodyRuns, bodyIndex) => React.createElement("div", {
|
|
1445
|
+
key: `card:${index}:line:${bodyIndex}`,
|
|
1446
|
+
className: "ps-chat-card-line",
|
|
1447
|
+
}, React.createElement(Runs, { runs: bodyRuns, theme })) )));
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (block.type === "table") {
|
|
1451
|
+
const headerRows = block.headerRows || [];
|
|
1452
|
+
const bodyRows = block.bodyRows || [];
|
|
1453
|
+
const columnCount = Math.max(
|
|
1454
|
+
1,
|
|
1455
|
+
...headerRows.map((row) => row.length),
|
|
1456
|
+
...bodyRows.map((row) => row.length),
|
|
1457
|
+
);
|
|
1458
|
+
const fitToWidth = columnCount <= 6;
|
|
1459
|
+
// Default: rely on the browser's auto-table-layout under
|
|
1460
|
+
// fit-content for short / uniform tables (best
|
|
1461
|
+
// proportional sizing). When one column has long prose
|
|
1462
|
+
// (Mechanism / Description / Notes), auto-layout squeezes
|
|
1463
|
+
// the rigid columns; computeFitWidthColumnLayout picks a
|
|
1464
|
+
// flex column and assigns explicit widths so the rigid
|
|
1465
|
+
// columns stay at content-fit and the flex column wraps to
|
|
1466
|
+
// absorb the remainder.
|
|
1467
|
+
const layout = fitToWidth
|
|
1468
|
+
? computeFitWidthColumnLayout([...headerRows, ...bodyRows])
|
|
1469
|
+
: null;
|
|
1470
|
+
const hasFlexColumn = !!layout;
|
|
1471
|
+
const flexIndex = layout ? layout.flexIndex : -1;
|
|
1472
|
+
const cellClass = (cellIndex) => (cellIndex === flexIndex ? "is-flex-column" : null);
|
|
1473
|
+
const columnLabels = buildTableColumnLabels(headerRows, columnCount);
|
|
1474
|
+
const tableClass = `ps-chat-table${fitToWidth ? " is-fit-width" : ""}${hasFlexColumn ? " has-flex-column" : ""}`;
|
|
1475
|
+
const wrapClass = `ps-chat-table-wrap${fitToWidth ? " is-fit-width" : ""}${hasFlexColumn ? " has-flex-column" : ""}`;
|
|
1476
|
+
return React.createElement("div", {
|
|
1477
|
+
key: `table:${index}`,
|
|
1478
|
+
className: wrapClass,
|
|
1479
|
+
},
|
|
1480
|
+
React.createElement("table", {
|
|
1481
|
+
className: tableClass,
|
|
1482
|
+
style: layout?.minWidth ? { "--ps-table-min-width": layout.minWidth } : undefined,
|
|
1483
|
+
},
|
|
1484
|
+
layout
|
|
1485
|
+
? React.createElement("colgroup", null,
|
|
1486
|
+
layout.widths.map((width, columnIndex) => React.createElement("col", {
|
|
1487
|
+
key: `col:${columnIndex}`,
|
|
1488
|
+
className: cellClass(columnIndex) || undefined,
|
|
1489
|
+
style: { width },
|
|
1490
|
+
})))
|
|
1491
|
+
: null,
|
|
1492
|
+
headerRows.length > 0
|
|
1493
|
+
? React.createElement("thead", null,
|
|
1494
|
+
headerRows.map((row, rowIndex) => React.createElement("tr", { key: `thead:${rowIndex}` },
|
|
1495
|
+
Array.from({ length: columnCount }, (_, cellIndex) => React.createElement("th", {
|
|
1496
|
+
key: `th:${rowIndex}:${cellIndex}`,
|
|
1497
|
+
className: cellClass(cellIndex) || undefined,
|
|
1498
|
+
},
|
|
1499
|
+
renderInlineMarkdown(row[cellIndex] || "", theme, `chat-table:${index}:head:${rowIndex}:${cellIndex}`))))))
|
|
1500
|
+
: null,
|
|
1501
|
+
React.createElement("tbody", null,
|
|
1502
|
+
bodyRows.map((row, rowIndex) => React.createElement("tr", { key: `tbody:${rowIndex}` },
|
|
1503
|
+
Array.from({ length: columnCount }, (_, cellIndex) => React.createElement("td", {
|
|
1504
|
+
key: `td:${rowIndex}:${cellIndex}`,
|
|
1505
|
+
className: cellClass(cellIndex) || undefined,
|
|
1506
|
+
"data-label": columnLabels[cellIndex] || undefined,
|
|
1507
|
+
},
|
|
1508
|
+
React.createElement("span", { className: "ps-table-cell-value" },
|
|
1509
|
+
renderInlineMarkdown(row[cellIndex] || "", theme, `chat-table:${index}:${rowIndex}:${cellIndex}`)))))))));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
return React.createElement(Line, { key: `line:${index}`, line: block.line, theme });
|
|
1513
|
+
}));
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function Panel({ title, titleRight = null, color = "gray", focused = false, actions = null, children, theme, className = "" }) {
|
|
1517
|
+
const accent = resolveColor(theme, color);
|
|
1518
|
+
return React.createElement("section", {
|
|
1519
|
+
className: `ps-panel${focused ? " is-focused" : ""}${className ? ` ${className}` : ""}`,
|
|
1520
|
+
style: { "--ps-panel-accent": accent || "var(--ps-border)" },
|
|
1521
|
+
},
|
|
1522
|
+
React.createElement("header", { className: "ps-panel-header" },
|
|
1523
|
+
React.createElement("div", { className: "ps-panel-title" },
|
|
1524
|
+
Array.isArray(title)
|
|
1525
|
+
? React.createElement(Runs, { runs: title, theme })
|
|
1526
|
+
: flattenTitleText(title)),
|
|
1527
|
+
titleRight || actions
|
|
1528
|
+
? React.createElement("div", { className: "ps-panel-header-right" },
|
|
1529
|
+
titleRight
|
|
1530
|
+
? React.createElement("div", { className: "ps-panel-title-right" },
|
|
1531
|
+
Array.isArray(titleRight)
|
|
1532
|
+
? React.createElement(Runs, { runs: titleRight, theme })
|
|
1533
|
+
: flattenTitleText(titleRight))
|
|
1534
|
+
: null,
|
|
1535
|
+
actions ? React.createElement("div", { className: "ps-panel-actions" }, actions) : null,
|
|
1536
|
+
)
|
|
1537
|
+
: null,
|
|
1538
|
+
),
|
|
1539
|
+
React.createElement("div", { className: "ps-panel-body" }, children));
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function PortalSequenceLines({ lines, theme, completionByTurn }) {
|
|
1543
|
+
const [expanded, setExpanded] = React.useState(() => new Set());
|
|
1544
|
+
const toggle = React.useCallback((key) => {
|
|
1545
|
+
setExpanded((current) => {
|
|
1546
|
+
const next = new Set(current);
|
|
1547
|
+
if (next.has(key)) next.delete(key);
|
|
1548
|
+
else next.add(key);
|
|
1549
|
+
return next;
|
|
1550
|
+
});
|
|
1551
|
+
}, []);
|
|
1552
|
+
const compactNumber = (value) => Number(value || 0).toLocaleString("en-US");
|
|
1553
|
+
const formatTokenK = (value) => {
|
|
1554
|
+
const tokens = Number(value || 0);
|
|
1555
|
+
if (!Number.isFinite(tokens)) return "?K";
|
|
1556
|
+
const scaled = tokens / 1000;
|
|
1557
|
+
const decimals = Math.abs(scaled) >= 10 ? 1 : 2;
|
|
1558
|
+
return `${scaled.toFixed(decimals)}K`;
|
|
1559
|
+
};
|
|
1560
|
+
const formatDurationSeconds = (value) => {
|
|
1561
|
+
const ms = Number(value || 0);
|
|
1562
|
+
if (!Number.isFinite(ms)) return "?s";
|
|
1563
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
1564
|
+
};
|
|
1565
|
+
const shortModelOnly = (value) => {
|
|
1566
|
+
const model = String(value || "").trim();
|
|
1567
|
+
if (!model) return "unknown";
|
|
1568
|
+
const parts = model.split(":").filter(Boolean);
|
|
1569
|
+
const last = parts[parts.length - 1] || "";
|
|
1570
|
+
const modelIndex = parts.length > 1 && REASONING_EFFORT_LABELS.has(last.toLowerCase())
|
|
1571
|
+
? parts.length - 2
|
|
1572
|
+
: parts.length - 1;
|
|
1573
|
+
const maybeModel = parts[Math.max(0, modelIndex)] || parts[0];
|
|
1574
|
+
return maybeModel || "unknown";
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
return React.createElement(React.Fragment, null,
|
|
1578
|
+
lines.map((line, index) => {
|
|
1579
|
+
// The selector tags the completed-turn run structurally; the display
|
|
1580
|
+
// text is width-truncated in narrow columns, so never regex it.
|
|
1581
|
+
const completedTurn = line?.kind === "runs" && Array.isArray(line.runs)
|
|
1582
|
+
? line.runs.find((run) => run?.completedTurn != null)?.completedTurn
|
|
1583
|
+
: null;
|
|
1584
|
+
const completion = completedTurn != null ? completionByTurn.get(Number(completedTurn)) : null;
|
|
1585
|
+
if (!completion) {
|
|
1586
|
+
return React.createElement(Line, { key: `line:${index}`, line, theme });
|
|
1587
|
+
}
|
|
1588
|
+
const data = completion.data || {};
|
|
1589
|
+
const key = String(completion.seq ?? `${data.turnIndex ?? completedTurn}:${completion.createdAt ?? index}`);
|
|
1590
|
+
const isExpanded = expanded.has(key);
|
|
1591
|
+
const fullModelLabel = data.reasoningEffort ? `${data.model || "(unknown)"}:${data.reasoningEffort}` : (data.model || "(unknown)");
|
|
1592
|
+
const modelLabel = shortModelOnly(data.model);
|
|
1593
|
+
const duration = data.durationMs != null ? formatDurationSeconds(data.durationMs) : "duration n/a";
|
|
1594
|
+
const durationDetail = data.durationMs != null ? `${compactNumber(data.durationMs)} ms` : "duration n/a";
|
|
1595
|
+
const tokens = `${formatTokenK(data.tokensInput)} / ${formatTokenK(data.tokensOutput)}`;
|
|
1596
|
+
const details = [
|
|
1597
|
+
["model", fullModelLabel],
|
|
1598
|
+
["started", data.startedAt ? new Date(data.startedAt).toLocaleString() : null],
|
|
1599
|
+
["ended", data.endedAt ? new Date(data.endedAt).toLocaleString() : null],
|
|
1600
|
+
["duration", durationDetail],
|
|
1601
|
+
["tokens", `${compactNumber(data.tokensInput)} in / ${compactNumber(data.tokensOutput)} out`],
|
|
1602
|
+
["cache", `${compactNumber(data.tokensCacheRead)} read / ${compactNumber(data.tokensCacheWrite)} write`],
|
|
1603
|
+
["tools", `${compactNumber(data.toolCalls)} calls / ${compactNumber(data.toolErrors)} errors`],
|
|
1604
|
+
["names", Array.isArray(data.toolNames) && data.toolNames.length ? data.toolNames.join(", ") : null],
|
|
1605
|
+
["worker", data.workerNodeId || null],
|
|
1606
|
+
["result", data.resultType || null],
|
|
1607
|
+
["error", data.errorMessage || null],
|
|
1608
|
+
].filter(([, value]) => value != null && value !== "");
|
|
1609
|
+
return React.createElement(React.Fragment, { key: `seq:${index}` },
|
|
1610
|
+
React.createElement(Line, { line, theme }),
|
|
1611
|
+
React.createElement("button", {
|
|
1612
|
+
type: "button",
|
|
1613
|
+
className: "ps-sequence-turn-divider",
|
|
1614
|
+
onClick: () => toggle(key),
|
|
1615
|
+
},
|
|
1616
|
+
React.createElement("span", { className: "ps-sequence-turn-caret" }, isExpanded ? "v" : ">"),
|
|
1617
|
+
React.createElement("span", { className: "ps-sequence-turn-rule" }, "--- "),
|
|
1618
|
+
React.createElement("span", { className: "ps-sequence-turn-key" }, "Mod: "),
|
|
1619
|
+
React.createElement("span", { className: "ps-sequence-turn-model" }, modelLabel),
|
|
1620
|
+
React.createElement("span", { className: "ps-sequence-turn-rule" }, " ("),
|
|
1621
|
+
React.createElement("span", { className: "ps-sequence-turn-key" }, "tok: "),
|
|
1622
|
+
React.createElement("span", { className: "ps-sequence-turn-tokens" }, tokens),
|
|
1623
|
+
React.createElement("span", { className: "ps-sequence-turn-rule" }, ", "),
|
|
1624
|
+
React.createElement("span", { className: "ps-sequence-turn-key" }, "dur: "),
|
|
1625
|
+
React.createElement("span", { className: "ps-sequence-turn-duration" }, duration),
|
|
1626
|
+
React.createElement("span", { className: "ps-sequence-turn-rule" }, ") ---"),
|
|
1627
|
+
),
|
|
1628
|
+
isExpanded
|
|
1629
|
+
? React.createElement("div", { className: "ps-sequence-turn-details" },
|
|
1630
|
+
details.map(([label, value]) => React.createElement("div", { key: label },
|
|
1631
|
+
React.createElement("span", { className: "ps-sequence-turn-detail-label" }, `${label}: `),
|
|
1632
|
+
React.createElement("span", null, String(value)),
|
|
1633
|
+
)))
|
|
1634
|
+
: null,
|
|
1635
|
+
);
|
|
1636
|
+
}),
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function ScrollLinesPanel({ title, titleRight = null, color, focused, actions, lines, stickyLines = [], bottomStickyLines = [], scrollOffset = 0, scrollMode = "top", paneKey, controller, className = "", panelClassName = "", topContent = null, bottomContent = null, structuredBlocks = false, stickyBottom = false, renderBody = null }) {
|
|
1641
|
+
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
1642
|
+
const theme = getTheme(themeId);
|
|
1643
|
+
const ref = React.useRef(null);
|
|
1644
|
+
const stickyRef = React.useRef(null);
|
|
1645
|
+
const syncingHorizontalRef = React.useRef(false);
|
|
1646
|
+
const { normalizedLines, onScroll, onWheel } = useScrollSync(ref, lines, scrollOffset, scrollMode, paneKey, controller, { stickyBottom });
|
|
1647
|
+
const normalizedSticky = React.useMemo(() => normalizeLines(stickyLines), [stickyLines]);
|
|
1648
|
+
const normalizedBottomSticky = React.useMemo(() => normalizeLines(bottomStickyLines), [bottomStickyLines]);
|
|
1649
|
+
const preserveHorizontalScroll = className.includes("is-preserve") && panelClassName.includes("has-preserved-sticky");
|
|
1650
|
+
|
|
1651
|
+
const syncScrollLeft = React.useCallback((source, target) => {
|
|
1652
|
+
if (!source || !target) return;
|
|
1653
|
+
if (Math.abs((target.scrollLeft || 0) - (source.scrollLeft || 0)) <= 1) return;
|
|
1654
|
+
syncingHorizontalRef.current = true;
|
|
1655
|
+
target.scrollLeft = source.scrollLeft;
|
|
1656
|
+
window.requestAnimationFrame(() => {
|
|
1657
|
+
syncingHorizontalRef.current = false;
|
|
1658
|
+
});
|
|
1659
|
+
}, []);
|
|
1660
|
+
|
|
1661
|
+
const handleBodyScroll = React.useCallback((event) => {
|
|
1662
|
+
onScroll();
|
|
1663
|
+
if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
|
|
1664
|
+
syncScrollLeft(event.currentTarget, stickyRef.current);
|
|
1665
|
+
}, [onScroll, preserveHorizontalScroll, syncScrollLeft]);
|
|
1666
|
+
|
|
1667
|
+
const handleStickyScroll = React.useCallback((event) => {
|
|
1668
|
+
if (!preserveHorizontalScroll || syncingHorizontalRef.current) return;
|
|
1669
|
+
syncScrollLeft(event.currentTarget, ref.current);
|
|
1670
|
+
}, [preserveHorizontalScroll, syncScrollLeft]);
|
|
1671
|
+
|
|
1672
|
+
return React.createElement(Panel, { title, titleRight, color, focused, actions, theme, className: panelClassName },
|
|
1673
|
+
topContent,
|
|
1674
|
+
normalizedSticky.length > 0
|
|
1675
|
+
? React.createElement("div", {
|
|
1676
|
+
ref: stickyRef,
|
|
1677
|
+
className: `ps-panel-sticky${preserveHorizontalScroll ? " is-scroll-sync" : ""}`,
|
|
1678
|
+
onScroll: handleStickyScroll,
|
|
1679
|
+
},
|
|
1680
|
+
normalizedSticky.map((line, index) => React.createElement(Line, { key: `sticky:${index}`, line, theme })),
|
|
1681
|
+
)
|
|
1682
|
+
: null,
|
|
1683
|
+
React.createElement("div", { ref, className: `ps-scroll-panel ${className}`.trim(), onScroll: handleBodyScroll, onWheel },
|
|
1684
|
+
typeof renderBody === "function"
|
|
1685
|
+
? renderBody(normalizedLines, theme)
|
|
1686
|
+
: structuredBlocks
|
|
1687
|
+
? React.createElement(StructuredChatBlocks, { lines: normalizedLines, theme })
|
|
1688
|
+
: normalizedLines.map((line, index) => React.createElement(Line, { key: `line:${index}`, line, theme })),
|
|
1689
|
+
),
|
|
1690
|
+
normalizedBottomSticky.length > 0
|
|
1691
|
+
? React.createElement("div", {
|
|
1692
|
+
className: "ps-panel-bottom-sticky",
|
|
1693
|
+
},
|
|
1694
|
+
normalizedBottomSticky.map((line, index) => React.createElement(Line, { key: `bottom-sticky:${index}`, line, theme })),
|
|
1695
|
+
)
|
|
1696
|
+
: null,
|
|
1697
|
+
bottomContent);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function SessionRowContent({ row, theme, structured = false }) {
|
|
1701
|
+
const hasStructuredRuns = structured && Array.isArray(row.titleRuns);
|
|
1702
|
+
if (!hasStructuredRuns) {
|
|
1703
|
+
return Array.isArray(row.runs)
|
|
1704
|
+
? React.createElement(Runs, { runs: row.runs, theme })
|
|
1705
|
+
: row.text;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const hasMeta = Array.isArray(row.metaRuns) && row.metaRuns.length > 0;
|
|
1709
|
+
const hasBadges = Array.isArray(row.badgeRuns) && row.badgeRuns.length > 0;
|
|
1710
|
+
const hasSelectedMeta = row.active && Array.isArray(row.selectedMetaRuns) && row.selectedMetaRuns.length > 0;
|
|
1711
|
+
|
|
1712
|
+
return React.createElement(React.Fragment, null,
|
|
1713
|
+
React.createElement("div", { className: "ps-session-row-title" },
|
|
1714
|
+
React.createElement(Runs, { runs: row.titleRuns, theme }),
|
|
1715
|
+
// Meta (timestamp / member count) sits inline on the title line.
|
|
1716
|
+
hasMeta
|
|
1717
|
+
? React.createElement("span", { className: "ps-session-row-meta" },
|
|
1718
|
+
React.createElement(Runs, { runs: [{ text: " ", color: "gray" }, ...row.metaRuns], theme }))
|
|
1719
|
+
: null),
|
|
1720
|
+
hasBadges
|
|
1721
|
+
? React.createElement("div", { className: "ps-session-row-badges" },
|
|
1722
|
+
React.createElement(Runs, { runs: row.badgeRuns, theme }))
|
|
1723
|
+
: null,
|
|
1724
|
+
hasSelectedMeta
|
|
1725
|
+
? React.createElement("div", { className: "ps-session-row-selected-meta" },
|
|
1726
|
+
React.createElement(Runs, { runs: row.selectedMetaRuns, theme }))
|
|
1727
|
+
: null);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function SessionPane({ controller, actions = null, panelClassName = "", structuredRows = false }) {
|
|
1731
|
+
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
1732
|
+
const theme = getTheme(themeId);
|
|
1733
|
+
const sessionButtonRefs = React.useRef(new Map());
|
|
1734
|
+
const viewState = useControllerSelector(controller, (state) => ({
|
|
1735
|
+
activeSessionId: state.sessions.activeSessionId,
|
|
1736
|
+
sessionsById: state.sessions.byId,
|
|
1737
|
+
sessionsFlat: state.sessions.flat,
|
|
1738
|
+
filterQuery: state.sessions.filterQuery || "",
|
|
1739
|
+
ownerFilter: state.sessions.ownerFilter,
|
|
1740
|
+
pinnedIds: state.sessions.pinnedIds,
|
|
1741
|
+
selectedIds: state.sessions.selectedIds,
|
|
1742
|
+
selectMode: state.sessions.selectMode,
|
|
1743
|
+
auth: state.auth,
|
|
1744
|
+
connectionMode: state.connection?.mode || "local",
|
|
1745
|
+
modalOpen: Boolean(state.ui.modal),
|
|
1746
|
+
focused: state.ui.focusRegion === "sessions",
|
|
1747
|
+
}), shallowEqualObject);
|
|
1748
|
+
const rows = React.useMemo(() => selectSessionRows({
|
|
1749
|
+
sessions: {
|
|
1750
|
+
activeSessionId: viewState.activeSessionId,
|
|
1751
|
+
byId: viewState.sessionsById,
|
|
1752
|
+
flat: viewState.sessionsFlat,
|
|
1753
|
+
filterQuery: viewState.filterQuery,
|
|
1754
|
+
ownerFilter: viewState.ownerFilter,
|
|
1755
|
+
pinnedIds: viewState.pinnedIds,
|
|
1756
|
+
selectedIds: viewState.selectedIds,
|
|
1757
|
+
selectMode: viewState.selectMode,
|
|
1758
|
+
},
|
|
1759
|
+
auth: viewState.auth,
|
|
1760
|
+
connection: {
|
|
1761
|
+
mode: viewState.connectionMode,
|
|
1762
|
+
},
|
|
1763
|
+
}), [viewState.activeSessionId, viewState.auth, viewState.connectionMode, viewState.filterQuery, viewState.ownerFilter, viewState.pinnedIds, viewState.selectedIds, viewState.selectMode, viewState.sessionsById, viewState.sessionsFlat]);
|
|
1764
|
+
const activeSession = viewState.activeSessionId
|
|
1765
|
+
? viewState.sessionsById[viewState.activeSessionId] || null
|
|
1766
|
+
: null;
|
|
1767
|
+
const canRenameActiveSession = Boolean(activeSession && !activeSession.isSystem);
|
|
1768
|
+
const selectedCount = Array.isArray(viewState.selectedIds) ? viewState.selectedIds.length : 0;
|
|
1769
|
+
const isBulkSelection = selectedCount > 1;
|
|
1770
|
+
const canPinActiveSession = Boolean(
|
|
1771
|
+
activeSession
|
|
1772
|
+
&& !activeSession.isSystem
|
|
1773
|
+
&& (activeSession.isGroup || (!activeSession.parentSessionId && !activeSession.groupId)),
|
|
1774
|
+
);
|
|
1775
|
+
const isActivePinned = Boolean(
|
|
1776
|
+
activeSession
|
|
1777
|
+
&& Array.isArray(viewState.pinnedIds)
|
|
1778
|
+
&& viewState.pinnedIds.includes(activeSession.sessionId),
|
|
1779
|
+
);
|
|
1780
|
+
const activeGroupCanDelete = Boolean(activeSession?.isGroup && Number(activeSession.memberCount || 0) === 0);
|
|
1781
|
+
const canTerminate = isBulkSelection
|
|
1782
|
+
? Array.isArray(viewState.selectedIds) && viewState.selectedIds.some((id) => {
|
|
1783
|
+
const s = viewState.sessionsById[id];
|
|
1784
|
+
return s && !s.isSystem && !s.isGroup;
|
|
1785
|
+
})
|
|
1786
|
+
: activeSession?.isGroup ? true : Boolean(activeSession);
|
|
1787
|
+
const activeSessionActionLabel = activeSession?.isGroup
|
|
1788
|
+
? "Delete"
|
|
1789
|
+
: isBulkSelection
|
|
1790
|
+
? `Terminate (${selectedCount})`
|
|
1791
|
+
: activeSession?.isSystem
|
|
1792
|
+
? "Restart"
|
|
1793
|
+
: "Terminate";
|
|
1794
|
+
const hasExplicitSelection = selectedCount > 0;
|
|
1795
|
+
const groupableIds = hasExplicitSelection
|
|
1796
|
+
? viewState.selectedIds.filter((id) => {
|
|
1797
|
+
const session = viewState.sessionsById[id];
|
|
1798
|
+
return session && !session.isSystem && !session.isGroup && !session.parentSessionId;
|
|
1799
|
+
})
|
|
1800
|
+
: (activeSession && !activeSession.isSystem && !activeSession.isGroup && !activeSession.parentSessionId ? [activeSession.sessionId] : []);
|
|
1801
|
+
const canMoveToGroup = groupableIds.length > 0;
|
|
1802
|
+
const combinedPanelClassName = `ps-session-pane${panelClassName ? ` ${panelClassName}` : ""}`;
|
|
1803
|
+
const setSessionButtonRef = React.useCallback((sessionId, node) => {
|
|
1804
|
+
if (!sessionId) return;
|
|
1805
|
+
if (node) {
|
|
1806
|
+
sessionButtonRefs.current.set(sessionId, node);
|
|
1807
|
+
} else {
|
|
1808
|
+
sessionButtonRefs.current.delete(sessionId);
|
|
1809
|
+
}
|
|
1810
|
+
}, []);
|
|
1811
|
+
|
|
1812
|
+
React.useEffect(() => {
|
|
1813
|
+
if (viewState.modalOpen || !viewState.focused || !viewState.activeSessionId) return;
|
|
1814
|
+
const activeButton = sessionButtonRefs.current.get(viewState.activeSessionId);
|
|
1815
|
+
if (!activeButton) return;
|
|
1816
|
+
|
|
1817
|
+
if (document.activeElement !== activeButton) {
|
|
1818
|
+
activeButton.focus({ preventScroll: true });
|
|
1819
|
+
}
|
|
1820
|
+
// Only pull the active row into view when the active session itself
|
|
1821
|
+
// changes (or on focus/modal transitions). Re-running scrollIntoView
|
|
1822
|
+
// on every `sessions/loaded` refresh would yank the list back to the
|
|
1823
|
+
// active row even when the user has deliberately scrolled away to
|
|
1824
|
+
// browse older sessions in a long list.
|
|
1825
|
+
activeButton.scrollIntoView({ block: "nearest" });
|
|
1826
|
+
}, [viewState.activeSessionId, viewState.focused, viewState.modalOpen]);
|
|
1827
|
+
|
|
1828
|
+
const panelActions = React.createElement(React.Fragment, null,
|
|
1829
|
+
isBulkSelection
|
|
1830
|
+
? React.createElement("span", {
|
|
1831
|
+
className: "ps-mini-button-label",
|
|
1832
|
+
style: { padding: "0 6px", fontSize: "12px", opacity: 0.85 },
|
|
1833
|
+
title: "Multiple sessions selected. Move to group or cancel selected sessions; click Clear to exit.",
|
|
1834
|
+
}, `${selectedCount} selected`)
|
|
1835
|
+
: null,
|
|
1836
|
+
isBulkSelection
|
|
1837
|
+
? React.createElement("button", {
|
|
1838
|
+
type: "button",
|
|
1839
|
+
className: "ps-mini-button",
|
|
1840
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.CLEAR_SESSION_SELECTION).catch(() => {}),
|
|
1841
|
+
title: "Clear multi-selection",
|
|
1842
|
+
}, "Clear")
|
|
1843
|
+
: React.createElement("button", {
|
|
1844
|
+
type: "button",
|
|
1845
|
+
className: "ps-mini-button",
|
|
1846
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.PIN_SESSION).catch(() => {}),
|
|
1847
|
+
disabled: !canPinActiveSession,
|
|
1848
|
+
title: canPinActiveSession
|
|
1849
|
+
? (isActivePinned
|
|
1850
|
+
? "Unpin this session"
|
|
1851
|
+
: "Pin this session to the top of the list")
|
|
1852
|
+
: "Only top-level non-system sessions can be pinned",
|
|
1853
|
+
}, isActivePinned ? "Unpin" : "Pin"),
|
|
1854
|
+
React.createElement("button", {
|
|
1855
|
+
type: "button",
|
|
1856
|
+
className: "ps-mini-button",
|
|
1857
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_MOVE_TO_GROUP).catch(() => {}),
|
|
1858
|
+
disabled: !canMoveToGroup,
|
|
1859
|
+
title: canMoveToGroup
|
|
1860
|
+
? (groupableIds.length > 1 ? `Move ${groupableIds.length} selected sessions to a group` : "Move this session to a group")
|
|
1861
|
+
: "Select a top-level non-system session to move to a group",
|
|
1862
|
+
}, groupableIds.length > 1 ? `Group (${groupableIds.length})` : "Group"),
|
|
1863
|
+
React.createElement("button", {
|
|
1864
|
+
type: "button",
|
|
1865
|
+
className: "ps-mini-button",
|
|
1866
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_RENAME_SESSION).catch(() => {}),
|
|
1867
|
+
disabled: !canRenameActiveSession || isBulkSelection,
|
|
1868
|
+
title: isBulkSelection ? "Disabled while multiple sessions are selected" : undefined,
|
|
1869
|
+
}, "Rename"),
|
|
1870
|
+
React.createElement("button", {
|
|
1871
|
+
type: "button",
|
|
1872
|
+
className: "ps-mini-button",
|
|
1873
|
+
onClick: () => controller.handleCommand(activeSession?.isGroup ? UI_COMMANDS.DELETE_SESSION : UI_COMMANDS.OPEN_TERMINATE_PICKER).catch(() => {}),
|
|
1874
|
+
disabled: !canTerminate,
|
|
1875
|
+
title: isBulkSelection
|
|
1876
|
+
? `Terminate ${selectedCount} selected sessions (Mark Completed, Cancel, or Delete)`
|
|
1877
|
+
: activeSession?.isGroup
|
|
1878
|
+
? activeGroupCanDelete ? "Delete this empty group" : "Show why this group cannot be deleted yet"
|
|
1879
|
+
: activeSession?.isSystem
|
|
1880
|
+
? "Restart this system session; choose complete, terminate, or hard delete disposition"
|
|
1881
|
+
: "Mark Completed, Cancel, or Delete the active session",
|
|
1882
|
+
}, activeSessionActionLabel),
|
|
1883
|
+
actions);
|
|
1884
|
+
|
|
1885
|
+
return React.createElement(Panel, {
|
|
1886
|
+
title: [{ text: "Sessions", color: "yellow", bold: true }],
|
|
1887
|
+
color: "yellow",
|
|
1888
|
+
focused: viewState.focused,
|
|
1889
|
+
theme,
|
|
1890
|
+
actions: panelActions,
|
|
1891
|
+
className: combinedPanelClassName,
|
|
1892
|
+
},
|
|
1893
|
+
React.createElement("div", { className: "ps-action-list ps-session-list" },
|
|
1894
|
+
rows.length === 0
|
|
1895
|
+
? React.createElement("div", { className: "ps-empty-state" }, viewState.filterQuery
|
|
1896
|
+
? `No sessions matched "@@${viewState.filterQuery}".`
|
|
1897
|
+
: "No sessions yet.")
|
|
1898
|
+
: rows.map((row) => React.createElement("button", {
|
|
1899
|
+
key: row.sessionId,
|
|
1900
|
+
type: "button",
|
|
1901
|
+
ref: (node) => setSessionButtonRef(row.sessionId, node),
|
|
1902
|
+
className: `ps-list-button ps-session-list-button${row.active ? " is-selected" : ""}${row.selected ? " is-multiselected" : ""}${row.pinned ? " is-pinned" : ""}`,
|
|
1903
|
+
tabIndex: row.active ? 0 : -1,
|
|
1904
|
+
"aria-selected": row.active ? "true" : "false",
|
|
1905
|
+
onClick: (event) => {
|
|
1906
|
+
// Cmd/Ctrl-click toggles multi-selection (any row).
|
|
1907
|
+
if (event.metaKey || event.ctrlKey) {
|
|
1908
|
+
event.preventDefault();
|
|
1909
|
+
if (!viewState.selectMode) {
|
|
1910
|
+
controller.dispatch({ type: "sessions/selectMode", enabled: true });
|
|
1911
|
+
}
|
|
1912
|
+
controller.dispatch({ type: "sessions/selectToggle", sessionId: row.sessionId });
|
|
1913
|
+
controller.setFocus("sessions");
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
// Shift-click selects a contiguous range from the active row.
|
|
1917
|
+
if (event.shiftKey && viewState.activeSessionId) {
|
|
1918
|
+
event.preventDefault();
|
|
1919
|
+
const ids = rows.map((r) => r.sessionId);
|
|
1920
|
+
const startIndex = ids.indexOf(viewState.activeSessionId);
|
|
1921
|
+
const endIndex = ids.indexOf(row.sessionId);
|
|
1922
|
+
if (startIndex >= 0 && endIndex >= 0) {
|
|
1923
|
+
const [from, to] = startIndex <= endIndex
|
|
1924
|
+
? [startIndex, endIndex]
|
|
1925
|
+
: [endIndex, startIndex];
|
|
1926
|
+
const range = ids.slice(from, to + 1);
|
|
1927
|
+
const merged = Array.from(new Set([...(viewState.selectedIds || []), ...range]));
|
|
1928
|
+
controller.setSessionSelection(merged);
|
|
1929
|
+
controller.setFocus("sessions");
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const shouldToggleChildren = row.hasChildren && row.active;
|
|
1934
|
+
if (shouldToggleChildren) {
|
|
1935
|
+
controller.dispatch({
|
|
1936
|
+
type: row.collapsed ? "sessions/expand" : "sessions/collapse",
|
|
1937
|
+
sessionId: row.sessionId,
|
|
1938
|
+
});
|
|
1939
|
+
controller.setFocus("sessions");
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
// A normal click on a different row clears any
|
|
1943
|
+
// multi-selection and switches the active session.
|
|
1944
|
+
if (viewState.selectMode) {
|
|
1945
|
+
controller.dispatch({ type: "sessions/selectClear" });
|
|
1946
|
+
}
|
|
1947
|
+
controller.setFocus("sessions");
|
|
1948
|
+
if (!row.active) {
|
|
1949
|
+
controller.loadSession(row.sessionId).catch(() => {});
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
1952
|
+
},
|
|
1953
|
+
React.createElement("div", {
|
|
1954
|
+
className: "ps-line ps-session-row-content",
|
|
1955
|
+
style: { paddingInlineStart: `${Math.max(0, row.depth) * 18}px` },
|
|
1956
|
+
},
|
|
1957
|
+
React.createElement(SessionRowContent, { row, theme, structured: structuredRows })),
|
|
1958
|
+
)),
|
|
1959
|
+
));
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function ChatPane({ controller, mobile = false, fullWidth = false, showComposer = true }) {
|
|
1963
|
+
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
1964
|
+
const theme = getTheme(themeId);
|
|
1965
|
+
const viewState = useControllerSelector(controller, (state) => {
|
|
1966
|
+
const activeSessionId = state.sessions.activeSessionId;
|
|
1967
|
+
const layout = computeStateLayout(state);
|
|
1968
|
+
const paneWidth = fullWidth || mobile
|
|
1969
|
+
? layout.totalWidth
|
|
1970
|
+
: layout.leftWidth;
|
|
1971
|
+
const contentWidth = Math.max(20, paneWidth - 4);
|
|
1972
|
+
return {
|
|
1973
|
+
activeSessionId,
|
|
1974
|
+
activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
|
|
1975
|
+
activeOutbox: activeSessionId && state.outbox?.bySessionId?.[activeSessionId]
|
|
1976
|
+
? state.outbox.bySessionId[activeSessionId]
|
|
1977
|
+
: [],
|
|
1978
|
+
branding: state.branding,
|
|
1979
|
+
connection: state.connection,
|
|
1980
|
+
sessionsById: state.sessions.byId,
|
|
1981
|
+
sessionsFlat: state.sessions.flat,
|
|
1982
|
+
inspectorTab: state.ui.inspectorTab,
|
|
1983
|
+
chatViewMode: state.ui.chatViewMode || "transcript",
|
|
1984
|
+
activeSessionIsGroup: Boolean(activeSessionId && state.sessions.byId[activeSessionId]?.isGroup),
|
|
1985
|
+
focused: state.ui.focusRegion === "chat",
|
|
1986
|
+
scroll: state.ui.scroll.chat,
|
|
1987
|
+
contentWidth,
|
|
1988
|
+
};
|
|
1989
|
+
}, shallowEqualObject);
|
|
1990
|
+
const selectorState = React.useMemo(() => ({
|
|
1991
|
+
branding: viewState.branding,
|
|
1992
|
+
connection: viewState.connection,
|
|
1993
|
+
sessions: {
|
|
1994
|
+
activeSessionId: viewState.activeSessionId,
|
|
1995
|
+
byId: viewState.sessionsById,
|
|
1996
|
+
flat: viewState.sessionsFlat,
|
|
1997
|
+
},
|
|
1998
|
+
history: {
|
|
1999
|
+
bySessionId: viewState.activeSessionId && viewState.activeHistory
|
|
2000
|
+
? new Map([[viewState.activeSessionId, viewState.activeHistory]])
|
|
2001
|
+
: new Map(),
|
|
2002
|
+
},
|
|
2003
|
+
outbox: {
|
|
2004
|
+
bySessionId: viewState.activeSessionId
|
|
2005
|
+
? { [viewState.activeSessionId]: viewState.activeOutbox }
|
|
2006
|
+
: {},
|
|
2007
|
+
},
|
|
2008
|
+
ui: {
|
|
2009
|
+
inspectorTab: viewState.inspectorTab,
|
|
2010
|
+
chatViewMode: viewState.chatViewMode,
|
|
2011
|
+
},
|
|
2012
|
+
}), [
|
|
2013
|
+
viewState.activeHistory,
|
|
2014
|
+
viewState.activeSessionId,
|
|
2015
|
+
viewState.activeOutbox,
|
|
2016
|
+
viewState.branding,
|
|
2017
|
+
viewState.connection,
|
|
2018
|
+
viewState.chatViewMode,
|
|
2019
|
+
viewState.inspectorTab,
|
|
2020
|
+
viewState.sessionsById,
|
|
2021
|
+
viewState.sessionsFlat,
|
|
2022
|
+
]);
|
|
2023
|
+
const chrome = React.useMemo(
|
|
2024
|
+
() => selectChatPaneChrome(selectorState, { width: viewState.contentWidth }),
|
|
2025
|
+
[selectorState, viewState.contentWidth],
|
|
2026
|
+
);
|
|
2027
|
+
const animatedDots = useAnimatedDots(Boolean(chrome.animateTitleRight));
|
|
2028
|
+
const titleRight = React.useMemo(
|
|
2029
|
+
() => appendAnimatedDotsToRuns(chrome.titleRight, chrome.animateTitleRight ? animatedDots : ""),
|
|
2030
|
+
[animatedDots, chrome.animateTitleRight, chrome.titleRight],
|
|
2031
|
+
);
|
|
2032
|
+
const lines = React.useMemo(
|
|
2033
|
+
() => selectChatLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" }),
|
|
2034
|
+
[selectorState, viewState.contentWidth],
|
|
2035
|
+
);
|
|
2036
|
+
const outboxLines = React.useMemo(
|
|
2037
|
+
() => selectOutboxOverlayLines(selectorState, viewState.contentWidth, { tableMode: "sentinel" }),
|
|
2038
|
+
[selectorState, viewState.contentWidth],
|
|
2039
|
+
);
|
|
2040
|
+
const composer = showComposer && !viewState.activeSessionIsGroup && viewState.chatViewMode !== "summary"
|
|
2041
|
+
? React.createElement("div", { className: "ps-chat-composer" },
|
|
2042
|
+
React.createElement(PromptComposer, { controller, mobile, active: true }))
|
|
2043
|
+
: null;
|
|
2044
|
+
|
|
2045
|
+
return React.createElement(ScrollLinesPanel, {
|
|
2046
|
+
controller,
|
|
2047
|
+
title: mobile ? compactTitleRuns(chrome.title, 28) : chrome.title,
|
|
2048
|
+
titleRight: mobile && titleRight ? compactTitleRuns(titleRight, 18) : titleRight,
|
|
2049
|
+
// Chat/Summary toggling lives on the top toolbar so the pane chrome
|
|
2050
|
+
// stays clean. The toolbar button is disabled for group sessions.
|
|
2051
|
+
actions: null,
|
|
2052
|
+
color: chrome.color,
|
|
2053
|
+
focused: viewState.focused,
|
|
2054
|
+
lines,
|
|
2055
|
+
bottomStickyLines: outboxLines,
|
|
2056
|
+
scrollOffset: viewState.scroll,
|
|
2057
|
+
scrollMode: "bottom",
|
|
2058
|
+
paneKey: "chat",
|
|
2059
|
+
className: "is-wrapped",
|
|
2060
|
+
panelClassName: "ps-chat-panel",
|
|
2061
|
+
bottomContent: composer,
|
|
2062
|
+
structuredBlocks: true,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function MobileWorkspace({ controller, sessionsCollapsed, setSessionsCollapsed }) {
|
|
2067
|
+
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
2068
|
+
const theme = getTheme(themeId);
|
|
2069
|
+
const sessionToggle = React.createElement("button", {
|
|
2070
|
+
type: "button",
|
|
2071
|
+
className: "ps-mini-button",
|
|
2072
|
+
onClick: () => setSessionsCollapsed((current) => !current),
|
|
2073
|
+
}, sessionsCollapsed ? "Show" : "Hide");
|
|
2074
|
+
|
|
2075
|
+
return React.createElement("div", { className: "ps-mobile-workspace" },
|
|
2076
|
+
sessionsCollapsed
|
|
2077
|
+
? React.createElement(Panel, {
|
|
2078
|
+
title: [{ text: "Sessions", color: "yellow", bold: true }],
|
|
2079
|
+
color: "yellow",
|
|
2080
|
+
focused: false,
|
|
2081
|
+
theme,
|
|
2082
|
+
actions: sessionToggle,
|
|
2083
|
+
className: "ps-mobile-session-collapsed",
|
|
2084
|
+
},
|
|
2085
|
+
React.createElement("div", { className: "ps-mobile-session-summary" }, "Session list collapsed."))
|
|
2086
|
+
: React.createElement(SessionPane, {
|
|
2087
|
+
controller,
|
|
2088
|
+
actions: sessionToggle,
|
|
2089
|
+
panelClassName: "ps-mobile-session-pane",
|
|
2090
|
+
}),
|
|
2091
|
+
React.createElement("div", { className: "ps-mobile-chat-pane" },
|
|
2092
|
+
React.createElement(ChatPane, { controller, mobile: true, fullWidth: true })));
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function InspectorTabs({ activeTab, controller }) {
|
|
2096
|
+
const visibleTabs = React.useMemo(() => getVisibleInspectorTabs(controller), [controller]);
|
|
2097
|
+
// Tab labels in mobile fall on a single row only when each label is
|
|
2098
|
+
// short. "Node Map" is the only multi-word label, so we shorten it
|
|
2099
|
+
// to "Map" below the mobile breakpoint to keep all six tabs on one
|
|
2100
|
+
// line.
|
|
2101
|
+
const isMobile = typeof window !== "undefined"
|
|
2102
|
+
&& (window.innerWidth || 0) > 0
|
|
2103
|
+
&& (window.innerWidth || 0) < MOBILE_BREAKPOINT;
|
|
2104
|
+
const labelFor = (tab) => {
|
|
2105
|
+
if (isMobile && tab === "nodes") return "Map";
|
|
2106
|
+
return INSPECTOR_TAB_LABELS[tab] || tab;
|
|
2107
|
+
};
|
|
2108
|
+
return React.createElement("div", { className: "ps-tab-row" },
|
|
2109
|
+
visibleTabs.map((tab) => React.createElement("button", {
|
|
2110
|
+
key: tab,
|
|
2111
|
+
type: "button",
|
|
2112
|
+
className: `ps-tab${activeTab === tab ? " is-active" : ""}`,
|
|
2113
|
+
title: `Switch to ${INSPECTOR_TAB_LABELS[tab] || tab}`,
|
|
2114
|
+
"aria-pressed": activeTab === tab,
|
|
2115
|
+
onClick: () => {
|
|
2116
|
+
controller.setFocus("inspector");
|
|
2117
|
+
controller.selectInspectorTab(tab).catch(() => {});
|
|
2118
|
+
},
|
|
2119
|
+
}, labelFor(tab))));
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function FilesPane({ controller, focused, mobile = false }) {
|
|
2123
|
+
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
2124
|
+
const theme = getTheme(themeId);
|
|
2125
|
+
const fileInputRef = React.useRef(null);
|
|
2126
|
+
const viewState = useControllerSelector(controller, (state) => {
|
|
2127
|
+
const activeSessionId = state.sessions.activeSessionId;
|
|
2128
|
+
const layout = computeStateLayout(state);
|
|
2129
|
+
const paneWidth = (mobile || state.files.fullscreen)
|
|
2130
|
+
? (state.ui.layout?.viewportWidth ?? 120)
|
|
2131
|
+
: layout.rightWidth;
|
|
2132
|
+
return {
|
|
2133
|
+
activeSessionId,
|
|
2134
|
+
sessionsById: state.sessions.byId,
|
|
2135
|
+
sessionsFlat: state.sessions.flat,
|
|
2136
|
+
filesBySessionId: state.files.bySessionId,
|
|
2137
|
+
filesFilter: state.files.filter,
|
|
2138
|
+
selectedArtifactId: state.files.selectedArtifactId,
|
|
2139
|
+
focused,
|
|
2140
|
+
previewScroll: state.ui.scroll.filePreview,
|
|
2141
|
+
fullscreen: Boolean(state.files.fullscreen),
|
|
2142
|
+
contentWidth: Math.max(20, paneWidth - 4),
|
|
2143
|
+
canBrowserUpload: supportsBrowserFileUploads(controller),
|
|
2144
|
+
canPathUpload: supportsPathArtifactUploads(controller),
|
|
2145
|
+
canDeleteArtifacts: supportsArtifactDelete(controller),
|
|
2146
|
+
canOpenLocally: supportsLocalFileOpen(controller),
|
|
2147
|
+
};
|
|
2148
|
+
}, shallowEqualObject);
|
|
2149
|
+
const selectorState = React.useMemo(() => ({
|
|
2150
|
+
sessions: {
|
|
2151
|
+
activeSessionId: viewState.activeSessionId,
|
|
2152
|
+
byId: viewState.sessionsById,
|
|
2153
|
+
flat: viewState.sessionsFlat,
|
|
2154
|
+
},
|
|
2155
|
+
files: {
|
|
2156
|
+
bySessionId: viewState.filesBySessionId,
|
|
2157
|
+
selectedArtifactId: viewState.selectedArtifactId,
|
|
2158
|
+
filter: viewState.filesFilter,
|
|
2159
|
+
fullscreen: viewState.fullscreen,
|
|
2160
|
+
},
|
|
2161
|
+
ui: {
|
|
2162
|
+
scroll: {
|
|
2163
|
+
filePreview: viewState.previewScroll,
|
|
2164
|
+
},
|
|
2165
|
+
},
|
|
2166
|
+
}), [
|
|
2167
|
+
viewState.activeSessionId,
|
|
2168
|
+
viewState.filesBySessionId,
|
|
2169
|
+
viewState.filesFilter,
|
|
2170
|
+
viewState.fullscreen,
|
|
2171
|
+
viewState.previewScroll,
|
|
2172
|
+
viewState.selectedArtifactId,
|
|
2173
|
+
viewState.sessionsById,
|
|
2174
|
+
viewState.sessionsFlat,
|
|
2175
|
+
]);
|
|
2176
|
+
const filesView = React.useMemo(() => selectFilesView(selectorState, {
|
|
2177
|
+
listWidth: Math.max(18, viewState.contentWidth - 4),
|
|
2178
|
+
previewWidth: Math.max(18, viewState.contentWidth - 4),
|
|
2179
|
+
showHints: false,
|
|
2180
|
+
}), [selectorState, viewState.contentWidth]);
|
|
2181
|
+
const items = React.useMemo(() => selectFileBrowserItems(selectorState), [selectorState]);
|
|
2182
|
+
const hasSelection = items.length > 0;
|
|
2183
|
+
|
|
2184
|
+
const uploadFiles = React.useCallback((files) => {
|
|
2185
|
+
const nextFiles = Array.isArray(files) ? files.filter(Boolean) : [];
|
|
2186
|
+
if (nextFiles.length === 0) return;
|
|
2187
|
+
controller.uploadArtifactFiles(nextFiles).catch(() => {});
|
|
2188
|
+
}, [controller]);
|
|
2189
|
+
|
|
2190
|
+
const openUploadPicker = React.useCallback(() => {
|
|
2191
|
+
if (viewState.canBrowserUpload && fileInputRef.current) {
|
|
2192
|
+
fileInputRef.current.click();
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
if (viewState.canPathUpload) {
|
|
2196
|
+
controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_UPLOAD).catch(() => {});
|
|
2197
|
+
}
|
|
2198
|
+
}, [controller, viewState.canBrowserUpload, viewState.canPathUpload]);
|
|
2199
|
+
|
|
2200
|
+
const panelActions = React.createElement(React.Fragment, null,
|
|
2201
|
+
React.createElement("input", {
|
|
2202
|
+
ref: fileInputRef,
|
|
2203
|
+
type: "file",
|
|
2204
|
+
className: "ps-hidden-file-input",
|
|
2205
|
+
multiple: true,
|
|
2206
|
+
tabIndex: -1,
|
|
2207
|
+
"aria-hidden": "true",
|
|
2208
|
+
onChange: (event) => {
|
|
2209
|
+
uploadFiles(Array.from(event.currentTarget.files || []));
|
|
2210
|
+
event.currentTarget.value = "";
|
|
2211
|
+
},
|
|
2212
|
+
}),
|
|
2213
|
+
React.createElement("button", {
|
|
2214
|
+
type: "button",
|
|
2215
|
+
className: "ps-mini-button",
|
|
2216
|
+
onClick: openUploadPicker,
|
|
2217
|
+
disabled: !viewState.canBrowserUpload && !viewState.canPathUpload,
|
|
2218
|
+
}, "Up"),
|
|
2219
|
+
React.createElement("button", {
|
|
2220
|
+
type: "button",
|
|
2221
|
+
className: "ps-mini-button",
|
|
2222
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.DOWNLOAD_SELECTED_FILE).catch(() => {}),
|
|
2223
|
+
disabled: !hasSelection,
|
|
2224
|
+
}, "Down"),
|
|
2225
|
+
viewState.canDeleteArtifacts ? React.createElement("button", {
|
|
2226
|
+
type: "button",
|
|
2227
|
+
className: "ps-mini-button",
|
|
2228
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.DELETE_SELECTED_FILE).catch(() => {}),
|
|
2229
|
+
disabled: !hasSelection,
|
|
2230
|
+
}, "Delete") : null,
|
|
2231
|
+
viewState.canOpenLocally ? React.createElement("button", {
|
|
2232
|
+
type: "button",
|
|
2233
|
+
className: "ps-mini-button",
|
|
2234
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_SELECTED_FILE).catch(() => {}),
|
|
2235
|
+
disabled: !hasSelection,
|
|
2236
|
+
}, "Open") : null,
|
|
2237
|
+
React.createElement("button", {
|
|
2238
|
+
type: "button",
|
|
2239
|
+
className: "ps-mini-button",
|
|
2240
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_FILES_FILTER).catch(() => {}),
|
|
2241
|
+
}, "Filter"),
|
|
2242
|
+
React.createElement("button", {
|
|
2243
|
+
type: "button",
|
|
2244
|
+
className: "ps-mini-button",
|
|
2245
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {}),
|
|
2246
|
+
}, viewState.fullscreen ? "Close" : "FS"));
|
|
2247
|
+
|
|
2248
|
+
const listContent = items.length === 0
|
|
2249
|
+
? normalizeLines(filesView.listBodyLines || []).map((line, index) => React.createElement(Line, {
|
|
2250
|
+
key: `empty:${index}`,
|
|
2251
|
+
line,
|
|
2252
|
+
theme,
|
|
2253
|
+
}))
|
|
2254
|
+
: items.map((item, index) => React.createElement("button", {
|
|
2255
|
+
key: item.id,
|
|
2256
|
+
type: "button",
|
|
2257
|
+
className: `ps-list-button${index === filesView.selectedIndex ? " is-selected" : ""}`,
|
|
2258
|
+
onClick: () => {
|
|
2259
|
+
controller.setFocus("inspector");
|
|
2260
|
+
controller.selectFileBrowserItem(item).catch(() => {});
|
|
2261
|
+
},
|
|
2262
|
+
}, React.createElement(Line, {
|
|
2263
|
+
line: normalizeLines([filesView.listBodyLines?.[index]])[0],
|
|
2264
|
+
theme,
|
|
2265
|
+
})));
|
|
2266
|
+
|
|
2267
|
+
const previewPane = filesView.previewRenderMode === "markdown"
|
|
2268
|
+
&& !filesView.previewLoading
|
|
2269
|
+
&& !filesView.previewError
|
|
2270
|
+
? React.createElement(MarkdownPreviewPanel, {
|
|
2271
|
+
controller,
|
|
2272
|
+
title: filesView.previewTitle,
|
|
2273
|
+
color: "cyan",
|
|
2274
|
+
focused: false,
|
|
2275
|
+
scrollOffset: viewState.previewScroll,
|
|
2276
|
+
paneKey: "filePreview",
|
|
2277
|
+
theme,
|
|
2278
|
+
content: filesView.previewContent || "",
|
|
2279
|
+
})
|
|
2280
|
+
: filesView.previewIsBinary
|
|
2281
|
+
&& !filesView.previewLoading
|
|
2282
|
+
&& !filesView.previewError
|
|
2283
|
+
? React.createElement(BinaryArtifactPreviewPanel, {
|
|
2284
|
+
title: filesView.previewTitle,
|
|
2285
|
+
color: "cyan",
|
|
2286
|
+
focused: false,
|
|
2287
|
+
theme,
|
|
2288
|
+
filename: filesView.selectedFilename,
|
|
2289
|
+
contentType: filesView.previewContentType,
|
|
2290
|
+
sizeBytes: filesView.previewSizeBytes,
|
|
2291
|
+
source: filesView.previewSource,
|
|
2292
|
+
uploadedAt: filesView.previewUploadedAt,
|
|
2293
|
+
})
|
|
2294
|
+
: React.createElement(ScrollLinesPanel, {
|
|
2295
|
+
controller,
|
|
2296
|
+
title: filesView.previewTitle,
|
|
2297
|
+
color: "cyan",
|
|
2298
|
+
focused: false,
|
|
2299
|
+
lines: filesView.previewLines,
|
|
2300
|
+
scrollOffset: viewState.previewScroll,
|
|
2301
|
+
scrollMode: "top",
|
|
2302
|
+
paneKey: "filePreview",
|
|
2303
|
+
className: "is-preview is-wrapped",
|
|
2304
|
+
});
|
|
2305
|
+
const view = viewState;
|
|
2306
|
+
|
|
2307
|
+
return React.createElement(Panel, {
|
|
2308
|
+
title: view.fullscreen
|
|
2309
|
+
? filesView.fullscreenTitle
|
|
2310
|
+
: (mobile && filesView.panelTitleMobile)
|
|
2311
|
+
? filesView.panelTitleMobile
|
|
2312
|
+
: filesView.panelTitle,
|
|
2313
|
+
color: "magenta",
|
|
2314
|
+
focused: view.focused,
|
|
2315
|
+
actions: panelActions,
|
|
2316
|
+
theme,
|
|
2317
|
+
},
|
|
2318
|
+
React.createElement(InspectorTabs, { activeTab: "files", controller }),
|
|
2319
|
+
// Fullscreen files mode shows only the preview pane; the list stays hidden.
|
|
2320
|
+
view.fullscreen
|
|
2321
|
+
? previewPane
|
|
2322
|
+
: React.createElement("div", { className: "ps-files-grid" },
|
|
2323
|
+
React.createElement(Panel, { title: filesView.listTitle, color: "cyan", theme },
|
|
2324
|
+
React.createElement("div", { className: "ps-action-list" }, listContent)),
|
|
2325
|
+
previewPane,
|
|
2326
|
+
));
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function InspectorPane({ controller, mobile = false, panelClassName = "", extraActions = null }) {
|
|
2330
|
+
const viewState = useControllerSelector(controller, (state) => {
|
|
2331
|
+
const layout = computeStateLayout(state);
|
|
2332
|
+
const inspectorTab = state.ui.inspectorTab;
|
|
2333
|
+
const paneWidth = mobile
|
|
2334
|
+
? (state.ui.layout?.viewportWidth ?? 120)
|
|
2335
|
+
: layout.rightWidth;
|
|
2336
|
+
return {
|
|
2337
|
+
inspectorTab,
|
|
2338
|
+
statsViewMode: state.ui.statsViewMode,
|
|
2339
|
+
activeSessionId: state.sessions.activeSessionId,
|
|
2340
|
+
sessionsById: state.sessions.byId,
|
|
2341
|
+
sessionsFlat: state.sessions.flat,
|
|
2342
|
+
historyBySessionId: state.history.bySessionId,
|
|
2343
|
+
sessionStats: state.sessionStats,
|
|
2344
|
+
fleetStats: state.fleetStats,
|
|
2345
|
+
connection: state.connection,
|
|
2346
|
+
orchestrationBySessionId: state.orchestration.bySessionId,
|
|
2347
|
+
executionHistoryBySessionId: state.executionHistory?.bySessionId || {},
|
|
2348
|
+
executionHistoryFormat: state.executionHistory?.format || "pretty",
|
|
2349
|
+
logs: state.logs,
|
|
2350
|
+
files: state.files,
|
|
2351
|
+
focused: state.ui.focusRegion === "inspector",
|
|
2352
|
+
scroll: state.ui.scroll.inspector,
|
|
2353
|
+
followBottom: state.ui.followBottom?.inspector !== false,
|
|
2354
|
+
logsTailing: state.logs.tailing,
|
|
2355
|
+
filesFullscreen: Boolean(state.files.fullscreen),
|
|
2356
|
+
contentWidth: getPortalInspectorContentWidth(paneWidth, inspectorTab),
|
|
2357
|
+
};
|
|
2358
|
+
}, shallowEqualObject);
|
|
2359
|
+
const selectorState = React.useMemo(() => ({
|
|
2360
|
+
sessions: {
|
|
2361
|
+
activeSessionId: viewState.activeSessionId,
|
|
2362
|
+
byId: viewState.sessionsById,
|
|
2363
|
+
flat: viewState.sessionsFlat,
|
|
2364
|
+
},
|
|
2365
|
+
history: {
|
|
2366
|
+
bySessionId: viewState.historyBySessionId,
|
|
2367
|
+
},
|
|
2368
|
+
sessionStats: viewState.sessionStats,
|
|
2369
|
+
fleetStats: viewState.fleetStats,
|
|
2370
|
+
connection: viewState.connection,
|
|
2371
|
+
ui: {
|
|
2372
|
+
inspectorTab: viewState.inspectorTab,
|
|
2373
|
+
statsViewMode: viewState.statsViewMode,
|
|
2374
|
+
scroll: {
|
|
2375
|
+
inspector: viewState.scroll,
|
|
2376
|
+
},
|
|
2377
|
+
},
|
|
2378
|
+
logs: viewState.logs,
|
|
2379
|
+
files: viewState.files,
|
|
2380
|
+
orchestration: {
|
|
2381
|
+
bySessionId: viewState.orchestrationBySessionId,
|
|
2382
|
+
},
|
|
2383
|
+
executionHistory: {
|
|
2384
|
+
bySessionId: viewState.executionHistoryBySessionId,
|
|
2385
|
+
format: viewState.executionHistoryFormat,
|
|
2386
|
+
},
|
|
2387
|
+
}), [
|
|
2388
|
+
viewState.activeSessionId,
|
|
2389
|
+
viewState.connection,
|
|
2390
|
+
viewState.executionHistoryBySessionId,
|
|
2391
|
+
viewState.executionHistoryFormat,
|
|
2392
|
+
viewState.fleetStats,
|
|
2393
|
+
viewState.files,
|
|
2394
|
+
viewState.historyBySessionId,
|
|
2395
|
+
viewState.inspectorTab,
|
|
2396
|
+
viewState.sessionStats,
|
|
2397
|
+
viewState.statsViewMode,
|
|
2398
|
+
viewState.logs,
|
|
2399
|
+
viewState.orchestrationBySessionId,
|
|
2400
|
+
viewState.scroll,
|
|
2401
|
+
viewState.sessionsById,
|
|
2402
|
+
viewState.sessionsFlat,
|
|
2403
|
+
]);
|
|
2404
|
+
const inspector = React.useMemo(() => selectInspector(selectorState, {
|
|
2405
|
+
width: viewState.contentWidth,
|
|
2406
|
+
allowWideColumns: mobile,
|
|
2407
|
+
}), [mobile, selectorState, viewState.contentWidth]);
|
|
2408
|
+
const completionByTurn = React.useMemo(() => {
|
|
2409
|
+
const history = viewState.activeSessionId
|
|
2410
|
+
? viewState.historyBySessionId?.get(viewState.activeSessionId) || null
|
|
2411
|
+
: null;
|
|
2412
|
+
const map = new Map();
|
|
2413
|
+
for (const event of history?.events || []) {
|
|
2414
|
+
if (event?.eventType !== "session.turn_completed") continue;
|
|
2415
|
+
const turn = Number(event?.data?.turnIndex ?? event?.data?.iteration);
|
|
2416
|
+
if (Number.isFinite(turn)) map.set(turn, event);
|
|
2417
|
+
}
|
|
2418
|
+
return map;
|
|
2419
|
+
}, [viewState.activeSessionId, viewState.historyBySessionId]);
|
|
2420
|
+
const renderSequenceBody = React.useCallback((lines, theme) => (
|
|
2421
|
+
React.createElement(PortalSequenceLines, { lines, theme, completionByTurn })
|
|
2422
|
+
), [completionByTurn]);
|
|
2423
|
+
|
|
2424
|
+
if (viewState.inspectorTab === "files" && !inspector.disabled) {
|
|
2425
|
+
return React.createElement(FilesPane, { controller, focused: viewState.focused, mobile });
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
const actions = [];
|
|
2429
|
+
if (viewState.inspectorTab === "logs") {
|
|
2430
|
+
actions.push(React.createElement("button", {
|
|
2431
|
+
key: "tail",
|
|
2432
|
+
type: "button",
|
|
2433
|
+
className: "ps-mini-button",
|
|
2434
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.TOGGLE_LOG_TAIL).catch(() => {}),
|
|
2435
|
+
}, viewState.logsTailing ? "Stop Tail" : "Tail"));
|
|
2436
|
+
actions.push(React.createElement("button", {
|
|
2437
|
+
key: "filter",
|
|
2438
|
+
type: "button",
|
|
2439
|
+
className: "ps-mini-button",
|
|
2440
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_LOG_FILTER).catch(() => {}),
|
|
2441
|
+
}, "Filter"));
|
|
2442
|
+
} else if (viewState.inspectorTab === "history") {
|
|
2443
|
+
actions.push(React.createElement("button", {
|
|
2444
|
+
key: "refresh",
|
|
2445
|
+
type: "button",
|
|
2446
|
+
className: "ps-mini-button",
|
|
2447
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.REFRESH_EXECUTION_HISTORY).catch(() => {}),
|
|
2448
|
+
}, "Refresh"));
|
|
2449
|
+
actions.push(React.createElement("button", {
|
|
2450
|
+
key: "save",
|
|
2451
|
+
type: "button",
|
|
2452
|
+
className: "ps-mini-button",
|
|
2453
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.EXPORT_EXECUTION_HISTORY).catch(() => {}),
|
|
2454
|
+
}, "Artifact"));
|
|
2455
|
+
} else if (viewState.inspectorTab === "stats") {
|
|
2456
|
+
for (const mode of ["session", "fleet", "users"]) {
|
|
2457
|
+
actions.push(React.createElement("button", {
|
|
2458
|
+
key: `stats-view:${mode}`,
|
|
2459
|
+
type: "button",
|
|
2460
|
+
className: `ps-mini-button${viewState.statsViewMode === mode ? " is-active" : ""}`,
|
|
2461
|
+
onClick: () => controller.setStatsViewMode(mode),
|
|
2462
|
+
}, mode.replace(/^./u, (char) => char.toUpperCase())));
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
const panelActions = extraActions
|
|
2467
|
+
? actions.concat(extraActions)
|
|
2468
|
+
: actions;
|
|
2469
|
+
|
|
2470
|
+
return React.createElement(ScrollLinesPanel, {
|
|
2471
|
+
controller,
|
|
2472
|
+
title: inspector.title,
|
|
2473
|
+
color: "magenta",
|
|
2474
|
+
focused: viewState.focused,
|
|
2475
|
+
actions: panelActions,
|
|
2476
|
+
topContent: React.createElement(InspectorTabs, { activeTab: inspector.activeTab, controller }),
|
|
2477
|
+
stickyLines: inspector.stickyLines || [],
|
|
2478
|
+
lines: inspector.lines,
|
|
2479
|
+
renderBody: inspector.activeTab === "sequence" ? renderSequenceBody : null,
|
|
2480
|
+
scrollOffset: viewState.scroll,
|
|
2481
|
+
scrollMode: inspector.activeTab === "sequence"
|
|
2482
|
+
? "bottom"
|
|
2483
|
+
: inspector.activeTab === "logs" && viewState.followBottom
|
|
2484
|
+
? "bottom"
|
|
2485
|
+
: "top",
|
|
2486
|
+
stickyBottom: inspector.activeTab === "logs",
|
|
2487
|
+
paneKey: "inspector",
|
|
2488
|
+
className: inspector.activeTab === "history" || inspector.activeTab === "logs" ? "is-wrapped" : "is-preserve",
|
|
2489
|
+
panelClassName: `${inspector.activeTab === "sequence" ? "has-preserved-sticky" : ""}${panelClassName ? ` ${panelClassName}` : ""}`.trim(),
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
function ActivityPane({ controller, panelClassName = "", extraActions = null }) {
|
|
2494
|
+
const viewState = useControllerSelector(controller, (state) => {
|
|
2495
|
+
const activeSessionId = state.sessions.activeSessionId;
|
|
2496
|
+
const layout = computeStateLayout(state);
|
|
2497
|
+
const maxLines = Math.max(3, layout.activityPaneHeight - 2);
|
|
2498
|
+
return {
|
|
2499
|
+
activeSessionId,
|
|
2500
|
+
activeSession: activeSessionId ? state.sessions.byId[activeSessionId] || null : null,
|
|
2501
|
+
activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
|
|
2502
|
+
focused: state.ui.focusRegion === "activity",
|
|
2503
|
+
scroll: state.ui.scroll.activity,
|
|
2504
|
+
followBottom: state.ui.followBottom?.activity !== false,
|
|
2505
|
+
maxLines,
|
|
2506
|
+
};
|
|
2507
|
+
}, shallowEqualObject);
|
|
2508
|
+
const selectorState = React.useMemo(() => ({
|
|
2509
|
+
sessions: {
|
|
2510
|
+
activeSessionId: viewState.activeSessionId,
|
|
2511
|
+
byId: viewState.activeSessionId && viewState.activeSession
|
|
2512
|
+
? { [viewState.activeSessionId]: viewState.activeSession }
|
|
2513
|
+
: {},
|
|
2514
|
+
},
|
|
2515
|
+
history: {
|
|
2516
|
+
bySessionId: viewState.activeSessionId && viewState.activeHistory
|
|
2517
|
+
? new Map([[viewState.activeSessionId, viewState.activeHistory]])
|
|
2518
|
+
: new Map(),
|
|
2519
|
+
},
|
|
2520
|
+
}), [viewState.activeHistory, viewState.activeSession, viewState.activeSessionId]);
|
|
2521
|
+
const activity = React.useMemo(
|
|
2522
|
+
() => selectActivityPane(selectorState, viewState.maxLines),
|
|
2523
|
+
[selectorState, viewState.maxLines],
|
|
2524
|
+
);
|
|
2525
|
+
|
|
2526
|
+
return React.createElement(ScrollLinesPanel, {
|
|
2527
|
+
controller,
|
|
2528
|
+
title: activity.title,
|
|
2529
|
+
color: "gray",
|
|
2530
|
+
focused: viewState.focused,
|
|
2531
|
+
actions: extraActions,
|
|
2532
|
+
lines: activity.lines,
|
|
2533
|
+
scrollOffset: viewState.scroll,
|
|
2534
|
+
scrollMode: viewState.followBottom ? "bottom" : "top",
|
|
2535
|
+
stickyBottom: true,
|
|
2536
|
+
paneKey: "activity",
|
|
2537
|
+
className: "is-wrapped",
|
|
2538
|
+
panelClassName,
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
const CHAT_FOCUS_PANES = [
|
|
2543
|
+
{ id: "sessions", label: "Sessions", side: "left" },
|
|
2544
|
+
{ id: "inspector", label: "Inspector", side: "right" },
|
|
2545
|
+
{ id: "activity", label: "Activity", side: "right" },
|
|
2546
|
+
];
|
|
2547
|
+
|
|
2548
|
+
function ChatFocusOverlay({ controller, pane, onClose, mobile = false }) {
|
|
2549
|
+
if (!pane) return null;
|
|
2550
|
+
|
|
2551
|
+
let content = null;
|
|
2552
|
+
if (pane === "sessions") {
|
|
2553
|
+
content = React.createElement(SessionPane, {
|
|
2554
|
+
controller,
|
|
2555
|
+
panelClassName: "ps-chat-focus-pane",
|
|
2556
|
+
structuredRows: mobile,
|
|
2557
|
+
actions: React.createElement("button", {
|
|
2558
|
+
type: "button",
|
|
2559
|
+
className: "ps-mini-button",
|
|
2560
|
+
onClick: onClose,
|
|
2561
|
+
}, "Close"),
|
|
2562
|
+
});
|
|
2563
|
+
} else if (pane === "inspector") {
|
|
2564
|
+
content = React.createElement(InspectorPane, {
|
|
2565
|
+
controller,
|
|
2566
|
+
mobile: false,
|
|
2567
|
+
panelClassName: "ps-chat-focus-pane",
|
|
2568
|
+
extraActions: React.createElement("button", {
|
|
2569
|
+
type: "button",
|
|
2570
|
+
className: "ps-mini-button",
|
|
2571
|
+
onClick: onClose,
|
|
2572
|
+
}, "Close"),
|
|
2573
|
+
});
|
|
2574
|
+
} else if (pane === "activity") {
|
|
2575
|
+
content = React.createElement(ActivityPane, {
|
|
2576
|
+
controller,
|
|
2577
|
+
panelClassName: "ps-chat-focus-pane",
|
|
2578
|
+
extraActions: React.createElement("button", {
|
|
2579
|
+
type: "button",
|
|
2580
|
+
className: "ps-mini-button",
|
|
2581
|
+
onClick: onClose,
|
|
2582
|
+
}, "Close"),
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
const paneMeta = CHAT_FOCUS_PANES.find((entry) => entry.id === pane);
|
|
2587
|
+
return React.createElement("div", {
|
|
2588
|
+
className: `ps-chat-focus-overlay${paneMeta?.side === "left" ? " is-left" : " is-right"}`,
|
|
2589
|
+
}, content);
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
function ChatFocusWorkspace({ controller, openPane, onTogglePane, onExitFocus, mobile = false }) {
|
|
2593
|
+
const focusRegion = useControllerSelector(controller, (state) => state.ui.focusRegion);
|
|
2594
|
+
|
|
2595
|
+
const rail = mobile
|
|
2596
|
+
? React.createElement("div", { className: "ps-chat-focus-rail" },
|
|
2597
|
+
React.createElement("button", {
|
|
2598
|
+
type: "button",
|
|
2599
|
+
className: "ps-mini-button ps-chat-focus-button",
|
|
2600
|
+
onClick: onExitFocus,
|
|
2601
|
+
}, "Exit Focus"),
|
|
2602
|
+
React.createElement("button", {
|
|
2603
|
+
type: "button",
|
|
2604
|
+
className: `ps-mini-button ps-chat-focus-button${openPane === "sessions" ? " is-active" : ""}`,
|
|
2605
|
+
onClick: () => onTogglePane("sessions"),
|
|
2606
|
+
}, "Sessions"))
|
|
2607
|
+
: React.createElement("div", { className: "ps-chat-focus-rail" },
|
|
2608
|
+
CHAT_FOCUS_PANES.map((pane) => React.createElement("button", {
|
|
2609
|
+
key: pane.id,
|
|
2610
|
+
type: "button",
|
|
2611
|
+
className: `ps-mini-button ps-chat-focus-button${openPane === pane.id ? " is-active" : ""}`,
|
|
2612
|
+
"aria-pressed": openPane === pane.id ? "true" : "false",
|
|
2613
|
+
onClick: () => onTogglePane(pane.id),
|
|
2614
|
+
}, pane.label)),
|
|
2615
|
+
React.createElement("div", { className: "ps-chat-focus-status" },
|
|
2616
|
+
openPane
|
|
2617
|
+
? `Focused: ${CHAT_FOCUS_PANES.find((pane) => pane.id === openPane)?.label || openPane}`
|
|
2618
|
+
: `Focused: ${focusRegion === "prompt" ? "Prompt" : "Chat"}`));
|
|
2619
|
+
|
|
2620
|
+
return React.createElement("div", { className: "ps-chat-focus-shell" },
|
|
2621
|
+
rail,
|
|
2622
|
+
React.createElement("div", { className: "ps-chat-focus-body" },
|
|
2623
|
+
React.createElement(ChatPane, { controller, mobile, fullWidth: true }),
|
|
2624
|
+
React.createElement(ChatFocusOverlay, {
|
|
2625
|
+
controller,
|
|
2626
|
+
pane: openPane,
|
|
2627
|
+
onClose: () => onTogglePane(openPane),
|
|
2628
|
+
mobile,
|
|
2629
|
+
})));
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
function PromptComposer({ controller, mobile, active = true, onAfterSend = null }) {
|
|
2633
|
+
const promptState = useControllerSelector(controller, (state) => {
|
|
2634
|
+
const activeSessionId = state.sessions.activeSessionId;
|
|
2635
|
+
const activeSession = activeSessionId ? state.sessions.byId[activeSessionId] || null : null;
|
|
2636
|
+
const outbox = activeSessionId && state.outbox?.bySessionId?.[activeSessionId]
|
|
2637
|
+
? state.outbox.bySessionId[activeSessionId]
|
|
2638
|
+
: [];
|
|
2639
|
+
return {
|
|
2640
|
+
value: state.ui.prompt,
|
|
2641
|
+
cursor: state.ui.promptCursor,
|
|
2642
|
+
focused: state.ui.focusRegion === "prompt",
|
|
2643
|
+
modalOpen: Boolean(state.ui.modal),
|
|
2644
|
+
answerMode: Boolean(activeSession?.pendingQuestion?.question),
|
|
2645
|
+
canStopTurn: canStopSessionTurn(activeSession),
|
|
2646
|
+
hasOutbox: outbox.length > 0,
|
|
2647
|
+
hasPendingOutbox: outbox.some((item) => item?.phase === "pending"),
|
|
2648
|
+
pendingCount: outbox.filter((item) => item?.phase === "pending").length,
|
|
2649
|
+
editingPending: state.ui.promptEdit?.sessionId === activeSessionId,
|
|
2650
|
+
selectedOutboxPhase: state.ui.promptEdit?.sessionId === activeSessionId
|
|
2651
|
+
? state.ui.promptEdit?.phase || null
|
|
2652
|
+
: null,
|
|
2653
|
+
};
|
|
2654
|
+
}, shallowEqualObject);
|
|
2655
|
+
const inputRef = React.useRef(null);
|
|
2656
|
+
|
|
2657
|
+
React.useEffect(() => {
|
|
2658
|
+
const inputNode = inputRef.current;
|
|
2659
|
+
if (!active || promptState.modalOpen || !promptState.focused || !inputNode) return;
|
|
2660
|
+
if (document.activeElement !== inputNode) {
|
|
2661
|
+
try {
|
|
2662
|
+
inputNode.focus({ preventScroll: true });
|
|
2663
|
+
} catch {
|
|
2664
|
+
inputNode.focus();
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
inputNode.setSelectionRange(promptState.cursor, promptState.cursor);
|
|
2668
|
+
}, [active, promptState.cursor, promptState.focused, promptState.modalOpen]);
|
|
2669
|
+
|
|
2670
|
+
const sendPrompt = React.useCallback(() => {
|
|
2671
|
+
controller.handleCommand(UI_COMMANDS.SEND_PROMPT)
|
|
2672
|
+
.catch(() => {})
|
|
2673
|
+
.finally(() => {
|
|
2674
|
+
onAfterSend?.();
|
|
2675
|
+
});
|
|
2676
|
+
}, [controller, onAfterSend]);
|
|
2677
|
+
|
|
2678
|
+
const [stoppingTurn, setStoppingTurn] = React.useState(false);
|
|
2679
|
+
const stopTurn = React.useCallback(() => {
|
|
2680
|
+
setStoppingTurn(true);
|
|
2681
|
+
controller.handleCommand(UI_COMMANDS.STOP_TURN)
|
|
2682
|
+
.catch(() => {})
|
|
2683
|
+
.finally(() => setStoppingTurn(false));
|
|
2684
|
+
}, [controller]);
|
|
2685
|
+
|
|
2686
|
+
const cancelPending = React.useCallback(() => {
|
|
2687
|
+
if (controller.getState().ui.promptEdit) {
|
|
2688
|
+
if (typeof controller.cancelSelectedOutboxPrompt === "function") {
|
|
2689
|
+
controller.cancelSelectedOutboxPrompt().catch(() => {});
|
|
2690
|
+
} else {
|
|
2691
|
+
controller.cancelSelectedPendingPrompt();
|
|
2692
|
+
}
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
if (typeof controller.cancelLatestQueuedOutbox === "function") {
|
|
2696
|
+
controller.cancelLatestQueuedOutbox().catch(() => {});
|
|
2697
|
+
}
|
|
2698
|
+
}, [controller]);
|
|
2699
|
+
|
|
2700
|
+
const sendLabel = promptState.editingPending || (promptState.hasPendingOutbox && !promptState.value.trim())
|
|
2701
|
+
? "⇪"
|
|
2702
|
+
: promptState.hasOutbox
|
|
2703
|
+
? "+"
|
|
2704
|
+
: "❯";
|
|
2705
|
+
const selectedQueued = promptState.selectedOutboxPhase === "queued";
|
|
2706
|
+
const selectedCancelling = promptState.selectedOutboxPhase === "cancelling";
|
|
2707
|
+
const selectedReadOnly = selectedQueued || selectedCancelling;
|
|
2708
|
+
|
|
2709
|
+
return React.createElement("div", {
|
|
2710
|
+
className: `ps-prompt-shell${mobile ? " is-mobile" : ""}`,
|
|
2711
|
+
},
|
|
2712
|
+
React.createElement("label", { className: "ps-prompt-label" }, promptState.answerMode ? "answer" : selectedCancelling ? "cancelling" : selectedQueued ? "queued" : promptState.editingPending ? "pending" : "you"),
|
|
2713
|
+
React.createElement("textarea", {
|
|
2714
|
+
ref: inputRef,
|
|
2715
|
+
className: "ps-prompt-input",
|
|
2716
|
+
rows: mobile ? 2 : Math.max(2, getPromptInputRows(promptState.value)),
|
|
2717
|
+
value: promptState.value,
|
|
2718
|
+
readOnly: selectedReadOnly,
|
|
2719
|
+
placeholder: promptState.answerMode
|
|
2720
|
+
? "Type an answer and press Enter"
|
|
2721
|
+
: promptState.editingPending
|
|
2722
|
+
? selectedCancelling
|
|
2723
|
+
? "Cancellation requested"
|
|
2724
|
+
: selectedQueued
|
|
2725
|
+
? "Queued message selected"
|
|
2726
|
+
: "Edit the pending message, then send or cancel it"
|
|
2727
|
+
: promptState.hasOutbox
|
|
2728
|
+
? "Type a message and press Enter to queue it behind the pending batch"
|
|
2729
|
+
: "Type a message and press Enter",
|
|
2730
|
+
enterKeyHint: "send",
|
|
2731
|
+
onFocus: () => controller.setFocus("prompt"),
|
|
2732
|
+
onSelect: (event) => controller.setPromptCursor(event.currentTarget.selectionStart || 0),
|
|
2733
|
+
onChange: (event) => controller.setPrompt(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
|
|
2734
|
+
onKeyDown: (event) => {
|
|
2735
|
+
if (event.key === "Tab" && !event.shiftKey && controller.acceptPromptReferenceAutocomplete()) {
|
|
2736
|
+
event.preventDefault();
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
if (event.key === "ArrowUp" && !event.shiftKey && !event.metaKey && !event.altKey) {
|
|
2740
|
+
event.preventDefault();
|
|
2741
|
+
controller.movePromptCursorVertical(-1);
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (event.key === "ArrowDown" && !event.shiftKey && !event.metaKey && !event.altKey) {
|
|
2745
|
+
event.preventDefault();
|
|
2746
|
+
controller.movePromptCursorVertical(1);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
if (event.key === "Escape" && promptState.editingPending) {
|
|
2750
|
+
event.preventDefault();
|
|
2751
|
+
if (selectedReadOnly) {
|
|
2752
|
+
controller.exitPendingPromptEdit({ restoreDraft: true });
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
cancelPending();
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !mobile) {
|
|
2759
|
+
event.preventDefault();
|
|
2760
|
+
sendPrompt();
|
|
2761
|
+
}
|
|
2762
|
+
},
|
|
2763
|
+
}),
|
|
2764
|
+
React.createElement("div", { className: "ps-prompt-actions" },
|
|
2765
|
+
promptState.editingPending && !selectedCancelling
|
|
2766
|
+
? React.createElement("button", {
|
|
2767
|
+
type: "button",
|
|
2768
|
+
className: "ps-mini-button",
|
|
2769
|
+
title: selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
|
|
2770
|
+
"aria-label": selectedQueued ? "Delete selected queued prompt" : "Cancel selected pending prompt",
|
|
2771
|
+
onClick: cancelPending,
|
|
2772
|
+
}, selectedQueued ? "Delete" : "Cancel")
|
|
2773
|
+
: null,
|
|
2774
|
+
promptState.canStopTurn || stoppingTurn
|
|
2775
|
+
? React.createElement("button", {
|
|
2776
|
+
type: "button",
|
|
2777
|
+
className: `ps-stop-button${stoppingTurn ? " is-stopping" : ""}`,
|
|
2778
|
+
title: "Stop the current turn (the session stays alive and returns to idle)",
|
|
2779
|
+
"aria-label": "Stop the current turn",
|
|
2780
|
+
disabled: stoppingTurn,
|
|
2781
|
+
onClick: stopTurn,
|
|
2782
|
+
}, "■")
|
|
2783
|
+
: null,
|
|
2784
|
+
React.createElement("button", {
|
|
2785
|
+
type: "button",
|
|
2786
|
+
className: `ps-send-button${mobile ? " is-inline" : ""}`,
|
|
2787
|
+
title: promptState.editingPending || (promptState.hasPendingOutbox && !promptState.value.trim())
|
|
2788
|
+
? "Send all queued prompts"
|
|
2789
|
+
: promptState.hasOutbox
|
|
2790
|
+
? "Queue prompt behind the pending batch"
|
|
2791
|
+
: "Send prompt",
|
|
2792
|
+
"aria-label": promptState.editingPending || (promptState.hasPendingOutbox && !promptState.value.trim())
|
|
2793
|
+
? "Send queued prompts"
|
|
2794
|
+
: promptState.hasOutbox
|
|
2795
|
+
? "Queue prompt"
|
|
2796
|
+
: "Send prompt",
|
|
2797
|
+
onClick: sendPrompt,
|
|
2798
|
+
}, sendLabel),
|
|
2799
|
+
),
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
function StatusStrip({ controller }) {
|
|
2804
|
+
const status = useControllerSelector(controller, (state) => selectStatusBar(state), shallowEqualObject);
|
|
2805
|
+
return React.createElement("div", { className: "ps-status-strip" },
|
|
2806
|
+
React.createElement("div", { className: "ps-status-left" }, status.left),
|
|
2807
|
+
React.createElement("div", { className: "ps-status-right" }, status.right),
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus = null, chatFocusDisabled = false }) {
|
|
2812
|
+
const adminVisible = useControllerSelector(controller, (state) => Boolean(state.admin?.visible));
|
|
2813
|
+
const chatView = useControllerSelector(controller, (state) => ({
|
|
2814
|
+
mode: state.ui.chatViewMode || "transcript",
|
|
2815
|
+
activeSessionIsGroup: Boolean(state.sessions.activeSessionId && state.sessions.byId[state.sessions.activeSessionId]?.isGroup),
|
|
2816
|
+
hasActiveSession: Boolean(state.sessions.activeSessionId && state.sessions.byId[state.sessions.activeSessionId]),
|
|
2817
|
+
}), shallowEqualObject);
|
|
2818
|
+
const switchModelDisabled = !chatView.hasActiveSession || chatView.activeSessionIsGroup;
|
|
2819
|
+
|
|
2820
|
+
const buttonDefs = [
|
|
2821
|
+
{
|
|
2822
|
+
key: "new",
|
|
2823
|
+
label: "New",
|
|
2824
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.NEW_SESSION).catch(() => {}),
|
|
2825
|
+
},
|
|
2826
|
+
{
|
|
2827
|
+
key: "model",
|
|
2828
|
+
label: mobile ? "Model" : "New + Model",
|
|
2829
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_MODEL_PICKER).catch(() => {}),
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
key: "switch-model",
|
|
2833
|
+
label: mobile ? "Switch" : "Switch Model",
|
|
2834
|
+
onClick: () => controller.openSwitchModelPicker().catch((err) => {
|
|
2835
|
+
controller.dispatch({ type: "ui/status", text: err?.message || String(err) || "Failed to switch model" });
|
|
2836
|
+
}),
|
|
2837
|
+
disabled: switchModelDisabled,
|
|
2838
|
+
title: switchModelDisabled ? "Select a session to switch its model" : "Switch the selected session model at the next turn boundary",
|
|
2839
|
+
},
|
|
2840
|
+
{
|
|
2841
|
+
key: "filter",
|
|
2842
|
+
label: "Filter",
|
|
2843
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_SESSION_FILTER).catch(() => {}),
|
|
2844
|
+
},
|
|
2845
|
+
{
|
|
2846
|
+
key: "theme",
|
|
2847
|
+
label: "Theme",
|
|
2848
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {}),
|
|
2849
|
+
},
|
|
2850
|
+
{
|
|
2851
|
+
key: "summary",
|
|
2852
|
+
label: chatView.mode === "summary" ? "Chat" : "Summary",
|
|
2853
|
+
onClick: () => controller.setChatViewMode(chatView.mode === "summary" ? "transcript" : "summary"),
|
|
2854
|
+
disabled: chatView.activeSessionIsGroup,
|
|
2855
|
+
title: chatView.activeSessionIsGroup ? "Groups show group details" : "Toggle Chat / Summary view",
|
|
2856
|
+
active: chatView.mode === "summary",
|
|
2857
|
+
},
|
|
2858
|
+
...(onToggleChatFocus ? [{
|
|
2859
|
+
key: "focus",
|
|
2860
|
+
label: mobile
|
|
2861
|
+
? (chatFocusMode ? "Exit Focus" : "Focus")
|
|
2862
|
+
: (chatFocusMode ? "Exit Focus" : "Chat Focus"),
|
|
2863
|
+
onClick: onToggleChatFocus,
|
|
2864
|
+
disabled: chatFocusDisabled,
|
|
2865
|
+
active: chatFocusMode,
|
|
2866
|
+
}] : []),
|
|
2867
|
+
{
|
|
2868
|
+
key: "admin",
|
|
2869
|
+
label: adminVisible ? "Close Admin" : "Admin",
|
|
2870
|
+
onClick: () => controller.handleCommand(adminVisible ? UI_COMMANDS.CLOSE_ADMIN_CONSOLE : UI_COMMANDS.OPEN_ADMIN_CONSOLE).catch(() => {}),
|
|
2871
|
+
active: adminVisible,
|
|
2872
|
+
},
|
|
2873
|
+
];
|
|
2874
|
+
|
|
2875
|
+
const renderButton = (def) => React.createElement("button", {
|
|
2876
|
+
key: def.key,
|
|
2877
|
+
type: "button",
|
|
2878
|
+
className: `ps-toolbar-button${def.active ? " is-active" : ""}`,
|
|
2879
|
+
onClick: def.onClick,
|
|
2880
|
+
disabled: Boolean(def.disabled),
|
|
2881
|
+
title: def.title,
|
|
2882
|
+
}, def.label);
|
|
2883
|
+
|
|
2884
|
+
if (mobile) {
|
|
2885
|
+
const firstRowButtons = buttonDefs.slice(0, 4);
|
|
2886
|
+
const secondRowButtons = buttonDefs.slice(4);
|
|
2887
|
+
|
|
2888
|
+
return React.createElement("div", { className: "ps-toolbar is-mobile" },
|
|
2889
|
+
React.createElement("div", { className: "ps-toolbar-row ps-toolbar-row-primary" },
|
|
2890
|
+
firstRowButtons.map(renderButton)),
|
|
2891
|
+
React.createElement("div", { className: "ps-toolbar-row ps-toolbar-row-secondary" },
|
|
2892
|
+
React.createElement("div", { className: "ps-toolbar-row-actions" }, secondRowButtons.map(renderButton))),
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
return React.createElement("div", { className: "ps-toolbar" },
|
|
2897
|
+
React.createElement("div", { className: "ps-toolbar-actions" }, buttonDefs.map(renderButton)),
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
function ColumnResizeHandle({ controller, paneAdjust = 0 }) {
|
|
2902
|
+
const dragStateRef = React.useRef(null);
|
|
2903
|
+
const [dragging, setDragging] = React.useState(false);
|
|
2904
|
+
|
|
2905
|
+
React.useEffect(() => {
|
|
2906
|
+
if (!dragging) return undefined;
|
|
2907
|
+
|
|
2908
|
+
const stopDragging = () => {
|
|
2909
|
+
dragStateRef.current = null;
|
|
2910
|
+
setDragging(false);
|
|
2911
|
+
document.body.classList.remove("is-resizing-pane-x");
|
|
2912
|
+
};
|
|
2913
|
+
|
|
2914
|
+
const onPointerMove = (event) => {
|
|
2915
|
+
const dragState = dragStateRef.current;
|
|
2916
|
+
if (!dragState) return;
|
|
2917
|
+
const deltaCells = Math.round((event.clientX - dragState.startX) / GRID_CELL_WIDTH);
|
|
2918
|
+
const deltaIncrement = deltaCells - dragState.appliedCells;
|
|
2919
|
+
if (!deltaIncrement) return;
|
|
2920
|
+
controller.adjustPaneSplit(deltaIncrement);
|
|
2921
|
+
dragState.appliedCells = deltaCells;
|
|
2922
|
+
};
|
|
2923
|
+
|
|
2924
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
2925
|
+
window.addEventListener("pointerup", stopDragging);
|
|
2926
|
+
window.addEventListener("pointercancel", stopDragging);
|
|
2927
|
+
|
|
2928
|
+
return () => {
|
|
2929
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
2930
|
+
window.removeEventListener("pointerup", stopDragging);
|
|
2931
|
+
window.removeEventListener("pointercancel", stopDragging);
|
|
2932
|
+
document.body.classList.remove("is-resizing-pane-x");
|
|
2933
|
+
};
|
|
2934
|
+
}, [controller, dragging]);
|
|
2935
|
+
|
|
2936
|
+
return React.createElement("button", {
|
|
2937
|
+
type: "button",
|
|
2938
|
+
className: `ps-column-resizer${dragging ? " is-dragging" : ""}`,
|
|
2939
|
+
title: "Drag to resize the inspector column. Double-click to reset.",
|
|
2940
|
+
"aria-label": "Resize inspector column",
|
|
2941
|
+
onPointerDown: (event) => {
|
|
2942
|
+
if (event.button !== 0) return;
|
|
2943
|
+
event.preventDefault();
|
|
2944
|
+
dragStateRef.current = {
|
|
2945
|
+
startX: event.clientX,
|
|
2946
|
+
appliedCells: 0,
|
|
2947
|
+
};
|
|
2948
|
+
setDragging(true);
|
|
2949
|
+
document.body.classList.add("is-resizing-pane-x");
|
|
2950
|
+
},
|
|
2951
|
+
onDoubleClick: () => {
|
|
2952
|
+
if (!paneAdjust) return;
|
|
2953
|
+
controller.adjustPaneSplit(-paneAdjust);
|
|
2954
|
+
},
|
|
2955
|
+
onKeyDown: (event) => {
|
|
2956
|
+
if (event.key === "ArrowLeft") {
|
|
2957
|
+
event.preventDefault();
|
|
2958
|
+
controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
if (event.key === "ArrowRight") {
|
|
2962
|
+
event.preventDefault();
|
|
2963
|
+
controller.handleCommand(UI_COMMANDS.GROW_LEFT_PANE).catch(() => {});
|
|
2964
|
+
}
|
|
2965
|
+
},
|
|
2966
|
+
},
|
|
2967
|
+
React.createElement("span", { className: "ps-column-resizer-handle", "aria-hidden": "true" },
|
|
2968
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
2969
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
2970
|
+
React.createElement("span", { className: "ps-column-resizer-dot" })));
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
function RowResizeHandle({ controller, sessionPaneAdjust = 0 }) {
|
|
2974
|
+
const dragStateRef = React.useRef(null);
|
|
2975
|
+
const [dragging, setDragging] = React.useState(false);
|
|
2976
|
+
|
|
2977
|
+
React.useEffect(() => {
|
|
2978
|
+
if (!dragging) return undefined;
|
|
2979
|
+
|
|
2980
|
+
const stopDragging = () => {
|
|
2981
|
+
dragStateRef.current = null;
|
|
2982
|
+
setDragging(false);
|
|
2983
|
+
document.body.classList.remove("is-resizing-pane-y");
|
|
2984
|
+
};
|
|
2985
|
+
|
|
2986
|
+
const onPointerMove = (event) => {
|
|
2987
|
+
const dragState = dragStateRef.current;
|
|
2988
|
+
if (!dragState) return;
|
|
2989
|
+
const deltaCells = Math.round((event.clientY - dragState.startY) / GRID_CELL_HEIGHT);
|
|
2990
|
+
const deltaIncrement = deltaCells - dragState.appliedCells;
|
|
2991
|
+
if (!deltaIncrement) return;
|
|
2992
|
+
controller.adjustSessionPaneSplit(deltaIncrement);
|
|
2993
|
+
dragState.appliedCells = deltaCells;
|
|
2994
|
+
};
|
|
2995
|
+
|
|
2996
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
2997
|
+
window.addEventListener("pointerup", stopDragging);
|
|
2998
|
+
window.addEventListener("pointercancel", stopDragging);
|
|
2999
|
+
|
|
3000
|
+
return () => {
|
|
3001
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
3002
|
+
window.removeEventListener("pointerup", stopDragging);
|
|
3003
|
+
window.removeEventListener("pointercancel", stopDragging);
|
|
3004
|
+
document.body.classList.remove("is-resizing-pane-y");
|
|
3005
|
+
};
|
|
3006
|
+
}, [controller, dragging]);
|
|
3007
|
+
|
|
3008
|
+
return React.createElement("button", {
|
|
3009
|
+
type: "button",
|
|
3010
|
+
className: `ps-row-resizer${dragging ? " is-dragging" : ""}`,
|
|
3011
|
+
title: "Drag to resize the sessions and chat panes. Double-click to reset.",
|
|
3012
|
+
"aria-label": "Resize sessions and chat panes",
|
|
3013
|
+
onPointerDown: (event) => {
|
|
3014
|
+
if (event.button !== 0) return;
|
|
3015
|
+
event.preventDefault();
|
|
3016
|
+
dragStateRef.current = {
|
|
3017
|
+
startY: event.clientY,
|
|
3018
|
+
appliedCells: 0,
|
|
3019
|
+
};
|
|
3020
|
+
setDragging(true);
|
|
3021
|
+
document.body.classList.add("is-resizing-pane-y");
|
|
3022
|
+
},
|
|
3023
|
+
onDoubleClick: () => {
|
|
3024
|
+
if (!sessionPaneAdjust) return;
|
|
3025
|
+
controller.adjustSessionPaneSplit(-sessionPaneAdjust);
|
|
3026
|
+
},
|
|
3027
|
+
onKeyDown: (event) => {
|
|
3028
|
+
if (event.key === "ArrowUp") {
|
|
3029
|
+
event.preventDefault();
|
|
3030
|
+
controller.adjustSessionPaneSplit(-1);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
if (event.key === "ArrowDown") {
|
|
3034
|
+
event.preventDefault();
|
|
3035
|
+
controller.adjustSessionPaneSplit(1);
|
|
3036
|
+
}
|
|
3037
|
+
},
|
|
3038
|
+
},
|
|
3039
|
+
React.createElement("span", { className: "ps-row-resizer-handle", "aria-hidden": "true" },
|
|
3040
|
+
React.createElement("span", { className: "ps-row-resizer-dot" }),
|
|
3041
|
+
React.createElement("span", { className: "ps-row-resizer-dot" }),
|
|
3042
|
+
React.createElement("span", { className: "ps-row-resizer-dot" })));
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
function SessionColumnResizeHandle({ controller, portalSessionColumnAdjust = 0 }) {
|
|
3046
|
+
const dragStateRef = React.useRef(null);
|
|
3047
|
+
const [dragging, setDragging] = React.useState(false);
|
|
3048
|
+
|
|
3049
|
+
React.useEffect(() => {
|
|
3050
|
+
if (!dragging) return undefined;
|
|
3051
|
+
|
|
3052
|
+
const stopDragging = () => {
|
|
3053
|
+
dragStateRef.current = null;
|
|
3054
|
+
setDragging(false);
|
|
3055
|
+
document.body.classList.remove("is-resizing-pane-x");
|
|
3056
|
+
};
|
|
3057
|
+
|
|
3058
|
+
const onPointerMove = (event) => {
|
|
3059
|
+
const dragState = dragStateRef.current;
|
|
3060
|
+
if (!dragState) return;
|
|
3061
|
+
const nextAdjust = clampNumber(
|
|
3062
|
+
dragState.startAdjust + (event.clientX - dragState.startX),
|
|
3063
|
+
dragState.minAdjust,
|
|
3064
|
+
dragState.maxAdjust,
|
|
3065
|
+
);
|
|
3066
|
+
controller.dispatch({
|
|
3067
|
+
type: "ui/portalSessionColumnAdjust",
|
|
3068
|
+
portalSessionColumnAdjust: nextAdjust,
|
|
3069
|
+
});
|
|
3070
|
+
};
|
|
3071
|
+
|
|
3072
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
3073
|
+
window.addEventListener("pointerup", stopDragging);
|
|
3074
|
+
window.addEventListener("pointercancel", stopDragging);
|
|
3075
|
+
|
|
3076
|
+
return () => {
|
|
3077
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
3078
|
+
window.removeEventListener("pointerup", stopDragging);
|
|
3079
|
+
window.removeEventListener("pointercancel", stopDragging);
|
|
3080
|
+
document.body.classList.remove("is-resizing-pane-x");
|
|
3081
|
+
};
|
|
3082
|
+
}, [controller, dragging]);
|
|
3083
|
+
|
|
3084
|
+
return React.createElement("button", {
|
|
3085
|
+
type: "button",
|
|
3086
|
+
className: `ps-column-resizer${dragging ? " is-dragging" : ""}`,
|
|
3087
|
+
title: "Drag to resize the sessions and chat columns. Double-click to reset.",
|
|
3088
|
+
"aria-label": "Resize sessions and chat columns",
|
|
3089
|
+
onPointerDown: (event) => {
|
|
3090
|
+
if (event.button !== 0) return;
|
|
3091
|
+
event.preventDefault();
|
|
3092
|
+
const gridNode = event.currentTarget.closest(".ps-workspace-main-grid");
|
|
3093
|
+
const bounds = portalSessionColumnBounds(gridNode?.getBoundingClientRect?.().width);
|
|
3094
|
+
dragStateRef.current = {
|
|
3095
|
+
startX: event.clientX,
|
|
3096
|
+
startAdjust: Number(portalSessionColumnAdjust) || 0,
|
|
3097
|
+
minAdjust: bounds.minAdjust,
|
|
3098
|
+
maxAdjust: bounds.maxAdjust,
|
|
3099
|
+
};
|
|
3100
|
+
setDragging(true);
|
|
3101
|
+
document.body.classList.add("is-resizing-pane-x");
|
|
3102
|
+
},
|
|
3103
|
+
onDoubleClick: () => {
|
|
3104
|
+
controller.dispatch({ type: "ui/portalSessionColumnAdjust", portalSessionColumnAdjust: 0 });
|
|
3105
|
+
},
|
|
3106
|
+
onKeyDown: (event) => {
|
|
3107
|
+
const gridNode = event.currentTarget.closest(".ps-workspace-main-grid");
|
|
3108
|
+
const bounds = portalSessionColumnBounds(gridNode?.getBoundingClientRect?.().width);
|
|
3109
|
+
const adjustBy = (delta) => {
|
|
3110
|
+
const nextAdjust = clampNumber(
|
|
3111
|
+
(Number(portalSessionColumnAdjust) || 0) + delta,
|
|
3112
|
+
bounds.minAdjust,
|
|
3113
|
+
bounds.maxAdjust,
|
|
3114
|
+
);
|
|
3115
|
+
controller.dispatch({
|
|
3116
|
+
type: "ui/portalSessionColumnAdjust",
|
|
3117
|
+
portalSessionColumnAdjust: nextAdjust,
|
|
3118
|
+
});
|
|
3119
|
+
};
|
|
3120
|
+
if (event.key === "ArrowLeft") {
|
|
3121
|
+
event.preventDefault();
|
|
3122
|
+
adjustBy(-24);
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
if (event.key === "ArrowRight") {
|
|
3126
|
+
event.preventDefault();
|
|
3127
|
+
adjustBy(24);
|
|
3128
|
+
}
|
|
3129
|
+
},
|
|
3130
|
+
},
|
|
3131
|
+
React.createElement("span", { className: "ps-column-resizer-handle", "aria-hidden": "true" },
|
|
3132
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
3133
|
+
React.createElement("span", { className: "ps-column-resizer-dot" }),
|
|
3134
|
+
React.createElement("span", { className: "ps-column-resizer-dot" })));
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
function ActivityRowResizeHandle({ controller, activityPaneAdjust = 0 }) {
|
|
3138
|
+
const dragStateRef = React.useRef(null);
|
|
3139
|
+
const [dragging, setDragging] = React.useState(false);
|
|
3140
|
+
|
|
3141
|
+
React.useEffect(() => {
|
|
3142
|
+
if (!dragging) return undefined;
|
|
3143
|
+
|
|
3144
|
+
const stopDragging = () => {
|
|
3145
|
+
dragStateRef.current = null;
|
|
3146
|
+
setDragging(false);
|
|
3147
|
+
document.body.classList.remove("is-resizing-pane-y");
|
|
3148
|
+
};
|
|
3149
|
+
|
|
3150
|
+
const onPointerMove = (event) => {
|
|
3151
|
+
const dragState = dragStateRef.current;
|
|
3152
|
+
if (!dragState) return;
|
|
3153
|
+
const deltaCells = Math.round((event.clientY - dragState.startY) / GRID_CELL_HEIGHT);
|
|
3154
|
+
const deltaIncrement = deltaCells - dragState.appliedCells;
|
|
3155
|
+
if (!deltaIncrement) return;
|
|
3156
|
+
controller.adjustActivityPaneSplit(-deltaIncrement);
|
|
3157
|
+
dragState.appliedCells = deltaCells;
|
|
3158
|
+
};
|
|
3159
|
+
|
|
3160
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
3161
|
+
window.addEventListener("pointerup", stopDragging);
|
|
3162
|
+
window.addEventListener("pointercancel", stopDragging);
|
|
3163
|
+
|
|
3164
|
+
return () => {
|
|
3165
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
3166
|
+
window.removeEventListener("pointerup", stopDragging);
|
|
3167
|
+
window.removeEventListener("pointercancel", stopDragging);
|
|
3168
|
+
document.body.classList.remove("is-resizing-pane-y");
|
|
3169
|
+
};
|
|
3170
|
+
}, [controller, dragging]);
|
|
3171
|
+
|
|
3172
|
+
return React.createElement("button", {
|
|
3173
|
+
type: "button",
|
|
3174
|
+
className: `ps-row-resizer${dragging ? " is-dragging" : ""}`,
|
|
3175
|
+
title: "Drag to resize the activity pane. Double-click to reset.",
|
|
3176
|
+
"aria-label": "Resize activity pane",
|
|
3177
|
+
onPointerDown: (event) => {
|
|
3178
|
+
if (event.button !== 0) return;
|
|
3179
|
+
event.preventDefault();
|
|
3180
|
+
dragStateRef.current = {
|
|
3181
|
+
startY: event.clientY,
|
|
3182
|
+
appliedCells: 0,
|
|
3183
|
+
};
|
|
3184
|
+
setDragging(true);
|
|
3185
|
+
document.body.classList.add("is-resizing-pane-y");
|
|
3186
|
+
},
|
|
3187
|
+
onDoubleClick: () => {
|
|
3188
|
+
if (!activityPaneAdjust) return;
|
|
3189
|
+
controller.adjustActivityPaneSplit(-activityPaneAdjust);
|
|
3190
|
+
},
|
|
3191
|
+
onKeyDown: (event) => {
|
|
3192
|
+
if (event.key === "ArrowUp") {
|
|
3193
|
+
event.preventDefault();
|
|
3194
|
+
controller.adjustActivityPaneSplit(1);
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
if (event.key === "ArrowDown") {
|
|
3198
|
+
event.preventDefault();
|
|
3199
|
+
controller.adjustActivityPaneSplit(-1);
|
|
3200
|
+
}
|
|
3201
|
+
},
|
|
3202
|
+
},
|
|
3203
|
+
React.createElement("span", { className: "ps-row-resizer-handle", "aria-hidden": "true" },
|
|
3204
|
+
React.createElement("span", { className: "ps-row-resizer-dot" }),
|
|
3205
|
+
React.createElement("span", { className: "ps-row-resizer-dot" }),
|
|
3206
|
+
React.createElement("span", { className: "ps-row-resizer-dot" })));
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function MobileNav({ activePane, setActivePane, controller }) {
|
|
3210
|
+
const tabs = [
|
|
3211
|
+
{ id: "workspace", label: "Main", focus: "chat" },
|
|
3212
|
+
{ id: "inspector", label: "Inspector", focus: "inspector" },
|
|
3213
|
+
{ id: "activity", label: "Activity", focus: "activity" },
|
|
3214
|
+
];
|
|
3215
|
+
return React.createElement("div", { className: "ps-mobile-nav" },
|
|
3216
|
+
tabs.map((tab) => React.createElement("button", {
|
|
3217
|
+
key: tab.id,
|
|
3218
|
+
type: "button",
|
|
3219
|
+
className: `ps-mobile-nav-button${activePane === tab.id ? " is-active" : ""}`,
|
|
3220
|
+
onClick: () => {
|
|
3221
|
+
setActivePane(tab.id);
|
|
3222
|
+
controller.setFocus(tab.focus);
|
|
3223
|
+
},
|
|
3224
|
+
}, tab.label)));
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function ModalLayer({ controller }) {
|
|
3228
|
+
const themeId = useControllerSelector(controller, (state) => state.ui.themeId);
|
|
3229
|
+
const theme = getTheme(themeId);
|
|
3230
|
+
const modalState = useControllerSelector(controller, (state) => ({
|
|
3231
|
+
rawModal: state.ui.modal,
|
|
3232
|
+
themePicker: selectThemePickerModal(state),
|
|
3233
|
+
modelPicker: selectModelPickerModal(state),
|
|
3234
|
+
reasoningEffortPicker: selectReasoningEffortPickerModal(state),
|
|
3235
|
+
sessionAgentPicker: selectSessionAgentPickerModal(state),
|
|
3236
|
+
sessionGroupPicker: selectSessionGroupPickerModal(state),
|
|
3237
|
+
sessionGroupName: selectSessionGroupNameModal(state),
|
|
3238
|
+
artifactPicker: selectArtifactPickerModal(state),
|
|
3239
|
+
logFilter: selectLogFilterModal(state),
|
|
3240
|
+
filesFilter: selectFilesFilterModal(state),
|
|
3241
|
+
historyFormat: selectHistoryFormatModal(state),
|
|
3242
|
+
renameSession: selectRenameSessionModal(state),
|
|
3243
|
+
sessionOwnerFilter: selectSessionOwnerFilterModal(state),
|
|
3244
|
+
artifactUpload: selectArtifactUploadModal(state),
|
|
3245
|
+
confirm: selectConfirmModal(state),
|
|
3246
|
+
logsFilter: state.logs.filter,
|
|
3247
|
+
filesFilterState: state.files.filter,
|
|
3248
|
+
historyFormatState: state.executionHistory?.format || "pretty",
|
|
3249
|
+
}), shallowEqualObject);
|
|
3250
|
+
const modal = modalState.rawModal;
|
|
3251
|
+
const renameInputRef = React.useRef(null);
|
|
3252
|
+
const groupNameInputRef = React.useRef(null);
|
|
3253
|
+
const listModalRef = React.useRef(null);
|
|
3254
|
+
|
|
3255
|
+
React.useEffect(() => {
|
|
3256
|
+
if (modal?.type !== "renameSession" || !modalState.renameSession) return;
|
|
3257
|
+
const inputNode = renameInputRef.current;
|
|
3258
|
+
if (!inputNode) return;
|
|
3259
|
+
if (document.activeElement !== inputNode) {
|
|
3260
|
+
try {
|
|
3261
|
+
inputNode.focus({ preventScroll: true });
|
|
3262
|
+
} catch {
|
|
3263
|
+
inputNode.focus();
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
inputNode.setSelectionRange(modalState.renameSession.cursorIndex, modalState.renameSession.cursorIndex);
|
|
3267
|
+
}, [modal?.type, modalState.renameSession?.cursorIndex, modalState.renameSession?.value]);
|
|
3268
|
+
|
|
3269
|
+
React.useEffect(() => {
|
|
3270
|
+
if (modal?.type !== "sessionGroupName" || !modalState.sessionGroupName) return;
|
|
3271
|
+
const inputNode = groupNameInputRef.current;
|
|
3272
|
+
if (!inputNode) return;
|
|
3273
|
+
if (document.activeElement !== inputNode) {
|
|
3274
|
+
try {
|
|
3275
|
+
inputNode.focus({ preventScroll: true });
|
|
3276
|
+
} catch {
|
|
3277
|
+
inputNode.focus();
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
inputNode.setSelectionRange(modalState.sessionGroupName.cursorIndex, modalState.sessionGroupName.cursorIndex);
|
|
3281
|
+
}, [modal?.type, modalState.sessionGroupName?.cursorIndex, modalState.sessionGroupName?.value]);
|
|
3282
|
+
|
|
3283
|
+
React.useEffect(() => {
|
|
3284
|
+
if (!modal) return;
|
|
3285
|
+
if (![
|
|
3286
|
+
"themePicker",
|
|
3287
|
+
"modelPicker",
|
|
3288
|
+
"reasoningEffortPicker",
|
|
3289
|
+
"sessionAgentPicker",
|
|
3290
|
+
"sessionGroupPicker",
|
|
3291
|
+
"artifactPicker",
|
|
3292
|
+
"sessionOwnerFilter",
|
|
3293
|
+
"logFilter",
|
|
3294
|
+
"filesFilter",
|
|
3295
|
+
"historyFormat",
|
|
3296
|
+
].includes(modal.type)) {
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
const listNode = listModalRef.current;
|
|
3301
|
+
if (!listNode) return;
|
|
3302
|
+
const selected = listNode.querySelector(".ps-list-button.is-selected");
|
|
3303
|
+
if (selected && typeof selected.scrollIntoView === "function") {
|
|
3304
|
+
selected.scrollIntoView({ block: "nearest" });
|
|
3305
|
+
}
|
|
3306
|
+
}, [
|
|
3307
|
+
modal?.type,
|
|
3308
|
+
modal?.selectedIndex,
|
|
3309
|
+
modalState.themePicker?.selectedRowIndex,
|
|
3310
|
+
modalState.modelPicker?.selectedRowIndex,
|
|
3311
|
+
modalState.reasoningEffortPicker?.selectedRowIndex,
|
|
3312
|
+
modalState.sessionAgentPicker?.selectedRowIndex,
|
|
3313
|
+
modalState.sessionGroupPicker?.selectedRowIndex,
|
|
3314
|
+
modalState.artifactPicker?.selectedRowIndex,
|
|
3315
|
+
modalState.sessionOwnerFilter?.selectedRowIndex,
|
|
3316
|
+
modalState.logFilter?.selectedRowIndex,
|
|
3317
|
+
modalState.filesFilter?.selectedRowIndex,
|
|
3318
|
+
modalState.historyFormat?.selectedRowIndex,
|
|
3319
|
+
]);
|
|
3320
|
+
|
|
3321
|
+
if (!modal) return null;
|
|
3322
|
+
|
|
3323
|
+
const close = () => controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
|
|
3324
|
+
|
|
3325
|
+
const renderListModal = (presentation, confirmLabel = "Apply") => {
|
|
3326
|
+
const rows = Array.isArray(presentation.rows) ? presentation.rows : [];
|
|
3327
|
+
const rowItemIndexes = Array.isArray(presentation.rowItemIndexes) ? presentation.rowItemIndexes : null;
|
|
3328
|
+
const usesHangingIndent = modal.type === "modelPicker" || modal.type === "reasoningEffortPicker" || modal.type === "sessionAgentPicker";
|
|
3329
|
+
const renderedList = rowItemIndexes && rowItemIndexes.length === rows.length
|
|
3330
|
+
? rows.map((row, rowIndex) => {
|
|
3331
|
+
const itemIndex = rowItemIndexes[rowIndex];
|
|
3332
|
+
const runs = Array.isArray(row)
|
|
3333
|
+
? row
|
|
3334
|
+
: normalizeLines([row])[0]?.runs || [{ text: row?.text || "", color: row?.color }];
|
|
3335
|
+
if (itemIndex == null || itemIndex < 0) {
|
|
3336
|
+
return React.createElement("div", {
|
|
3337
|
+
key: `row:${rowIndex}`,
|
|
3338
|
+
className: "ps-line ps-modal-list-heading-line",
|
|
3339
|
+
}, React.createElement(Runs, { runs, theme }));
|
|
3340
|
+
}
|
|
3341
|
+
const item = modal.items?.[itemIndex];
|
|
3342
|
+
return React.createElement("button", {
|
|
3343
|
+
key: item?.id || `row:${rowIndex}`,
|
|
3344
|
+
type: "button",
|
|
3345
|
+
className: `ps-list-button ps-modal-list-button${itemIndex === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}`,
|
|
3346
|
+
onClick: () => controller.dispatch({ type: "ui/modalSelection", index: itemIndex }),
|
|
3347
|
+
},
|
|
3348
|
+
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
|
3349
|
+
React.createElement(Runs, { runs, theme })));
|
|
3350
|
+
})
|
|
3351
|
+
: (modal.items || []).map((item, index) => React.createElement("button", {
|
|
3352
|
+
key: item.id || index,
|
|
3353
|
+
type: "button",
|
|
3354
|
+
className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}`,
|
|
3355
|
+
onClick: () => controller.dispatch({ type: "ui/modalSelection", index }),
|
|
3356
|
+
},
|
|
3357
|
+
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
|
3358
|
+
React.createElement(Runs, {
|
|
3359
|
+
runs: Array.isArray(rows?.[index])
|
|
3360
|
+
? rows[index]
|
|
3361
|
+
: normalizeLines([rows?.[index]])[0]?.runs || [{ text: rows?.[index]?.text || "", color: rows?.[index]?.color }],
|
|
3362
|
+
theme,
|
|
3363
|
+
}))));
|
|
3364
|
+
|
|
3365
|
+
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
3366
|
+
React.createElement("div", { className: `ps-modal${modal.type === "themePicker" ? " is-theme-picker" : ""}`, onClick: (event) => event.stopPropagation() },
|
|
3367
|
+
React.createElement("div", { className: "ps-modal-header" },
|
|
3368
|
+
React.createElement("div", { className: "ps-modal-title" }, presentation.title),
|
|
3369
|
+
React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
|
|
3370
|
+
),
|
|
3371
|
+
React.createElement("div", { className: "ps-modal-grid" },
|
|
3372
|
+
React.createElement("div", { ref: listModalRef, className: "ps-modal-list" },
|
|
3373
|
+
renderedList,
|
|
3374
|
+
),
|
|
3375
|
+
React.createElement("div", { className: "ps-modal-details" },
|
|
3376
|
+
React.createElement("div", { className: "ps-modal-details-title" }, presentation.detailsTitle || "Details"),
|
|
3377
|
+
normalizeLines(presentation.detailsLines || []).map((line, index) => React.createElement(Line, { key: `detail:${index}`, line, theme, className: "ps-modal-detail-line" })),
|
|
3378
|
+
),
|
|
3379
|
+
),
|
|
3380
|
+
React.createElement("div", { className: "ps-modal-footer" },
|
|
3381
|
+
React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
|
|
3382
|
+
React.createElement("button", {
|
|
3383
|
+
type: "button",
|
|
3384
|
+
className: "ps-modal-button is-primary",
|
|
3385
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
|
|
3386
|
+
}, confirmLabel)),
|
|
3387
|
+
));
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3390
|
+
if (modal.type === "confirm" && modalState.confirm) {
|
|
3391
|
+
const isAlert = Boolean(modal.alert);
|
|
3392
|
+
const isDestructive = !isAlert && modal.action === "deleteSession";
|
|
3393
|
+
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
3394
|
+
React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
|
|
3395
|
+
React.createElement("div", { className: "ps-modal-header" },
|
|
3396
|
+
React.createElement("div", { className: "ps-modal-title" }, modalState.confirm.title),
|
|
3397
|
+
React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
|
|
3398
|
+
),
|
|
3399
|
+
React.createElement("div", { className: "ps-modal-body", style: { padding: "16px 20px" } },
|
|
3400
|
+
React.createElement("p", { style: { color: "#94a3b8", margin: 0 } }, modalState.confirm.message),
|
|
3401
|
+
),
|
|
3402
|
+
React.createElement("div", { className: "ps-modal-footer" },
|
|
3403
|
+
isAlert ? null : React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
|
|
3404
|
+
React.createElement("button", {
|
|
3405
|
+
type: "button",
|
|
3406
|
+
className: `ps-modal-button ${isDestructive ? "is-danger" : "is-primary"}`,
|
|
3407
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
|
|
3408
|
+
}, modalState.confirm.confirmLabel)),
|
|
3409
|
+
));
|
|
3410
|
+
}
|
|
3411
|
+
if (modal.type === "themePicker" && modalState.themePicker) {
|
|
3412
|
+
return renderListModal(modalState.themePicker, "Apply Theme");
|
|
3413
|
+
}
|
|
3414
|
+
// In switch-model mode the model/reasoning pickers retarget an existing
|
|
3415
|
+
// session, so the confirm action is "Switch Model", not "Create Session".
|
|
3416
|
+
const pickerConfirmLabel = modal.sessionOptions?.mode === "switchModel" ? "Switch Model" : "Create Session";
|
|
3417
|
+
if (modal.type === "modelPicker" && modalState.modelPicker) {
|
|
3418
|
+
return renderListModal(modalState.modelPicker, pickerConfirmLabel);
|
|
3419
|
+
}
|
|
3420
|
+
if (modal.type === "reasoningEffortPicker" && modalState.reasoningEffortPicker) {
|
|
3421
|
+
return renderListModal(modalState.reasoningEffortPicker, pickerConfirmLabel);
|
|
3422
|
+
}
|
|
3423
|
+
if (modal.type === "sessionAgentPicker" && modalState.sessionAgentPicker) {
|
|
3424
|
+
return renderListModal(modalState.sessionAgentPicker, "Create Session");
|
|
3425
|
+
}
|
|
3426
|
+
if (modal.type === "sessionGroupPicker" && modalState.sessionGroupPicker) {
|
|
3427
|
+
return renderListModal(modalState.sessionGroupPicker, "Move");
|
|
3428
|
+
}
|
|
3429
|
+
if (modal.type === "artifactPicker" && modalState.artifactPicker) {
|
|
3430
|
+
return renderListModal(modalState.artifactPicker, modalState.artifactPicker.confirmLabel || "Open / Download");
|
|
3431
|
+
}
|
|
3432
|
+
if (modal.type === "sessionOwnerFilter" && modalState.sessionOwnerFilter) {
|
|
3433
|
+
const presentation = modalState.sessionOwnerFilter;
|
|
3434
|
+
const rows = Array.isArray(presentation.rows) ? presentation.rows : [];
|
|
3435
|
+
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
3436
|
+
React.createElement("div", { className: "ps-modal", onClick: (event) => event.stopPropagation() },
|
|
3437
|
+
React.createElement("div", { className: "ps-modal-header" },
|
|
3438
|
+
React.createElement("div", { className: "ps-modal-title" }, presentation.title),
|
|
3439
|
+
React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
|
|
3440
|
+
),
|
|
3441
|
+
React.createElement("div", { className: "ps-modal-grid" },
|
|
3442
|
+
React.createElement("div", { ref: listModalRef, className: "ps-modal-list" },
|
|
3443
|
+
(modal.items || []).map((item, index) => React.createElement("button", {
|
|
3444
|
+
key: item.id || index,
|
|
3445
|
+
type: "button",
|
|
3446
|
+
className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}`,
|
|
3447
|
+
onClick: () => controller.toggleSessionOwnerFilter(index),
|
|
3448
|
+
},
|
|
3449
|
+
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
|
3450
|
+
React.createElement(Runs, {
|
|
3451
|
+
runs: Array.isArray(rows?.[index])
|
|
3452
|
+
? rows[index]
|
|
3453
|
+
: normalizeLines([rows?.[index]])[0]?.runs || [{ text: rows?.[index]?.text || "", color: rows?.[index]?.color }],
|
|
3454
|
+
theme,
|
|
3455
|
+
})))),
|
|
3456
|
+
),
|
|
3457
|
+
React.createElement("div", { className: "ps-modal-details" },
|
|
3458
|
+
React.createElement("div", { className: "ps-modal-details-title" }, presentation.detailsTitle || "Details"),
|
|
3459
|
+
normalizeLines(presentation.detailsLines || []).map((line, index) => React.createElement(Line, { key: `detail:${index}`, line, theme, className: "ps-modal-detail-line" })),
|
|
3460
|
+
),
|
|
3461
|
+
),
|
|
3462
|
+
React.createElement("div", { className: "ps-modal-footer" },
|
|
3463
|
+
React.createElement("button", { type: "button", className: "ps-modal-button is-primary", onClick: close }, "Done")),
|
|
3464
|
+
));
|
|
3465
|
+
}
|
|
3466
|
+
if (modal.type === "renameSession" && modalState.renameSession) {
|
|
3467
|
+
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
3468
|
+
React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
|
|
3469
|
+
React.createElement("div", { className: "ps-modal-header" },
|
|
3470
|
+
React.createElement("div", { className: "ps-modal-title" }, modalState.renameSession.title),
|
|
3471
|
+
React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
|
|
3472
|
+
),
|
|
3473
|
+
React.createElement("input", {
|
|
3474
|
+
ref: renameInputRef,
|
|
3475
|
+
className: "ps-modal-input",
|
|
3476
|
+
value: modalState.renameSession.value,
|
|
3477
|
+
placeholder: modalState.renameSession.placeholder,
|
|
3478
|
+
onChange: (event) => controller.setRenameSessionValue(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length),
|
|
3479
|
+
onKeyDown: (event) => {
|
|
3480
|
+
if (event.key === "Enter") {
|
|
3481
|
+
event.preventDefault();
|
|
3482
|
+
controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
|
|
3483
|
+
}
|
|
3484
|
+
},
|
|
3485
|
+
autoFocus: true,
|
|
3486
|
+
}),
|
|
3487
|
+
React.createElement("div", { className: "ps-modal-details" },
|
|
3488
|
+
normalizeLines(modalState.renameSession.helpLines || []).map((line, index) => React.createElement(Line, { key: `help:${index}`, line, theme, className: "ps-modal-detail-line" })),
|
|
3489
|
+
),
|
|
3490
|
+
React.createElement("div", { className: "ps-modal-footer" },
|
|
3491
|
+
React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
|
|
3492
|
+
React.createElement("button", {
|
|
3493
|
+
type: "button",
|
|
3494
|
+
className: "ps-modal-button is-primary",
|
|
3495
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
|
|
3496
|
+
}, "Save")),
|
|
3497
|
+
));
|
|
3498
|
+
}
|
|
3499
|
+
if (modal.type === "sessionGroupName" && modalState.sessionGroupName) {
|
|
3500
|
+
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
3501
|
+
React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
|
|
3502
|
+
React.createElement("div", { className: "ps-modal-header" },
|
|
3503
|
+
React.createElement("div", { className: "ps-modal-title" }, modalState.sessionGroupName.title),
|
|
3504
|
+
React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
|
|
3505
|
+
),
|
|
3506
|
+
React.createElement("input", {
|
|
3507
|
+
ref: groupNameInputRef,
|
|
3508
|
+
className: "ps-modal-input",
|
|
3509
|
+
value: modalState.sessionGroupName.value,
|
|
3510
|
+
placeholder: modalState.sessionGroupName.placeholder,
|
|
3511
|
+
onChange: (event) => controller.setSessionGroupNameValue(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length),
|
|
3512
|
+
onKeyDown: (event) => {
|
|
3513
|
+
if (event.key === "Enter") {
|
|
3514
|
+
event.preventDefault();
|
|
3515
|
+
controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
|
|
3516
|
+
}
|
|
3517
|
+
},
|
|
3518
|
+
autoFocus: true,
|
|
3519
|
+
}),
|
|
3520
|
+
React.createElement("div", { className: "ps-modal-details" },
|
|
3521
|
+
normalizeLines(modalState.sessionGroupName.helpLines || []).map((line, index) => React.createElement(Line, { key: `help:${index}`, line, theme, className: "ps-modal-detail-line" })),
|
|
3522
|
+
),
|
|
3523
|
+
React.createElement("div", { className: "ps-modal-footer" },
|
|
3524
|
+
React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
|
|
3525
|
+
React.createElement("button", {
|
|
3526
|
+
type: "button",
|
|
3527
|
+
className: "ps-modal-button is-primary",
|
|
3528
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
|
|
3529
|
+
}, "Create and Move")),
|
|
3530
|
+
));
|
|
3531
|
+
}
|
|
3532
|
+
if (modal.type === "terminatePicker") {
|
|
3533
|
+
const isBulk = Number(modal.bulkCount) > 1;
|
|
3534
|
+
const isSystemRestart = Boolean(modal.systemRestart);
|
|
3535
|
+
const sessionLabel = String(modal.sessionTitle || "").trim() || "this session";
|
|
3536
|
+
const bulkCount = Number(modal.bulkCount) || 0;
|
|
3537
|
+
const bodyText = isBulk
|
|
3538
|
+
? `What should happen to the ${bulkCount} selected sessions? Choose an action; you'll be asked to confirm.`
|
|
3539
|
+
: isSystemRestart
|
|
3540
|
+
? `How should "${sessionLabel}" be restarted? Choose a disposition; you'll be asked to confirm.`
|
|
3541
|
+
: `What should happen to "${sessionLabel}"? Choose an action; you'll be asked to confirm.`;
|
|
3542
|
+
const pick = (action) => () => {
|
|
3543
|
+
controller.pickTerminateAction(action).catch(() => {});
|
|
3544
|
+
};
|
|
3545
|
+
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
3546
|
+
React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
|
|
3547
|
+
React.createElement("div", { className: "ps-modal-header" },
|
|
3548
|
+
React.createElement("div", { className: "ps-modal-title" }, modal.title || "Terminate session"),
|
|
3549
|
+
React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
|
|
3550
|
+
),
|
|
3551
|
+
React.createElement("div", { className: "ps-modal-body", style: { padding: "12px 16px 4px" } },
|
|
3552
|
+
React.createElement("p", { style: { color: "#94a3b8", margin: 0 } }, bodyText)),
|
|
3553
|
+
React.createElement("div", {
|
|
3554
|
+
className: "ps-modal-body",
|
|
3555
|
+
style: { padding: "10px 16px 16px", display: "flex", flexDirection: "column", gap: 8 },
|
|
3556
|
+
},
|
|
3557
|
+
React.createElement("button", {
|
|
3558
|
+
type: "button",
|
|
3559
|
+
className: "ps-modal-button is-primary",
|
|
3560
|
+
style: { width: "100%", justifyContent: "flex-start" },
|
|
3561
|
+
onClick: pick("complete"),
|
|
3562
|
+
}, isBulk ? `Mark ${bulkCount} Completed` : isSystemRestart ? "Complete & Restart" : "Mark Completed"),
|
|
3563
|
+
React.createElement("button", {
|
|
3564
|
+
type: "button",
|
|
3565
|
+
className: "ps-modal-button",
|
|
3566
|
+
style: { width: "100%", justifyContent: "flex-start" },
|
|
3567
|
+
onClick: pick("cancel"),
|
|
3568
|
+
}, isBulk ? `Cancel ${bulkCount} Sessions` : isSystemRestart ? "Terminate & Restart" : "Cancel Session"),
|
|
3569
|
+
React.createElement("button", {
|
|
3570
|
+
type: "button",
|
|
3571
|
+
className: "ps-modal-button is-danger",
|
|
3572
|
+
style: { width: "100%", justifyContent: "flex-start" },
|
|
3573
|
+
onClick: pick("delete"),
|
|
3574
|
+
}, isBulk ? `Hard Delete ${bulkCount} Sessions` : isSystemRestart ? "Hard Delete & Restart" : "Delete Session"),
|
|
3575
|
+
),
|
|
3576
|
+
React.createElement("div", { className: "ps-modal-footer" },
|
|
3577
|
+
React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Close")),
|
|
3578
|
+
));
|
|
3579
|
+
}
|
|
3580
|
+
if (modal.type === "artifactUpload" && modalState.artifactUpload) {
|
|
3581
|
+
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
3582
|
+
React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
|
|
3583
|
+
React.createElement("div", { className: "ps-modal-header" },
|
|
3584
|
+
React.createElement("div", { className: "ps-modal-title" }, modalState.artifactUpload.title),
|
|
3585
|
+
React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
|
|
3586
|
+
),
|
|
3587
|
+
React.createElement("input", {
|
|
3588
|
+
className: "ps-modal-input",
|
|
3589
|
+
value: modalState.artifactUpload.value,
|
|
3590
|
+
placeholder: modalState.artifactUpload.placeholder,
|
|
3591
|
+
onChange: (event) => controller.setArtifactUploadValue(event.currentTarget.value, event.currentTarget.selectionStart || event.currentTarget.value.length),
|
|
3592
|
+
autoFocus: true,
|
|
3593
|
+
}),
|
|
3594
|
+
React.createElement("div", { className: "ps-modal-details" },
|
|
3595
|
+
normalizeLines(modalState.artifactUpload.helpLines || []).map((line, index) => React.createElement(Line, { key: `help:${index}`, line, theme, className: "ps-modal-detail-line" })),
|
|
3596
|
+
),
|
|
3597
|
+
React.createElement("div", { className: "ps-modal-footer" },
|
|
3598
|
+
React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
|
|
3599
|
+
React.createElement("button", {
|
|
3600
|
+
type: "button",
|
|
3601
|
+
className: "ps-modal-button is-primary",
|
|
3602
|
+
onClick: () => controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {}),
|
|
3603
|
+
}, "Attach")),
|
|
3604
|
+
));
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
const filterPresentation = modal.type === "logFilter"
|
|
3608
|
+
? modalState.logFilter
|
|
3609
|
+
: modal.type === "filesFilter"
|
|
3610
|
+
? modalState.filesFilter
|
|
3611
|
+
: modal.type === "historyFormat"
|
|
3612
|
+
? modalState.historyFormat
|
|
3613
|
+
: null;
|
|
3614
|
+
if (filterPresentation) {
|
|
3615
|
+
return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
|
|
3616
|
+
React.createElement("div", { className: "ps-modal is-wide", onClick: (event) => event.stopPropagation() },
|
|
3617
|
+
React.createElement("div", { className: "ps-modal-header" },
|
|
3618
|
+
React.createElement("div", { className: "ps-modal-title" }, filterPresentation.title),
|
|
3619
|
+
React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
|
|
3620
|
+
),
|
|
3621
|
+
React.createElement("div", { className: "ps-filter-grid" },
|
|
3622
|
+
(modal.items || []).map((item, itemIndex) => {
|
|
3623
|
+
const currentValue = modal.type === "filesFilter"
|
|
3624
|
+
? modalState.filesFilterState?.[item.id] || item.options?.[0]?.id
|
|
3625
|
+
: modal.type === "historyFormat"
|
|
3626
|
+
? modalState.historyFormatState
|
|
3627
|
+
: modalState.logsFilter?.[item.id] || item.options?.[0]?.id;
|
|
3628
|
+
return React.createElement("div", { key: item.id || itemIndex, className: "ps-filter-column" },
|
|
3629
|
+
React.createElement("div", { className: "ps-filter-title" }, item.label),
|
|
3630
|
+
(item.options || []).map((option) => React.createElement("button", {
|
|
3631
|
+
key: option.id,
|
|
3632
|
+
type: "button",
|
|
3633
|
+
className: `ps-filter-option${option.id === currentValue ? " is-selected" : ""}`,
|
|
3634
|
+
onClick: () => {
|
|
3635
|
+
controller.dispatch({ type: "ui/modalSelection", index: itemIndex });
|
|
3636
|
+
if (modal.type === "historyFormat") {
|
|
3637
|
+
controller.dispatch({ type: "executionHistory/format", format: option.id });
|
|
3638
|
+
} else if (modal.type === "filesFilter") {
|
|
3639
|
+
controller.dispatch({ type: "files/filter", filter: { [item.id]: option.id } });
|
|
3640
|
+
controller.ensureFilesForScope(option.id).catch(() => {});
|
|
3641
|
+
} else {
|
|
3642
|
+
controller.dispatch({ type: "logs/filter", filter: { [item.id]: option.id } });
|
|
3643
|
+
}
|
|
3644
|
+
},
|
|
3645
|
+
}, option.label)),
|
|
3646
|
+
);
|
|
3647
|
+
}),
|
|
3648
|
+
),
|
|
3649
|
+
React.createElement("div", { className: "ps-modal-footer" },
|
|
3650
|
+
React.createElement("button", { type: "button", className: "ps-modal-button is-primary", onClick: close }, "Done")),
|
|
3651
|
+
));
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
return null;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
function useKeyboardShortcuts(controller, mobile) {
|
|
3658
|
+
React.useEffect(() => {
|
|
3659
|
+
const handler = (event) => {
|
|
3660
|
+
const target = event.target;
|
|
3661
|
+
const editable = target instanceof HTMLElement
|
|
3662
|
+
&& (target.tagName === "TEXTAREA" || target.tagName === "INPUT" || target.isContentEditable);
|
|
3663
|
+
const modal = controller.getState().ui.modal;
|
|
3664
|
+
const visibleInspectorTabs = getVisibleInspectorTabs(controller);
|
|
3665
|
+
const currentInspectorTab = controller.getState().ui.inspectorTab;
|
|
3666
|
+
const focusRegion = controller.getState().ui.focusRegion;
|
|
3667
|
+
const isPlainShortcut = !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3668
|
+
const isShiftTheme = !event.metaKey && !event.ctrlKey && !event.altKey && event.key === "T" && event.shiftKey;
|
|
3669
|
+
const isShiftModel = !event.metaKey && !event.ctrlKey && !event.altKey && event.key === "N" && event.shiftKey;
|
|
3670
|
+
const selectVisibleInspectorTab = (delta) => {
|
|
3671
|
+
const nextTab = cycleTabs(visibleInspectorTabs, currentInspectorTab, delta);
|
|
3672
|
+
controller.selectInspectorTab(nextTab).catch(() => {});
|
|
3673
|
+
};
|
|
3674
|
+
|
|
3675
|
+
if (!editable && isShiftTheme) {
|
|
3676
|
+
event.preventDefault();
|
|
3677
|
+
controller.handleCommand(UI_COMMANDS.OPEN_THEME_PICKER).catch(() => {});
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
if (!editable && isShiftModel) {
|
|
3681
|
+
event.preventDefault();
|
|
3682
|
+
controller.handleCommand(UI_COMMANDS.OPEN_MODEL_PICKER).catch(() => {});
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
if (modal && !editable) {
|
|
3687
|
+
if (event.key === "Escape" || (modal.type === "confirm" && event.key === "n")) {
|
|
3688
|
+
event.preventDefault();
|
|
3689
|
+
controller.handleCommand(UI_COMMANDS.CLOSE_MODAL).catch(() => {});
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
if (event.key === "Enter" || (modal.type === "confirm" && event.key === "y")) {
|
|
3693
|
+
event.preventDefault();
|
|
3694
|
+
controller.handleCommand(UI_COMMANDS.MODAL_CONFIRM).catch(() => {});
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
if (event.key === "Tab" && event.shiftKey) {
|
|
3698
|
+
event.preventDefault();
|
|
3699
|
+
controller.handleCommand(UI_COMMANDS.MODAL_PANE_PREV).catch(() => {});
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
if (event.key === "Tab") {
|
|
3703
|
+
event.preventDefault();
|
|
3704
|
+
controller.handleCommand(UI_COMMANDS.MODAL_PANE_NEXT).catch(() => {});
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
if (event.key === "ArrowUp" || event.key === "k") {
|
|
3708
|
+
event.preventDefault();
|
|
3709
|
+
controller.handleCommand(UI_COMMANDS.MODAL_PREV).catch(() => {});
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
if (event.key === "ArrowDown" || event.key === "j") {
|
|
3713
|
+
event.preventDefault();
|
|
3714
|
+
controller.handleCommand(UI_COMMANDS.MODAL_NEXT).catch(() => {});
|
|
3715
|
+
}
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
if (editable) {
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
if (event.key === "r" && isPlainShortcut && focusRegion !== "prompt") {
|
|
3724
|
+
event.preventDefault();
|
|
3725
|
+
controller.handleCommand(UI_COMMANDS.REFRESH).catch(() => {});
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
if (focusRegion === "chat" && event.key === "s" && isPlainShortcut) {
|
|
3729
|
+
event.preventDefault();
|
|
3730
|
+
controller.handleCommand(UI_COMMANDS.TOGGLE_CHAT_VIEW).catch(() => {});
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
if (event.key === "n" && isPlainShortcut) {
|
|
3734
|
+
event.preventDefault();
|
|
3735
|
+
controller.handleCommand(UI_COMMANDS.NEW_SESSION).catch(() => {});
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
if (
|
|
3739
|
+
focusRegion === "inspector"
|
|
3740
|
+
&& currentInspectorTab === "files"
|
|
3741
|
+
&& (
|
|
3742
|
+
(event.key === "u" && isPlainShortcut)
|
|
3743
|
+
|| ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a")
|
|
3744
|
+
)
|
|
3745
|
+
) {
|
|
3746
|
+
event.preventDefault();
|
|
3747
|
+
controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_UPLOAD).catch(() => {});
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "a" && isPlainShortcut) {
|
|
3751
|
+
event.preventDefault();
|
|
3752
|
+
controller.handleCommand(UI_COMMANDS.DOWNLOAD_SELECTED_FILE).catch(() => {});
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "x" && isPlainShortcut) {
|
|
3756
|
+
event.preventDefault();
|
|
3757
|
+
controller.handleCommand(UI_COMMANDS.DELETE_SELECTED_FILE).catch(() => {});
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "o" && isPlainShortcut) {
|
|
3761
|
+
event.preventDefault();
|
|
3762
|
+
controller.handleCommand(UI_COMMANDS.OPEN_SELECTED_FILE).catch(() => {});
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "f" && isPlainShortcut) {
|
|
3766
|
+
event.preventDefault();
|
|
3767
|
+
controller.handleCommand(UI_COMMANDS.OPEN_FILES_FILTER).catch(() => {});
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
if (focusRegion === "inspector" && currentInspectorTab === "stats" && event.key === "f" && isPlainShortcut) {
|
|
3771
|
+
event.preventDefault();
|
|
3772
|
+
controller.handleCommand(UI_COMMANDS.TOGGLE_STATS_VIEW).catch(() => {});
|
|
3773
|
+
return;
|
|
3774
|
+
}
|
|
3775
|
+
if (focusRegion === "inspector" && currentInspectorTab === "files" && event.key === "v" && isPlainShortcut) {
|
|
3776
|
+
event.preventDefault();
|
|
3777
|
+
controller.handleCommand(UI_COMMANDS.TOGGLE_FILE_PREVIEW_FULLSCREEN).catch(() => {});
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
if (focusRegion === "inspector" && currentInspectorTab === "logs" && event.key === "t" && isPlainShortcut) {
|
|
3781
|
+
event.preventDefault();
|
|
3782
|
+
controller.handleCommand(UI_COMMANDS.TOGGLE_LOG_TAIL).catch(() => {});
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
if (focusRegion === "inspector" && currentInspectorTab === "logs" && event.key === "f" && isPlainShortcut) {
|
|
3786
|
+
event.preventDefault();
|
|
3787
|
+
controller.handleCommand(UI_COMMANDS.OPEN_LOG_FILTER).catch(() => {});
|
|
3788
|
+
return;
|
|
3789
|
+
}
|
|
3790
|
+
if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "f" && isPlainShortcut) {
|
|
3791
|
+
event.preventDefault();
|
|
3792
|
+
controller.handleCommand(UI_COMMANDS.OPEN_HISTORY_FORMAT).catch(() => {});
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "r" && isPlainShortcut) {
|
|
3796
|
+
event.preventDefault();
|
|
3797
|
+
controller.handleCommand(UI_COMMANDS.REFRESH_EXECUTION_HISTORY).catch(() => {});
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
if (focusRegion === "inspector" && currentInspectorTab === "history" && event.key === "a" && isPlainShortcut) {
|
|
3801
|
+
event.preventDefault();
|
|
3802
|
+
controller.handleCommand(UI_COMMANDS.EXPORT_EXECUTION_HISTORY).catch(() => {});
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
if (event.key === "a" && isPlainShortcut) {
|
|
3806
|
+
event.preventDefault();
|
|
3807
|
+
controller.handleCommand(UI_COMMANDS.OPEN_ARTIFACT_PICKER).catch(() => {});
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3810
|
+
if (event.key === "p" && isPlainShortcut) {
|
|
3811
|
+
event.preventDefault();
|
|
3812
|
+
controller.handleCommand(UI_COMMANDS.FOCUS_PROMPT).catch(() => {});
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
if (event.key === "c" && isPlainShortcut) {
|
|
3816
|
+
event.preventDefault();
|
|
3817
|
+
controller.handleCommand(UI_COMMANDS.CANCEL_SESSION).catch(() => {});
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
if (event.key === "d" && isPlainShortcut) {
|
|
3821
|
+
event.preventDefault();
|
|
3822
|
+
controller.handleCommand(UI_COMMANDS.DONE_SESSION).catch(() => {});
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
if (event.key === "D" && event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
3826
|
+
event.preventDefault();
|
|
3827
|
+
controller.handleCommand(UI_COMMANDS.DELETE_SESSION).catch(() => {});
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
if (event.key === "m" && isPlainShortcut && focusRegion === "inspector") {
|
|
3831
|
+
event.preventDefault();
|
|
3832
|
+
selectVisibleInspectorTab(1);
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
if (event.key === "[" && isPlainShortcut) {
|
|
3836
|
+
event.preventDefault();
|
|
3837
|
+
controller.handleCommand(UI_COMMANDS.GROW_LEFT_PANE).catch(() => {});
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
if (event.key === "]" && isPlainShortcut) {
|
|
3841
|
+
event.preventDefault();
|
|
3842
|
+
controller.handleCommand(UI_COMMANDS.GROW_RIGHT_PANE).catch(() => {});
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
if (event.key === "{" && isPlainShortcut) {
|
|
3846
|
+
event.preventDefault();
|
|
3847
|
+
controller.handleCommand(UI_COMMANDS.SHRINK_SESSION_PANE).catch(() => {});
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
if (event.key === "}" && isPlainShortcut) {
|
|
3851
|
+
event.preventDefault();
|
|
3852
|
+
controller.handleCommand(UI_COMMANDS.GROW_SESSION_PANE).catch(() => {});
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
if (focusRegion === "inspector" && event.key === "ArrowLeft") {
|
|
3856
|
+
event.preventDefault();
|
|
3857
|
+
selectVisibleInspectorTab(-1);
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
if (focusRegion === "inspector" && event.key === "ArrowRight") {
|
|
3861
|
+
event.preventDefault();
|
|
3862
|
+
selectVisibleInspectorTab(1);
|
|
3863
|
+
return;
|
|
3864
|
+
}
|
|
3865
|
+
if (focusRegion === "sessions" && (event.key === "ArrowUp" || event.key === "k")) {
|
|
3866
|
+
event.preventDefault();
|
|
3867
|
+
controller.handleCommand(UI_COMMANDS.MOVE_SESSION_UP).catch(() => {});
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
if (focusRegion === "sessions" && (event.key === "ArrowDown" || event.key === "j")) {
|
|
3871
|
+
event.preventDefault();
|
|
3872
|
+
controller.handleCommand(UI_COMMANDS.MOVE_SESSION_DOWN).catch(() => {});
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
if (focusRegion === "sessions" && event.ctrlKey && event.key.toLowerCase() === "g" && !event.metaKey && !event.altKey) {
|
|
3876
|
+
event.preventDefault();
|
|
3877
|
+
controller.handleCommand(UI_COMMANDS.OPEN_MOVE_TO_GROUP).catch(() => {});
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
if (focusRegion === "sessions" && (event.key === "PageUp" || (event.ctrlKey && event.key.toLowerCase() === "u"))) {
|
|
3881
|
+
event.preventDefault();
|
|
3882
|
+
controller.handleCommand(UI_COMMANDS.PAGE_UP).catch(() => {});
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
if (focusRegion === "sessions" && (event.key === "PageDown" || (event.ctrlKey && event.key.toLowerCase() === "d"))) {
|
|
3886
|
+
event.preventDefault();
|
|
3887
|
+
controller.handleCommand(UI_COMMANDS.PAGE_DOWN).catch(() => {});
|
|
3888
|
+
return;
|
|
3889
|
+
}
|
|
3890
|
+
if (focusRegion === "sessions" && event.key === "t" && isPlainShortcut) {
|
|
3891
|
+
event.preventDefault();
|
|
3892
|
+
controller.handleCommand(UI_COMMANDS.OPEN_RENAME_SESSION).catch(() => {});
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
if (!mobile && (event.key === "PageUp" || (event.ctrlKey && event.key.toLowerCase() === "u"))) {
|
|
3896
|
+
event.preventDefault();
|
|
3897
|
+
controller.handleCommand(UI_COMMANDS.PAGE_UP).catch(() => {});
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3900
|
+
if (!mobile && (event.key === "PageDown" || (event.ctrlKey && event.key.toLowerCase() === "d"))) {
|
|
3901
|
+
event.preventDefault();
|
|
3902
|
+
controller.handleCommand(UI_COMMANDS.PAGE_DOWN).catch(() => {});
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3905
|
+
if (!mobile && event.key === "g" && isPlainShortcut) {
|
|
3906
|
+
event.preventDefault();
|
|
3907
|
+
controller.handleCommand(UI_COMMANDS.SCROLL_TOP).catch(() => {});
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
if (!mobile && event.key === "G" && event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
3911
|
+
event.preventDefault();
|
|
3912
|
+
controller.handleCommand(UI_COMMANDS.SCROLL_BOTTOM).catch(() => {});
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
if (focusRegion !== "prompt" && event.key === "h" && isPlainShortcut) {
|
|
3916
|
+
event.preventDefault();
|
|
3917
|
+
controller.handleCommand(UI_COMMANDS.FOCUS_LEFT).catch(() => {});
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
if (focusRegion !== "prompt" && event.key === "l" && isPlainShortcut) {
|
|
3921
|
+
event.preventDefault();
|
|
3922
|
+
controller.handleCommand(UI_COMMANDS.FOCUS_RIGHT).catch(() => {});
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
if (!mobile && (event.key === "ArrowUp" || event.key === "k")) {
|
|
3926
|
+
event.preventDefault();
|
|
3927
|
+
controller.handleCommand(UI_COMMANDS.SCROLL_UP).catch(() => {});
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
if (!mobile && (event.key === "ArrowDown" || event.key === "j")) {
|
|
3931
|
+
event.preventDefault();
|
|
3932
|
+
controller.handleCommand(UI_COMMANDS.SCROLL_DOWN).catch(() => {});
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
if (!mobile && event.key === "Escape") {
|
|
3936
|
+
event.preventDefault();
|
|
3937
|
+
controller.handleCommand(UI_COMMANDS.FOCUS_SESSIONS).catch(() => {});
|
|
3938
|
+
}
|
|
3939
|
+
};
|
|
3940
|
+
|
|
3941
|
+
window.addEventListener("keydown", handler);
|
|
3942
|
+
return () => window.removeEventListener("keydown", handler);
|
|
3943
|
+
}, [controller, mobile]);
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
function formatAdminPrincipalLabel(principal) {
|
|
3947
|
+
if (!principal) return "Unknown user";
|
|
3948
|
+
const name = String(principal.displayName || "").trim();
|
|
3949
|
+
const email = String(principal.email || "").trim();
|
|
3950
|
+
if (name && email && name.toLowerCase() !== email.toLowerCase()) return `${name} <${email}>`;
|
|
3951
|
+
if (name) return name;
|
|
3952
|
+
if (email) return email;
|
|
3953
|
+
const provider = String(principal.provider || "").trim();
|
|
3954
|
+
const subject = String(principal.subject || "").trim();
|
|
3955
|
+
return [provider, subject].filter(Boolean).join(":") || "user";
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
function AdminConsolePanel({ controller }) {
|
|
3959
|
+
const view = useControllerSelector(controller, selectAdminConsole, shallowEqualObject);
|
|
3960
|
+
const draftRef = React.useRef(null);
|
|
3961
|
+
|
|
3962
|
+
React.useEffect(() => {
|
|
3963
|
+
if (view.ghcpKey.editing) {
|
|
3964
|
+
// Defer focus to next tick so the input has mounted.
|
|
3965
|
+
const handle = window.requestAnimationFrame(() => {
|
|
3966
|
+
if (draftRef.current) draftRef.current.focus();
|
|
3967
|
+
});
|
|
3968
|
+
return () => window.cancelAnimationFrame(handle);
|
|
3969
|
+
}
|
|
3970
|
+
return undefined;
|
|
3971
|
+
}, [view.ghcpKey.editing]);
|
|
3972
|
+
|
|
3973
|
+
const onClose = React.useCallback(() => {
|
|
3974
|
+
controller.closeAdminConsole();
|
|
3975
|
+
}, [controller]);
|
|
3976
|
+
const onRefresh = React.useCallback(() => {
|
|
3977
|
+
controller.refreshAdminProfile().catch(() => {});
|
|
3978
|
+
}, [controller]);
|
|
3979
|
+
const onBeginEdit = React.useCallback(() => {
|
|
3980
|
+
controller.beginAdminEditGhcpKey();
|
|
3981
|
+
}, [controller]);
|
|
3982
|
+
const onCancelEdit = React.useCallback(() => {
|
|
3983
|
+
controller.cancelAdminEditGhcpKey();
|
|
3984
|
+
}, [controller]);
|
|
3985
|
+
const onClear = React.useCallback(() => {
|
|
3986
|
+
controller.clearAdminGhcpKey().catch(() => {});
|
|
3987
|
+
}, [controller]);
|
|
3988
|
+
const onSubmit = React.useCallback((event) => {
|
|
3989
|
+
event.preventDefault();
|
|
3990
|
+
controller.saveAdminGhcpKey().catch(() => {});
|
|
3991
|
+
}, [controller]);
|
|
3992
|
+
const onDraftChange = React.useCallback((event) => {
|
|
3993
|
+
controller.setAdminGhcpKeyDraft(event.target.value);
|
|
3994
|
+
}, [controller]);
|
|
3995
|
+
|
|
3996
|
+
const principalLabel = formatAdminPrincipalLabel(view.principal);
|
|
3997
|
+
|
|
3998
|
+
return React.createElement("div", { className: "ps-admin-console" },
|
|
3999
|
+
React.createElement("header", { className: "ps-admin-console__header" },
|
|
4000
|
+
React.createElement("h2", null, "Admin Console"),
|
|
4001
|
+
React.createElement("button", {
|
|
4002
|
+
type: "button",
|
|
4003
|
+
className: "ps-mini-button",
|
|
4004
|
+
onClick: onClose,
|
|
4005
|
+
}, "Close")),
|
|
4006
|
+
React.createElement("section", { className: "ps-admin-console__identity" },
|
|
4007
|
+
React.createElement("dl", null,
|
|
4008
|
+
React.createElement("dt", null, "Signed in as"),
|
|
4009
|
+
React.createElement("dd", null, principalLabel))),
|
|
4010
|
+
view.loadError
|
|
4011
|
+
? React.createElement("div", { className: "ps-admin-console__error", role: "alert" }, view.loadError)
|
|
4012
|
+
: null,
|
|
4013
|
+
React.createElement("section", { className: "ps-admin-console__section" },
|
|
4014
|
+
React.createElement("h3", null, "GitHub Copilot key"),
|
|
4015
|
+
React.createElement("p", { className: "ps-admin-console__hint" },
|
|
4016
|
+
"Per-user override for the GitHub Copilot model provider token. ",
|
|
4017
|
+
"When set, this key is used instead of the worker's env-supplied ",
|
|
4018
|
+
"GITHUB_TOKEN for sessions you own. Clearing the key reverts to ",
|
|
4019
|
+
"the worker default."),
|
|
4020
|
+
React.createElement("p", { className: `ps-admin-console__status${view.ghcpKey.error ? " is-error" : ""}` },
|
|
4021
|
+
view.ghcpKey.error || view.ghcpKey.statusText),
|
|
4022
|
+
view.ghcpKey.editing
|
|
4023
|
+
? React.createElement("form", { className: "ps-admin-console__form", onSubmit },
|
|
4024
|
+
React.createElement("input", {
|
|
4025
|
+
ref: draftRef,
|
|
4026
|
+
type: "password",
|
|
4027
|
+
autoComplete: "off",
|
|
4028
|
+
spellCheck: false,
|
|
4029
|
+
value: view.ghcpKey.draft,
|
|
4030
|
+
disabled: view.ghcpKey.saving,
|
|
4031
|
+
placeholder: "Paste GitHub Copilot key",
|
|
4032
|
+
onChange: onDraftChange,
|
|
4033
|
+
}),
|
|
4034
|
+
React.createElement("div", { className: "ps-admin-console__actions" },
|
|
4035
|
+
React.createElement("button", {
|
|
4036
|
+
type: "submit",
|
|
4037
|
+
className: "ps-primary-button",
|
|
4038
|
+
disabled: view.ghcpKey.saving,
|
|
4039
|
+
}, view.ghcpKey.saving ? "Saving..." : "Save"),
|
|
4040
|
+
React.createElement("button", {
|
|
4041
|
+
type: "button",
|
|
4042
|
+
className: "ps-mini-button",
|
|
4043
|
+
onClick: onCancelEdit,
|
|
4044
|
+
disabled: view.ghcpKey.saving,
|
|
4045
|
+
}, "Cancel")))
|
|
4046
|
+
: React.createElement("div", { className: "ps-admin-console__actions" },
|
|
4047
|
+
React.createElement("button", {
|
|
4048
|
+
type: "button",
|
|
4049
|
+
className: "ps-primary-button",
|
|
4050
|
+
onClick: onBeginEdit,
|
|
4051
|
+
}, view.ghcpKey.configured ? "Replace key" : "Set key"),
|
|
4052
|
+
view.ghcpKey.configured
|
|
4053
|
+
? React.createElement("button", {
|
|
4054
|
+
type: "button",
|
|
4055
|
+
className: "ps-mini-button",
|
|
4056
|
+
onClick: onClear,
|
|
4057
|
+
}, "Clear key")
|
|
4058
|
+
: null,
|
|
4059
|
+
React.createElement("button", {
|
|
4060
|
+
type: "button",
|
|
4061
|
+
className: "ps-mini-button",
|
|
4062
|
+
onClick: onRefresh,
|
|
4063
|
+
}, view.loading ? "Refreshing..." : "Refresh"))));
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
export function createWebPilotSwarmController({ transport, mode = "remote", branding = null } = {}) {
|
|
4067
|
+
clearBrowserPreferenceCache();
|
|
4068
|
+
const store = createStore(appReducer, createInitialState({
|
|
4069
|
+
mode,
|
|
4070
|
+
branding,
|
|
4071
|
+
}));
|
|
4072
|
+
return new PilotSwarmUiController({ store, transport });
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
export function PilotSwarmWebApp({ controller }) {
|
|
4076
|
+
const viewportRef = React.useRef(null);
|
|
4077
|
+
const mainGridRef = React.useRef(null);
|
|
4078
|
+
const viewport = useMeasuredViewport(viewportRef);
|
|
4079
|
+
const mainGridViewport = useMeasuredViewport(mainGridRef);
|
|
4080
|
+
const gridViewport = computeGridViewport(viewport);
|
|
4081
|
+
const [chatFocusMode, setChatFocusMode] = React.useState(false);
|
|
4082
|
+
const [chatFocusPane, setChatFocusPane] = React.useState(null);
|
|
4083
|
+
const state = useControllerSelector(controller, (rootState) => ({
|
|
4084
|
+
themeId: rootState.ui.themeId,
|
|
4085
|
+
ownerFilter: rootState.sessions.ownerFilter,
|
|
4086
|
+
pinnedIds: rootState.sessions.pinnedIds,
|
|
4087
|
+
// Pass the live Set reference here so shallow-equal sees the same
|
|
4088
|
+
// identity across renders (a fresh array would break memoization).
|
|
4089
|
+
// Conversion to a sorted array happens inside `normalizeProfileSettings`.
|
|
4090
|
+
collapsedSessionIds: rootState.sessions.collapsedIds,
|
|
4091
|
+
activeSessionId: rootState.sessions.activeSessionId || null,
|
|
4092
|
+
promptRows: getStatePromptRows(rootState),
|
|
4093
|
+
chatViewMode: rootState.ui.chatViewMode || "transcript",
|
|
4094
|
+
activeSessionIsGroup: Boolean(rootState.sessions.activeSessionId && rootState.sessions.byId[rootState.sessions.activeSessionId]?.isGroup),
|
|
4095
|
+
paneAdjust: rootState.ui.layout?.paneAdjust ?? 0,
|
|
4096
|
+
sessionPaneAdjust: rootState.ui.layout?.sessionPaneAdjust ?? 0,
|
|
4097
|
+
portalSessionColumnAdjust: rootState.ui.layout?.portalSessionColumnAdjust ?? 0,
|
|
4098
|
+
activityPaneAdjust: rootState.ui.layout?.activityPaneAdjust ?? 0,
|
|
4099
|
+
focusRegion: rootState.ui.focusRegion,
|
|
4100
|
+
inspectorTab: rootState.ui.inspectorTab,
|
|
4101
|
+
filesFullscreen: Boolean(rootState.files.fullscreen),
|
|
4102
|
+
adminVisible: Boolean(rootState.admin?.visible),
|
|
4103
|
+
}), shallowEqualObject);
|
|
4104
|
+
const profileSettingsHydratedRef = React.useRef(false);
|
|
4105
|
+
const lastProfileSettingsJsonRef = React.useRef(null);
|
|
4106
|
+
const profileSettingsSaveTimerRef = React.useRef(null);
|
|
4107
|
+
const profileSettingsPollTimerRef = React.useRef(null);
|
|
4108
|
+
const profileSettingsPollInFlightRef = React.useRef(false);
|
|
4109
|
+
const profileSettingsSaveInFlightRef = React.useRef(false);
|
|
4110
|
+
const appliedProfileSettingsJsonRef = React.useRef(null);
|
|
4111
|
+
const defaultProfileSettingsRef = React.useRef(null);
|
|
4112
|
+
const [mobilePane, setMobilePane] = React.useState("workspace");
|
|
4113
|
+
const [mobileSessionsCollapsed, setMobileSessionsCollapsed] = React.useState(false);
|
|
4114
|
+
const mobile = (viewport.width || window.innerWidth || 0) < MOBILE_BREAKPOINT;
|
|
4115
|
+
const readOnlyChatPane = state.activeSessionIsGroup || state.chatViewMode === "summary";
|
|
4116
|
+
const effectivePromptRows = readOnlyChatPane ? 0 : state.promptRows;
|
|
4117
|
+
|
|
4118
|
+
useKeyboardShortcuts(controller, mobile);
|
|
4119
|
+
|
|
4120
|
+
React.useEffect(() => {
|
|
4121
|
+
controller.setViewport(gridViewport);
|
|
4122
|
+
}, [controller, gridViewport.height, gridViewport.width]);
|
|
4123
|
+
|
|
4124
|
+
React.useEffect(() => {
|
|
4125
|
+
let active = true;
|
|
4126
|
+
profileSettingsHydratedRef.current = false;
|
|
4127
|
+
lastProfileSettingsJsonRef.current = null;
|
|
4128
|
+
appliedProfileSettingsJsonRef.current = null;
|
|
4129
|
+
defaultProfileSettingsRef.current = buildDefaultProfileSettingsFromState(controller.getState());
|
|
4130
|
+
if (profileSettingsSaveTimerRef.current) {
|
|
4131
|
+
clearTimeout(profileSettingsSaveTimerRef.current);
|
|
4132
|
+
profileSettingsSaveTimerRef.current = null;
|
|
4133
|
+
}
|
|
4134
|
+
if (profileSettingsPollTimerRef.current) {
|
|
4135
|
+
clearInterval(profileSettingsPollTimerRef.current);
|
|
4136
|
+
profileSettingsPollTimerRef.current = null;
|
|
4137
|
+
}
|
|
4138
|
+
profileSettingsPollInFlightRef.current = false;
|
|
4139
|
+
profileSettingsSaveInFlightRef.current = false;
|
|
4140
|
+
|
|
4141
|
+
const transport = controller.transport;
|
|
4142
|
+
if (typeof transport?.getCurrentUserProfile !== "function"
|
|
4143
|
+
|| typeof transport?.setCurrentUserProfileSettings !== "function") {
|
|
4144
|
+
profileSettingsHydratedRef.current = true;
|
|
4145
|
+
return () => {
|
|
4146
|
+
active = false;
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
const pollProfileSettings = async () => {
|
|
4151
|
+
if (!active || profileSettingsPollInFlightRef.current) return;
|
|
4152
|
+
profileSettingsPollInFlightRef.current = true;
|
|
4153
|
+
try {
|
|
4154
|
+
const profile = await transport.getCurrentUserProfile();
|
|
4155
|
+
if (!active) return;
|
|
4156
|
+
const settings = materializeProfileSettings(
|
|
4157
|
+
profile?.profileSettings,
|
|
4158
|
+
defaultProfileSettingsRef.current,
|
|
4159
|
+
);
|
|
4160
|
+
const settingsJson = JSON.stringify(settings);
|
|
4161
|
+
const currentSettingsBeforeApply = profileSettingsFromViewState(controller.getState());
|
|
4162
|
+
const currentSettingsBeforeApplyJson = JSON.stringify(currentSettingsBeforeApply);
|
|
4163
|
+
const hasUnpersistedLocalChange = profileSettingsHydratedRef.current
|
|
4164
|
+
&& lastProfileSettingsJsonRef.current != null
|
|
4165
|
+
&& currentSettingsBeforeApplyJson !== lastProfileSettingsJsonRef.current;
|
|
4166
|
+
const hasPendingLocalWrite = Boolean(profileSettingsSaveTimerRef.current)
|
|
4167
|
+
|| profileSettingsSaveInFlightRef.current
|
|
4168
|
+
|| hasUnpersistedLocalChange;
|
|
4169
|
+
if (!hasPendingLocalWrite && appliedProfileSettingsJsonRef.current !== settingsJson) {
|
|
4170
|
+
controller.dispatch({ type: "profileSettings/apply", settings });
|
|
4171
|
+
appliedProfileSettingsJsonRef.current = settingsJson;
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
const currentSettings = profileSettingsFromViewState(controller.getState());
|
|
4175
|
+
lastProfileSettingsJsonRef.current = JSON.stringify(currentSettings);
|
|
4176
|
+
profileSettingsHydratedRef.current = true;
|
|
4177
|
+
} catch {
|
|
4178
|
+
if (!active) return;
|
|
4179
|
+
profileSettingsHydratedRef.current = true;
|
|
4180
|
+
} finally {
|
|
4181
|
+
profileSettingsPollInFlightRef.current = false;
|
|
4182
|
+
}
|
|
4183
|
+
};
|
|
4184
|
+
|
|
4185
|
+
pollProfileSettings().catch(() => {});
|
|
4186
|
+
profileSettingsPollTimerRef.current = setInterval(() => {
|
|
4187
|
+
pollProfileSettings().catch(() => {});
|
|
4188
|
+
}, PROFILE_SETTINGS_POLL_MS);
|
|
4189
|
+
|
|
4190
|
+
return () => {
|
|
4191
|
+
active = false;
|
|
4192
|
+
if (profileSettingsSaveTimerRef.current) {
|
|
4193
|
+
clearTimeout(profileSettingsSaveTimerRef.current);
|
|
4194
|
+
profileSettingsSaveTimerRef.current = null;
|
|
4195
|
+
}
|
|
4196
|
+
if (profileSettingsPollTimerRef.current) {
|
|
4197
|
+
clearInterval(profileSettingsPollTimerRef.current);
|
|
4198
|
+
profileSettingsPollTimerRef.current = null;
|
|
4199
|
+
}
|
|
4200
|
+
profileSettingsPollInFlightRef.current = false;
|
|
4201
|
+
};
|
|
4202
|
+
}, [controller]);
|
|
4203
|
+
|
|
4204
|
+
React.useEffect(() => {
|
|
4205
|
+
if (!profileSettingsHydratedRef.current) return undefined;
|
|
4206
|
+
const settings = profileSettingsFromViewState(state);
|
|
4207
|
+
const settingsJson = JSON.stringify(settings);
|
|
4208
|
+
if (lastProfileSettingsJsonRef.current === settingsJson) return undefined;
|
|
4209
|
+
lastProfileSettingsJsonRef.current = settingsJson;
|
|
4210
|
+
if (profileSettingsSaveTimerRef.current) {
|
|
4211
|
+
clearTimeout(profileSettingsSaveTimerRef.current);
|
|
4212
|
+
}
|
|
4213
|
+
profileSettingsSaveTimerRef.current = setTimeout(() => {
|
|
4214
|
+
profileSettingsSaveTimerRef.current = null;
|
|
4215
|
+
profileSettingsSaveInFlightRef.current = true;
|
|
4216
|
+
saveProfileSettings(controller, settings)
|
|
4217
|
+
.catch(() => {})
|
|
4218
|
+
.finally(() => {
|
|
4219
|
+
profileSettingsSaveInFlightRef.current = false;
|
|
4220
|
+
});
|
|
4221
|
+
}, 400);
|
|
4222
|
+
return undefined;
|
|
4223
|
+
}, [controller, state.activeSessionId, state.activityPaneAdjust, state.chatViewMode, state.collapsedSessionIds, state.ownerFilter, state.paneAdjust, state.pinnedIds, state.portalSessionColumnAdjust, state.sessionPaneAdjust, state.themeId]);
|
|
4224
|
+
|
|
4225
|
+
React.useEffect(() => {
|
|
4226
|
+
applyDocumentTheme(state.themeId);
|
|
4227
|
+
}, [state.themeId]);
|
|
4228
|
+
|
|
4229
|
+
React.useEffect(() => {
|
|
4230
|
+
if (mobile && state.focusRegion !== "prompt") {
|
|
4231
|
+
setMobilePane(state.focusRegion === "activity"
|
|
4232
|
+
? "activity"
|
|
4233
|
+
: state.focusRegion === "inspector"
|
|
4234
|
+
? "inspector"
|
|
4235
|
+
: "workspace");
|
|
4236
|
+
}
|
|
4237
|
+
}, [mobile, state.focusRegion]);
|
|
4238
|
+
|
|
4239
|
+
React.useEffect(() => {
|
|
4240
|
+
const visibleTabs = getVisibleInspectorTabs(controller);
|
|
4241
|
+
if (!visibleTabs.includes(state.inspectorTab) && visibleTabs.length > 0) {
|
|
4242
|
+
controller.selectInspectorTab(visibleTabs[0]).catch(() => {});
|
|
4243
|
+
}
|
|
4244
|
+
}, [controller, state.inspectorTab]);
|
|
4245
|
+
|
|
4246
|
+
const layout = React.useMemo(
|
|
4247
|
+
() => computeLegacyLayout(gridViewport, state.paneAdjust, effectivePromptRows, state.sessionPaneAdjust, state.activityPaneAdjust),
|
|
4248
|
+
[gridViewport, state.activityPaneAdjust, state.paneAdjust, effectivePromptRows, state.sessionPaneAdjust],
|
|
4249
|
+
);
|
|
4250
|
+
const filesFullscreenActive = state.filesFullscreen && state.inspectorTab === "files";
|
|
4251
|
+
|
|
4252
|
+
React.useEffect(() => {
|
|
4253
|
+
if (!filesFullscreenActive || !chatFocusMode) return;
|
|
4254
|
+
setChatFocusMode(false);
|
|
4255
|
+
setChatFocusPane(null);
|
|
4256
|
+
}, [chatFocusMode, filesFullscreenActive]);
|
|
4257
|
+
|
|
4258
|
+
const toggleChatFocusMode = React.useCallback(() => {
|
|
4259
|
+
setChatFocusMode((current) => {
|
|
4260
|
+
const next = !current;
|
|
4261
|
+
if (!next) {
|
|
4262
|
+
setChatFocusPane(null);
|
|
4263
|
+
} else {
|
|
4264
|
+
controller.setFocus("chat");
|
|
4265
|
+
}
|
|
4266
|
+
return next;
|
|
4267
|
+
});
|
|
4268
|
+
}, [controller]);
|
|
4269
|
+
|
|
4270
|
+
const toggleChatFocusPane = React.useCallback((paneId) => {
|
|
4271
|
+
setChatFocusPane((current) => {
|
|
4272
|
+
const next = current === paneId ? null : paneId;
|
|
4273
|
+
controller.setFocus(next || "chat");
|
|
4274
|
+
return next;
|
|
4275
|
+
});
|
|
4276
|
+
}, [controller]);
|
|
4277
|
+
|
|
4278
|
+
const estimatedMainGridWidth = Math.max(0, (viewport.width || 0) * 0.68);
|
|
4279
|
+
const measuredMainGridWidth = mainGridViewport.width || estimatedMainGridWidth;
|
|
4280
|
+
const sessionColumnWidth = portalSessionColumnWidth(measuredMainGridWidth, state.portalSessionColumnAdjust);
|
|
4281
|
+
const sessionColumnMode = portalSessionColumnMode(sessionColumnWidth);
|
|
4282
|
+
const sessionColumnTrack = sessionColumnMode === "hidden"
|
|
4283
|
+
? "0px"
|
|
4284
|
+
: `clamp(0px, calc(${PORTAL_SESSION_COLUMN_RATIO * 100}% + ${Number(state.portalSessionColumnAdjust) || 0}px), max(0px, calc(100% - 14rem - 32px)))`;
|
|
4285
|
+
|
|
4286
|
+
const desktopWorkspace = React.createElement("div", {
|
|
4287
|
+
className: "ps-workspace-grid",
|
|
4288
|
+
style: {
|
|
4289
|
+
gridTemplateColumns: `minmax(0, ${layout.leftWidth}fr) 16px minmax(0, ${layout.rightWidth}fr)`,
|
|
4290
|
+
},
|
|
4291
|
+
},
|
|
4292
|
+
React.createElement("div", {
|
|
4293
|
+
ref: mainGridRef,
|
|
4294
|
+
className: `ps-workspace-main-grid is-session-${sessionColumnMode}`,
|
|
4295
|
+
style: {
|
|
4296
|
+
gridTemplateColumns: `${sessionColumnTrack} 16px minmax(14rem, 1fr)`,
|
|
4297
|
+
},
|
|
4298
|
+
},
|
|
4299
|
+
sessionColumnMode !== "hidden" ? React.createElement("div", {
|
|
4300
|
+
className: "ps-workspace-pane-slot",
|
|
4301
|
+
style: { gridColumn: "1" },
|
|
4302
|
+
},
|
|
4303
|
+
React.createElement(SessionPane, { controller, structuredRows: true })) : null,
|
|
4304
|
+
React.createElement("div", {
|
|
4305
|
+
style: {
|
|
4306
|
+
gridColumn: "2",
|
|
4307
|
+
minWidth: 0,
|
|
4308
|
+
display: "flex",
|
|
4309
|
+
},
|
|
4310
|
+
},
|
|
4311
|
+
React.createElement(SessionColumnResizeHandle, { controller, portalSessionColumnAdjust: state.portalSessionColumnAdjust })),
|
|
4312
|
+
React.createElement("div", {
|
|
4313
|
+
className: "ps-workspace-pane-slot",
|
|
4314
|
+
style: { gridColumn: "3" },
|
|
4315
|
+
},
|
|
4316
|
+
React.createElement(ChatPane, { controller }))),
|
|
4317
|
+
React.createElement(ColumnResizeHandle, { controller, paneAdjust: state.paneAdjust }),
|
|
4318
|
+
React.createElement("div", {
|
|
4319
|
+
className: "ps-workspace-column",
|
|
4320
|
+
style: { gridTemplateRows: `${layout.inspectorPaneHeight}fr 16px ${layout.activityPaneHeight}fr` },
|
|
4321
|
+
},
|
|
4322
|
+
React.createElement("div", {
|
|
4323
|
+
className: "ps-workspace-pane-slot",
|
|
4324
|
+
style: { gridRow: "1" },
|
|
4325
|
+
},
|
|
4326
|
+
!layout.inspectorHidden ? React.createElement(InspectorPane, { controller, mobile: false }) : null),
|
|
4327
|
+
React.createElement("div", {
|
|
4328
|
+
style: {
|
|
4329
|
+
gridRow: "2",
|
|
4330
|
+
minHeight: 0,
|
|
4331
|
+
display: "flex",
|
|
4332
|
+
flexDirection: "column",
|
|
4333
|
+
},
|
|
4334
|
+
},
|
|
4335
|
+
React.createElement(ActivityRowResizeHandle, { controller, activityPaneAdjust: state.activityPaneAdjust })),
|
|
4336
|
+
React.createElement("div", {
|
|
4337
|
+
className: "ps-workspace-pane-slot",
|
|
4338
|
+
style: { gridRow: "3" },
|
|
4339
|
+
},
|
|
4340
|
+
!layout.activityHidden ? React.createElement(ActivityPane, { controller }) : null)));
|
|
4341
|
+
const chatFocusWorkspace = React.createElement(ChatFocusWorkspace, {
|
|
4342
|
+
controller,
|
|
4343
|
+
openPane: chatFocusPane,
|
|
4344
|
+
onTogglePane: toggleChatFocusPane,
|
|
4345
|
+
onExitFocus: toggleChatFocusMode,
|
|
4346
|
+
mobile,
|
|
4347
|
+
});
|
|
4348
|
+
const fullscreenWorkspace = React.createElement("div", { className: "ps-workspace-full" },
|
|
4349
|
+
React.createElement(InspectorPane, { controller, mobile: false }));
|
|
4350
|
+
|
|
4351
|
+
let mobileContent = null;
|
|
4352
|
+
if (filesFullscreenActive) mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
|
|
4353
|
+
React.createElement(InspectorPane, { controller, mobile: true }));
|
|
4354
|
+
else if (mobilePane === "inspector") mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
|
|
4355
|
+
React.createElement(InspectorPane, { controller, mobile: true }));
|
|
4356
|
+
else if (mobilePane === "activity") mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
|
|
4357
|
+
React.createElement(ActivityPane, { controller }));
|
|
4358
|
+
else mobileContent = React.createElement(MobileWorkspace, {
|
|
4359
|
+
controller,
|
|
4360
|
+
sessionsCollapsed: mobileSessionsCollapsed,
|
|
4361
|
+
setSessionsCollapsed: setMobileSessionsCollapsed,
|
|
4362
|
+
});
|
|
4363
|
+
|
|
4364
|
+
return React.createElement("div", { ref: viewportRef, className: "ps-web-shell" },
|
|
4365
|
+
// Hide the top toolbar on mobile inspector/activity panes (and in
|
|
4366
|
+
// chat-focus mode). Those panes are pure read-only surfaces;
|
|
4367
|
+
// session/model/theme actions stay reachable from the Main pane,
|
|
4368
|
+
// and dropping the toolbar buys ~3 lines of vertical real estate
|
|
4369
|
+
// on a phone, which matters for the fleet-skills card and the
|
|
4370
|
+
// logs/sequence inspectors.
|
|
4371
|
+
(mobile && (chatFocusMode || mobilePane === "inspector" || mobilePane === "activity"))
|
|
4372
|
+
? null
|
|
4373
|
+
: React.createElement(Toolbar, {
|
|
4374
|
+
controller,
|
|
4375
|
+
mobile,
|
|
4376
|
+
chatFocusMode,
|
|
4377
|
+
onToggleChatFocus: toggleChatFocusMode,
|
|
4378
|
+
chatFocusDisabled: filesFullscreenActive,
|
|
4379
|
+
}),
|
|
4380
|
+
React.createElement("div", { className: "ps-workspace" },
|
|
4381
|
+
state.adminVisible
|
|
4382
|
+
? React.createElement(AdminConsolePanel, { controller })
|
|
4383
|
+
: (filesFullscreenActive
|
|
4384
|
+
? fullscreenWorkspace
|
|
4385
|
+
: (chatFocusMode
|
|
4386
|
+
? chatFocusWorkspace
|
|
4387
|
+
: (mobile ? mobileContent : desktopWorkspace)))),
|
|
4388
|
+
mobile && !chatFocusMode ? React.createElement(MobileNav, { activePane: mobilePane, setActivePane: setMobilePane, controller }) : null,
|
|
4389
|
+
React.createElement(ModalLayer, { controller }));
|
|
4390
|
+
}
|