pilotswarm 0.5.59 → 0.5.60
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/package.json +2 -2
- package/ui/core/src/controller.js +39 -13
- package/ui/core/src/history.js +15 -1
- package/ui/core/src/index.js +2 -0
- package/ui/core/src/moa.js +47 -0
- package/ui/core/src/reducer.js +19 -2
- package/ui/core/src/selectors.js +27 -13
- package/ui/core/src/session-errors.js +97 -0
- package/ui/core/src/state.js +1 -0
- package/ui/react/src/web-app.js +215 -77
- package/web/dist/assets/{abnfDiagram-VRR7QNED-D9Rv2VpV.js → abnfDiagram-VRR7QNED-CXEHJkTQ.js} +1 -1
- package/web/dist/assets/{arc-BATiucnq.js → arc-lInHlEVm.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CSTMt0si.js → architectureDiagram-ZJ3FMSHR-vrocx6re.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-C_RGqfeY.js → blockDiagram-677ZJIJ3-B1KA5QIB.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-DH__s__0.js → c4Diagram-LMCZKHZV-Du9xJMjz.js} +1 -1
- package/web/dist/assets/channel-6wPrR6sC.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-C197fD3B.js → chunk-2Q5K7J3B-BuHucQOQ.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-BjpuBroU.js → chunk-32BRIVSS-T-AE5Yz0.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-W_jL8euj.js → chunk-5VM5RSS4-B_kTsNzt.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-a9h0dtlp.js → chunk-EX3LRPZG-CtqgceBQ.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-CQN0qE7F.js → chunk-JWPE2WC7-C_Fv9QUd.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-D-Y6KQ7t.js → chunk-MOJQB5TN-D5GeF8ZZ.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-jPwseC5f.js → chunk-RYQCIY6F-Cg63wDst.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DRNCEcQh.js → chunk-V7JOEXUC-Bz6G3lev.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DzhUcoZ7.js → chunk-VR4S4FIN-DKs_hvUD.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-ldJfyPXK.js → chunk-XXDRQBXY-B9AtUlbn.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-vvXyIfbC.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-vvXyIfbC.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-BBVo21wR.js → cose-bilkent-JH36ORCC-rAG6oS0i.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CgRQfdW1.js → cynefin-VYW2F7L2-CUy2C8UT.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Ckz2X0t3.js → cynefinDiagram-TSTJHNR4-C1W8blXH.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-C1SanJiY.js → dagre-VKFMJZFB-DC0XqwcC.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-dBdEifVy.js → diagram-FQU43EPY-COAY9zCP.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-Byqc1VCB.js → diagram-G47NLZAW-DLPr8eWG.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-9aeTaCsX.js → diagram-NH7WQ7WH-DoNgwoDz.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-CL0a0o0k.js → diagram-OA4YK3LP-D3-5TINE.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-YlMQOJfY.js → diagram-WEI45ONY-DplCkBJC.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-qMZQEnws.js → ebnfDiagram-CCIWWBDH-dvcVh7yr.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-C3CWBCtq.js → erDiagram-Q63AITRT-Cde8cTY6.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-BN79sybL.js → flowDiagram-23GEKE2U-MGMNsh2E.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-BOWhiAgB.js → ganttDiagram-NO4QXBWP-BiKw7AfS.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-CKX9OACp.js → gitGraphDiagram-IHSO6WYX-vC7fsylq.js} +1 -1
- package/web/dist/assets/index-D9Ab1VTY.css +1 -0
- package/web/dist/assets/index-DDAyy127.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-QQEunPNs.js → infoDiagram-FWYZ7A6U-Du1uirB0.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CvMwrnLQ.js → ishikawaDiagram-FXEZZL3T-Dk--Qshr.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-giNpEVpj.js → journeyDiagram-5HDEW3XC-iDE4zf_N.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CUPPpo27.js → kanban-definition-HUTT4EX6-D7WI8iRR.js} +1 -1
- package/web/dist/assets/{linear-AXqadfOR.js → linear-B1_kxCBK.js} +1 -1
- package/web/dist/assets/{mermaid.core-C78wdk8Y.js → mermaid.core-DNmC5O4Y.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BGUmh9bK.js → mindmap-definition-LN4V7U3C-DIbLKLMA.js} +1 -1
- package/web/dist/assets/{papaparse.min-Bh8RLCk_.js → papaparse.min-BdWinjY-.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BHsKXKhK.js → pegDiagram-2B236MQR-CkxxR07G.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-D3EvWTdH.js → pieDiagram-ENE6RG2P-Dxvp1mn7.js} +1 -1
- package/web/dist/assets/pilotswarm-493nqGmy.js +98 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CbkxfLkV.js → quadrantDiagram-ABIIQ3AL-CrL0hjZW.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-H4M6JSDW.js → railroadDiagram-RFXS5EU6-CmVmFBPa.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-BJo7JW1v.js → requirementDiagram-TGXJPOKE-DJc3NRXb.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-DL19MeOP.js → sankeyDiagram-HTMAVEWB-BJNqWqBb.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DyVFc5pQ.js → sequenceDiagram-DBY2YBRQ-C0wddECV.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CKbcKIEQ.js → sizeCapture-X5ZJPWSS-Cw0dqPWS.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BSukSXzS.js → stateDiagram-2N3HPSRC-C5793RHZ.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-qGaRayHj.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-BUd7UhsF.js → swimlanes-5IMT3BWC-Bhpm77Zu.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BXjNM_Zw.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-Dt1gludn.js → timeline-definition-FHXFAJF6-CwbE4NsH.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-BHxt00GV.js → vennDiagram-L72KCM5P-DZnJEL_w.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-BT3fIDbe.js → wardleyDiagram-EHGQE667-DwTgBDev.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-ChoYDS2b.js → xychartDiagram-FW5EYKEG-BiPjN3Qc.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/channel-CqgY9oZh.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-Uz4DYZs2.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-Uz4DYZs2.js +0 -1
- package/web/dist/assets/index-B4s2lTsK.js +0 -24
- package/web/dist/assets/index-DhGdE2Nd.css +0 -1
- package/web/dist/assets/pilotswarm-UV6T1_QI.js +0 -98
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BhKG3pJB.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CgoFkXJE.js +0 -8
package/README.md
CHANGED
|
@@ -260,3 +260,9 @@ Docs: [Quick Start](https://github.com/affandar/pilotswarm/blob/main/docs/quicks
|
|
|
260
260
|
[MCP Setup](https://github.com/affandar/pilotswarm/blob/main/docs/user-guide/mcp-local-setup.md) ·
|
|
261
261
|
[Web API Reference](https://github.com/affandar/pilotswarm/blob/main/docs/api/reference.md) ·
|
|
262
262
|
[Architecture / Layering](https://github.com/affandar/pilotswarm/blob/main/docs/architecture/layering.md)
|
|
263
|
+
|
|
264
|
+
## Master of Agents
|
|
265
|
+
|
|
266
|
+
The desktop portal includes a personal multi-session workspace with resizable
|
|
267
|
+
chat/canvas panels, one focused composer, and zen mode. Layouts save automatically.
|
|
268
|
+
See the [MoA guide](https://github.com/affandar/PilotSwarm/blob/main/docs/user-guide/master-of-agents.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotswarm",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.60",
|
|
4
4
|
"description": "PilotSwarm application package: terminal UI, browser portal + Web API server, and MCP server — one install, three bins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"ink": "^6.8.0",
|
|
82
82
|
"jose": "^6.2.2",
|
|
83
83
|
"mermaid": "^11.16.0",
|
|
84
|
-
"pilotswarm-sdk": "0.5.
|
|
84
|
+
"pilotswarm-sdk": "0.5.60",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
|
@@ -3255,6 +3255,8 @@ export class PilotSwarmUiController {
|
|
|
3255
3255
|
*/
|
|
3256
3256
|
revealCreatedSession() {
|
|
3257
3257
|
if (this.getState().admin?.visible) this.closeAdminConsole();
|
|
3258
|
+
this.closeBudget();
|
|
3259
|
+
this.dispatch({ type: "ui/revealCreatedSession", sessionId: this.getState().sessions.activeSessionId });
|
|
3258
3260
|
this.setFocus(FOCUS_REGIONS.PROMPT);
|
|
3259
3261
|
}
|
|
3260
3262
|
|
|
@@ -5454,7 +5456,9 @@ export class PilotSwarmUiController {
|
|
|
5454
5456
|
return;
|
|
5455
5457
|
}
|
|
5456
5458
|
await this.ensureSessionHistory(sessionId, { force: true });
|
|
5459
|
+
if (this.getState().sessions.activeSessionId !== sessionId) return;
|
|
5457
5460
|
await this.syncSessionDetail(sessionId).catch(() => {});
|
|
5461
|
+
if (this.getState().sessions.activeSessionId !== sessionId) return;
|
|
5458
5462
|
this.attachActiveSession(sessionId);
|
|
5459
5463
|
this.ensureInspectorData().catch(() => {});
|
|
5460
5464
|
// Canvas snapshot rides the selection burst. Invalidate-then-fetch on
|
|
@@ -5590,8 +5594,9 @@ export class PilotSwarmUiController {
|
|
|
5590
5594
|
* history pipeline recognizes the canonical prefix and keeps it out of
|
|
5591
5595
|
* the portal chat pane; the transcript still records it for provenance.
|
|
5592
5596
|
*/
|
|
5593
|
-
submitCanvasAction(sessionId, message) {
|
|
5597
|
+
submitCanvasAction(sessionId, message, slot = 1) {
|
|
5594
5598
|
if (!sessionId) return Promise.resolve({ ok: false, reason: "no session" });
|
|
5599
|
+
if (!Number.isInteger(slot) || slot < 1 || slot > 5) return Promise.resolve({ ok: false, reason: "invalid canvas slot" });
|
|
5595
5600
|
// Creator-only, mirrored from the server's enforcement (the server is
|
|
5596
5601
|
// the authority; this is the friendly refusal). The canvas mutates —
|
|
5597
5602
|
// a shared viewer may be looking at a different revision than the one
|
|
@@ -5601,7 +5606,7 @@ export class PilotSwarmUiController {
|
|
|
5601
5606
|
// longer pre-refuses non-owners: a write-shared collaborator's click
|
|
5602
5607
|
// must reach the agent, and the server's refusal for a read-only
|
|
5603
5608
|
// viewer comes back as the result's reason.
|
|
5604
|
-
const contract = this.getState().canvas.bySessionId[sessionId]?.responseContract || null;
|
|
5609
|
+
const contract = this.getState().canvas.bySessionId[canvasPrefKey(sessionId, slot)]?.responseContract || null;
|
|
5605
5610
|
const verdict = validateCanvasAction(contract, message);
|
|
5606
5611
|
if (!verdict.ok) return Promise.resolve(verdict);
|
|
5607
5612
|
if (!this.canvasActionLimiters) this.canvasActionLimiters = new Map();
|
|
@@ -6011,15 +6016,42 @@ export class PilotSwarmUiController {
|
|
|
6011
6016
|
this.maybeFlushQueuedOutbox(sessionId, session);
|
|
6012
6017
|
}
|
|
6013
6018
|
|
|
6019
|
+
async openCreatedSession(created, options = {}) {
|
|
6020
|
+
const sessionId = created.sessionId;
|
|
6021
|
+
// Creation is explicit navigation, even if a paged/stale catalog or
|
|
6022
|
+
// the current filters omit the new row. Seed the response, preserve
|
|
6023
|
+
// the outgoing draft, and latch the same visibility rules as a link.
|
|
6024
|
+
// Invalidate catalog reads that started before this row existed.
|
|
6025
|
+
this.sessionRefreshSeq = (this.sessionRefreshSeq || 0) + 1;
|
|
6026
|
+
if (this.getState().ui.promptEdit) this.exitPendingPromptEdit({ restoreDraft: true });
|
|
6027
|
+
this.dispatch({ type: "sessions/selected", sessionId });
|
|
6028
|
+
this.dispatch({ type: "ui/sequenceExpandedTurns", turns: [] });
|
|
6029
|
+
this.dispatch({ type: "ui/sequenceSelectedTurn", turn: null });
|
|
6030
|
+
this.dispatch({ type: "sessions/navigationIntent", sessionId });
|
|
6031
|
+
this.dispatch({ type: "sessions/merged", session: normalizeSessionListRow({
|
|
6032
|
+
...created,
|
|
6033
|
+
...(options.groupId ? { viewerGroupId: options.groupId } : {}),
|
|
6034
|
+
}) });
|
|
6035
|
+
this.revealCreatedSession();
|
|
6036
|
+
this.dispatch({ type: "ui/status", text: `Created session ${sessionId.slice(0, 8)}` });
|
|
6037
|
+
// A follow-up read failure must not masquerade as a failed create and
|
|
6038
|
+
// invite a duplicate. The regular refresh loop can finish hydration.
|
|
6039
|
+
try {
|
|
6040
|
+
await this.loadSession(sessionId);
|
|
6041
|
+
} catch (error) {
|
|
6042
|
+
if (this.getState().sessions.activeSessionId === sessionId) {
|
|
6043
|
+
this.dispatch({ type: "ui/status", text: `Created session ${sessionId.slice(0, 8)}; could not load it yet: ${error?.message || error}` });
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
this.scheduleSessionsRefresh(0);
|
|
6047
|
+
}
|
|
6048
|
+
|
|
6014
6049
|
async createSession(options = {}) {
|
|
6015
6050
|
try {
|
|
6016
6051
|
const requestOptions = this.applyActiveGroupDefault(options);
|
|
6017
6052
|
const created = await this.transport.createSession(requestOptions);
|
|
6018
6053
|
await this.placeCreatedSessionInGroup(created, requestOptions.groupId ?? null);
|
|
6019
|
-
await this.
|
|
6020
|
-
await this.loadSession(created.sessionId);
|
|
6021
|
-
this.revealCreatedSession();
|
|
6022
|
-
this.dispatch({ type: "ui/status", text: `Created session ${created.sessionId.slice(0, 8)}` });
|
|
6054
|
+
await this.openCreatedSession(created, requestOptions);
|
|
6023
6055
|
return created;
|
|
6024
6056
|
} catch (error) {
|
|
6025
6057
|
this.dispatch({ type: "ui/status", text: error?.message || String(error) || "Failed to create session" });
|
|
@@ -6039,13 +6071,7 @@ export class PilotSwarmUiController {
|
|
|
6039
6071
|
// session that was just created, and a lost count only reorders a list.
|
|
6040
6072
|
this._recordAgentPickerUse(agentName);
|
|
6041
6073
|
await this.placeCreatedSessionInGroup(created, requestOptions.groupId ?? null);
|
|
6042
|
-
await this.
|
|
6043
|
-
await this.loadSession(created.sessionId);
|
|
6044
|
-
this.revealCreatedSession();
|
|
6045
|
-
this.dispatch({
|
|
6046
|
-
type: "ui/status",
|
|
6047
|
-
text: `Created ${formatAgentDisplayTitle(agentName, options.title)} session ${created.sessionId.slice(0, 8)}`,
|
|
6048
|
-
});
|
|
6074
|
+
await this.openCreatedSession(created, requestOptions);
|
|
6049
6075
|
return created;
|
|
6050
6076
|
} catch (error) {
|
|
6051
6077
|
this.dispatch({ type: "ui/status", text: error?.message || String(error) || "Failed to create session" });
|
package/ui/core/src/history.js
CHANGED
|
@@ -2,6 +2,7 @@ import { formatHumanDurationSeconds, formatTimestamp, shortModelName, shortSessi
|
|
|
2
2
|
import { isCanvasActionContent, parseCanvasActionContent } from "./canvas-actions.js";
|
|
3
3
|
import { formatCompactionActivityRuns } from "./context-usage.js";
|
|
4
4
|
import { canonicalSystemTitle } from "./system-titles.js";
|
|
5
|
+
import { buildSessionWarning } from "./session-errors.js";
|
|
5
6
|
|
|
6
7
|
export const DEFAULT_HISTORY_EVENT_LIMIT = 300;
|
|
7
8
|
export const HISTORY_EVENT_LIMIT_STEPS = [
|
|
@@ -16,6 +17,7 @@ export const HISTORY_EVENT_LIMIT_STEPS = [
|
|
|
16
17
|
// noisy sessions (thousands of tool/orchestration events between messages)
|
|
17
18
|
// load transcript pages instead of raw-stream pages.
|
|
18
19
|
export const CHAT_HISTORY_EVENT_TYPES = [
|
|
20
|
+
"session.error",
|
|
19
21
|
"user.message",
|
|
20
22
|
"assistant.message",
|
|
21
23
|
// Needed to distinguish interim assistant output from the final answer
|
|
@@ -412,7 +414,8 @@ function areMessagesEquivalent(left, right) {
|
|
|
412
414
|
// event per rev), and share identical text by design (the artifact://
|
|
413
415
|
// link) — the redelivery time-window below would eat rev N-1 on every
|
|
414
416
|
// reload, making the transcript disagree with what live append showed.
|
|
415
|
-
if (left.kind === "canvas-update" || right.kind === "canvas-update"
|
|
417
|
+
if (left.kind === "canvas-update" || right.kind === "canvas-update"
|
|
418
|
+
|| left.kind === "session-warning" || right.kind === "session-warning") {
|
|
416
419
|
return left.kind === right.kind && left.id === right.id;
|
|
417
420
|
}
|
|
418
421
|
|
|
@@ -1211,6 +1214,10 @@ export function buildHistoryModel(events = [], options = {}) {
|
|
|
1211
1214
|
|
|
1212
1215
|
for (const event of events) {
|
|
1213
1216
|
storedEvents.push(event);
|
|
1217
|
+
if (event.eventType === "session.error") {
|
|
1218
|
+
const warning = buildSessionWarning(event);
|
|
1219
|
+
if (warning) chat.push(warning);
|
|
1220
|
+
}
|
|
1214
1221
|
if (["user.message", "session.turn_completed", "session.turn_stopped", "session.epoch_committed"].includes(event.eventType)) {
|
|
1215
1222
|
settleAssistantResponses(chat, event);
|
|
1216
1223
|
}
|
|
@@ -1308,6 +1315,13 @@ export function appendEventToHistory(history, event) {
|
|
|
1308
1315
|
stoppedMessageIds: Array.isArray(history?.stoppedMessageIds) ? history.stoppedMessageIds : [],
|
|
1309
1316
|
};
|
|
1310
1317
|
|
|
1318
|
+
if (event.eventType === "session.error") {
|
|
1319
|
+
const warning = buildSessionWarning(event);
|
|
1320
|
+
if (warning && !next.chat.some(item => item.id === warning.id)) {
|
|
1321
|
+
next.chat = clampHistoryItems([...next.chat, warning], loadedEventLimit);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1311
1325
|
if (["user.message", "session.turn_completed", "session.turn_stopped", "session.epoch_committed"].includes(event.eventType)) {
|
|
1312
1326
|
settleAssistantResponses(next.chat, event);
|
|
1313
1327
|
}
|
package/ui/core/src/index.js
CHANGED
|
@@ -16,3 +16,5 @@ export { isThemeLight } from "./themes/helpers.js";
|
|
|
16
16
|
export * from "./themes/index.js";
|
|
17
17
|
export { validateCanvasAction, formatCanvasActionPrompt, parseCanvasActionContent, isCanvasActionContent, createCanvasActionLimiter, CANVAS_ACTION_PREFIX } from "./canvas-actions.js";
|
|
18
18
|
export * from "./table-layout.js";
|
|
19
|
+
|
|
20
|
+
export * from "./moa.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// One personal desktop layout, containing references and geometry only.
|
|
2
|
+
export const MOA_MAX_PANELS = 16;
|
|
3
|
+
export const MOA_BREAKPOINT = 920;
|
|
4
|
+
const object = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
5
|
+
const id = (v) => typeof v === "string" && /^[a-zA-Z0-9_-]{1,100}$/.test(v);
|
|
6
|
+
export const emptyMoaPanel = () => ({ id: crypto.randomUUID(), type: "empty" });
|
|
7
|
+
export const moaLeaves = (n) => !n ? [] : n.type === "split" ? [...moaLeaves(n.first), ...moaLeaves(n.second)] : [n];
|
|
8
|
+
export function normalizeMoaLayout(value) {
|
|
9
|
+
if (!object(value)) throw new Error("Invalid MoA layout.");
|
|
10
|
+
const seen = new Set(); let panels = 0; let nodes = 0;
|
|
11
|
+
function walk(n, depth = 0) {
|
|
12
|
+
if (!object(n) || depth > 15 || ++nodes > 31 || !id(n.id) || seen.has(n.id)) throw new Error("Invalid MoA panel tree.");
|
|
13
|
+
seen.add(n.id);
|
|
14
|
+
if (n.type === "split") {
|
|
15
|
+
if (!["row", "column"].includes(n.direction) || !Number.isFinite(n.ratio)) throw new Error("Invalid MoA split.");
|
|
16
|
+
return { id: n.id, type: "split", direction: n.direction, ratio: Math.min(90, Math.max(10, n.ratio)), first: walk(n.first, depth + 1), second: walk(n.second, depth + 1) };
|
|
17
|
+
}
|
|
18
|
+
if (++panels > MOA_MAX_PANELS) throw new Error(`A MoA supports up to ${MOA_MAX_PANELS} panels.`);
|
|
19
|
+
if (n.type === "empty") return { id: n.id, type: "empty" };
|
|
20
|
+
if (!["chat", "canvas"].includes(n.type) || !id(n.sessionId)) throw new Error("Invalid MoA session reference.");
|
|
21
|
+
if (n.type === "canvas" && (!Number.isInteger(n.slot) || n.slot < 1 || n.slot > 5)) throw new Error("Invalid canvas slot.");
|
|
22
|
+
return { id: n.id, type: n.type, sessionId: n.sessionId, ...(n.type === "canvas" ? { slot: n.slot } : {}) };
|
|
23
|
+
}
|
|
24
|
+
return { name: typeof value.name === "string" ? value.name.trim().slice(0, 64) || "Untitled MoA" : "Untitled MoA", tree: value.tree == null ? null : walk(value.tree) };
|
|
25
|
+
}
|
|
26
|
+
export function normalizeMoa(value) {
|
|
27
|
+
const safeTree = layout => { try { return normalizeMoaLayout(layout).tree; } catch { return null; } };
|
|
28
|
+
// Version 2 has one layout. A deliberately cleared layout must stay empty.
|
|
29
|
+
if (value?.version === 2 || (object(value) && Object.hasOwn(value, "tree"))) {
|
|
30
|
+
return { version: 2, tree: safeTree(value) };
|
|
31
|
+
}
|
|
32
|
+
// Migrate the selected legacy dashboard when populated; otherwise keep
|
|
33
|
+
// the first populated dashboard rather than a previously selected blank tab.
|
|
34
|
+
const slots = Array.isArray(value?.slots) ? value.slots.slice(0, 5) : [];
|
|
35
|
+
const active = Number.isInteger(value?.activeSlot) ? Math.max(0, Math.min(4, value.activeSlot)) : 0;
|
|
36
|
+
const tree = safeTree(slots[active]) || slots.map(safeTree).find(Boolean) || null;
|
|
37
|
+
return { version: 2, tree };
|
|
38
|
+
}
|
|
39
|
+
export function replaceMoaNode(tree, nodeId, next) {
|
|
40
|
+
if (!tree) return null;
|
|
41
|
+
if (tree.id === nodeId) return next;
|
|
42
|
+
if (tree.type !== "split") return tree;
|
|
43
|
+
const first = replaceMoaNode(tree.first, nodeId, next), second = replaceMoaNode(tree.second, nodeId, next);
|
|
44
|
+
if (!first) return second;
|
|
45
|
+
if (!second) return first;
|
|
46
|
+
return first === tree.first && second === tree.second ? tree : { ...tree, first, second };
|
|
47
|
+
}
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeMoa } from "./moa.js";
|
|
2
|
+
import { retainSessionWarnings } from "./session-errors.js";
|
|
1
3
|
import { buildSessionTree, isManuallyOrderableSession } from "./session-tree.js";
|
|
2
4
|
import { FOCUS_REGIONS } from "./commands.js";
|
|
3
5
|
import { DEFAULT_HISTORY_EVENT_LIMIT, dedupeChatMessages } from "./history.js";
|
|
@@ -995,6 +997,10 @@ function baseReducer(state, action) {
|
|
|
995
997
|
if (Boolean(action.collapsed) === Boolean(state.ui.sessionDetailCollapsed)) return state;
|
|
996
998
|
return { ...state, ui: { ...state.ui, sessionDetailCollapsed: Boolean(action.collapsed) } };
|
|
997
999
|
|
|
1000
|
+
case "ui/moaSaveStatus":
|
|
1001
|
+
return { ...state, ui: { ...state.ui, moaSaveStatus: action.status, moaDirty: action.status === "saved" && state.ui.moaRevision === action.revision ? false : state.ui.moaDirty } };
|
|
1002
|
+
case "ui/moa":
|
|
1003
|
+
return { ...state, ui: { ...state.ui, moa: normalizeMoa(action.value), moaDirty: true, moaRevision: (state.ui.moaRevision || 0) + 1 } };
|
|
998
1004
|
case "profileSettings/apply": {
|
|
999
1005
|
const settings = action.settings && typeof action.settings === "object" && !Array.isArray(action.settings)
|
|
1000
1006
|
? action.settings
|
|
@@ -1098,6 +1104,8 @@ function baseReducer(state, action) {
|
|
|
1098
1104
|
sessions: selection.sessions,
|
|
1099
1105
|
ui: {
|
|
1100
1106
|
...selection.ui,
|
|
1107
|
+
moa: !state.ui.moaDirty && Object.prototype.hasOwnProperty.call(settings, "moa") ? normalizeMoa(settings.moa) : selection.ui.moa,
|
|
1108
|
+
moaLoaded: true,
|
|
1101
1109
|
themeId: hasTheme ? settings.themeId.trim() : selection.ui.themeId,
|
|
1102
1110
|
touchScale: hasTouchScale ? settings.touchScale : selection.ui.touchScale,
|
|
1103
1111
|
sessionDetailCollapsed: hasSessionDetailCollapsed
|
|
@@ -1624,7 +1632,8 @@ function baseReducer(state, action) {
|
|
|
1624
1632
|
}
|
|
1625
1633
|
for (const session of action.sessions) {
|
|
1626
1634
|
const previous = state.sessions.byId[session.sessionId];
|
|
1627
|
-
byId[session.sessionId] = mergeDefinedSessionFields(previous, session)
|
|
1635
|
+
byId[session.sessionId] = retainSessionWarnings(previous, mergeDefinedSessionFields(previous, session),
|
|
1636
|
+
state.history.bySessionId.get(session.sessionId)?.events, nowMs);
|
|
1628
1637
|
}
|
|
1629
1638
|
if (
|
|
1630
1639
|
state.sessions.activeSessionId
|
|
@@ -1807,7 +1816,8 @@ function baseReducer(state, action) {
|
|
|
1807
1816
|
case "sessions/merged": {
|
|
1808
1817
|
if (!action.session?.sessionId) return state;
|
|
1809
1818
|
const previousSession = state.sessions.byId[action.session.sessionId];
|
|
1810
|
-
const mergedSession = mergeDefinedSessionFields(previousSession, action.session)
|
|
1819
|
+
const mergedSession = retainSessionWarnings(previousSession, mergeDefinedSessionFields(previousSession, action.session),
|
|
1820
|
+
state.history.bySessionId.get(action.session.sessionId)?.events);
|
|
1811
1821
|
// A single session going running/idle changes its ANCESTORS' counts
|
|
1812
1822
|
// and therefore their visual status, so both passes run over the
|
|
1813
1823
|
// whole map rather than the one row. buildSessionTree below is
|
|
@@ -1939,6 +1949,13 @@ function baseReducer(state, action) {
|
|
|
1939
1949
|
};
|
|
1940
1950
|
}
|
|
1941
1951
|
|
|
1952
|
+
case "ui/revealCreatedSession":
|
|
1953
|
+
return {
|
|
1954
|
+
...state,
|
|
1955
|
+
files: { ...state.files, fullscreen: false, paneOpen: false },
|
|
1956
|
+
ui: { ...state.ui, revealedCreatedSessionId: action.sessionId, fullscreenPane: null },
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1942
1959
|
case "ui/fullscreenPane": {
|
|
1943
1960
|
const fullscreenPane = normalizeFullscreenPane(action.fullscreenPane);
|
|
1944
1961
|
return {
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getContextHeaderBadge,
|
|
29
29
|
} from "./context-usage.js";
|
|
30
30
|
import { canonicalSystemTitle } from "./system-titles.js";
|
|
31
|
+
import { withSessionWarnings } from "./session-errors.js";
|
|
31
32
|
import {
|
|
32
33
|
BUDGET_PERIODS,
|
|
33
34
|
BUDGET_SERIES_DAYS,
|
|
@@ -1894,13 +1895,15 @@ export function selectActiveChat(state) {
|
|
|
1894
1895
|
? buildPendingQuestionMessage(session, events)
|
|
1895
1896
|
: null;
|
|
1896
1897
|
const answeredQuestionMessage = buildAnsweredPendingQuestionMessage(session, chat);
|
|
1897
|
-
|
|
1898
|
+
// Legacy/synthetic selector callers may not have passed through the
|
|
1899
|
+
// reducer that captures status-only notices. Keep their fallback too.
|
|
1900
|
+
const sessionErrorMessage = session?.chatWarnings?.length ? null : buildSessionErrorMessage(session, events);
|
|
1898
1901
|
|
|
1899
|
-
if ((!history || chat.length === 0) && !pendingQuestionMessage && !answeredQuestionMessage && !sessionErrorMessage) {
|
|
1902
|
+
if ((!history || chat.length === 0) && !pendingQuestionMessage && !answeredQuestionMessage && !sessionErrorMessage && !session?.chatWarnings?.length) {
|
|
1900
1903
|
return createSplashCard(state.branding, session);
|
|
1901
1904
|
}
|
|
1902
1905
|
|
|
1903
|
-
const messages = chat.length > 0 ?
|
|
1906
|
+
const messages = withSessionWarnings(chat.length > 0 ? chat : createSplashCard(state.branding, session), session, events);
|
|
1904
1907
|
if (pendingQuestionMessage) {
|
|
1905
1908
|
messages.push(pendingQuestionMessage);
|
|
1906
1909
|
}
|
|
@@ -1908,7 +1911,8 @@ export function selectActiveChat(state) {
|
|
|
1908
1911
|
messages.push(answeredQuestionMessage);
|
|
1909
1912
|
}
|
|
1910
1913
|
if (sessionErrorMessage) {
|
|
1911
|
-
messages.
|
|
1914
|
+
const index = messages.findIndex(message => sessionErrorMessage.createdAt != null && message.createdAt > sessionErrorMessage.createdAt);
|
|
1915
|
+
messages.splice(index < 0 ? messages.length : index, 0, sessionErrorMessage);
|
|
1912
1916
|
}
|
|
1913
1917
|
return messages;
|
|
1914
1918
|
}
|
|
@@ -2520,19 +2524,23 @@ function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
|
|
|
2520
2524
|
return buildThinkingCardLines(message, maxWidth);
|
|
2521
2525
|
}
|
|
2522
2526
|
|
|
2523
|
-
//
|
|
2524
|
-
//
|
|
2525
|
-
//
|
|
2526
|
-
//
|
|
2527
|
-
//
|
|
2528
|
-
// place while only the body grows. The TUI deliberately keeps its existing
|
|
2529
|
-
// line-oriented rendering.
|
|
2527
|
+
// One stable disclosure spans live output, saved updates and the final
|
|
2528
|
+
// answer. Its identity is not evidence of streaming: durable history uses
|
|
2529
|
+
// this shell too, including turns created with PILOTSWARM_LIVE_TURN=0.
|
|
2530
|
+
// Only actual transient content may use preview labels/status. The native
|
|
2531
|
+
// TUI keeps its line-oriented rendering of these same durable messages.
|
|
2530
2532
|
if ((message?.liveTurn || message?.streamSettling || message?.assistantPreview) && options.tableMode === "sentinel") {
|
|
2533
|
+
// streamSettling also preserves DOM through durable arrival; it does
|
|
2534
|
+
// not mean those saved bytes are still provisional.
|
|
2535
|
+
const provisional = !message?.assistantPreview && Boolean(message?.liveTurn || message?.streamSettling);
|
|
2531
2536
|
// Carry raw markdown: collapsed previews never parse their hidden body.
|
|
2532
2537
|
return [{
|
|
2533
2538
|
kind: "assistantPreview",
|
|
2534
2539
|
width: maxWidth,
|
|
2535
|
-
text: "Message preview",
|
|
2540
|
+
text: provisional ? "Message preview" : "Agent update",
|
|
2541
|
+
statusText: !provisional ? "" : message?.liveTurn
|
|
2542
|
+
? (message?.text ? "Responding" : "Thinking")
|
|
2543
|
+
: message?.streamSettling ? "Finishing" : "",
|
|
2536
2544
|
previewKey: `assistant:${message?.liveKey || message?.messageId || message?.id || "active-turn"}`,
|
|
2537
2545
|
body: decorateArtifactLinksForChat(message?.text || ""),
|
|
2538
2546
|
reasoningText: String(message?.reasoningText || message?.liveReasoningText || ""),
|
|
@@ -2971,11 +2979,16 @@ let chatLinesMemo = [];
|
|
|
2971
2979
|
|
|
2972
2980
|
export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
2973
2981
|
const memoSessionId = state?.sessions?.activeSessionId ?? null;
|
|
2982
|
+
const memoSession = memoSessionId ? state?.sessions?.byId?.[memoSessionId] ?? null : null;
|
|
2974
2983
|
const memoKey = {
|
|
2975
2984
|
sessionId: memoSessionId,
|
|
2976
|
-
session:
|
|
2985
|
+
session: memoSession,
|
|
2977
2986
|
byId: state?.sessions?.byId ?? null,
|
|
2978
2987
|
chat: state?.history?.bySessionId?.get?.(memoSessionId)?.chat ?? null,
|
|
2988
|
+
// Raw tool activity affects warning/question reconciliation, but must
|
|
2989
|
+
// not re-wrap an ordinary, unchanged transcript on every event.
|
|
2990
|
+
events: memoSession?.chatWarnings?.length || memoSession?.error || memoSession?.pendingQuestion
|
|
2991
|
+
? state?.history?.bySessionId?.get?.(memoSessionId)?.events ?? null : null,
|
|
2979
2992
|
branding: state?.branding ?? null,
|
|
2980
2993
|
principal: state?.auth?.principal ?? null,
|
|
2981
2994
|
maxWidth,
|
|
@@ -2986,6 +2999,7 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
|
|
|
2986
2999
|
&& entry.session === memoKey.session
|
|
2987
3000
|
&& entry.byId === memoKey.byId
|
|
2988
3001
|
&& entry.chat === memoKey.chat
|
|
3002
|
+
&& entry.events === memoKey.events
|
|
2989
3003
|
&& entry.viewMode === memoKey.viewMode
|
|
2990
3004
|
&& entry.branding === memoKey.branding
|
|
2991
3005
|
&& entry.principal === memoKey.principal
|
|
@@ -17,3 +17,100 @@ export function shouldKeepSessionWarning(previousSession, nextSession) {
|
|
|
17
17
|
const nextAt = time(nextSession?.updatedAt);
|
|
18
18
|
return previousAt > 0 && nextAt > 0 && nextAt <= previousAt;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
const timeMs = value => value instanceof Date ? value.getTime()
|
|
22
|
+
: typeof value === "number" ? value : Date.parse(value || "");
|
|
23
|
+
const errorText = event => typeof event?.data === "string" ? event.data
|
|
24
|
+
: String(event?.data?.message || event?.data?.error || event?.data?.text || "").trim();
|
|
25
|
+
const sameError = (statusText, eventText) => Boolean(eventText) && statusText.includes(eventText);
|
|
26
|
+
|
|
27
|
+
export function buildSessionWarning(event) {
|
|
28
|
+
const text = errorText(event);
|
|
29
|
+
if (!text) return null;
|
|
30
|
+
const failed = event?.data?.fatal === true;
|
|
31
|
+
return {
|
|
32
|
+
id: `${event.sessionId}:${event.seq}:warning`,
|
|
33
|
+
kind: "session-warning", role: "system", text,
|
|
34
|
+
warningSeq: Number(event.seq),
|
|
35
|
+
createdAt: timeMs(event.createdAt) || null,
|
|
36
|
+
cardTitle: failed ? "Error" : "Warning",
|
|
37
|
+
cardTitleColor: failed ? "red" : "yellow",
|
|
38
|
+
cardBorderColor: failed ? "red" : "yellow",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Status-only failures have no chat event yet (some never get one). Capture
|
|
43
|
+
// their position ONCE in shared state, not on each render/poll. Recovery ends
|
|
44
|
+
// the active episode but does not erase its notice. Durable error events are
|
|
45
|
+
// rendered separately and survive reload/history paging.
|
|
46
|
+
export function retainSessionWarnings(previous, next, events = [], now = Date.now()) {
|
|
47
|
+
const warnings = previous?.chatWarnings || [];
|
|
48
|
+
const last = warnings.at(-1);
|
|
49
|
+
const text = String(next?.error || "").trim();
|
|
50
|
+
const failed = next?.status === "failed" || next?.orchestrationStatus === "Failed";
|
|
51
|
+
const active = Boolean(text) && (failed || !["completed", "cancelled", "terminated", "input_required"].includes(next?.status))
|
|
52
|
+
&& !["Completed", "Terminated"].includes(next?.orchestrationStatus);
|
|
53
|
+
if (!active) {
|
|
54
|
+
if (!last?.active) return next;
|
|
55
|
+
return { ...next, chatWarnings: [...warnings.slice(0, -1), { ...last, active: false }] };
|
|
56
|
+
}
|
|
57
|
+
const since = last?.createdAt || 0;
|
|
58
|
+
const event = events.findLast(e => e.eventType === "session.error"
|
|
59
|
+
&& timeMs(e.createdAt) > since && sameError(text, errorText(e)));
|
|
60
|
+
// A retained error field can outlive a terminal/wait state. A later
|
|
61
|
+
// running snapshot alone is not evidence of another failure episode.
|
|
62
|
+
if (last && !last.active && text === last.text && text === String(previous?.error || "").trim() && !event) return next;
|
|
63
|
+
const movedOn = last && text !== last.text && events.some(e =>
|
|
64
|
+
Number(e.seq) > last.afterSeq && timeMs(e.createdAt) > last.createdAt
|
|
65
|
+
&& ["user.message", "assistant.message"].includes(e.eventType));
|
|
66
|
+
if (last?.active && !movedOn) {
|
|
67
|
+
if (last.text === text && last.failed === failed) return next;
|
|
68
|
+
return { ...next, chatWarnings: [...warnings.slice(0, -1), { ...last, text, failed }] };
|
|
69
|
+
}
|
|
70
|
+
const ordinal = (last?.ordinal || 0) + 1;
|
|
71
|
+
const previousWarnings = last?.active ? [...warnings.slice(0, -1), { ...last, active: false }] : warnings;
|
|
72
|
+
return { ...next, chatWarnings: [...previousWarnings, {
|
|
73
|
+
id: `session-error:${next.sessionId}:${ordinal}`, ordinal, text, failed, active: true,
|
|
74
|
+
eventSeq: event ? Number(event.seq) : null,
|
|
75
|
+
afterSeq: Number(events.at(-1)?.seq) || 0,
|
|
76
|
+
since,
|
|
77
|
+
createdAt: timeMs(event?.createdAt) || timeMs(next.updatedAt) || now,
|
|
78
|
+
}].slice(-300) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function withSessionWarnings(chat, session, events = []) {
|
|
82
|
+
const messages = [...chat];
|
|
83
|
+
for (const warning of session?.chatWarnings || []) {
|
|
84
|
+
// Status often wins the race against its durable event. Match only
|
|
85
|
+
// that boundary, never a later failure after a new user/agent message.
|
|
86
|
+
const following = events.filter(e => Number(e.seq) > warning.afterSeq);
|
|
87
|
+
const boundary = following.find(e => ["session.error", "user.message", "assistant.message"].includes(e.eventType));
|
|
88
|
+
// On initial load the catalog can arrive before any history at all.
|
|
89
|
+
// Find its matching older event by the captured failure time, without
|
|
90
|
+
// reusing an event from a previous error episode with identical text.
|
|
91
|
+
// Chat can outlive the raw-event window when tool activity is noisy.
|
|
92
|
+
// Reconcile from the retained warning rows, not only from raw events.
|
|
93
|
+
const recorded = chat.findLast(m => m.kind === "session-warning"
|
|
94
|
+
&& timeMs(m.createdAt) > (warning.since || 0)
|
|
95
|
+
&& timeMs(m.createdAt) <= warning.createdAt
|
|
96
|
+
&& sameError(warning.text, m.text));
|
|
97
|
+
const eventSeq = warning.eventSeq ?? recorded?.warningSeq
|
|
98
|
+
?? (boundary?.eventType === "session.error" && sameError(warning.text, errorText(boundary)) ? Number(boundary.seq) : null);
|
|
99
|
+
const index = messages.findIndex(m => m.kind === "session-warning" && m.warningSeq === eventSeq);
|
|
100
|
+
const notice = {
|
|
101
|
+
id: warning.id, kind: "session-warning", role: "system", text: warning.text,
|
|
102
|
+
createdAt: warning.createdAt,
|
|
103
|
+
cardTitle: warning.failed ? "Error" : "Warning",
|
|
104
|
+
cardTitleColor: warning.failed ? "red" : "yellow",
|
|
105
|
+
cardBorderColor: warning.failed ? "red" : "yellow",
|
|
106
|
+
};
|
|
107
|
+
if (index >= 0) {
|
|
108
|
+
// Preserve the mounted card while the durable row takes over.
|
|
109
|
+
messages[index] = { ...messages[index], ...notice, createdAt: messages[index].createdAt };
|
|
110
|
+
} else {
|
|
111
|
+
const nextIndex = messages.findIndex(m => timeMs(m.createdAt) > warning.createdAt);
|
|
112
|
+
messages.splice(nextIndex < 0 ? messages.length : nextIndex, 0, notice);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return messages;
|
|
116
|
+
}
|
package/ui/core/src/state.js
CHANGED
|
@@ -436,6 +436,7 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
436
436
|
agentPickerUsage: {},
|
|
437
437
|
modal: null,
|
|
438
438
|
fullscreenPane: null,
|
|
439
|
+
revealedCreatedSessionId: null,
|
|
439
440
|
layout: {
|
|
440
441
|
...initialLayoutAdjustments,
|
|
441
442
|
viewportWidth: 120,
|