pilotswarm 0.5.13 → 0.5.14
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 +6 -0
- package/mcp/README.md +12 -0
- package/mcp/dist/src/context.d.ts +13 -0
- package/mcp/dist/src/context.d.ts.map +1 -1
- package/mcp/dist/src/context.js +20 -0
- package/mcp/dist/src/context.js.map +1 -1
- package/mcp/dist/src/tools/capabilities.d.ts +3 -0
- package/mcp/dist/src/tools/capabilities.d.ts.map +1 -1
- package/mcp/dist/src/tools/capabilities.js +8 -0
- package/mcp/dist/src/tools/capabilities.js.map +1 -1
- package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
- package/mcp/dist/src/tools/sessions.js +114 -0
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/package.json +3 -2
- package/tui/src/app.js +19 -2
- package/tui/src/auth/cli.js +13 -0
- package/tui/src/node-sdk-transport.js +53 -8
- package/tui/tui-splash-mobile.txt +5 -7
- package/tui/tui-splash.txt +13 -9
- package/ui/core/src/commands.js +2 -0
- package/ui/core/src/controller.js +275 -6
- package/ui/core/src/history.js +19 -1
- package/ui/core/src/reducer.js +4 -0
- package/ui/core/src/selectors.js +139 -14
- package/ui/core/src/themes/helpers.js +4 -0
- package/ui/react/src/components.js +95 -6
- package/ui/react/src/web-app.js +555 -117
- package/web/api/router.js +7 -6
- package/web/api/ws.js +9 -0
- package/web/auth/index.js +5 -0
- package/web/auth/providers/dev.js +119 -0
- package/web/authz.js +142 -0
- package/web/dist/assets/index-BnxC8cNG.js +24 -0
- package/web/dist/assets/{index-oldX95Tp.css → index-Bx6KHIaj.css} +1 -1
- package/web/dist/assets/pilotswarm-NE7H63ha.js +90 -0
- package/web/dist/assets/react-l0sNRNKZ.js +1 -0
- package/web/dist/index.html +3 -4
- package/web/runtime.js +453 -9
- package/web/server.js +2 -2
- package/web/dist/assets/index-bQ2QInMX.js +0 -24
- package/web/dist/assets/pilotswarm-DRs6o-lA.js +0 -90
- package/web/dist/assets/react-C9iQPS2h.js +0 -1
package/ui/core/src/selectors.js
CHANGED
|
@@ -32,6 +32,8 @@ export const ACTIVE_HIGHLIGHT_BACKGROUND = "activeHighlightBackground";
|
|
|
32
32
|
export const ACTIVE_HIGHLIGHT_FOREGROUND = "activeHighlightForeground";
|
|
33
33
|
const USER_CHAT_COLOR = "userChat";
|
|
34
34
|
const USER_CHAT_LABEL_COLOR = "userChatLabel";
|
|
35
|
+
// Speaker label for a message from another person in a shared session.
|
|
36
|
+
const OTHER_PERSON_CHAT_LABEL_COLOR = "otherUserChatLabel";
|
|
35
37
|
|
|
36
38
|
const totalDescendantCountsCache = new WeakMap();
|
|
37
39
|
const visibleDescendantCountsCache = new WeakMap();
|
|
@@ -613,9 +615,13 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
613
615
|
const titleRuns = [...prefixRuns];
|
|
614
616
|
// Owner chip — only when the list actually surfaces more than one human
|
|
615
617
|
// owner (shouldDecorateSessionOwners). Otherwise it's noise on every row.
|
|
618
|
+
// Bracketed + bold so it reads as an owner badge, not part of the title.
|
|
619
|
+
// The current viewer's own sessions get a distinct color so "mine" pops.
|
|
616
620
|
if (shouldDecorateSessionOwners(state) && !session?.isSystem && !session?.isGroup) {
|
|
617
621
|
const initials = effectiveOwner ? ownerInitials(effectiveOwner) : "?";
|
|
618
|
-
|
|
622
|
+
const viewerKey = ownerKeyForOwner(state?.auth?.principal);
|
|
623
|
+
const isMine = viewerKey && ownerKeyForOwner(effectiveOwner) === viewerKey;
|
|
624
|
+
titleRuns.push({ text: `[${initials}] `, color: isMine ? "green" : "cyan", bold: true });
|
|
619
625
|
}
|
|
620
626
|
if (hasRealTitle) {
|
|
621
627
|
titleRuns.push({ text: rawTitle, color: mainColor, bold: Boolean(session?.isSystem || session?.isGroup) });
|
|
@@ -682,6 +688,15 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
|
|
|
682
688
|
const childCount = totalDescendantCounts?.[session?.sessionId];
|
|
683
689
|
if (childCount) { pushSep(); detailRuns.push({ text: `${childCount} child${childCount === 1 ? "" : "ren"}`, color: "gray" }); }
|
|
684
690
|
if (cronBadge) { pushSep(); detailRuns.push({ text: cronBadge.text, color: cronBadge.color }); }
|
|
691
|
+
// Shared-session marker (security model): private needs no marker;
|
|
692
|
+
// shared_read/shared_write surface here in the selected-row details.
|
|
693
|
+
if (session?.visibility === "shared_read" || session?.visibility === "shared_write") {
|
|
694
|
+
pushSep();
|
|
695
|
+
detailRuns.push({
|
|
696
|
+
text: session.visibility === "shared_write" ? "shared·write" : "shared·read",
|
|
697
|
+
color: "magenta",
|
|
698
|
+
});
|
|
699
|
+
}
|
|
685
700
|
}
|
|
686
701
|
|
|
687
702
|
// Flat runs for the TUI: title + (for titled rows) dim age + context.
|
|
@@ -868,9 +883,11 @@ function buildPendingOutboxMessage(sessionId, item) {
|
|
|
868
883
|
createdAt: Number(item.createdAt) || Date.now(),
|
|
869
884
|
pendingPhase: item.phase === "cancelling"
|
|
870
885
|
? "cancelling"
|
|
871
|
-
: item.phase === "
|
|
872
|
-
? "
|
|
873
|
-
: "
|
|
886
|
+
: item.phase === "rejected"
|
|
887
|
+
? "rejected"
|
|
888
|
+
: item.phase === "queued"
|
|
889
|
+
? "queued"
|
|
890
|
+
: "pending",
|
|
874
891
|
};
|
|
875
892
|
}
|
|
876
893
|
|
|
@@ -1281,20 +1298,44 @@ function prefixRuns(text, color = "gray", options = {}) {
|
|
|
1281
1298
|
}];
|
|
1282
1299
|
}
|
|
1283
1300
|
|
|
1284
|
-
function buildChatMessagePrefix(message) {
|
|
1301
|
+
function buildChatMessagePrefix(message, options = {}) {
|
|
1285
1302
|
const time = formatTimestamp(message?.createdAt || message?.time);
|
|
1286
|
-
|
|
1287
|
-
|
|
1303
|
+
// A user.message is labeled from the CURRENT VIEWER's perspective: "You"
|
|
1304
|
+
// for the viewer's own messages, the sender's name for anyone else. When
|
|
1305
|
+
// the sender is the session owner, an "(owner)" tag is appended — so the
|
|
1306
|
+
// viewing owner sees "You (owner):" and everyone else sees "Alice (owner):".
|
|
1307
|
+
// A distinct color marks messages from other people.
|
|
1308
|
+
const sender = message?.sender;
|
|
1309
|
+
const senderKey = sender?.kind === "user" ? ownerKeyForOwner(sender) : null;
|
|
1310
|
+
const viewerKey = options?.viewerKey || null;
|
|
1311
|
+
const ownerKey = options?.ownerKey || null;
|
|
1312
|
+
// Only distinguish speakers / show the owner tag in a SHARED session — a
|
|
1313
|
+
// private solo session stays plain "You" / "Agent" (no noise).
|
|
1314
|
+
const sharedContext = options?.sharedContext === true;
|
|
1315
|
+
const isUser = message?.role === "user";
|
|
1316
|
+
const isSelf = isUser && senderKey && viewerKey && senderKey === viewerKey;
|
|
1317
|
+
const fromOtherPerson = sharedContext && isUser && senderKey && !isSelf;
|
|
1318
|
+
// Owner tag: the sender is the session owner (or, for a sender-less own
|
|
1319
|
+
// message, the viewer themselves is the owner).
|
|
1320
|
+
const senderIsOwner = sharedContext && isUser && ownerKey && (
|
|
1321
|
+
(senderKey && senderKey === ownerKey)
|
|
1322
|
+
|| (!senderKey && viewerKey && viewerKey === ownerKey)
|
|
1323
|
+
);
|
|
1324
|
+
const ownerSuffix = senderIsOwner ? " (owner)" : "";
|
|
1325
|
+
|
|
1326
|
+
const roleLabel = isUser
|
|
1327
|
+
? ((fromOtherPerson ? (sender.display || sender.subject || "User") : "You") + ownerSuffix)
|
|
1288
1328
|
: message?.role === "assistant"
|
|
1289
1329
|
? "Agent"
|
|
1290
1330
|
: message?.role === "system"
|
|
1291
1331
|
? "System"
|
|
1292
1332
|
: "PilotSwarm";
|
|
1293
1333
|
|
|
1294
|
-
//
|
|
1334
|
+
// Delivery glyph for user messages:
|
|
1295
1335
|
// ○ pending — client outbox, not yet durable
|
|
1296
1336
|
// ✓ queued — durably enqueued, waiting for orchestration to drain
|
|
1297
1337
|
// x cancelling — durable cancel requested, waiting for runtime outcome
|
|
1338
|
+
// x rejected — server refused the send (authz); auto-dropped shortly
|
|
1298
1339
|
// ✓✓ sent — persisted as user.message in CMS, LLM has it
|
|
1299
1340
|
let glyph = null;
|
|
1300
1341
|
let glyphColor = null;
|
|
@@ -1304,7 +1345,7 @@ function buildChatMessagePrefix(message) {
|
|
|
1304
1345
|
} else if (message?.pendingPhase === "queued") {
|
|
1305
1346
|
glyph = "✓";
|
|
1306
1347
|
glyphColor = "cyan";
|
|
1307
|
-
} else if (message?.pendingPhase === "cancelling") {
|
|
1348
|
+
} else if (message?.pendingPhase === "cancelling" || message?.pendingPhase === "rejected") {
|
|
1308
1349
|
glyph = "x";
|
|
1309
1350
|
glyphColor = "red";
|
|
1310
1351
|
} else if (message?.role === "user" && !message?.optimistic && !message?.pendingPhase) {
|
|
@@ -1324,8 +1365,10 @@ function buildChatMessagePrefix(message) {
|
|
|
1324
1365
|
? "yellow"
|
|
1325
1366
|
: message?.pendingPhase === "queued"
|
|
1326
1367
|
? "cyan"
|
|
1327
|
-
: message?.pendingPhase === "cancelling"
|
|
1368
|
+
: message?.pendingPhase === "cancelling" || message?.pendingPhase === "rejected"
|
|
1328
1369
|
? "red"
|
|
1370
|
+
: fromOtherPerson
|
|
1371
|
+
? OTHER_PERSON_CHAT_LABEL_COLOR
|
|
1329
1372
|
: message?.role === "user"
|
|
1330
1373
|
? USER_CHAT_LABEL_COLOR
|
|
1331
1374
|
: message?.role === "assistant"
|
|
@@ -1907,7 +1950,7 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
|
|
|
1907
1950
|
? "red"
|
|
1908
1951
|
: message?.role === "user" ? USER_CHAT_COLOR : null,
|
|
1909
1952
|
);
|
|
1910
|
-
const prefix = options.skipPrefix ? [] : buildChatMessagePrefix(message);
|
|
1953
|
+
const prefix = options.skipPrefix ? [] : buildChatMessagePrefix(message, options);
|
|
1911
1954
|
|
|
1912
1955
|
if (tintedMarkdownLines.length === 0) {
|
|
1913
1956
|
return prefix.length > 0 ? [prefix] : [];
|
|
@@ -2120,7 +2163,26 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
|
2120
2163
|
return [{ text: "No messages yet.", color: "gray" }];
|
|
2121
2164
|
}
|
|
2122
2165
|
|
|
2123
|
-
|
|
2166
|
+
// The current viewer's identity key — a user.message whose sender differs
|
|
2167
|
+
// is labeled with the sender's name (others) vs "You" (the viewer). The
|
|
2168
|
+
// session owner's messages additionally carry an "(owner)" tag.
|
|
2169
|
+
const viewerKey = ownerKeyForOwner(state?.auth?.principal);
|
|
2170
|
+
const activeSessionId = state?.sessions?.activeSessionId;
|
|
2171
|
+
const activeSession = activeSessionId ? state?.sessions?.byId?.[activeSessionId] : null;
|
|
2172
|
+
const ownerKey = ownerKeyForOwner(activeSession?.owner);
|
|
2173
|
+
// A shared context = the session is shared deployment-wide, or the viewer
|
|
2174
|
+
// is not its owner (someone shared it with them). Only then does the
|
|
2175
|
+
// transcript name speakers and tag the owner.
|
|
2176
|
+
const sharedContext = Boolean(activeSession && (
|
|
2177
|
+
(activeSession.visibility && activeSession.visibility !== "private")
|
|
2178
|
+
|| (ownerKey && viewerKey && ownerKey !== viewerKey)
|
|
2179
|
+
));
|
|
2180
|
+
const buildOptions = {
|
|
2181
|
+
...(options?.tableMode ? { tableMode: options.tableMode } : {}),
|
|
2182
|
+
...(viewerKey ? { viewerKey } : {}),
|
|
2183
|
+
...(ownerKey ? { ownerKey } : {}),
|
|
2184
|
+
...(sharedContext ? { sharedContext: true } : {}),
|
|
2185
|
+
};
|
|
2124
2186
|
const lines = [];
|
|
2125
2187
|
for (const [index, message] of messages.entries()) {
|
|
2126
2188
|
const messageLines = buildChatMessageLines(message, maxWidth, buildOptions);
|
|
@@ -2145,10 +2207,12 @@ export function selectOutboxOverlayLines(state, maxWidth = 80, options = {}) {
|
|
|
2145
2207
|
const queuedCount = messages.filter((message) => message.pendingPhase === "queued").length;
|
|
2146
2208
|
const pendingCount = messages.filter((message) => message.pendingPhase === "pending").length;
|
|
2147
2209
|
const cancellingCount = messages.filter((message) => message.pendingPhase === "cancelling").length;
|
|
2210
|
+
const rejectedCount = messages.filter((message) => message.pendingPhase === "rejected").length;
|
|
2148
2211
|
const parts = [];
|
|
2149
2212
|
if (pendingCount > 0) parts.push(`${pendingCount} pending`);
|
|
2150
2213
|
if (queuedCount > 0) parts.push(`${queuedCount} queued`);
|
|
2151
2214
|
if (cancellingCount > 0) parts.push(`${cancellingCount} cancelling`);
|
|
2215
|
+
if (rejectedCount > 0) parts.push(`${rejectedCount} rejected`);
|
|
2152
2216
|
const label = parts.length > 0 ? parts.join(" · ") : "queued prompts";
|
|
2153
2217
|
const labelText = ` queued prompts: ${label} `;
|
|
2154
2218
|
const rightRule = Math.max(1, safeWidth - labelText.length);
|
|
@@ -2969,6 +3033,12 @@ export function selectStatusBar(state) {
|
|
|
2969
3033
|
right: "type title · left/right move · enter save · esc cancel",
|
|
2970
3034
|
};
|
|
2971
3035
|
}
|
|
3036
|
+
if (state.ui.modal?.type === "shareSession") {
|
|
3037
|
+
return {
|
|
3038
|
+
left: "Share the selected session",
|
|
3039
|
+
right: "name [r|w] grants · -name revokes · enter apply · esc close",
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
2972
3042
|
if (state.ui.modal?.type === "artifactPicker") {
|
|
2973
3043
|
return {
|
|
2974
3044
|
left: "Select a linked artifact or URL",
|
|
@@ -3040,7 +3110,7 @@ export function selectStatusBar(state) {
|
|
|
3040
3110
|
const chatViewMode = state.ui?.chatViewMode === "summary" ? "summary" : "transcript";
|
|
3041
3111
|
const chatViewHint = chatViewMode === "summary" ? "s transcript" : "s summary";
|
|
3042
3112
|
const hints = {
|
|
3043
|
-
[FOCUS_REGIONS.SESSIONS]: `up/down switch · ctrl-u/ctrl-d page · ctrl-g move group · f filter · P pin · V select · d done · D delete · r refresh · t title ·
|
|
3113
|
+
[FOCUS_REGIONS.SESSIONS]: `up/down switch · ctrl-u/ctrl-d page · ctrl-g move group · f filter · P pin · V select · d done · D delete · r refresh · t title · v visibility · S share · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane · p prompt`,
|
|
3044
3114
|
[FOCUS_REGIONS.CHAT]: `${chatViewHint} · j/k scroll · ctrl-u/ctrl-d page · e older history · g/G top/bottom · d done · ${fullscreenHint} · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane · p prompt`,
|
|
3045
3115
|
[FOCUS_REGIONS.INSPECTOR]: state.ui.inspectorTab === "logs"
|
|
3046
3116
|
? `j/k scroll · ctrl-u/ctrl-d page · g/G top/bottom · d done · t tail · f filter · ${fullscreenHint} · left/right tab · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane`
|
|
@@ -4324,6 +4394,59 @@ export function selectRenameSessionModal(state, maxWidth = 76) {
|
|
|
4324
4394
|
};
|
|
4325
4395
|
}
|
|
4326
4396
|
|
|
4397
|
+
export function selectShareSessionModal(state, maxWidth = 76) {
|
|
4398
|
+
const modal = state.ui.modal;
|
|
4399
|
+
if (!modal || modal.type !== "shareSession") return null;
|
|
4400
|
+
|
|
4401
|
+
const value = String(modal.value || "");
|
|
4402
|
+
const shares = Array.isArray(modal.shares) ? modal.shares : [];
|
|
4403
|
+
const detailsLines = [
|
|
4404
|
+
[{
|
|
4405
|
+
text: "General: ",
|
|
4406
|
+
color: "gray",
|
|
4407
|
+
}, {
|
|
4408
|
+
text: String(modal.visibility || "private"),
|
|
4409
|
+
color: "white",
|
|
4410
|
+
bold: true,
|
|
4411
|
+
}],
|
|
4412
|
+
...(shares.length === 0
|
|
4413
|
+
? [[{ text: "No individual grants.", color: "gray" }]]
|
|
4414
|
+
: shares.map((row) => [{
|
|
4415
|
+
text: `${row.displayName || row.subject} `,
|
|
4416
|
+
color: "white",
|
|
4417
|
+
}, {
|
|
4418
|
+
text: `can ${row.access}`,
|
|
4419
|
+
color: "gray",
|
|
4420
|
+
}])),
|
|
4421
|
+
];
|
|
4422
|
+
|
|
4423
|
+
return {
|
|
4424
|
+
title: modal.title || "Share Session",
|
|
4425
|
+
value,
|
|
4426
|
+
cursorIndex: Math.max(0, Math.min(Number(modal.cursorIndex) || 0, value.length)),
|
|
4427
|
+
placeholder: "Name, email, or id",
|
|
4428
|
+
helpTitle: "Share Rules",
|
|
4429
|
+
helpLines: [
|
|
4430
|
+
[{
|
|
4431
|
+
text: "name-or-email [r|w] grants · -name revokes · Enter apply · Esc close",
|
|
4432
|
+
color: "gray",
|
|
4433
|
+
}],
|
|
4434
|
+
[{
|
|
4435
|
+
text: "Grantee needn't have signed in — an email grant binds at their first sign-in.",
|
|
4436
|
+
color: "gray",
|
|
4437
|
+
}],
|
|
4438
|
+
],
|
|
4439
|
+
detailsLines,
|
|
4440
|
+
idealWidth: Math.min(
|
|
4441
|
+
Math.max(
|
|
4442
|
+
56,
|
|
4443
|
+
...detailsLines.map((line) => flattenRunsLength(line) + 6),
|
|
4444
|
+
),
|
|
4445
|
+
maxWidth,
|
|
4446
|
+
),
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4327
4450
|
export function selectSessionGroupNameModal(state, maxWidth = 76) {
|
|
4328
4451
|
const modal = state.ui.modal;
|
|
4329
4452
|
if (!modal || modal.type !== "sessionGroupName") return null;
|
|
@@ -5825,7 +5948,7 @@ const KEYBINDING_HELP = [
|
|
|
5825
5948
|
["p", "focus the prompt"],
|
|
5826
5949
|
["[ ]", "shrink / grow the focused pane"],
|
|
5827
5950
|
["{ }", "grow left / right column"],
|
|
5828
|
-
["v", "fullscreen the focused pane"],
|
|
5951
|
+
["v", "fullscreen the focused pane (sessions pane: cycle visibility)"],
|
|
5829
5952
|
["n / r", "new session / refresh"],
|
|
5830
5953
|
["a", "linked items — artifacts to download, links to open"],
|
|
5831
5954
|
["m", "cycle inspector tab"],
|
|
@@ -5843,6 +5966,8 @@ const KEYBINDING_HELP = [
|
|
|
5843
5966
|
["+ / -", "expand / collapse subtree"],
|
|
5844
5967
|
["t", "rename"],
|
|
5845
5968
|
["P", "pin / unpin"],
|
|
5969
|
+
["v", "cycle visibility (private / shared read / shared write)"],
|
|
5970
|
+
["S", "share — grant / revoke individual access"],
|
|
5846
5971
|
["V / space", "select mode / toggle selection"],
|
|
5847
5972
|
["f", "filter"],
|
|
5848
5973
|
] },
|
|
@@ -57,6 +57,10 @@ export function createTheme({ id, label, description, page, terminal, tui = {} }
|
|
|
57
57
|
cyan: terminal?.cyan || "#55ffff",
|
|
58
58
|
userChat: "#ffd866",
|
|
59
59
|
userChatLabel: "#ffec99",
|
|
60
|
+
// Speaker label for a message from ANOTHER person in a shared session
|
|
61
|
+
// (distinct from the viewer's gold "You", the agent's green, and
|
|
62
|
+
// System's yellow). Every theme inherits this; a theme may override it.
|
|
63
|
+
otherUserChatLabel: terminal?.brightMagenta || terminal?.magenta || "#c9a3ff",
|
|
60
64
|
activeHighlightBackground: terminal?.blue || "#5555ff",
|
|
61
65
|
activeHighlightForeground: terminal?.background || "#000000",
|
|
62
66
|
selectionBackground: terminal?.cursor || terminal?.blue || "#5555ff",
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
selectContextTierPickerModal,
|
|
26
26
|
selectRenameSessionModal,
|
|
27
27
|
selectSessionAgentPickerModal,
|
|
28
|
+
selectShareSessionModal,
|
|
28
29
|
selectSessionGroupNameModal,
|
|
29
30
|
selectSessionGroupPickerModal,
|
|
30
31
|
selectSessionOwnerFilterModal,
|
|
@@ -138,8 +139,30 @@ function formatProcessRssTitleRuns(rssBytes) {
|
|
|
138
139
|
];
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
// The signed-in identity shown in the sessions header. Admins are marked with
|
|
143
|
+
// a leading "(*)" so an operator can tell at a glance they hold elevated rights.
|
|
144
|
+
function formatSignedInIdentityLabel(auth) {
|
|
145
|
+
const principal = auth?.principal;
|
|
146
|
+
if (!principal) return null;
|
|
147
|
+
const base = principal.displayName || principal.email || principal.subject || null;
|
|
148
|
+
if (!base) return null;
|
|
149
|
+
const isAdmin = auth?.authorization?.role === "admin"
|
|
150
|
+
|| (Array.isArray(principal.roles) && principal.roles.includes("admin"));
|
|
151
|
+
return isAdmin ? `(*) ${base}` : base;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildSessionTitleRightRuns(rssRuns, versionLabel = null, identityLabel = null) {
|
|
155
|
+
const titleRuns = [];
|
|
156
|
+
// Signed-in identity (multi-user deployments) sits left of the rss gauge
|
|
157
|
+
// so it's always visible without giving up a whole line.
|
|
158
|
+
const normalizedIdentityLabel = typeof identityLabel === "string" ? identityLabel.trim() : "";
|
|
159
|
+
if (normalizedIdentityLabel) {
|
|
160
|
+
titleRuns.push({ text: normalizedIdentityLabel, color: "yellow", bold: true });
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(rssRuns) && rssRuns.length > 0) {
|
|
163
|
+
if (titleRuns.length > 0) titleRuns.push({ text: " ", color: "gray" });
|
|
164
|
+
titleRuns.push(...rssRuns);
|
|
165
|
+
}
|
|
143
166
|
const normalizedVersionLabel = typeof versionLabel === "string" ? versionLabel.trim() : "";
|
|
144
167
|
if (!normalizedVersionLabel) return titleRuns.length > 0 ? titleRuns : null;
|
|
145
168
|
if (titleRuns.length > 0) {
|
|
@@ -269,16 +292,19 @@ function buildWorkspacePaneFrames(layout) {
|
|
|
269
292
|
const SessionList = React.memo(function SessionList({ controller, maxRows, width, height, frame, versionLabel = null }) {
|
|
270
293
|
const platform = useUiPlatform();
|
|
271
294
|
const rssTitleRuns = useProcessRssTitleRuns();
|
|
272
|
-
const titleRightRuns = React.useMemo(
|
|
273
|
-
() => buildSessionTitleRightRuns(rssTitleRuns, versionLabel),
|
|
274
|
-
[rssTitleRuns, versionLabel],
|
|
275
|
-
);
|
|
276
295
|
const sessionView = useControllerSelector(controller, (state) => ({
|
|
277
296
|
sessions: state.sessions,
|
|
278
297
|
mode: state.connection?.mode || "local",
|
|
279
298
|
brandingTitle: state.branding?.title || "PilotSwarm",
|
|
280
299
|
focused: state.ui.focusRegion === "sessions",
|
|
300
|
+
// Who am I signed in as (null on identity-less/local deployments);
|
|
301
|
+
// admins are prefixed with "(*)".
|
|
302
|
+
identityLabel: formatSignedInIdentityLabel(state.auth),
|
|
281
303
|
}), shallowEqualObject);
|
|
304
|
+
const titleRightRuns = React.useMemo(
|
|
305
|
+
() => buildSessionTitleRightRuns(rssTitleRuns, versionLabel, sessionView.identityLabel),
|
|
306
|
+
[rssTitleRuns, versionLabel, sessionView.identityLabel],
|
|
307
|
+
);
|
|
282
308
|
const selectorState = React.useMemo(() => ({
|
|
283
309
|
sessions: sessionView.sessions,
|
|
284
310
|
connection: { mode: sessionView.mode },
|
|
@@ -320,6 +346,9 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
|
|
|
320
346
|
activeSession: activeSessionId ? state.sessions.byId[activeSessionId] || null : null,
|
|
321
347
|
activeHistory: activeSessionId ? state.history.bySessionId.get(activeSessionId) || null : null,
|
|
322
348
|
activeOutbox: activeSessionId ? state.outbox?.bySessionId?.[activeSessionId] || null : null,
|
|
349
|
+
// Viewer identity — so the transcript can say "You" for the viewer's
|
|
350
|
+
// own messages (and tag the owner) in shared sessions.
|
|
351
|
+
authPrincipal: state.auth?.principal || null,
|
|
323
352
|
branding: state.branding,
|
|
324
353
|
connectionError: state.connection.error,
|
|
325
354
|
connectionMode: state.connection.mode,
|
|
@@ -336,6 +365,7 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
|
|
|
336
365
|
}
|
|
337
366
|
return {
|
|
338
367
|
branding: chatView.branding,
|
|
368
|
+
auth: { principal: chatView.authPrincipal },
|
|
339
369
|
connection: {
|
|
340
370
|
error: chatView.connectionError,
|
|
341
371
|
mode: chatView.connectionMode,
|
|
@@ -365,6 +395,7 @@ const ChatPane = React.memo(function ChatPane({ controller, width, height, frame
|
|
|
365
395
|
chatView.activeSessionId,
|
|
366
396
|
chatView.activeSession,
|
|
367
397
|
chatView.activeOutbox,
|
|
398
|
+
chatView.authPrincipal,
|
|
368
399
|
chatView.branding,
|
|
369
400
|
chatView.chatViewMode,
|
|
370
401
|
chatView.connectionError,
|
|
@@ -1420,6 +1451,63 @@ function RenameSessionModalContainer({ controller }) {
|
|
|
1420
1451
|
return React.createElement(RenameSessionModal, { state });
|
|
1421
1452
|
}
|
|
1422
1453
|
|
|
1454
|
+
function ShareSessionModal({ state }) {
|
|
1455
|
+
const platform = useUiPlatform();
|
|
1456
|
+
const modal = selectShareSessionModal(state);
|
|
1457
|
+
if (!modal) return null;
|
|
1458
|
+
|
|
1459
|
+
const viewport = typeof platform.getViewport === "function"
|
|
1460
|
+
? platform.getViewport()
|
|
1461
|
+
: { width: 120, height: 40 };
|
|
1462
|
+
const width = Math.max(56, Math.min(modal.idealWidth || 72, (viewport.width || 120) - 12));
|
|
1463
|
+
const detailsHeight = Math.max(6, Math.min((modal.detailsLines?.length || 0) + 2, 14, (viewport.height || 40) - 14));
|
|
1464
|
+
const helpHeight = Math.max(4, Math.min(5, (viewport.height || 40) - detailsHeight - 8));
|
|
1465
|
+
|
|
1466
|
+
return React.createElement(platform.Overlay, null,
|
|
1467
|
+
React.createElement(platform.Column, { width },
|
|
1468
|
+
React.createElement(platform.Panel, {
|
|
1469
|
+
title: modal.title,
|
|
1470
|
+
color: "cyan",
|
|
1471
|
+
focused: false,
|
|
1472
|
+
width,
|
|
1473
|
+
height: detailsHeight,
|
|
1474
|
+
lines: modal.detailsLines,
|
|
1475
|
+
scrollOffset: 0,
|
|
1476
|
+
scrollMode: "top",
|
|
1477
|
+
marginBottom: 1,
|
|
1478
|
+
fillColor: "surface",
|
|
1479
|
+
}),
|
|
1480
|
+
React.createElement(platform.Input, {
|
|
1481
|
+
label: "grant",
|
|
1482
|
+
value: modal.value,
|
|
1483
|
+
cursorIndex: modal.cursorIndex,
|
|
1484
|
+
focused: true,
|
|
1485
|
+
placeholder: modal.placeholder,
|
|
1486
|
+
rows: 1,
|
|
1487
|
+
}),
|
|
1488
|
+
React.createElement(platform.Panel, {
|
|
1489
|
+
title: modal.helpTitle || "Share Rules",
|
|
1490
|
+
color: "cyan",
|
|
1491
|
+
focused: false,
|
|
1492
|
+
width,
|
|
1493
|
+
height: helpHeight,
|
|
1494
|
+
lines: modal.helpLines,
|
|
1495
|
+
scrollOffset: 0,
|
|
1496
|
+
scrollMode: "top",
|
|
1497
|
+
fillColor: "surface",
|
|
1498
|
+
}),
|
|
1499
|
+
));
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function ShareSessionModalContainer({ controller }) {
|
|
1503
|
+
const state = useControllerSelector(controller, (rootState) => ({
|
|
1504
|
+
ui: {
|
|
1505
|
+
modal: rootState.ui.modal,
|
|
1506
|
+
},
|
|
1507
|
+
}), shallowEqualObject);
|
|
1508
|
+
return React.createElement(ShareSessionModal, { state });
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1423
1511
|
function ArtifactUploadModal({ state }) {
|
|
1424
1512
|
const platform = useUiPlatform();
|
|
1425
1513
|
const modal = selectArtifactUploadModal(state);
|
|
@@ -2106,6 +2194,7 @@ export function SharedPilotSwarmApp({ controller, versionLabel = null }) {
|
|
|
2106
2194
|
React.createElement(StatusBar, { controller }),
|
|
2107
2195
|
React.createElement(PromptBar, { controller, rows: layoutState.promptRows }),
|
|
2108
2196
|
React.createElement(RenameSessionModalContainer, { controller }),
|
|
2197
|
+
React.createElement(ShareSessionModalContainer, { controller }),
|
|
2109
2198
|
React.createElement(ArtifactUploadModalContainer, { controller }),
|
|
2110
2199
|
React.createElement(ArtifactPickerModalContainer, { controller }),
|
|
2111
2200
|
React.createElement(HelpModalContainer, { controller }),
|