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
|
@@ -710,10 +710,48 @@ export class NodeSdkTransport {
|
|
|
710
710
|
return this.mgmt.listSessionsPage(opts);
|
|
711
711
|
}
|
|
712
712
|
|
|
713
|
+
async listSessionsVisible(viewer) {
|
|
714
|
+
return this.mgmt.listSessionsVisible(viewer);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async listKnownUsers(opts) {
|
|
718
|
+
return this.mgmt.listKnownUsers(opts);
|
|
719
|
+
}
|
|
720
|
+
|
|
713
721
|
async getSession(sessionId) {
|
|
714
722
|
return this.mgmt.getSession(sessionId);
|
|
715
723
|
}
|
|
716
724
|
|
|
725
|
+
// ── Session sharing / access (security model) ────────────────
|
|
726
|
+
|
|
727
|
+
async getSessionAccess(sessionId, viewer) {
|
|
728
|
+
return this.mgmt.getSessionAccess(sessionId, viewer);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async setSessionVisibility(sessionId, visibility) {
|
|
732
|
+
return this.mgmt.setSessionVisibility(sessionId, visibility);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async grantSessionShare(sessionId, grantee, access, grantedBy) {
|
|
736
|
+
return this.mgmt.grantSessionShare(sessionId, grantee, access, grantedBy);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async revokeSessionShare(sessionId, grantee) {
|
|
740
|
+
return this.mgmt.revokeSessionShare(sessionId, grantee);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async listSessionShares(sessionId) {
|
|
744
|
+
return this.mgmt.listSessionShares(sessionId);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async recordAuthzAudit(entry) {
|
|
748
|
+
return this.mgmt.recordAuthzAudit(entry);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async listAuthzAudit(opts) {
|
|
752
|
+
return this.mgmt.listAuthzAudit(opts);
|
|
753
|
+
}
|
|
754
|
+
|
|
717
755
|
async getOrchestrationStats(sessionId) {
|
|
718
756
|
return this.mgmt.getOrchestrationStats(sessionId);
|
|
719
757
|
}
|
|
@@ -963,7 +1001,7 @@ export class NodeSdkTransport {
|
|
|
963
1001
|
);
|
|
964
1002
|
}
|
|
965
1003
|
|
|
966
|
-
async createSession({ model, reasoningEffort, contextTier, owner, groupId } = {}) {
|
|
1004
|
+
async createSession({ model, reasoningEffort, contextTier, owner, groupId, visibility } = {}) {
|
|
967
1005
|
const effectiveModel = await this.assertSessionModelCreatable({ model, owner });
|
|
968
1006
|
const session = await this.client.createSession({
|
|
969
1007
|
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
@@ -971,12 +1009,13 @@ export class NodeSdkTransport {
|
|
|
971
1009
|
...(contextTier ? { contextTier } : {}),
|
|
972
1010
|
...(owner ? { owner } : {}),
|
|
973
1011
|
...(groupId ? { groupId } : {}),
|
|
1012
|
+
...(visibility ? { visibility } : {}),
|
|
974
1013
|
});
|
|
975
1014
|
this.sessionHandles.set(session.sessionId, session);
|
|
976
1015
|
return { sessionId: session.sessionId, model: effectiveModel, reasoningEffort: reasoningEffort || undefined, contextTier: contextTier || undefined };
|
|
977
1016
|
}
|
|
978
1017
|
|
|
979
|
-
async createSessionForAgent(agentName, { model, reasoningEffort, contextTier, title, splash, splashMobile, initialPrompt, owner, groupId } = {}) {
|
|
1018
|
+
async createSessionForAgent(agentName, { model, reasoningEffort, contextTier, title, splash, splashMobile, initialPrompt, owner, groupId, visibility } = {}) {
|
|
980
1019
|
const effectiveModel = await this.assertSessionModelCreatable({ model, owner });
|
|
981
1020
|
const session = await this.client.createSessionForAgent(agentName, {
|
|
982
1021
|
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
@@ -988,6 +1027,7 @@ export class NodeSdkTransport {
|
|
|
988
1027
|
...(initialPrompt ? { initialPrompt } : {}),
|
|
989
1028
|
...(owner ? { owner } : {}),
|
|
990
1029
|
...(groupId ? { groupId } : {}),
|
|
1030
|
+
...(visibility ? { visibility } : {}),
|
|
991
1031
|
});
|
|
992
1032
|
this.sessionHandles.set(session.sessionId, session);
|
|
993
1033
|
return {
|
|
@@ -1027,9 +1067,14 @@ export class NodeSdkTransport {
|
|
|
1027
1067
|
throw new Error(buildTerminalSendError(sessionId, session));
|
|
1028
1068
|
}
|
|
1029
1069
|
|
|
1030
|
-
const sendOptions =
|
|
1031
|
-
|
|
1032
|
-
|
|
1070
|
+
const sendOptions = {
|
|
1071
|
+
...(options?.clientMessageIds && Array.isArray(options.clientMessageIds) && options.clientMessageIds.length > 0
|
|
1072
|
+
? { clientMessageIds: options.clientMessageIds }
|
|
1073
|
+
: {}),
|
|
1074
|
+
// Server-stamped sender identity (security model) — set by the
|
|
1075
|
+
// portal runtime from the validated auth context.
|
|
1076
|
+
...(options?.sender && typeof options.sender === "object" ? { sender: options.sender } : {}),
|
|
1077
|
+
};
|
|
1033
1078
|
|
|
1034
1079
|
if (options?.enqueueOnly) {
|
|
1035
1080
|
// enqueueOnly originally routed through mgmt.sendMessage to skip
|
|
@@ -1054,11 +1099,11 @@ export class NodeSdkTransport {
|
|
|
1054
1099
|
// "Working…" forever. Propagate the error so the caller can retry
|
|
1055
1100
|
// through the full sessionHandle.send path that owns the start.
|
|
1056
1101
|
const sessionHandle = await this.getSessionHandle(sessionId);
|
|
1057
|
-
await sessionHandle.send(prompt);
|
|
1102
|
+
await sessionHandle.send(prompt, sendOptions);
|
|
1058
1103
|
}
|
|
1059
1104
|
|
|
1060
|
-
async sendAnswer(sessionId, answer) {
|
|
1061
|
-
await this.mgmt.sendAnswer(sessionId, answer);
|
|
1105
|
+
async sendAnswer(sessionId, answer, options = {}) {
|
|
1106
|
+
await this.mgmt.sendAnswer(sessionId, answer, options?.sender ? { sender: options.sender } : undefined);
|
|
1062
1107
|
}
|
|
1063
1108
|
|
|
1064
1109
|
async sendSessionEvent(sessionId, eventName, data) {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
{bold}{
|
|
2
|
-
{bold}{
|
|
3
|
-
{
|
|
4
|
-
{bold}{white-fg}
|
|
5
|
-
{
|
|
6
|
-
{yellow-fg}Sub-agents{/yellow-fg} · {green-fg}Multi-node scaling{/green-fg}
|
|
7
|
-
{gray-fg}Powered by duroxide + Copilot SDK{/gray-fg}
|
|
1
|
+
{bold}{green-fg}█▀█ █ █ █▀█ ▀█▀ █▀ █ █ █ ▄▀█ █▀█ █▀▄▀█{/green-fg}{/bold}
|
|
2
|
+
{bold}{green-fg}█▀▀ █ █▄▄ █▄█ █ ▄█ ▀▄▀▄▀ █▀█ █▀▄ █ ▀ █{/green-fg}{/bold}
|
|
3
|
+
{green-fg}░▒▓██████████████████████████████████▓▒░{/green-fg}
|
|
4
|
+
{bold}{white-fg}Cluster Orchestrator{/white-fg}{/bold}
|
|
5
|
+
{green-fg}Agents{/green-fg} · {yellow-fg}Infra{/yellow-fg} · {cyan-fg}Maintenance{/cyan-fg}
|
package/tui/tui-splash.txt
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
{bold}
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
{
|
|
5
|
-
|
|
6
|
-
{
|
|
1
|
+
{bold}
|
|
2
|
+
{red-fg}██████╗ ██╗██╗ ██████╗ ████████╗███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗{/red-fg}
|
|
3
|
+
{yellow-fg}██╔══██╗██║██║ ██╔═══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║{/yellow-fg}
|
|
4
|
+
{green-fg}██████╔╝██║██║ ██║ ██║ ██║ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║{/green-fg}
|
|
5
|
+
{cyan-fg}██╔═══╝ ██║██║ ██║ ██║ ██║ ╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║{/cyan-fg}
|
|
6
|
+
{blue-fg}██║ ██║███████╗╚██████╔╝ ██║ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║{/blue-fg}
|
|
7
|
+
{magenta-fg}╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝{/magenta-fg}
|
|
7
8
|
{/bold}
|
|
9
|
+
{cyan-fg} ╔═════════════════════════════════════════════════════════════════════╗{/cyan-fg}
|
|
10
|
+
{cyan-fg} ║{/cyan-fg}{bold}{white-fg} C l u s t e r O r c h e s t r a t o r {/white-fg}{/bold}{cyan-fg}║{/cyan-fg}
|
|
11
|
+
{cyan-fg} ╚═════════════════════════════════════════════════════════════════════╝{/cyan-fg}
|
|
8
12
|
|
|
9
|
-
{bold}{
|
|
10
|
-
{
|
|
11
|
-
{gray-fg}
|
|
13
|
+
{bold}{cyan-fg}Agents{/cyan-fg} · {yellow-fg}Infrastructure{/yellow-fg} · {green-fg}Maintenance{/green-fg} · {magenta-fg}Monitoring{/magenta-fg}{/bold}
|
|
14
|
+
{bold}{red-fg}Spawn{/red-fg} · {cyan-fg}Steer{/cyan-fg} · {yellow-fg}Sweep{/yellow-fg} · {blue-fg}Scale{/blue-fg}{/bold}
|
|
15
|
+
{gray-fg}Many agents. One formation.{/gray-fg}
|
package/ui/core/src/commands.js
CHANGED
|
@@ -76,6 +76,8 @@ export const UI_COMMANDS = {
|
|
|
76
76
|
STOP_TURN: "stopTurn",
|
|
77
77
|
DELETE_SESSION: "deleteSession",
|
|
78
78
|
PIN_SESSION: "pinSession",
|
|
79
|
+
CYCLE_SESSION_VISIBILITY: "cycleSessionVisibility",
|
|
80
|
+
OPEN_SHARE_SESSION: "openShareSession",
|
|
79
81
|
TOGGLE_SELECT_MODE: "toggleSelectMode",
|
|
80
82
|
TOGGLE_SESSION_SELECTION: "toggleSessionSelection",
|
|
81
83
|
CLEAR_SESSION_SELECTION: "clearSessionSelection",
|
|
@@ -1423,13 +1423,33 @@ export class PilotSwarmUiController {
|
|
|
1423
1423
|
this.scheduleSessionsRefresh(250);
|
|
1424
1424
|
return true;
|
|
1425
1425
|
})().catch((error) => {
|
|
1426
|
-
|
|
1427
|
-
|
|
1426
|
+
const authRefused = error?.code === "FORBIDDEN" || error?.code === "UNAUTHORIZED"
|
|
1427
|
+
|| error?.status === 403 || error?.status === 401;
|
|
1428
1428
|
const items = this.getSessionOutbox(sessionId);
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1429
|
+
if (authRefused) {
|
|
1430
|
+
// Authorization refusals are terminal — retrying can't succeed.
|
|
1431
|
+
// Mark the envelope rejected (renders as the red ✗, same as a
|
|
1432
|
+
// cancelled send) and drop it shortly after instead of leaving
|
|
1433
|
+
// a forever-pending item.
|
|
1434
|
+
const rejected = items.map((item) => (
|
|
1435
|
+
item.id === mergedItem.id ? { ...item, phase: "rejected" } : item
|
|
1436
|
+
));
|
|
1437
|
+
this.setSessionOutboxItems(sessionId, rejected);
|
|
1438
|
+
setTimeout(() => {
|
|
1439
|
+
const current = this.getSessionOutbox(sessionId);
|
|
1440
|
+
const remaining = current.filter((item) => !(item.id === mergedItem.id && item.phase === "rejected"));
|
|
1441
|
+
if (remaining.length !== current.length) {
|
|
1442
|
+
this.setSessionOutboxItems(sessionId, remaining);
|
|
1443
|
+
}
|
|
1444
|
+
}, 6000);
|
|
1445
|
+
} else {
|
|
1446
|
+
// Transient failure: revert the merged envelope back to the
|
|
1447
|
+
// original pending items so the user can edit/retry them.
|
|
1448
|
+
const reverted = items.flatMap((item) => (
|
|
1449
|
+
item.id === mergedItem.id ? pendingItems : [item]
|
|
1450
|
+
));
|
|
1451
|
+
this.setSessionOutboxItems(sessionId, reverted);
|
|
1452
|
+
}
|
|
1433
1453
|
this.dispatch({
|
|
1434
1454
|
type: "ui/status",
|
|
1435
1455
|
text: error?.message || String(error),
|
|
@@ -4351,6 +4371,245 @@ export class PilotSwarmUiController {
|
|
|
4351
4371
|
}
|
|
4352
4372
|
}
|
|
4353
4373
|
|
|
4374
|
+
/**
|
|
4375
|
+
* Cycle the active session's general visibility:
|
|
4376
|
+
* private → shared_read → shared_write → private. Visibility is a
|
|
4377
|
+
* per-session property, so system sessions and group rows are refused.
|
|
4378
|
+
*/
|
|
4379
|
+
async cycleSessionVisibility() {
|
|
4380
|
+
const state = this.getState();
|
|
4381
|
+
const sessionId = state.sessions.activeSessionId;
|
|
4382
|
+
const session = sessionId ? state.sessions.byId[sessionId] : null;
|
|
4383
|
+
if (!session) {
|
|
4384
|
+
this.dispatch({ type: "ui/status", text: "No session selected" });
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
if (session.isSystem) {
|
|
4388
|
+
this.dispatch({ type: "ui/status", text: "System sessions are always private" });
|
|
4389
|
+
return;
|
|
4390
|
+
}
|
|
4391
|
+
if (session.isGroup) {
|
|
4392
|
+
this.dispatch({ type: "ui/status", text: "Groups don't have visibility" });
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
if (typeof this.transport.setSessionVisibility !== "function") {
|
|
4396
|
+
this.dispatch({ type: "ui/status", text: "Not supported by this transport" });
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
const next = cycleValue(
|
|
4400
|
+
["private", "shared_read", "shared_write"],
|
|
4401
|
+
session.visibility || "private",
|
|
4402
|
+
1,
|
|
4403
|
+
);
|
|
4404
|
+
try {
|
|
4405
|
+
await this.transport.setSessionVisibility(sessionId, next);
|
|
4406
|
+
await this.refreshSessions();
|
|
4407
|
+
this.dispatch({ type: "ui/status", text: `Visibility → ${next}` });
|
|
4408
|
+
} catch (error) {
|
|
4409
|
+
this.dispatch({ type: "ui/status", text: error?.message || String(error) });
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
/**
|
|
4414
|
+
* Open the Share modal for the active session: shows the general
|
|
4415
|
+
* visibility plus the per-person grant list, with a single input line
|
|
4416
|
+
* for edits — `name [r|w]` grants, `-name` revokes. The current grants
|
|
4417
|
+
* are loaded up front so the modal (and revoke matching) can render
|
|
4418
|
+
* them without another round trip.
|
|
4419
|
+
*/
|
|
4420
|
+
async openShareSessionModal() {
|
|
4421
|
+
const state = this.getState();
|
|
4422
|
+
const sessionId = state.sessions.activeSessionId;
|
|
4423
|
+
const session = sessionId ? state.sessions.byId[sessionId] : null;
|
|
4424
|
+
if (!session) {
|
|
4425
|
+
this.dispatch({ type: "ui/status", text: "No session selected" });
|
|
4426
|
+
return;
|
|
4427
|
+
}
|
|
4428
|
+
if (session.isSystem) {
|
|
4429
|
+
this.dispatch({ type: "ui/status", text: "System sessions are always private" });
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
4432
|
+
if (session.isGroup) {
|
|
4433
|
+
this.dispatch({ type: "ui/status", text: "Groups can't be shared" });
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
if (typeof this.transport.listSessionShares !== "function") {
|
|
4437
|
+
this.dispatch({ type: "ui/status", text: "Session sharing is not supported by this transport" });
|
|
4438
|
+
return;
|
|
4439
|
+
}
|
|
4440
|
+
let shares;
|
|
4441
|
+
try {
|
|
4442
|
+
shares = await this.transport.listSessionShares(sessionId);
|
|
4443
|
+
} catch (error) {
|
|
4444
|
+
this.dispatch({ type: "ui/status", text: error?.message || String(error) });
|
|
4445
|
+
return;
|
|
4446
|
+
}
|
|
4447
|
+
this.dispatch({
|
|
4448
|
+
type: "ui/modal",
|
|
4449
|
+
modal: {
|
|
4450
|
+
type: "shareSession",
|
|
4451
|
+
title: `Share (${shortSessionIdValue(sessionId)})`,
|
|
4452
|
+
sessionId,
|
|
4453
|
+
previousFocus: state.ui.focusRegion,
|
|
4454
|
+
value: "",
|
|
4455
|
+
cursorIndex: 0,
|
|
4456
|
+
shares: Array.isArray(shares) ? shares : [],
|
|
4457
|
+
visibility: session.visibility || "private",
|
|
4458
|
+
},
|
|
4459
|
+
});
|
|
4460
|
+
this.dispatch({ type: "ui/status", text: "name [r|w] grants · -name revokes · Enter apply" });
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
updateShareSessionModal(updater) {
|
|
4464
|
+
const modal = this.getState().ui.modal;
|
|
4465
|
+
if (!modal || modal.type !== "shareSession") return null;
|
|
4466
|
+
const nextModal = typeof updater === "function" ? updater(modal) : updater;
|
|
4467
|
+
if (!nextModal) return null;
|
|
4468
|
+
this.dispatch({ type: "ui/modal", modal: { ...modal, ...nextModal } });
|
|
4469
|
+
return this.getState().ui.modal;
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
setShareSessionValue(value, cursorIndex = String(value || "").length) {
|
|
4473
|
+
const modal = this.getState().ui.modal;
|
|
4474
|
+
if (!modal || modal.type !== "shareSession") return;
|
|
4475
|
+
const safeValue = clampRenameSessionValue(value, 120);
|
|
4476
|
+
const safeCursor = clampPromptCursor(safeValue, cursorIndex);
|
|
4477
|
+
this.updateShareSessionModal({ value: safeValue, cursorIndex: safeCursor });
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
insertShareSessionText(text) {
|
|
4481
|
+
const modal = this.getState().ui.modal;
|
|
4482
|
+
if (!modal || modal.type !== "shareSession") return;
|
|
4483
|
+
const next = insertPromptTextAtCursor(modal.value || "", modal.cursorIndex || 0, clampRenameSessionValue(text, 120));
|
|
4484
|
+
this.setShareSessionValue(next.prompt, next.cursor);
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
deleteShareSessionChar() {
|
|
4488
|
+
const modal = this.getState().ui.modal;
|
|
4489
|
+
if (!modal || modal.type !== "shareSession") return;
|
|
4490
|
+
const next = deletePromptCharBackward(modal.value || "", modal.cursorIndex || 0);
|
|
4491
|
+
this.setShareSessionValue(next.prompt, next.cursor);
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
moveShareSessionCursor(delta) {
|
|
4495
|
+
const modal = this.getState().ui.modal;
|
|
4496
|
+
if (!modal || modal.type !== "shareSession") return;
|
|
4497
|
+
this.setShareSessionValue(modal.value || "", clampPromptCursor(modal.value || "", (modal.cursorIndex || 0) + delta));
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
moveShareSessionCursorToBoundary(kind) {
|
|
4501
|
+
const modal = this.getState().ui.modal;
|
|
4502
|
+
if (!modal || modal.type !== "shareSession") return;
|
|
4503
|
+
this.setShareSessionValue(modal.value || "", kind === "start" ? 0 : String(modal.value || "").length);
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
/**
|
|
4507
|
+
* Apply the typed share command. Empty input just closes the modal.
|
|
4508
|
+
* `-name` (or `revoke name`) revokes an existing grant; anything else
|
|
4509
|
+
* grants, with an optional trailing r/read/w/write access token
|
|
4510
|
+
* (default read). The grantee is resolved against the member directory
|
|
4511
|
+
* when the transport exposes it; unmatched text falls back to a raw
|
|
4512
|
+
* subject so a grant can target someone who has never signed in —
|
|
4513
|
+
* an email-keyed grant binds at their first sign-in.
|
|
4514
|
+
*/
|
|
4515
|
+
async confirmShareSessionModal() {
|
|
4516
|
+
const modal = this.getState().ui.modal;
|
|
4517
|
+
if (!modal || modal.type !== "shareSession") return;
|
|
4518
|
+
const sessionId = modal.sessionId;
|
|
4519
|
+
if (!sessionId) return;
|
|
4520
|
+
|
|
4521
|
+
const raw = String(modal.value || "").trim();
|
|
4522
|
+
const previousFocus = modal.previousFocus;
|
|
4523
|
+
if (!raw) {
|
|
4524
|
+
this.dispatch({ type: "ui/modal", modal: null });
|
|
4525
|
+
if (previousFocus) this.setFocus(previousFocus);
|
|
4526
|
+
return;
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
// Revoke: `-name` or `revoke name` targets an existing grant by
|
|
4530
|
+
// subject, email, or display name (case-insensitive).
|
|
4531
|
+
const revokeWho = raw.startsWith("-")
|
|
4532
|
+
? raw.slice(1).trim()
|
|
4533
|
+
: (raw.toLowerCase().startsWith("revoke ") ? raw.slice("revoke ".length).trim() : null);
|
|
4534
|
+
if (revokeWho !== null) {
|
|
4535
|
+
const needle = revokeWho.toLowerCase();
|
|
4536
|
+
const grant = (modal.shares || []).find((row) =>
|
|
4537
|
+
(row.subject && String(row.subject).toLowerCase() === needle)
|
|
4538
|
+
|| (row.email && String(row.email).toLowerCase() === needle)
|
|
4539
|
+
|| (row.displayName && String(row.displayName).toLowerCase() === needle));
|
|
4540
|
+
if (!grant) {
|
|
4541
|
+
this.dispatch({ type: "ui/status", text: `No grant matching "${revokeWho}"` });
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
4544
|
+
this.dispatch({ type: "ui/modal", modal: null });
|
|
4545
|
+
if (previousFocus) this.setFocus(previousFocus);
|
|
4546
|
+
try {
|
|
4547
|
+
await this.transport.revokeSessionShare(sessionId, { provider: grant.provider, subject: grant.subject });
|
|
4548
|
+
this.dispatch({ type: "ui/status", text: `Revoked ${revokeWho}` });
|
|
4549
|
+
} catch (error) {
|
|
4550
|
+
this.dispatch({ type: "ui/status", text: error?.message || String(error) });
|
|
4551
|
+
}
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
// Grant: an optional trailing access token picks read/write.
|
|
4556
|
+
const tokens = raw.split(/\s+/);
|
|
4557
|
+
const accessByToken = { r: "read", read: "read", w: "write", write: "write" };
|
|
4558
|
+
const lastToken = tokens.length > 1 ? tokens[tokens.length - 1].toLowerCase() : null;
|
|
4559
|
+
const access = lastToken && accessByToken[lastToken] ? accessByToken[lastToken] : "read";
|
|
4560
|
+
const who = lastToken && accessByToken[lastToken] ? tokens.slice(0, -1).join(" ") : raw;
|
|
4561
|
+
|
|
4562
|
+
// Resolve the typed text against the member directory: an exact
|
|
4563
|
+
// displayName/email/subject match wins; otherwise a unique partial
|
|
4564
|
+
// match is accepted and multiple partial matches are ambiguous.
|
|
4565
|
+
const needle = who.toLowerCase();
|
|
4566
|
+
let grantee = null;
|
|
4567
|
+
if (typeof this.transport.listKnownUsers === "function") {
|
|
4568
|
+
let users = [];
|
|
4569
|
+
try {
|
|
4570
|
+
users = await this.transport.listKnownUsers({ limit: 500 });
|
|
4571
|
+
} catch {
|
|
4572
|
+
users = [];
|
|
4573
|
+
}
|
|
4574
|
+
const candidates = Array.isArray(users) ? users : [];
|
|
4575
|
+
grantee = candidates.find((user) =>
|
|
4576
|
+
(user.displayName && user.displayName.toLowerCase() === needle)
|
|
4577
|
+
|| (user.email && user.email.toLowerCase() === needle)
|
|
4578
|
+
|| (user.subject && user.subject.toLowerCase() === needle)) || null;
|
|
4579
|
+
if (!grantee) {
|
|
4580
|
+
const partial = candidates.filter((user) =>
|
|
4581
|
+
(user.displayName && user.displayName.toLowerCase().includes(needle))
|
|
4582
|
+
|| (user.email && user.email.toLowerCase().includes(needle))
|
|
4583
|
+
|| (user.subject && user.subject.toLowerCase().includes(needle)));
|
|
4584
|
+
if (partial.length === 1) {
|
|
4585
|
+
grantee = partial[0];
|
|
4586
|
+
} else if (partial.length > 1) {
|
|
4587
|
+
this.dispatch({ type: "ui/status", text: `"${who}" is ambiguous (${partial.length} matches)` });
|
|
4588
|
+
return;
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
if (!grantee) {
|
|
4593
|
+
// Not-yet-seen user: treat the text as a raw subject under the
|
|
4594
|
+
// caller's provider (mirrors the portal's resolveGrantee).
|
|
4595
|
+
const principal = this.getState().auth?.principal || null;
|
|
4596
|
+
grantee = { provider: principal?.provider || "dev", subject: who, email: null, displayName: null };
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
this.dispatch({ type: "ui/modal", modal: null });
|
|
4600
|
+
if (previousFocus) this.setFocus(previousFocus);
|
|
4601
|
+
try {
|
|
4602
|
+
await this.transport.grantSessionShare(
|
|
4603
|
+
sessionId,
|
|
4604
|
+
{ provider: grantee.provider, subject: grantee.subject, email: grantee.email ?? null, displayName: grantee.displayName ?? null },
|
|
4605
|
+
access,
|
|
4606
|
+
);
|
|
4607
|
+
this.dispatch({ type: "ui/status", text: `Granted ${access} to ${who}` });
|
|
4608
|
+
} catch (error) {
|
|
4609
|
+
this.dispatch({ type: "ui/status", text: error?.message || String(error) });
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
|
|
4354
4613
|
closeModal() {
|
|
4355
4614
|
const modal = this.getState().ui.modal;
|
|
4356
4615
|
if (!modal) return;
|
|
@@ -4455,6 +4714,10 @@ export class PilotSwarmUiController {
|
|
|
4455
4714
|
await this.confirmRenameSessionModal();
|
|
4456
4715
|
return;
|
|
4457
4716
|
}
|
|
4717
|
+
if (modal.type === "shareSession") {
|
|
4718
|
+
await this.confirmShareSessionModal();
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4458
4721
|
if (modal.type === "sessionGroupPicker") {
|
|
4459
4722
|
await this.confirmSessionGroupPickerModal();
|
|
4460
4723
|
return;
|
|
@@ -6261,6 +6524,12 @@ export class PilotSwarmUiController {
|
|
|
6261
6524
|
case UI_COMMANDS.OPEN_RENAME_SESSION:
|
|
6262
6525
|
this.openRenameSessionModal();
|
|
6263
6526
|
return;
|
|
6527
|
+
case UI_COMMANDS.CYCLE_SESSION_VISIBILITY:
|
|
6528
|
+
await this.cycleSessionVisibility();
|
|
6529
|
+
return;
|
|
6530
|
+
case UI_COMMANDS.OPEN_SHARE_SESSION:
|
|
6531
|
+
await this.openShareSessionModal();
|
|
6532
|
+
return;
|
|
6264
6533
|
case UI_COMMANDS.OPEN_SESSION_FILTER:
|
|
6265
6534
|
this.openSessionOwnerFilter();
|
|
6266
6535
|
return;
|
package/ui/core/src/history.js
CHANGED
|
@@ -353,16 +353,33 @@ export function dedupeChatMessages(chat = []) {
|
|
|
353
353
|
return deduped;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
// In a multi-writer session the runtime prepends a `[FROM: name (relation)]`
|
|
357
|
+
// attribution line to the prompt so the agent knows who is speaking. The chat
|
|
358
|
+
// pane conveys the same thing through the message's speaker label + color, so
|
|
359
|
+
// strip the raw marker from the DISPLAY text (the structured `sender` below
|
|
360
|
+
// drives the label). Only a leading marker is removed.
|
|
361
|
+
function stripLeadingSenderMarker(text) {
|
|
362
|
+
return typeof text === "string"
|
|
363
|
+
? text.replace(/^\[FROM:[^\]\n]*\]\n?/, "")
|
|
364
|
+
: text;
|
|
365
|
+
}
|
|
366
|
+
|
|
356
367
|
function buildChatMessage(event, role) {
|
|
357
368
|
const rawText = messageTextFromEvent(event);
|
|
358
369
|
const sessionMessageCard = buildSessionMessageChatCard(event, rawText);
|
|
359
370
|
if (sessionMessageCard) return sessionMessageCard;
|
|
360
371
|
|
|
361
|
-
const text = extractVisibleChatText(rawText, role);
|
|
372
|
+
const text = stripLeadingSenderMarker(extractVisibleChatText(rawText, role));
|
|
362
373
|
if (!hasVisibleMessageText(text)) return null;
|
|
363
374
|
const clientMessageIds = Array.isArray(event?.data?.clientMessageIds)
|
|
364
375
|
? event.data.clientMessageIds.filter((id) => typeof id === "string" && id)
|
|
365
376
|
: [];
|
|
377
|
+
// Structured sender identity (security model): who sent this message. The
|
|
378
|
+
// chat selector uses it to label the line with the sender's name and a
|
|
379
|
+
// distinct color when it is not the current viewer.
|
|
380
|
+
const sender = event?.data?.sender && typeof event.data.sender === "object"
|
|
381
|
+
? event.data.sender
|
|
382
|
+
: null;
|
|
366
383
|
return {
|
|
367
384
|
id: `${event.sessionId}:${event.seq}`,
|
|
368
385
|
role: deriveChatRole(event, role, text),
|
|
@@ -370,6 +387,7 @@ function buildChatMessage(event, role) {
|
|
|
370
387
|
time: formatTimestamp(event.createdAt),
|
|
371
388
|
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
372
389
|
...(clientMessageIds.length > 0 ? { clientMessageIds } : {}),
|
|
390
|
+
...(sender ? { sender } : {}),
|
|
373
391
|
};
|
|
374
392
|
}
|
|
375
393
|
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -1000,6 +1000,10 @@ export function appReducer(state, action) {
|
|
|
1000
1000
|
ui: {
|
|
1001
1001
|
...state.ui,
|
|
1002
1002
|
chatScrollBySession: savedChatScroll,
|
|
1003
|
+
// Status notices are per-moment, usually per-session (send
|
|
1004
|
+
// refusals, access hints) — a stale one must not follow the
|
|
1005
|
+
// user to the next session.
|
|
1006
|
+
statusText: previousActiveId !== action.sessionId ? "" : state.ui.statusText,
|
|
1003
1007
|
scroll: {
|
|
1004
1008
|
...state.ui.scroll,
|
|
1005
1009
|
chat: Number(savedChatScroll[action.sessionId]) || 0,
|