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.
Files changed (42) hide show
  1. package/README.md +6 -0
  2. package/mcp/README.md +12 -0
  3. package/mcp/dist/src/context.d.ts +13 -0
  4. package/mcp/dist/src/context.d.ts.map +1 -1
  5. package/mcp/dist/src/context.js +20 -0
  6. package/mcp/dist/src/context.js.map +1 -1
  7. package/mcp/dist/src/tools/capabilities.d.ts +3 -0
  8. package/mcp/dist/src/tools/capabilities.d.ts.map +1 -1
  9. package/mcp/dist/src/tools/capabilities.js +8 -0
  10. package/mcp/dist/src/tools/capabilities.js.map +1 -1
  11. package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
  12. package/mcp/dist/src/tools/sessions.js +114 -0
  13. package/mcp/dist/src/tools/sessions.js.map +1 -1
  14. package/package.json +3 -2
  15. package/tui/src/app.js +19 -2
  16. package/tui/src/auth/cli.js +13 -0
  17. package/tui/src/node-sdk-transport.js +53 -8
  18. package/tui/tui-splash-mobile.txt +5 -7
  19. package/tui/tui-splash.txt +13 -9
  20. package/ui/core/src/commands.js +2 -0
  21. package/ui/core/src/controller.js +275 -6
  22. package/ui/core/src/history.js +19 -1
  23. package/ui/core/src/reducer.js +4 -0
  24. package/ui/core/src/selectors.js +139 -14
  25. package/ui/core/src/themes/helpers.js +4 -0
  26. package/ui/react/src/components.js +95 -6
  27. package/ui/react/src/web-app.js +555 -117
  28. package/web/api/router.js +7 -6
  29. package/web/api/ws.js +9 -0
  30. package/web/auth/index.js +5 -0
  31. package/web/auth/providers/dev.js +119 -0
  32. package/web/authz.js +142 -0
  33. package/web/dist/assets/index-BnxC8cNG.js +24 -0
  34. package/web/dist/assets/{index-oldX95Tp.css → index-Bx6KHIaj.css} +1 -1
  35. package/web/dist/assets/pilotswarm-NE7H63ha.js +90 -0
  36. package/web/dist/assets/react-l0sNRNKZ.js +1 -0
  37. package/web/dist/index.html +3 -4
  38. package/web/runtime.js +453 -9
  39. package/web/server.js +2 -2
  40. package/web/dist/assets/index-bQ2QInMX.js +0 -24
  41. package/web/dist/assets/pilotswarm-DRs6o-lA.js +0 -90
  42. package/web/dist/assets/react-C9iQPS2h.js +0 -1
@@ -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
- titleRuns.push({ text: `${initials} · `, color: "cyan" });
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 === "queued"
872
- ? "queued"
873
- : "pending",
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
- const roleLabel = message?.role === "user"
1287
- ? "You"
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
- // 3-state delivery glyph for user messages:
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
- const buildOptions = options?.tableMode ? { tableMode: options.tableMode } : {};
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 · ${fullscreenHint} · [/] resize pane · {/} columns · T themes · ? help · a linked items · drag copy · tab next pane · p prompt`,
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
- function buildSessionTitleRightRuns(rssRuns, versionLabel = null) {
142
- const titleRuns = [...(Array.isArray(rssRuns) ? rssRuns : [])];
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 }),