pilotswarm 0.5.14 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/README.md +2 -2
- package/mcp/dist/src/server.d.ts.map +1 -1
- package/mcp/dist/src/server.js +5 -1
- package/mcp/dist/src/server.js.map +1 -1
- package/mcp/dist/src/tools/capabilities.d.ts +1 -0
- package/mcp/dist/src/tools/capabilities.d.ts.map +1 -1
- package/mcp/dist/src/tools/capabilities.js +5 -0
- package/mcp/dist/src/tools/capabilities.js.map +1 -1
- package/mcp/dist/src/tools/groups.d.ts +5 -4
- package/mcp/dist/src/tools/groups.d.ts.map +1 -1
- package/mcp/dist/src/tools/groups.js +43 -20
- package/mcp/dist/src/tools/groups.js.map +1 -1
- package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
- package/mcp/dist/src/tools/sessions.js +4 -2
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/package.json +2 -2
- package/tui/plugins/.mcp.json +7 -1
- package/tui/plugins/agents/deepwiki.agent.md +44 -0
- package/tui/src/node-sdk-transport.js +61 -8
- package/ui/core/src/controller.js +264 -40
- package/ui/core/src/formatting.js +25 -0
- package/ui/core/src/reducer.js +117 -8
- package/ui/core/src/selectors.js +76 -3
- package/ui/core/src/state.js +3 -0
- package/ui/react/src/web-app.js +363 -127
- package/web/api/router.js +3 -1
- package/web/dist/assets/index-Df1mD9p0.js +24 -0
- package/web/dist/assets/index-whIft7M6.css +1 -0
- package/web/dist/assets/pilotswarm-DR5j-z8y.js +90 -0
- package/web/dist/index.html +3 -3
- package/web/runtime.js +114 -42
- package/web/dist/assets/index-BnxC8cNG.js +0 -24
- package/web/dist/assets/index-Bx6KHIaj.css +0 -1
- package/web/dist/assets/pilotswarm-NE7H63ha.js +0 -90
package/ui/react/src/web-app.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
selectInspector,
|
|
34
34
|
selectLogFilterModal,
|
|
35
35
|
selectModelPickerModal,
|
|
36
|
+
selectNavigationError,
|
|
36
37
|
selectReasoningEffortPickerModal,
|
|
37
38
|
selectContextTierPickerModal,
|
|
38
39
|
selectRenameSessionModal,
|
|
@@ -132,6 +133,39 @@ function supportsLocalFileOpen(controller) {
|
|
|
132
133
|
return typeof controller?.transport?.openPathInDefaultApp === "function";
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
const SESSION_LINK_COPIED_STATUS = "Session link copied to clipboard";
|
|
137
|
+
const SESSION_LINK_PRIVATE_WARNING = "Only people with access can open this link.";
|
|
138
|
+
|
|
139
|
+
function buildSessionLinkUrl(sessionId) {
|
|
140
|
+
if (!sessionId || typeof window === "undefined" || !window.location) return null;
|
|
141
|
+
return `${window.location.origin}${window.location.pathname}?session=${encodeURIComponent(sessionId)}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function copySessionLinkText(url) {
|
|
145
|
+
if (navigator?.clipboard?.writeText) navigator.clipboard.writeText(url).catch(() => {});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Best-effort probe: is this session's deep link openable only by the owner/
|
|
149
|
+
// admins right now (private, with no targeted grants)? Used to warn in the
|
|
150
|
+
// copy-link dialog. Failure resolves false (no warning).
|
|
151
|
+
async function resolveSessionLinkWarn(controller, sessionId) {
|
|
152
|
+
try {
|
|
153
|
+
const transport = controller.transport;
|
|
154
|
+
if (typeof transport?.getSessionAccess === "function") {
|
|
155
|
+
const access = await transport.getSessionAccess(sessionId);
|
|
156
|
+
if ((access?.visibility || "private") === "private") {
|
|
157
|
+
const shares = typeof transport.listSessionShares === "function"
|
|
158
|
+
? await transport.listSessionShares(sessionId).catch(() => [])
|
|
159
|
+
: [];
|
|
160
|
+
return !Array.isArray(shares) || shares.length === 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// Fall through to no warning.
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
135
169
|
function clearBrowserPreferenceCache() {
|
|
136
170
|
if (typeof window === "undefined") return;
|
|
137
171
|
try {
|
|
@@ -153,7 +187,13 @@ function normalizeProfileSettings(settings) {
|
|
|
153
187
|
normalized.themeId = candidate.themeId.trim();
|
|
154
188
|
}
|
|
155
189
|
if (hasOwn(candidate, "sessionOwnerFilter") && candidate.sessionOwnerFilter && typeof candidate.sessionOwnerFilter === "object") {
|
|
156
|
-
|
|
190
|
+
const storedFilter = candidate.sessionOwnerFilter;
|
|
191
|
+
// Profiles saved before the "Shared with me" bucket existed have no
|
|
192
|
+
// includeShared key; default it on so shared sessions stay visible for
|
|
193
|
+
// existing users. An explicit false (the user turned it off) is kept.
|
|
194
|
+
normalized.sessionOwnerFilter = hasOwn(storedFilter, "includeShared")
|
|
195
|
+
? storedFilter
|
|
196
|
+
: { ...storedFilter, includeShared: true };
|
|
157
197
|
}
|
|
158
198
|
if (hasOwn(candidate, "layoutAdjustments")) {
|
|
159
199
|
normalized.layoutAdjustments = normalizeStoredLayoutAdjustments(candidate.layoutAdjustments);
|
|
@@ -382,8 +422,9 @@ function normalizeLines(lines) {
|
|
|
382
422
|
continue;
|
|
383
423
|
}
|
|
384
424
|
// Sentinel kinds preserved as-is so parseStructuredChatBlocks can
|
|
385
|
-
// recognize and render them (e.g. markdownTable → HTML <table
|
|
386
|
-
|
|
425
|
+
// recognize and render them (e.g. markdownTable → HTML <table>,
|
|
426
|
+
// cardStart/cardEnd → styled card with structured body).
|
|
427
|
+
if (line?.kind === "markdownTable" || line?.kind === "cardStart" || line?.kind === "cardEnd") {
|
|
387
428
|
normalized.push(line);
|
|
388
429
|
continue;
|
|
389
430
|
}
|
|
@@ -1375,6 +1416,30 @@ function parseStructuredChatBlocks(lines = []) {
|
|
|
1375
1416
|
continue;
|
|
1376
1417
|
}
|
|
1377
1418
|
|
|
1419
|
+
// Sentinel card bounds emitted by buildMessageCardLines in sentinel
|
|
1420
|
+
// mode. The body lines between the bounds are UNWRAPPED, so parse
|
|
1421
|
+
// them recursively — box-drawn/markdown tables inside the card
|
|
1422
|
+
// become real HTML tables instead of hard-wrapped box art.
|
|
1423
|
+
if (currentLine?.kind === "cardStart") {
|
|
1424
|
+
const innerLines = [];
|
|
1425
|
+
index += 1;
|
|
1426
|
+
while (index < lines.length && lines[index]?.kind !== "cardEnd") {
|
|
1427
|
+
innerLines.push(lines[index]);
|
|
1428
|
+
index += 1;
|
|
1429
|
+
}
|
|
1430
|
+
if (index < lines.length) index += 1;
|
|
1431
|
+
if (index < lines.length && lineText(lines[index]).trim().length === 0) {
|
|
1432
|
+
index += 1;
|
|
1433
|
+
}
|
|
1434
|
+
blocks.push({
|
|
1435
|
+
type: "card",
|
|
1436
|
+
headerRuns: Array.isArray(currentLine.runs) ? currentLine.runs : [],
|
|
1437
|
+
borderColor: currentLine.borderColor || "gray",
|
|
1438
|
+
blocks: parseStructuredChatBlocks(innerLines),
|
|
1439
|
+
});
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1378
1443
|
// Sentinel markdown-table line emitted by parseMarkdownLines when
|
|
1379
1444
|
// tableMode === "sentinel". Carries the raw header + rows so the
|
|
1380
1445
|
// portal renders a real HTML table with markdown cell content (so
|
|
@@ -1523,9 +1588,15 @@ function parseStructuredChatBlocks(lines = []) {
|
|
|
1523
1588
|
|
|
1524
1589
|
function StructuredChatBlocks({ lines, theme }) {
|
|
1525
1590
|
const blocks = React.useMemo(() => parseStructuredChatBlocks(lines), [lines]);
|
|
1591
|
+
return React.createElement(StructuredBlockList, { blocks, theme });
|
|
1592
|
+
}
|
|
1526
1593
|
|
|
1594
|
+
// Renders parsed chat blocks; sentinel card blocks recurse through this list
|
|
1595
|
+
// so structured content (box/markdown tables, code fences) inside a card
|
|
1596
|
+
// renders exactly the same as it does at top level.
|
|
1597
|
+
function StructuredBlockList({ blocks, theme }) {
|
|
1527
1598
|
return React.createElement(React.Fragment, null,
|
|
1528
|
-
blocks.map((block, index) => {
|
|
1599
|
+
(blocks || []).map((block, index) => {
|
|
1529
1600
|
if (block.type === "preserve") {
|
|
1530
1601
|
const variantClass = block.splashVariant ? ` is-splash-${block.splashVariant}` : "";
|
|
1531
1602
|
return React.createElement("div", { key: `preserve:${index}`, className: `ps-chat-preserve-block${variantClass}` },
|
|
@@ -1553,10 +1624,12 @@ function StructuredChatBlocks({ lines, theme }) {
|
|
|
1553
1624
|
React.createElement("header", { className: "ps-chat-card-header" },
|
|
1554
1625
|
React.createElement(Runs, { runs: block.headerRuns, theme })),
|
|
1555
1626
|
React.createElement("div", { className: "ps-chat-card-body" },
|
|
1556
|
-
(block.
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1627
|
+
Array.isArray(block.blocks)
|
|
1628
|
+
? React.createElement(StructuredBlockList, { blocks: block.blocks, theme })
|
|
1629
|
+
: (block.bodyLines || []).map((bodyRuns, bodyIndex) => React.createElement("div", {
|
|
1630
|
+
key: `card:${index}:line:${bodyIndex}`,
|
|
1631
|
+
className: "ps-chat-card-line",
|
|
1632
|
+
}, React.createElement(Runs, { runs: bodyRuns, theme })) )));
|
|
1560
1633
|
}
|
|
1561
1634
|
|
|
1562
1635
|
if (block.type === "table") {
|
|
@@ -1894,12 +1967,48 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
1894
1967
|
const activeSession = viewState.activeSessionId
|
|
1895
1968
|
? viewState.sessionsById[viewState.activeSessionId] || null
|
|
1896
1969
|
: null;
|
|
1897
|
-
// "
|
|
1898
|
-
// so the composer chrome stays minimal, esp. on
|
|
1899
|
-
// are owner/admin-only (session:manage /
|
|
1900
|
-
// disabled for anyone else — the server
|
|
1901
|
-
// is clearer than a 403.
|
|
1902
|
-
const [
|
|
1970
|
+
// "Manage session" combines rename, model, and sharing in one tabbed modal
|
|
1971
|
+
// (opened from the toolbar so the composer chrome stays minimal, esp. on
|
|
1972
|
+
// mobile). Rename and sharing are owner/admin-only (session:manage /
|
|
1973
|
+
// session:share), so the button is disabled for anyone else — the server
|
|
1974
|
+
// enforces too, but a disabled button is clearer than a 403.
|
|
1975
|
+
const [manageOpen, setManageOpen] = React.useState(false);
|
|
1976
|
+
const [linkModal, setLinkModal] = React.useState(null); // { url, warn } | null
|
|
1977
|
+
// The model picker is a separate (controller-owned) modal that would render
|
|
1978
|
+
// behind the Manage modal, so we close Manage while it is open and reopen
|
|
1979
|
+
// it when the picker closes — cancelling returns the user to Manage.
|
|
1980
|
+
const reopenManageRef = React.useRef(false);
|
|
1981
|
+
// The switch-model flow is multi-step (model → reasoning effort → context
|
|
1982
|
+
// tier); watch every step so Manage doesn't reopen between them. It reopens
|
|
1983
|
+
// only when the whole flow ends, and only if the switch was NOT applied
|
|
1984
|
+
// (cancel returns to Manage; confirm closes everything).
|
|
1985
|
+
const MODEL_FLOW_MODALS = ["modelPicker", "reasoningEffortPicker", "contextTierPicker"];
|
|
1986
|
+
const modelFlowOpen = useControllerSelector(controller, (state) => MODEL_FLOW_MODALS.includes(state.ui.modal?.type));
|
|
1987
|
+
React.useEffect(() => {
|
|
1988
|
+
if (!modelFlowOpen && reopenManageRef.current) {
|
|
1989
|
+
reopenManageRef.current = false;
|
|
1990
|
+
setManageOpen(true);
|
|
1991
|
+
}
|
|
1992
|
+
}, [modelFlowOpen]);
|
|
1993
|
+
const requestSwitchModel = () => {
|
|
1994
|
+
reopenManageRef.current = true;
|
|
1995
|
+
setManageOpen(false);
|
|
1996
|
+
controller.openSwitchModelPicker(() => {
|
|
1997
|
+
// Applied (not cancelled): close everything, don't reopen Manage.
|
|
1998
|
+
reopenManageRef.current = false;
|
|
1999
|
+
}).then(() => {
|
|
2000
|
+
// If the picker never actually opened (e.g. unsupported transport),
|
|
2001
|
+
// don't strand the Manage modal closed with no way back.
|
|
2002
|
+
if (!MODEL_FLOW_MODALS.includes(controller.getState().ui.modal?.type) && reopenManageRef.current) {
|
|
2003
|
+
reopenManageRef.current = false;
|
|
2004
|
+
setManageOpen(true);
|
|
2005
|
+
}
|
|
2006
|
+
}).catch((err) => {
|
|
2007
|
+
reopenManageRef.current = false;
|
|
2008
|
+
setManageOpen(true);
|
|
2009
|
+
controller.dispatch({ type: "ui/status", text: err?.message || String(err) || "Failed to switch model" });
|
|
2010
|
+
});
|
|
2011
|
+
};
|
|
1903
2012
|
const authPrincipal = viewState.auth?.principal || null;
|
|
1904
2013
|
const viewerRole = viewState.auth?.authorization?.role;
|
|
1905
2014
|
const isAdminViewer = viewerRole === "admin" || viewerRole === "anonymous";
|
|
@@ -2009,23 +2118,27 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
2009
2118
|
}),
|
|
2010
2119
|
React.createElement(IconButton, {
|
|
2011
2120
|
className: "ps-mini-button",
|
|
2012
|
-
icon: React.createElement(
|
|
2121
|
+
icon: React.createElement(ManageGlyph),
|
|
2122
|
+
onClick: () => setManageOpen(true),
|
|
2123
|
+
disabled: !canModifyActiveSession || isBulkSelection,
|
|
2124
|
+
label: isBulkSelection ? "Disabled while multiple sessions are selected" : "Manage session — rename, switch model, and sharing",
|
|
2125
|
+
}),
|
|
2126
|
+
React.createElement(IconButton, {
|
|
2127
|
+
className: "ps-mini-button",
|
|
2128
|
+
icon: React.createElement(LinkGlyph),
|
|
2013
2129
|
onClick: () => {
|
|
2014
2130
|
const sid = activeSession?.sessionId;
|
|
2015
|
-
if (!sid
|
|
2016
|
-
const url =
|
|
2017
|
-
if (
|
|
2018
|
-
|
|
2131
|
+
if (!sid) return;
|
|
2132
|
+
const url = buildSessionLinkUrl(sid);
|
|
2133
|
+
if (!url) return;
|
|
2134
|
+
copySessionLinkText(url);
|
|
2135
|
+
setLinkModal({ url, warn: false });
|
|
2136
|
+
resolveSessionLinkWarn(controller, sid).then((warn) => {
|
|
2137
|
+
setLinkModal((cur) => (cur && cur.url === url ? { ...cur, warn } : cur));
|
|
2138
|
+
}).catch(() => {});
|
|
2019
2139
|
},
|
|
2020
2140
|
disabled: !activeSession || activeSession.isGroup || isBulkSelection,
|
|
2021
|
-
label: "
|
|
2022
|
-
}),
|
|
2023
|
-
React.createElement(IconButton, {
|
|
2024
|
-
className: "ps-mini-button",
|
|
2025
|
-
icon: "✎",
|
|
2026
|
-
onClick: () => setModifyOpen(true),
|
|
2027
|
-
disabled: !canModifyActiveSession || isBulkSelection,
|
|
2028
|
-
label: isBulkSelection ? "Disabled while multiple sessions are selected" : "Modify — rename and share access",
|
|
2141
|
+
label: "Copy link — copy a direct link to this session",
|
|
2029
2142
|
}),
|
|
2030
2143
|
React.createElement(IconButton, {
|
|
2031
2144
|
className: "ps-mini-button",
|
|
@@ -2113,42 +2226,96 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
|
|
|
2113
2226
|
},
|
|
2114
2227
|
React.createElement("div", {
|
|
2115
2228
|
className: "ps-line ps-session-row-content",
|
|
2116
|
-
style: { paddingInlineStart: `${Math.max(0, row.depth) *
|
|
2229
|
+
style: { paddingInlineStart: `${Math.max(0, row.depth) * 4}px` },
|
|
2117
2230
|
},
|
|
2118
2231
|
React.createElement(SessionRowContent, { row, theme, structured: structuredRows })),
|
|
2119
2232
|
)),
|
|
2120
2233
|
)),
|
|
2121
|
-
(
|
|
2234
|
+
(manageOpen && activeSession && !activeSession.isGroup)
|
|
2122
2235
|
? React.createElement(SessionModifyModal, {
|
|
2123
2236
|
controller,
|
|
2124
2237
|
sessionId: activeSession.sessionId,
|
|
2125
2238
|
initialTitle: activeSession.title || "",
|
|
2239
|
+
currentModel: activeSession.model || "",
|
|
2240
|
+
currentReasoningEffort: activeSession.reasoningEffort || "",
|
|
2126
2241
|
principal: viewState.auth?.principal || null,
|
|
2127
|
-
onClose: () =>
|
|
2242
|
+
onClose: () => setManageOpen(false),
|
|
2243
|
+
onSwitchModel: requestSwitchModel,
|
|
2128
2244
|
onChanged: () => {},
|
|
2129
2245
|
})
|
|
2246
|
+
: null,
|
|
2247
|
+
linkModal
|
|
2248
|
+
? React.createElement(SessionLinkModal, {
|
|
2249
|
+
url: linkModal.url,
|
|
2250
|
+
warn: linkModal.warn,
|
|
2251
|
+
onCopyAgain: () => {
|
|
2252
|
+
copySessionLinkText(linkModal.url);
|
|
2253
|
+
controller.dispatch({ type: "ui/status", text: SESSION_LINK_COPIED_STATUS });
|
|
2254
|
+
},
|
|
2255
|
+
onClose: () => setLinkModal(null),
|
|
2256
|
+
})
|
|
2130
2257
|
: null);
|
|
2131
2258
|
}
|
|
2132
2259
|
|
|
2260
|
+
// Compact confirmation dialog for the Copy link button: shows the copied URL
|
|
2261
|
+
// (selectable), confirms the copy, and warns when the link points at a private
|
|
2262
|
+
// session no one else can open yet.
|
|
2263
|
+
function SessionLinkModal({ url, warn, onCopyAgain, onClose }) {
|
|
2264
|
+
const inputRef = React.useRef(null);
|
|
2265
|
+
React.useEffect(() => {
|
|
2266
|
+
const node = inputRef.current;
|
|
2267
|
+
if (node) { node.focus(); node.select(); }
|
|
2268
|
+
}, []);
|
|
2269
|
+
const stop = (e) => e.stopPropagation();
|
|
2270
|
+
return React.createElement("div", { className: "ps-share-overlay", onClick: onClose },
|
|
2271
|
+
React.createElement("div", { className: "ps-link-modal", onClick: stop },
|
|
2272
|
+
React.createElement("div", { className: "ps-share-modal-head" },
|
|
2273
|
+
React.createElement("span", null, "Link copied"),
|
|
2274
|
+
React.createElement("button", { className: "ps-modal-close", onClick: onClose }, "✕")),
|
|
2275
|
+
React.createElement("div", { className: "ps-share-section-sub" },
|
|
2276
|
+
"Copied to your clipboard — anyone with access can open the session from this link."),
|
|
2277
|
+
React.createElement("div", { className: "ps-link-row" },
|
|
2278
|
+
React.createElement("input", {
|
|
2279
|
+
ref: inputRef, className: "ps-link-input", readOnly: true, value: url,
|
|
2280
|
+
onFocus: () => inputRef.current?.select(),
|
|
2281
|
+
}),
|
|
2282
|
+
React.createElement("button", { className: "ps-mini-button", onClick: onCopyAgain }, "Copy")),
|
|
2283
|
+
warn
|
|
2284
|
+
? React.createElement("div", { className: "ps-link-warn" }, SESSION_LINK_PRIVATE_WARNING)
|
|
2285
|
+
: null));
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2133
2288
|
const VISIBILITY_META = {
|
|
2134
2289
|
private: { glyph: "🔒", label: "Private" },
|
|
2135
2290
|
shared_read: { glyph: "👁", label: "Shared · read" },
|
|
2136
2291
|
shared_write: { glyph: "✎", label: "Shared · write" },
|
|
2137
2292
|
};
|
|
2138
2293
|
|
|
2139
|
-
// The
|
|
2140
|
-
//
|
|
2141
|
-
function
|
|
2294
|
+
// The "manage" glyph (sliders) for the Manage session button — reads as
|
|
2295
|
+
// settings/controls rather than the narrower share-nodes glyph.
|
|
2296
|
+
function ManageGlyph() {
|
|
2142
2297
|
return React.createElement("svg", {
|
|
2143
2298
|
className: "ps-share-glyph", viewBox: "0 0 24 24", fill: "none",
|
|
2144
2299
|
stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round",
|
|
2145
2300
|
"aria-hidden": "true",
|
|
2146
2301
|
},
|
|
2147
|
-
React.createElement("
|
|
2148
|
-
React.createElement("
|
|
2149
|
-
React.createElement("
|
|
2150
|
-
React.createElement("
|
|
2151
|
-
React.createElement("
|
|
2302
|
+
React.createElement("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
2303
|
+
React.createElement("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
2304
|
+
React.createElement("line", { x1: "4", y1: "18", x2: "20", y2: "18" }),
|
|
2305
|
+
React.createElement("circle", { cx: "9", cy: "6", r: "2", fill: "currentColor" }),
|
|
2306
|
+
React.createElement("circle", { cx: "15", cy: "12", r: "2", fill: "currentColor" }),
|
|
2307
|
+
React.createElement("circle", { cx: "8", cy: "18", r: "2", fill: "currentColor" }));
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// The standard "link" glyph (two chain segments). Used for the Copy link button.
|
|
2311
|
+
function LinkGlyph() {
|
|
2312
|
+
return React.createElement("svg", {
|
|
2313
|
+
className: "ps-share-glyph", viewBox: "0 0 24 24", fill: "none",
|
|
2314
|
+
stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round",
|
|
2315
|
+
"aria-hidden": "true",
|
|
2316
|
+
},
|
|
2317
|
+
React.createElement("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
|
|
2318
|
+
React.createElement("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }));
|
|
2152
2319
|
}
|
|
2153
2320
|
|
|
2154
2321
|
/**
|
|
@@ -2177,13 +2344,16 @@ function useActiveSessionAccess(controller, activeSessionId, isGroup) {
|
|
|
2177
2344
|
return { access: state.access, loading: state.loading, reload };
|
|
2178
2345
|
}
|
|
2179
2346
|
|
|
2180
|
-
// Combined "
|
|
2181
|
-
// (for the owner/admin) sharing — visibility +
|
|
2182
|
-
// own access snapshot so callers only pass the
|
|
2183
|
-
|
|
2347
|
+
// Combined "Share & settings" modal opened from the session list toolbar:
|
|
2348
|
+
// rename, copy link, plus (for the owner/admin) sharing — visibility +
|
|
2349
|
+
// per-person grants. Fetches its own access snapshot so callers only pass the
|
|
2350
|
+
// session id + current title.
|
|
2351
|
+
function SessionModifyModal({ controller, sessionId, initialTitle, currentModel, currentReasoningEffort, principal, onClose, onSwitchModel, onChanged }) {
|
|
2352
|
+
const [tab, setTab] = React.useState("general");
|
|
2184
2353
|
const [access, setAccess] = React.useState(null);
|
|
2185
2354
|
const [title, setTitle] = React.useState(initialTitle || "");
|
|
2186
|
-
const [shares, setShares] = React.useState([]);
|
|
2355
|
+
const [shares, setShares] = React.useState([]); // committed baseline
|
|
2356
|
+
const [draftShares, setDraftShares] = React.useState([]); // staged, applied on Apply
|
|
2187
2357
|
const [visibility, setVisibility] = React.useState("private");
|
|
2188
2358
|
const [granteeQuery, setGranteeQuery] = React.useState("");
|
|
2189
2359
|
const [granteeAccess, setGranteeAccess] = React.useState("write");
|
|
@@ -2207,8 +2377,8 @@ function SessionModifyModal({ controller, sessionId, initialTitle, principal, on
|
|
|
2207
2377
|
|
|
2208
2378
|
const loadShares = React.useCallback(() => {
|
|
2209
2379
|
controller.transport.listSessionShares(sessionId)
|
|
2210
|
-
.then((rows) =>
|
|
2211
|
-
.catch(() => setShares([]));
|
|
2380
|
+
.then((rows) => { const list = Array.isArray(rows) ? rows : []; setShares(list); setDraftShares(list); })
|
|
2381
|
+
.catch(() => { setShares([]); setDraftShares([]); });
|
|
2212
2382
|
}, [controller, sessionId]);
|
|
2213
2383
|
React.useEffect(() => { loadShares(); }, [loadShares]);
|
|
2214
2384
|
|
|
@@ -2222,10 +2392,6 @@ function SessionModifyModal({ controller, sessionId, initialTitle, principal, on
|
|
|
2222
2392
|
const saveTitle = () => run(async () => {
|
|
2223
2393
|
await controller.transport.renameSession(sessionId, title.trim());
|
|
2224
2394
|
});
|
|
2225
|
-
const applyVisibility = (value) => run(async () => {
|
|
2226
|
-
await controller.transport.setSessionVisibility(sessionId, value);
|
|
2227
|
-
setVisibility(value);
|
|
2228
|
-
});
|
|
2229
2395
|
// Resolve the typed text to a directory member (by name, email, or id).
|
|
2230
2396
|
// Falls back to treating the text as a raw subject for a not-yet-seen user.
|
|
2231
2397
|
const resolveGrantee = (text) => {
|
|
@@ -2238,67 +2404,136 @@ function SessionModifyModal({ controller, sessionId, initialTitle, principal, on
|
|
|
2238
2404
|
if (match) return match;
|
|
2239
2405
|
return { provider: principal?.provider || "dev", subject: text.trim(), email: null, displayName: null };
|
|
2240
2406
|
};
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
)
|
|
2407
|
+
const shareKey = (r) => `${r.provider}${r.subject}`;
|
|
2408
|
+
// Access edits are staged into draftShares/visibility and committed only
|
|
2409
|
+
// on Apply, so the button is a deliberate, satisfying confirmation.
|
|
2410
|
+
const stageGrant = (grantee) => {
|
|
2411
|
+
const key = shareKey(grantee);
|
|
2412
|
+
setDraftShares((cur) => [
|
|
2413
|
+
...cur.filter((r) => shareKey(r) !== key),
|
|
2414
|
+
{ provider: grantee.provider, subject: grantee.subject, email: grantee.email ?? null, displayName: grantee.displayName ?? null, access: granteeAccess },
|
|
2415
|
+
]);
|
|
2247
2416
|
setGranteeQuery("");
|
|
2248
|
-
|
|
2249
|
-
});
|
|
2417
|
+
};
|
|
2250
2418
|
const addGrant = () => {
|
|
2251
2419
|
const grantee = resolveGrantee(granteeQuery);
|
|
2252
|
-
if (grantee)
|
|
2420
|
+
if (grantee) stageGrant(grantee);
|
|
2421
|
+
};
|
|
2422
|
+
const stageRevoke = (row) => {
|
|
2423
|
+
const key = shareKey(row);
|
|
2424
|
+
setDraftShares((cur) => cur.filter((r) => shareKey(r) !== key));
|
|
2253
2425
|
};
|
|
2254
2426
|
|
|
2255
2427
|
// Autocomplete suggestions: directory members matching the query, minus
|
|
2256
2428
|
// the owner and anyone already granted.
|
|
2257
|
-
const grantedKeys = new Set(
|
|
2429
|
+
const grantedKeys = new Set(draftShares.map(shareKey));
|
|
2258
2430
|
const ownerKey = access?.owner ? `${access.owner.provider}${access.owner.subject}` : null;
|
|
2259
2431
|
const q = granteeQuery.trim().toLowerCase();
|
|
2260
2432
|
const suggestions = q
|
|
2261
2433
|
? directory.filter((u) => {
|
|
2262
|
-
const key =
|
|
2434
|
+
const key = shareKey(u);
|
|
2263
2435
|
if (key === ownerKey || grantedKeys.has(key)) return false;
|
|
2264
2436
|
return (u.displayName && u.displayName.toLowerCase().includes(q))
|
|
2265
2437
|
|| (u.email && u.email.toLowerCase().includes(q))
|
|
2266
2438
|
|| (u.subject && u.subject.toLowerCase().includes(q));
|
|
2267
2439
|
}).slice(0, 25)
|
|
2268
2440
|
: [];
|
|
2269
|
-
const revoke = (row) => run(async () => {
|
|
2270
|
-
await controller.transport.revokeSessionShare(sessionId, { provider: row.provider, subject: row.subject });
|
|
2271
|
-
loadShares();
|
|
2272
|
-
});
|
|
2273
|
-
|
|
2274
2441
|
const canManage = Boolean(access?.canManage);
|
|
2442
|
+
// Dirty = staged access differs from the committed baseline (the `:level`
|
|
2443
|
+
// suffix catches a grant whose access level changed).
|
|
2444
|
+
const baselineVisibility = access?.visibility || "private";
|
|
2445
|
+
const draftSig = new Set(draftShares.map((r) => `${shareKey(r)}:${r.access}`));
|
|
2446
|
+
const baseSig = new Set(shares.map((r) => `${shareKey(r)}:${r.access}`));
|
|
2447
|
+
const sharesChanged = draftSig.size !== baseSig.size || [...draftSig].some((k) => !baseSig.has(k));
|
|
2448
|
+
const accessDirty = canManage && (visibility !== baselineVisibility || sharesChanged);
|
|
2449
|
+
const applyAccess = () => run(async () => {
|
|
2450
|
+
if (visibility !== baselineVisibility) {
|
|
2451
|
+
await controller.transport.setSessionVisibility(sessionId, visibility);
|
|
2452
|
+
}
|
|
2453
|
+
const baseByKey = new Map(shares.map((r) => [shareKey(r), r]));
|
|
2454
|
+
const draftByKey = new Map(draftShares.map((r) => [shareKey(r), r]));
|
|
2455
|
+
for (const [key, r] of draftByKey) {
|
|
2456
|
+
const b = baseByKey.get(key);
|
|
2457
|
+
if (!b || b.access !== r.access) {
|
|
2458
|
+
await controller.transport.grantSessionShare(
|
|
2459
|
+
sessionId,
|
|
2460
|
+
{ provider: r.provider, subject: r.subject, email: r.email ?? null, displayName: r.displayName ?? null },
|
|
2461
|
+
r.access,
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
for (const [key, r] of baseByKey) {
|
|
2466
|
+
if (!draftByKey.has(key)) {
|
|
2467
|
+
await controller.transport.revokeSessionShare(sessionId, { provider: r.provider, subject: r.subject });
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
const rows = await controller.transport.listSessionShares(sessionId).catch(() => null);
|
|
2471
|
+
if (Array.isArray(rows)) { setShares(rows); setDraftShares(rows); }
|
|
2472
|
+
const a = await controller.transport.getSessionAccess(sessionId).catch(() => null);
|
|
2473
|
+
if (a) { setAccess(a); setVisibility(a.visibility || "private"); }
|
|
2474
|
+
controller.dispatch({ type: "ui/status", text: "Access updated." });
|
|
2475
|
+
});
|
|
2476
|
+
const switchModel = onSwitchModel || (() => {
|
|
2477
|
+
onClose();
|
|
2478
|
+
controller.openSwitchModelPicker().catch((err) => {
|
|
2479
|
+
controller.dispatch({ type: "ui/status", text: err?.message || String(err) || "Failed to switch model" });
|
|
2480
|
+
});
|
|
2481
|
+
});
|
|
2482
|
+
const modelLabel = currentModel
|
|
2483
|
+
? (currentReasoningEffort ? `${currentModel}:${currentReasoningEffort}` : currentModel)
|
|
2484
|
+
: "—";
|
|
2485
|
+
// Access is owner/admin-only; hide the tab entirely for viewers who can
|
|
2486
|
+
// only rename/switch model on their own session.
|
|
2487
|
+
const tabs = [
|
|
2488
|
+
{ id: "general", label: "General" },
|
|
2489
|
+
...(canManage ? [{ id: "access", label: "Access" }] : []),
|
|
2490
|
+
];
|
|
2491
|
+
const activeTab = tabs.some((t) => t.id === tab) ? tab : "general";
|
|
2275
2492
|
const stop = (e) => e.stopPropagation();
|
|
2276
2493
|
return React.createElement("div", { className: "ps-share-overlay", onClick: onClose },
|
|
2277
2494
|
React.createElement("div", { className: "ps-share-modal", onClick: stop },
|
|
2278
2495
|
React.createElement("div", { className: "ps-share-modal-head" },
|
|
2279
|
-
React.createElement("span", null, "
|
|
2496
|
+
React.createElement("span", null, "Manage session"),
|
|
2280
2497
|
React.createElement("button", { className: "ps-modal-close", onClick: onClose }, "✕")),
|
|
2281
2498
|
|
|
2282
|
-
// ──
|
|
2283
|
-
React.createElement("div", { className: "ps-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2499
|
+
// ── Tab bar ───────────────────────────────────────────────
|
|
2500
|
+
React.createElement("div", { className: "ps-manage-tabs", role: "tablist" },
|
|
2501
|
+
tabs.map((t) => React.createElement("button", {
|
|
2502
|
+
key: t.id,
|
|
2503
|
+
type: "button",
|
|
2504
|
+
role: "tab",
|
|
2505
|
+
"aria-selected": activeTab === t.id ? "true" : "false",
|
|
2506
|
+
className: `ps-manage-tab${activeTab === t.id ? " is-active" : ""}`,
|
|
2507
|
+
onClick: () => setTab(t.id),
|
|
2508
|
+
}, t.label))),
|
|
2509
|
+
|
|
2510
|
+
// ── General: rename + model ───────────────────────────────
|
|
2511
|
+
activeTab === "general" ? React.createElement(React.Fragment, null,
|
|
2512
|
+
React.createElement("div", { className: "ps-share-section-label" }, "Name"),
|
|
2513
|
+
React.createElement("div", { className: "ps-share-add-row" },
|
|
2514
|
+
React.createElement("input", {
|
|
2515
|
+
className: "ps-share-add-input", placeholder: "Session title",
|
|
2516
|
+
value: title, disabled: busy,
|
|
2517
|
+
onChange: (e) => setTitle(e.target.value),
|
|
2518
|
+
onKeyDown: (e) => { if (e.key === "Enter") saveTitle(); },
|
|
2519
|
+
}),
|
|
2520
|
+
React.createElement("button", { className: "ps-mini-button", disabled: busy || !title.trim(), onClick: saveTitle }, "Save")),
|
|
2521
|
+
React.createElement("div", { className: "ps-share-section-label" }, "Model"),
|
|
2522
|
+
React.createElement("div", { className: "ps-share-section-sub" }, "The model this session uses on its next turn."),
|
|
2523
|
+
React.createElement("div", { className: "ps-share-add-row" },
|
|
2524
|
+
React.createElement("span", { className: "ps-manage-model-current" }, modelLabel),
|
|
2525
|
+
React.createElement("button", { className: "ps-mini-button", disabled: busy, onClick: switchModel }, "Switch model…")))
|
|
2526
|
+
: null,
|
|
2292
2527
|
|
|
2293
|
-
// ──
|
|
2294
|
-
canManage ? React.createElement(React.Fragment, null,
|
|
2528
|
+
// ── Access (owner / admin only) ───────────────────────────
|
|
2529
|
+
(activeTab === "access" && canManage) ? React.createElement(React.Fragment, null,
|
|
2295
2530
|
React.createElement("div", { className: "ps-share-section-label" }, "General access"),
|
|
2296
2531
|
React.createElement("div", { className: "ps-share-section-sub" }, "The baseline level for everyone signed in to this workspace."),
|
|
2297
2532
|
["private", "shared_read", "shared_write"].map((value) =>
|
|
2298
2533
|
React.createElement("label", { key: value, className: `ps-share-radio${visibility === value ? " is-active" : ""}` },
|
|
2299
2534
|
React.createElement("input", {
|
|
2300
2535
|
type: "radio", name: "visibility", checked: visibility === value,
|
|
2301
|
-
disabled: busy, onChange: () =>
|
|
2536
|
+
disabled: busy, onChange: () => setVisibility(value),
|
|
2302
2537
|
}),
|
|
2303
2538
|
React.createElement("span", { className: "ps-share-radio-glyph" }, VISIBILITY_META[value].glyph),
|
|
2304
2539
|
React.createElement("span", null, VISIBILITY_META[value].label),
|
|
@@ -2308,12 +2543,12 @@ function SessionModifyModal({ controller, sessionId, initialTitle, principal, on
|
|
|
2308
2543
|
: "everyone here can view and send"))),
|
|
2309
2544
|
React.createElement("div", { className: "ps-share-section-label" }, "Special access"),
|
|
2310
2545
|
React.createElement("div", { className: "ps-share-section-sub" }, "Give specific people more than the general level. A person's grant wins over general access."),
|
|
2311
|
-
|
|
2546
|
+
draftShares.length === 0
|
|
2312
2547
|
? React.createElement("div", { className: "ps-share-empty" }, "No individual grants — everyone has the general access above.")
|
|
2313
|
-
:
|
|
2548
|
+
: draftShares.map((row) => React.createElement("div", { key: `${row.provider}/${row.subject}`, className: "ps-share-grant-row" },
|
|
2314
2549
|
React.createElement("span", { className: "ps-share-grant-name" }, row.displayName || row.subject),
|
|
2315
2550
|
React.createElement("span", { className: "ps-share-grant-access" }, `can ${row.access}`),
|
|
2316
|
-
React.createElement("button", { className: "ps-mini-button", disabled: busy, onClick: () =>
|
|
2551
|
+
React.createElement("button", { className: "ps-mini-button", disabled: busy, onClick: () => stageRevoke(row) }, "Remove"))),
|
|
2317
2552
|
React.createElement("div", { className: "ps-share-add-wrap" },
|
|
2318
2553
|
React.createElement("div", { className: "ps-share-add-row" },
|
|
2319
2554
|
React.createElement("input", {
|
|
@@ -2334,7 +2569,7 @@ function SessionModifyModal({ controller, sessionId, initialTitle, principal, on
|
|
|
2334
2569
|
suggestions.map((u) => React.createElement("button", {
|
|
2335
2570
|
key: `${u.provider}/${u.subject}`,
|
|
2336
2571
|
type: "button", className: "ps-share-suggestion", disabled: busy,
|
|
2337
|
-
onClick: () =>
|
|
2572
|
+
onClick: () => stageGrant(u),
|
|
2338
2573
|
},
|
|
2339
2574
|
React.createElement("span", { className: "ps-share-suggestion-name" }, u.displayName || u.subject),
|
|
2340
2575
|
u.email ? React.createElement("span", { className: "ps-share-suggestion-email" }, u.email) : null)))
|
|
@@ -2342,7 +2577,15 @@ function SessionModifyModal({ controller, sessionId, initialTitle, principal, on
|
|
|
2342
2577
|
React.createElement("div", { className: "ps-share-foot-hint" },
|
|
2343
2578
|
"Sharing applies to this session and its sub-agents. Suggestions are people who have "
|
|
2344
2579
|
+ "signed in before — you can also grant by email to someone who hasn't; it takes effect "
|
|
2345
|
-
+ "when they first sign in.")
|
|
2580
|
+
+ "when they first sign in."),
|
|
2581
|
+
React.createElement("div", { className: "ps-manage-apply-bar" },
|
|
2582
|
+
React.createElement("span", { className: "ps-share-section-sub" },
|
|
2583
|
+
accessDirty ? "Unsaved access changes." : "No changes to apply."),
|
|
2584
|
+
React.createElement("button", {
|
|
2585
|
+
className: "ps-mini-button ps-manage-apply",
|
|
2586
|
+
disabled: busy || !accessDirty,
|
|
2587
|
+
onClick: applyAccess,
|
|
2588
|
+
}, "Apply")))
|
|
2346
2589
|
: null,
|
|
2347
2590
|
error ? React.createElement("div", { className: "ps-share-error" }, error) : null));
|
|
2348
2591
|
}
|
|
@@ -2451,6 +2694,7 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
2451
2694
|
const { access } = useActiveSessionAccess(
|
|
2452
2695
|
controller, viewState.activeSessionId, viewState.activeSessionIsGroup,
|
|
2453
2696
|
);
|
|
2697
|
+
const navigationError = useControllerSelector(controller, selectNavigationError, shallowEqualObject);
|
|
2454
2698
|
const composerBase = showComposer && !viewState.activeSessionIsGroup && viewState.chatViewMode !== "summary";
|
|
2455
2699
|
const readOnly = Boolean(access) && access.canWrite === false;
|
|
2456
2700
|
const composer = composerBase
|
|
@@ -2461,6 +2705,31 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
2461
2705
|
: React.createElement(PromptComposer, { controller, mobile, active: true }))
|
|
2462
2706
|
: null;
|
|
2463
2707
|
|
|
2708
|
+
// A failed deep-link intent with nothing else loaded replaces the pane
|
|
2709
|
+
// body with the nav-error empty state (the reducer refuses fallback
|
|
2710
|
+
// selection while the intent exists, so there is no transcript to show).
|
|
2711
|
+
const hasLoadableActiveSession = Boolean(
|
|
2712
|
+
viewState.activeSessionId && viewState.sessionsById[viewState.activeSessionId],
|
|
2713
|
+
);
|
|
2714
|
+
if (navigationError && !hasLoadableActiveSession) {
|
|
2715
|
+
return React.createElement(Panel, {
|
|
2716
|
+
title: chrome.title,
|
|
2717
|
+
color: chrome.color,
|
|
2718
|
+
focused: viewState.focused,
|
|
2719
|
+
theme,
|
|
2720
|
+
className: "ps-chat-panel",
|
|
2721
|
+
},
|
|
2722
|
+
React.createElement("div", { className: "ps-empty-state ps-nav-error-state" },
|
|
2723
|
+
React.createElement("div", { className: "ps-nav-error-message" }, navigationError.message),
|
|
2724
|
+
navigationError.retryable
|
|
2725
|
+
? React.createElement("button", {
|
|
2726
|
+
type: "button",
|
|
2727
|
+
className: "ps-mini-button",
|
|
2728
|
+
onClick: () => controller.setNavigationIntent(navigationError.sessionId),
|
|
2729
|
+
}, "Retry")
|
|
2730
|
+
: null));
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2464
2733
|
return React.createElement(ScrollLinesPanel, {
|
|
2465
2734
|
controller,
|
|
2466
2735
|
title: mobile ? compactTitleRuns(chrome.title, 28) : chrome.title,
|
|
@@ -2482,31 +2751,14 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
|
|
|
2482
2751
|
});
|
|
2483
2752
|
}
|
|
2484
2753
|
|
|
2485
|
-
function MobileWorkspace({ controller
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
const sessionToggle = React.createElement("button", {
|
|
2489
|
-
type: "button",
|
|
2490
|
-
className: "ps-mini-button",
|
|
2491
|
-
onClick: () => setSessionsCollapsed((current) => !current),
|
|
2492
|
-
}, sessionsCollapsed ? "Show" : "Hide");
|
|
2493
|
-
|
|
2754
|
+
function MobileWorkspace({ controller }) {
|
|
2755
|
+
// The session list is always shown; use the toolbar Focus button to give
|
|
2756
|
+
// the chat the full screen (the old Show/Hide toggle was redundant with it).
|
|
2494
2757
|
return React.createElement("div", { className: "ps-mobile-workspace" },
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
focused: false,
|
|
2500
|
-
theme,
|
|
2501
|
-
actions: sessionToggle,
|
|
2502
|
-
className: "ps-mobile-session-collapsed",
|
|
2503
|
-
},
|
|
2504
|
-
React.createElement("div", { className: "ps-mobile-session-summary" }, "Session list collapsed."))
|
|
2505
|
-
: React.createElement(SessionPane, {
|
|
2506
|
-
controller,
|
|
2507
|
-
actions: sessionToggle,
|
|
2508
|
-
panelClassName: "ps-mobile-session-pane",
|
|
2509
|
-
}),
|
|
2758
|
+
React.createElement(SessionPane, {
|
|
2759
|
+
controller,
|
|
2760
|
+
panelClassName: "ps-mobile-session-pane",
|
|
2761
|
+
}),
|
|
2510
2762
|
React.createElement("div", { className: "ps-mobile-chat-pane" },
|
|
2511
2763
|
React.createElement(ChatPane, { controller, mobile: true, fullWidth: true })));
|
|
2512
2764
|
}
|
|
@@ -3373,9 +3625,7 @@ function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus
|
|
|
3373
3625
|
const chatView = useControllerSelector(controller, (state) => ({
|
|
3374
3626
|
mode: state.ui.chatViewMode || "transcript",
|
|
3375
3627
|
activeSessionIsGroup: Boolean(state.sessions.activeSessionId && state.sessions.byId[state.sessions.activeSessionId]?.isGroup),
|
|
3376
|
-
hasActiveSession: Boolean(state.sessions.activeSessionId && state.sessions.byId[state.sessions.activeSessionId]),
|
|
3377
3628
|
}), shallowEqualObject);
|
|
3378
|
-
const switchModelDisabled = !chatView.hasActiveSession || chatView.activeSessionIsGroup;
|
|
3379
3629
|
|
|
3380
3630
|
// Icon-first toolbar: the glyph is the affordance, the label rides a
|
|
3381
3631
|
// tooltip (desktop hover via title; mobile long-press via IconButton).
|
|
@@ -3392,15 +3642,6 @@ function Toolbar({ controller, mobile, chatFocusMode = false, onToggleChatFocus
|
|
|
3392
3642
|
label: "New session + choose model",
|
|
3393
3643
|
onClick: () => controller.handleCommand(UI_COMMANDS.OPEN_MODEL_PICKER).catch(() => {}),
|
|
3394
3644
|
},
|
|
3395
|
-
{
|
|
3396
|
-
key: "switch-model",
|
|
3397
|
-
icon: "⇄",
|
|
3398
|
-
label: switchModelDisabled ? "Select a session to switch its model" : "Switch the selected session's model",
|
|
3399
|
-
onClick: () => controller.openSwitchModelPicker().catch((err) => {
|
|
3400
|
-
controller.dispatch({ type: "ui/status", text: err?.message || String(err) || "Failed to switch model" });
|
|
3401
|
-
}),
|
|
3402
|
-
disabled: switchModelDisabled,
|
|
3403
|
-
},
|
|
3404
3645
|
{
|
|
3405
3646
|
key: "filter",
|
|
3406
3647
|
icon: "▾",
|
|
@@ -3911,7 +4152,7 @@ function ModalLayer({ controller }) {
|
|
|
3911
4152
|
return React.createElement("button", {
|
|
3912
4153
|
key: item?.id || `row:${rowIndex}`,
|
|
3913
4154
|
type: "button",
|
|
3914
|
-
className: `ps-list-button ps-modal-list-button${itemIndex === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}`,
|
|
4155
|
+
className: `ps-list-button ps-modal-list-button${itemIndex === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}${item?.disabled ? " is-disabled" : ""}`,
|
|
3915
4156
|
onClick: () => controller.dispatch({ type: "ui/modalSelection", index: itemIndex }),
|
|
3916
4157
|
},
|
|
3917
4158
|
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
|
@@ -3920,7 +4161,7 @@ function ModalLayer({ controller }) {
|
|
|
3920
4161
|
: (modal.items || []).map((item, index) => React.createElement("button", {
|
|
3921
4162
|
key: item.id || index,
|
|
3922
4163
|
type: "button",
|
|
3923
|
-
className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}`,
|
|
4164
|
+
className: `ps-list-button ps-modal-list-button${index === modal.selectedIndex ? " is-selected" : ""}${usesHangingIndent ? " is-hanging" : ""}${item?.disabled ? " is-disabled" : ""}`,
|
|
3924
4165
|
onClick: () => controller.dispatch({ type: "ui/modalSelection", index }),
|
|
3925
4166
|
},
|
|
3926
4167
|
React.createElement("div", { className: "ps-line ps-modal-list-line" },
|
|
@@ -4735,7 +4976,6 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
4735
4976
|
const appliedProfileSettingsJsonRef = React.useRef(null);
|
|
4736
4977
|
const defaultProfileSettingsRef = React.useRef(null);
|
|
4737
4978
|
const [mobilePane, setMobilePane] = React.useState("workspace");
|
|
4738
|
-
const [mobileSessionsCollapsed, setMobileSessionsCollapsed] = React.useState(false);
|
|
4739
4979
|
const mobile = (viewport.width || window.innerWidth || 0) < MOBILE_BREAKPOINT;
|
|
4740
4980
|
const readOnlyChatPane = state.activeSessionIsGroup || state.chatViewMode === "summary";
|
|
4741
4981
|
const effectivePromptRows = readOnlyChatPane ? 0 : state.promptRows;
|
|
@@ -4985,11 +5225,7 @@ export function PilotSwarmWebApp({ controller }) {
|
|
|
4985
5225
|
React.createElement(InspectorPane, { controller, mobile: true }));
|
|
4986
5226
|
else if (mobilePane === "activity") mobileContent = React.createElement("div", { className: "ps-mobile-pane-fill" },
|
|
4987
5227
|
React.createElement(ActivityPane, { controller }));
|
|
4988
|
-
else mobileContent = React.createElement(MobileWorkspace, {
|
|
4989
|
-
controller,
|
|
4990
|
-
sessionsCollapsed: mobileSessionsCollapsed,
|
|
4991
|
-
setSessionsCollapsed: setMobileSessionsCollapsed,
|
|
4992
|
-
});
|
|
5228
|
+
else mobileContent = React.createElement(MobileWorkspace, { controller });
|
|
4993
5229
|
|
|
4994
5230
|
return React.createElement("div", { ref: viewportRef, className: "ps-web-shell" },
|
|
4995
5231
|
// Hide the top toolbar on mobile inspector/activity panes (and in
|