pilotswarm 0.5.34 → 0.5.35
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/package.json +2 -2
- package/ui/core/src/controller.js +152 -12
- package/ui/core/src/formatting.js +13 -1
- package/ui/core/src/history.js +8 -0
- package/ui/core/src/reducer.js +47 -0
- package/ui/core/src/selectors.js +10 -5
- package/ui/core/src/state.js +28 -0
- package/ui/core/src/themes/duroxide.js +136 -0
- package/ui/core/src/themes/index.js +2 -0
- package/ui/react/src/web-app.js +946 -46
- package/web/dist/assets/{abnfDiagram-VRR7QNED-BFOimVWK.js → abnfDiagram-VRR7QNED-BwJEohLG.js} +1 -1
- package/web/dist/assets/{arc-uUHjvwEo.js → arc-CWUJ_S5s.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CxeX_vea.js → architectureDiagram-ZJ3FMSHR-i6mrLxuE.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Cno8kltG.js → blockDiagram-677ZJIJ3-C_KG-qPQ.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-viONVybf.js → c4Diagram-LMCZKHZV-BBJH7hbB.js} +1 -1
- package/web/dist/assets/channel-5Ddqi9ib.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CKfICIGD.js → chunk-2Q5K7J3B-CXwaFcaM.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-CxmwEY56.js → chunk-32BRIVSS-C8ujjkK-.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-_eVYLGll.js → chunk-5VM5RSS4-M9rz7YYk.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-C4aqbjrf.js → chunk-EX3LRPZG-bk6tRxOA.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-Bh8ZxrI8.js → chunk-JWPE2WC7-DZ4WSHGH.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-BXgLlWVP.js → chunk-MOJQB5TN-B5-y6tMF.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-B1rMIdfF.js → chunk-RYQCIY6F-yKcnHxm0.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-C3AnSHXk.js → chunk-V7JOEXUC-CpoNcATD.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-CeoMJIBs.js → chunk-VR4S4FIN-Bf1yD2fn.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DzaxQ9iP.js → chunk-XXDRQBXY-EnPvDVhP.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B8u1fZiB.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B8u1fZiB.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-G46fvUOx.js → cose-bilkent-JH36ORCC-qhKoA2qj.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DXwqVhWA.js → cynefin-VYW2F7L2-jcyYelJS.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-DNNFmaXb.js → cynefinDiagram-TSTJHNR4-D4RLufLP.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-BCfh-h3o.js → dagre-VKFMJZFB-C9BA3GTO.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-CyG9b6bj.js → diagram-FQU43EPY-DbSSrVUJ.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-DdxDahK3.js → diagram-G47NLZAW-3egISQXQ.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-CjrZ_eja.js → diagram-NH7WQ7WH-8IKEMGRg.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-5NAjQpk2.js → diagram-OA4YK3LP-VnaogqtK.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-fLuDb54-.js → diagram-WEI45ONY-DxigXH0T.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-DL9qnZpa.js → ebnfDiagram-CCIWWBDH-BSkNwJ9x.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-C_ynfrWY.js → erDiagram-Q63AITRT-CKN8JXl6.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-dYuH8HYv.js → flowDiagram-23GEKE2U-DDaD5ExP.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-e8Meh3f3.js → ganttDiagram-NO4QXBWP-tOrBOuoI.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-D_tZ4VL9.js → gitGraphDiagram-IHSO6WYX-Cdp867ON.js} +1 -1
- package/web/dist/assets/{index-DcQes9h8.js → index-BmwU43_u.js} +6 -6
- package/web/dist/assets/index-D-nA7gUe.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-Dffhqp73.js → infoDiagram-FWYZ7A6U-DwcKHD2h.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-DYt9H7kU.js → ishikawaDiagram-FXEZZL3T-CWffK5kz.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-Qv1vVu0F.js → journeyDiagram-5HDEW3XC-BEAgS-EZ.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CTAUo40G.js → kanban-definition-HUTT4EX6-xDm-mRRj.js} +1 -1
- package/web/dist/assets/{linear-CqLsWFLj.js → linear-oP9MA0as.js} +1 -1
- package/web/dist/assets/{mermaid.core-okRCGPQQ.js → mermaid.core-DgXXt1OS.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-DVOOrIwD.js → mindmap-definition-LN4V7U3C-BBQgVrYC.js} +1 -1
- package/web/dist/assets/{papaparse.min-Ddflu8oC.js → papaparse.min-CR6JmBWm.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BLJlC0MZ.js → pegDiagram-2B236MQR-C2O0cf3j.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-DdOwICOg.js → pieDiagram-ENE6RG2P-D08pUtYU.js} +1 -1
- package/web/dist/assets/pilotswarm-D7NLCbuc.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-B0Th1YO5.js → quadrantDiagram-ABIIQ3AL-D-Z7YoEK.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-BF2XKMKf.js → railroadDiagram-RFXS5EU6-ClkdveLI.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-Mr1iSwbS.js → requirementDiagram-TGXJPOKE-IY4xbn-Q.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-CrOjVZyg.js → sankeyDiagram-HTMAVEWB-Cpt5i7KM.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-H0X4Lydk.js → sequenceDiagram-DBY2YBRQ-c07lALcz.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-Dus1QEqh.js → sizeCapture-X5ZJPWSS-CmCDiNbI.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BOBUSwdR.js → stateDiagram-2N3HPSRC-Dueu17zX.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-AGpIV7wW.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-BoqT-Lh7.js → swimlanes-5IMT3BWC-Csc2Fup8.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BTGZSV0N.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-BOrqUQHX.js → timeline-definition-FHXFAJF6-4-lf702e.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-DDkNL-Oo.js → vennDiagram-L72KCM5P-CmlaKpLI.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-XUAJQ2X0.js → wardleyDiagram-EHGQE667-B9Qe44pi.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-ph69PV9y.js → xychartDiagram-FW5EYKEG-BFcc0CLn.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/channel-IURmSsjn.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-FsfpM3iu.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-FsfpM3iu.js +0 -1
- package/web/dist/assets/index-Ct7SQv1M.css +0 -1
- package/web/dist/assets/pilotswarm-CKh2MxfL.js +0 -99
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DABTDwFh.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BF5mFN4t.js +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotswarm",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.35",
|
|
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.35",
|
|
85
85
|
"react": "^19.2.4",
|
|
86
86
|
"react-dom": "^19.2.4",
|
|
87
87
|
"ws": "^8.18.2"
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
MIN_ACTIVITY_PANE_HEIGHT,
|
|
25
25
|
MIN_INSPECTOR_PANE_HEIGHT,
|
|
26
26
|
DEFAULT_ACTIVITY_PANE_RATIO,
|
|
27
|
+
DEFAULT_LEFT_PANE_RATIO,
|
|
28
|
+
COLLAPSE_RIGHT_THRESHOLD,
|
|
27
29
|
normalizeFocusRegion,
|
|
28
30
|
} from "./layout.js";
|
|
29
31
|
import { parseTerminalMarkupRuns } from "./formatting.js";
|
|
@@ -680,7 +682,7 @@ function getRenameSessionPrefix(session) {
|
|
|
680
682
|
if (separatorIndex > 0) {
|
|
681
683
|
return currentTitle.slice(0, separatorIndex).trim() || null;
|
|
682
684
|
}
|
|
683
|
-
return
|
|
685
|
+
return null;
|
|
684
686
|
}
|
|
685
687
|
|
|
686
688
|
function getRenameSessionMaxLength(session) {
|
|
@@ -1243,6 +1245,12 @@ function isBinaryPreview(filename, contentType = "") {
|
|
|
1243
1245
|
|| normalizedType === "application/wasm";
|
|
1244
1246
|
}
|
|
1245
1247
|
|
|
1248
|
+
// How recent a `session.artifact_presented` event must be for the portal to
|
|
1249
|
+
// act on it. Long enough to survive a slow turn and a brief reconnect; short
|
|
1250
|
+
// enough that a catch-up burst after a long disconnect does not reorganize the
|
|
1251
|
+
// workspace around something the agent said ages ago.
|
|
1252
|
+
const PRESENTED_ARTIFACT_FRESHNESS_MS = 120_000;
|
|
1253
|
+
|
|
1246
1254
|
function truncateFilePreview(content, limit = FILE_PREVIEW_CHAR_LIMIT) {
|
|
1247
1255
|
const text = String(content ?? "");
|
|
1248
1256
|
if (text.length <= limit) return text;
|
|
@@ -4195,6 +4203,32 @@ export class PilotSwarmUiController {
|
|
|
4195
4203
|
}
|
|
4196
4204
|
}
|
|
4197
4205
|
|
|
4206
|
+
/**
|
|
4207
|
+
* Act on a `session.artifact_presented` event from the live stream.
|
|
4208
|
+
*
|
|
4209
|
+
* Separate from mergeSessionEvent so the guards are testable on their own
|
|
4210
|
+
* and so the merge path stays a straight line. Returns whether it revealed.
|
|
4211
|
+
*/
|
|
4212
|
+
maybeRevealPresentedArtifact(sessionId, event) {
|
|
4213
|
+
const filename = String(event?.data?.filename || "").trim();
|
|
4214
|
+
if (!filename) return false;
|
|
4215
|
+
if (sessionId !== this.getState().sessions.activeSessionId) return false;
|
|
4216
|
+
|
|
4217
|
+
const createdAt = event?.createdAt ? Date.parse(event.createdAt) : Number.NaN;
|
|
4218
|
+
if (Number.isFinite(createdAt) && Date.now() - createdAt > PRESENTED_ARTIFACT_FRESHNESS_MS) {
|
|
4219
|
+
return false;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
// Same destination a user click reaches: the artifact reader, with the
|
|
4223
|
+
// chat still beside it. `fullscreen` from the tool means "give it the
|
|
4224
|
+
// whole window" and stays the phone/fullscreen path.
|
|
4225
|
+
const fullscreen = event?.data?.fullscreen === true;
|
|
4226
|
+
this.revealArtifact(sessionId, filename, fullscreen
|
|
4227
|
+
? { fullscreen: true }
|
|
4228
|
+
: { pane: true }).catch(() => {});
|
|
4229
|
+
return true;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4198
4232
|
mergeSessionEvent(sessionId, event) {
|
|
4199
4233
|
if (!sessionId || !event) return false;
|
|
4200
4234
|
const state = this.getState();
|
|
@@ -4227,6 +4261,24 @@ export class PilotSwarmUiController {
|
|
|
4227
4261
|
},
|
|
4228
4262
|
});
|
|
4229
4263
|
}
|
|
4264
|
+
// show_artifact: the agent is presenting something to look at, so the
|
|
4265
|
+
// inspector switches to Files and opens that preview live.
|
|
4266
|
+
//
|
|
4267
|
+
// Three guards, because "the UI moves on its own" is only acceptable
|
|
4268
|
+
// when it is unmistakably a response to what is happening right now:
|
|
4269
|
+
//
|
|
4270
|
+
// - active session only. A background session finishing a dashboard
|
|
4271
|
+
// must never yank the pane away from the session being read.
|
|
4272
|
+
// - fresh events only. This path also runs for the catch-up burst
|
|
4273
|
+
// after a reconnect; replaying an hour-old presentation as if it
|
|
4274
|
+
// just happened would be a jump scare, not a feature.
|
|
4275
|
+
// - live path only. Bulk history loads (ensureSessionHistory) do not
|
|
4276
|
+
// come through here, so merely OPENING a session never auto-opens
|
|
4277
|
+
// whatever it last presented. Reopening is what the artifact link
|
|
4278
|
+
// in the transcript and the deep link are for.
|
|
4279
|
+
if (event.eventType === "session.artifact_presented") {
|
|
4280
|
+
this.maybeRevealPresentedArtifact(sessionId, event);
|
|
4281
|
+
}
|
|
4230
4282
|
this.reconcileOutboxAgainstEvent(sessionId, event);
|
|
4231
4283
|
const derivedModel = extractSessionModelFromEvent(event);
|
|
4232
4284
|
const currentSession = this.getState().sessions.byId[sessionId] || { sessionId };
|
|
@@ -5262,7 +5314,11 @@ export class PilotSwarmUiController {
|
|
|
5262
5314
|
* READ. Switches the inspector to Files, selects the artifact, and warms
|
|
5263
5315
|
* the preview so the pane is populated by the time it renders.
|
|
5264
5316
|
*/
|
|
5265
|
-
async revealArtifact(sessionId, filename, {
|
|
5317
|
+
async revealArtifact(sessionId, filename, {
|
|
5318
|
+
force = true,
|
|
5319
|
+
fullscreen = false,
|
|
5320
|
+
pane = false,
|
|
5321
|
+
} = {}) {
|
|
5266
5322
|
const resolvedSessionId = sessionId || this.getState().sessions.activeSessionId;
|
|
5267
5323
|
const name = String(filename || "").trim();
|
|
5268
5324
|
if (!resolvedSessionId || !name) return false;
|
|
@@ -5278,12 +5334,37 @@ export class PilotSwarmUiController {
|
|
|
5278
5334
|
if (this.getState().sessions.activeSessionId !== resolvedSessionId) {
|
|
5279
5335
|
await this.loadSession(resolvedSessionId).catch(() => null);
|
|
5280
5336
|
}
|
|
5281
|
-
|
|
5282
|
-
//
|
|
5283
|
-
//
|
|
5284
|
-
//
|
|
5285
|
-
//
|
|
5286
|
-
|
|
5337
|
+
// The reader REPLACES the inspector, so it has no business changing
|
|
5338
|
+
// which inspector tab is selected. Switching to Files here meant that
|
|
5339
|
+
// opening an artifact from a session you were watching on the Sequence
|
|
5340
|
+
// tab silently reassigned that tab, and ✕ dropped you on a file list
|
|
5341
|
+
// you never opened. Leaving it alone makes ✕ restore the tab for free,
|
|
5342
|
+
// with no state to capture. The non-pane paths still need Files,
|
|
5343
|
+
// because there the inspector IS the preview.
|
|
5344
|
+
if (!pane) this.dispatch({ type: "ui/inspectorTab", inspectorTab: "files" });
|
|
5345
|
+
// The takeover pane is its own focus surface and, unlike the inspector,
|
|
5346
|
+
// it is what the user is looking at — so it must NOT steal focus to the
|
|
5347
|
+
// artifact list underneath. Chat keeps focus, which is the point: you
|
|
5348
|
+
// followed a link from the transcript and should still be able to type.
|
|
5349
|
+
if (pane) {
|
|
5350
|
+
// Whether the column was already collapsed is read HERE rather than
|
|
5351
|
+
// taken from the caller, so every entry point — a chat card, a deep
|
|
5352
|
+
// link, an agent's show_artifact — gets the same answer.
|
|
5353
|
+
//
|
|
5354
|
+
// And if it WAS collapsed, the column has to be opened: rendering
|
|
5355
|
+
// the reader into a zero-width track produced a pane that existed
|
|
5356
|
+
// in the DOM, reported itself open, and could not be seen. Closing
|
|
5357
|
+
// collapses it again, so the detour is invisible.
|
|
5358
|
+
const wasHidden = Boolean(this.getCurrentLayout()?.rightHidden);
|
|
5359
|
+
if (wasHidden) this.expandRightColumn();
|
|
5360
|
+
this.dispatch({ type: "files/pane", open: true, restoresToHidden: wasHidden });
|
|
5361
|
+
} else {
|
|
5362
|
+
// Focus the inspector, not just the tab. On desktop this makes j/k
|
|
5363
|
+
// drive the artifact list immediately; on mobile the visible pane is
|
|
5364
|
+
// derived from focusRegion, so without this the preview would open on
|
|
5365
|
+
// a pane the user cannot see.
|
|
5366
|
+
this.setFocus("inspector");
|
|
5367
|
+
}
|
|
5287
5368
|
await this.ensureFilesForSession(resolvedSessionId, { force }).catch(() => null);
|
|
5288
5369
|
this.dispatch({ type: "files/select", sessionId: resolvedSessionId, filename: name });
|
|
5289
5370
|
await this.ensureFilePreview(resolvedSessionId, name, { force }).catch(() => null);
|
|
@@ -5291,6 +5372,57 @@ export class PilotSwarmUiController {
|
|
|
5291
5372
|
return true;
|
|
5292
5373
|
}
|
|
5293
5374
|
|
|
5375
|
+
/**
|
|
5376
|
+
* Close the artifact takeover pane, restoring the right column.
|
|
5377
|
+
*
|
|
5378
|
+
* Restores the inspector/activity split it replaced — unless the column was
|
|
5379
|
+
* already collapsed when the pane opened, in which case it collapses again.
|
|
5380
|
+
* Opening a reader must not become a way to un-hide panes the user had
|
|
5381
|
+
* deliberately resized away.
|
|
5382
|
+
*/
|
|
5383
|
+
async closeArtifactPane() {
|
|
5384
|
+
const restoresToHidden = this.getState().files.paneRestoresToHidden;
|
|
5385
|
+
this.dispatch({ type: "files/pane", open: false });
|
|
5386
|
+
this.dispatch({ type: "files/fullscreen", fullscreen: false });
|
|
5387
|
+
// Selection is deliberately left alone: the artifact stays selected in
|
|
5388
|
+
// the Files list, so reopening the column shows what you were reading
|
|
5389
|
+
// rather than an empty preview.
|
|
5390
|
+
this.dispatch({ type: "files/previewOrigin", origin: null });
|
|
5391
|
+
if (restoresToHidden) this.collapseRightColumn();
|
|
5392
|
+
this.dispatch({ type: "ui/focus", focusRegion: FOCUS_REGIONS.CHAT });
|
|
5393
|
+
return { collapsedRightColumn: Boolean(restoresToHidden) };
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5396
|
+
/**
|
|
5397
|
+
* Drive the left/right split far enough right that the layout's own
|
|
5398
|
+
* collapse rule hides the right column.
|
|
5399
|
+
*
|
|
5400
|
+
* Expressed as a paneAdjust rather than a new "hidden" flag so there stays
|
|
5401
|
+
* exactly one source of truth for column width — the same number the
|
|
5402
|
+
* resizer writes. A second flag would let the two disagree.
|
|
5403
|
+
*/
|
|
5404
|
+
/** Return the left/right split to its default, un-collapsing the column. */
|
|
5405
|
+
expandRightColumn() {
|
|
5406
|
+
this.dispatch({ type: "ui/paneAdjust", paneAdjust: 0 });
|
|
5407
|
+
const nextLayout = this.getCurrentLayout({ paneAdjust: 0 });
|
|
5408
|
+
const currentFocus = this.getState().ui.focusRegion;
|
|
5409
|
+
const safeFocus = normalizeFocusRegion(currentFocus, nextLayout);
|
|
5410
|
+
if (safeFocus !== currentFocus) this.setFocus(safeFocus);
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
collapseRightColumn() {
|
|
5414
|
+
const layoutState = this.getState().ui.layout || {};
|
|
5415
|
+
const totalWidth = layoutState.viewportWidth ?? 120;
|
|
5416
|
+
const baseLeftWidth = Math.floor(totalWidth * DEFAULT_LEFT_PANE_RATIO);
|
|
5417
|
+
// One past the threshold: `<=` collapses, so land strictly inside it.
|
|
5418
|
+
const paneAdjust = totalWidth - COLLAPSE_RIGHT_THRESHOLD - baseLeftWidth;
|
|
5419
|
+
this.dispatch({ type: "ui/paneAdjust", paneAdjust });
|
|
5420
|
+
const nextLayout = this.getCurrentLayout({ paneAdjust });
|
|
5421
|
+
const currentFocus = this.getState().ui.focusRegion;
|
|
5422
|
+
const safeFocus = normalizeFocusRegion(currentFocus, nextLayout);
|
|
5423
|
+
if (safeFocus !== currentFocus) this.setFocus(safeFocus);
|
|
5424
|
+
}
|
|
5425
|
+
|
|
5294
5426
|
/**
|
|
5295
5427
|
* Leave the artifact preview, returning wherever the user came FROM.
|
|
5296
5428
|
*
|
|
@@ -5612,7 +5744,9 @@ export class PilotSwarmUiController {
|
|
|
5612
5744
|
type: "ui/modal",
|
|
5613
5745
|
modal: {
|
|
5614
5746
|
type: "renameSession",
|
|
5615
|
-
|
|
5747
|
+
// A group row's id is synthetic, so showing its short form
|
|
5748
|
+
// reads as a stray hex string rather than an identifier.
|
|
5749
|
+
title: session.isGroup ? "Rename folder" : `Rename (${shortSessionIdValue(sessionId)})`,
|
|
5616
5750
|
sessionId,
|
|
5617
5751
|
previousFocus: state.ui.focusRegion,
|
|
5618
5752
|
value,
|
|
@@ -5620,13 +5754,19 @@ export class PilotSwarmUiController {
|
|
|
5620
5754
|
agentTitlePrefix,
|
|
5621
5755
|
currentTitle: String(session.title || "").trim(),
|
|
5622
5756
|
maxLength,
|
|
5757
|
+
// Folders share this modal but not its caveats: a group has no
|
|
5758
|
+
// agent and no LLM-generated title, so the selector suppresses
|
|
5759
|
+
// the auto-title warning for them.
|
|
5760
|
+
isGroup: Boolean(session.isGroup),
|
|
5623
5761
|
},
|
|
5624
5762
|
});
|
|
5625
5763
|
this.dispatch({
|
|
5626
5764
|
type: "ui/status",
|
|
5627
|
-
text:
|
|
5628
|
-
?
|
|
5629
|
-
:
|
|
5765
|
+
text: session.isGroup
|
|
5766
|
+
? "Type a new folder name and press Enter to save"
|
|
5767
|
+
: agentTitlePrefix
|
|
5768
|
+
? `Rename title for ${agentTitlePrefix}; the agent-name prefix stays fixed`
|
|
5769
|
+
: "Type a new session title and press Enter to save",
|
|
5630
5770
|
});
|
|
5631
5771
|
}
|
|
5632
5772
|
|
|
@@ -9,6 +9,18 @@ const DEFAULT_STYLE = {
|
|
|
9
9
|
const ARTIFACT_URI_RE = /artifact:\/\/([a-f0-9-]+)\/([^\s"'{}]+)/g;
|
|
10
10
|
const SESSION_URI_RE = /session:\/\/([A-Za-z0-9-]+)/g;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The TUI's affordance for an artifact link, appended when a bare
|
|
14
|
+
* `artifact://` URI is decorated for display.
|
|
15
|
+
*
|
|
16
|
+
* Exported because the PORTAL has to remove it again: there, an artifact link
|
|
17
|
+
* renders as a card you click, so an instruction to press a key that does not
|
|
18
|
+
* exist in a browser is noise. Hosts disagree about the affordance but share
|
|
19
|
+
* the decorator, so the string is named in one place rather than matched by
|
|
20
|
+
* two copies of a literal that can drift apart.
|
|
21
|
+
*/
|
|
22
|
+
export const ARTIFACT_DOWNLOAD_HINT = " (press a to download)";
|
|
23
|
+
|
|
12
24
|
const COLOR_NAME_MAP = {
|
|
13
25
|
black: "black",
|
|
14
26
|
red: "red",
|
|
@@ -495,7 +507,7 @@ export function decorateArtifactLinksForChat(text) {
|
|
|
495
507
|
if (whole.slice(Math.max(0, offset - 2), offset) === "](") return _match;
|
|
496
508
|
const filename = normalizeArtifactFilename(rawFilename);
|
|
497
509
|
if (!sessionId || !filename) return _match;
|
|
498
|
-
return `[artifact: ${filename}](artifact://${sessionId}/${filename})
|
|
510
|
+
return `[artifact: ${filename}](artifact://${sessionId}/${filename})${ARTIFACT_DOWNLOAD_HINT}`;
|
|
499
511
|
});
|
|
500
512
|
SESSION_URI_RE.lastIndex = 0;
|
|
501
513
|
return withArtifacts.replace(SESSION_URI_RE, (_match, sessionId) => {
|
package/ui/core/src/history.js
CHANGED
|
@@ -703,6 +703,14 @@ function formatActivity(event) {
|
|
|
703
703
|
case "assistant.streaming_progress":
|
|
704
704
|
return null;
|
|
705
705
|
|
|
706
|
+
case "session.artifact_presented": {
|
|
707
|
+
const data = (event?.data ?? {}) || {};
|
|
708
|
+
const filename = String(data.filename || "artifact");
|
|
709
|
+
const note = typeof data.note === "string" && data.note.trim() ? ` — ${data.note.trim()}` : "";
|
|
710
|
+
runs = buildLabeledActivityRuns(time, "[show]", "cyan", `${filename}${note}`, "white");
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
|
|
706
714
|
case "tool.execution_start":
|
|
707
715
|
runs = formatToolActivityRuns(time, event, "start");
|
|
708
716
|
break;
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -2568,6 +2568,53 @@ export function appReducer(state, action) {
|
|
|
2568
2568
|
},
|
|
2569
2569
|
};
|
|
2570
2570
|
|
|
2571
|
+
// The artifact takeover pane. `restoresToHidden` is captured ONCE, on
|
|
2572
|
+
// the opening transition — reopening while already open must not
|
|
2573
|
+
// overwrite it with "the column is visible", which is trivially true
|
|
2574
|
+
// whenever the pane itself is showing and would strand a user who had
|
|
2575
|
+
// collapsed the column with an inspector they never asked for.
|
|
2576
|
+
case "files/pane":
|
|
2577
|
+
return {
|
|
2578
|
+
...state,
|
|
2579
|
+
files: {
|
|
2580
|
+
...state.files,
|
|
2581
|
+
paneOpen: Boolean(action.open),
|
|
2582
|
+
paneRestoresToHidden: action.open
|
|
2583
|
+
? (state.files.paneOpen
|
|
2584
|
+
? state.files.paneRestoresToHidden
|
|
2585
|
+
: Boolean(action.restoresToHidden))
|
|
2586
|
+
: false,
|
|
2587
|
+
},
|
|
2588
|
+
};
|
|
2589
|
+
|
|
2590
|
+
case "files/htmlViewMode":
|
|
2591
|
+
return {
|
|
2592
|
+
...state,
|
|
2593
|
+
files: {
|
|
2594
|
+
...state.files,
|
|
2595
|
+
htmlViewMode: action.mode === "source" ? "source" : "rendered",
|
|
2596
|
+
},
|
|
2597
|
+
};
|
|
2598
|
+
|
|
2599
|
+
case "files/htmlFitWidth":
|
|
2600
|
+
return {
|
|
2601
|
+
...state,
|
|
2602
|
+
files: {
|
|
2603
|
+
...state.files,
|
|
2604
|
+
htmlFitWidth: Boolean(action.enabled),
|
|
2605
|
+
},
|
|
2606
|
+
};
|
|
2607
|
+
|
|
2608
|
+
// Clamped here rather than at the call site so every entry point —
|
|
2609
|
+
// buttons, keyboard, a restored profile — lands in the same range.
|
|
2610
|
+
case "files/htmlZoom": {
|
|
2611
|
+
const requested = Number(action.zoom);
|
|
2612
|
+
const zoom = Number.isFinite(requested)
|
|
2613
|
+
? Math.min(3, Math.max(0.5, Math.round(requested * 100) / 100))
|
|
2614
|
+
: 1;
|
|
2615
|
+
return { ...state, files: { ...state.files, htmlZoom: zoom } };
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2571
2618
|
case "files/clearMarks":
|
|
2572
2619
|
return { ...state, files: { ...state.files, markedIds: [] } };
|
|
2573
2620
|
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -5801,11 +5801,16 @@ export function selectRenameSessionModal(state, maxWidth = 76) {
|
|
|
5801
5801
|
text: " cancel",
|
|
5802
5802
|
color: "gray",
|
|
5803
5803
|
}],
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5804
|
+
// A folder has no agent writing titles for it, so the auto-title
|
|
5805
|
+
// caveat is noise there — and worse, it implies a folder is a
|
|
5806
|
+
// session.
|
|
5807
|
+
...(modal.isGroup ? [] : [
|
|
5808
|
+
[{ text: "", color: "gray" }],
|
|
5809
|
+
[{
|
|
5810
|
+
text: "Manual titles stop future automatic LLM title changes for this session.",
|
|
5811
|
+
color: "gray",
|
|
5812
|
+
}],
|
|
5813
|
+
]),
|
|
5809
5814
|
],
|
|
5810
5815
|
detailsLines,
|
|
5811
5816
|
idealWidth: Math.min(
|
package/ui/core/src/state.js
CHANGED
|
@@ -268,6 +268,34 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
268
268
|
// Selection to restore when leaving a chat-opened preview, so
|
|
269
269
|
// following a chat link never disturbs the list.
|
|
270
270
|
restoreArtifactId: null,
|
|
271
|
+
// The portal's artifact takeover pane: a full-height reader that
|
|
272
|
+
// replaces the inspector AND activity panes in the right column.
|
|
273
|
+
// Opened by chat artifact cards and artifact deep links; the Files
|
|
274
|
+
// list keeps its own stacked list-above-preview view, because
|
|
275
|
+
// taking over the column would hide the list being browsed.
|
|
276
|
+
//
|
|
277
|
+
// The TUI ignores this entirely — it has no such pane.
|
|
278
|
+
paneOpen: false,
|
|
279
|
+
// What the right column looked like BEFORE the pane opened, so ✕
|
|
280
|
+
// can put it back. When the user had already resized the column
|
|
281
|
+
// out of existence, closing must return to that — not conjure an
|
|
282
|
+
// inspector they had deliberately dismissed.
|
|
283
|
+
paneRestoresToHidden: false,
|
|
284
|
+
// Rendered page vs. HTML source. Lifted out of the preview
|
|
285
|
+
// component so the reader pane's single header can own the toggle
|
|
286
|
+
// — the alternative was a second header row stacked under the
|
|
287
|
+
// first, which is exactly the box-in-a-box the pane exists to undo.
|
|
288
|
+
htmlViewMode: "rendered",
|
|
289
|
+
// Zoom for the rendered HTML frame ONLY. Browser zoom scales the
|
|
290
|
+
// whole workspace — the session list and transcript included —
|
|
291
|
+
// when all you wanted was to read a dense chart. This scales the
|
|
292
|
+
// artifact and nothing else.
|
|
293
|
+
htmlZoom: 1,
|
|
294
|
+
// Fit-to-width for the rendered HTML frame. Lifted here for the
|
|
295
|
+
// same reason htmlViewMode is: on mobile the panel is chromeless
|
|
296
|
+
// and the artifact bar owns the controls, so the two surfaces have
|
|
297
|
+
// to read one value.
|
|
298
|
+
htmlFitWidth: false,
|
|
271
299
|
filter: {
|
|
272
300
|
scope: "selectedSession",
|
|
273
301
|
query: "",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createTheme, THEME_GROUP_OTHER } from "./helpers.js";
|
|
2
|
+
|
|
3
|
+
// Duroxide (2025-) - the durable execution runtime, wearing its own brand.
|
|
4
|
+
//
|
|
5
|
+
// This is NOT the Rust theme with a different accent. Rust is rustc OUTPUT:
|
|
6
|
+
// hot Ferris orange on cold graphite, with the compiler's error/warning/help
|
|
7
|
+
// levels as the status palette. Duroxide is the PROJECT BRAND: the banner is
|
|
8
|
+
// a sheet of near-black felt with the mark pressed into it, and the mark is
|
|
9
|
+
// not orange at all - it is iron oxide, a terracotta so muted it reads as a
|
|
10
|
+
// material rather than a highlight. Ground, accent and mood all differ, which
|
|
11
|
+
// is what keeps two warm-dark themes from collapsing into one.
|
|
12
|
+
//
|
|
13
|
+
// Every value below is MEASURED off the banner artwork rather than picked by
|
|
14
|
+
// eye. The ground is #282627 with a paper grain running about +/-12 levels
|
|
15
|
+
// either side; the mark's modal colour is #a35838, its lit edge #ba714c.
|
|
16
|
+
//
|
|
17
|
+
// The catch: #a35838 on #282627 is 2.87:1. The exact brand colour cannot be
|
|
18
|
+
// text. So the brand oxide is spent on CHROME - the logo, borders, rules -
|
|
19
|
+
// and the interactive accent is a brightened sibling, #d2764a, which clears
|
|
20
|
+
// 4.5:1 on both grounds while staying recognisably the same pigment. Getting
|
|
21
|
+
// that backwards is how a brand palette becomes an unreadable interface.
|
|
22
|
+
//
|
|
23
|
+
// Rust's DNA survives where it earns its place: the status slots keep rustc's
|
|
24
|
+
// three-level vocabulary (error red, warning amber, help blue) with cargo
|
|
25
|
+
// green for success - a status palette a session list can use directly - but
|
|
26
|
+
// each is desaturated to sit inside the brand's restraint rather than shout
|
|
27
|
+
// over it.
|
|
28
|
+
//
|
|
29
|
+
// The banner's felt grain is not expressible as a palette entry, so it rides
|
|
30
|
+
// on [data-ps-theme="duroxide"] in the stylesheet, the same hook Win95 uses
|
|
31
|
+
// for its bevels.
|
|
32
|
+
|
|
33
|
+
// The brand oxide, sampled from the mark itself. Chrome only - see above.
|
|
34
|
+
const OXIDE = "#a35838";
|
|
35
|
+
|
|
36
|
+
// The mark itself, traced from the banner artwork at 1.8px tolerance: hexagon
|
|
37
|
+
// ring, eight-tooth gear, and the replay arrow sweeping its lower arc. Four
|
|
38
|
+
// subpaths in one string - outers wound clockwise, holes counter-clockwise.
|
|
39
|
+
const DUROXIDE_MARK =
|
|
40
|
+
"M4.14 33.09L4.14 18.89L5.7 15.77L30.05 1.4L33.95 1.4L43.32 6.56L57.99 15.3L59.08 16.7L59.7"
|
|
41
|
+
+ "1 18.58L59.71 45.27L59.08 47.14L56.9 49.64L41.44 58.38L38.32 60.57L33.64 62.91L30.52 62.91"
|
|
42
|
+
+ "L27.71 61.66L14.6 53.7L6.79 49.48L5.54 48.23L4.45 46.2L4.14 44.49L4.14 33.09ZM55.8 26.07L5"
|
|
43
|
+
+ "5.8 19.36L55.02 18.42L33.8 6.09L31.45 5.78L30.05 6.24L8.51 18.89L8.51 45.11L10.22 46.67L18"
|
|
44
|
+
+ ".03 50.89L30.05 58.22L31.77 58.85L33.17 58.69L53.93 46.67L55.65 44.96L55.8 26.07ZM17.09 34"
|
|
45
|
+
+ ".81L16.78 33.56L13.19 32.62L13.19 27.0L17.09 26.54L18.97 23.26L16.94 19.67L21.0 15.61L24.4"
|
|
46
|
+
+ "3 17.95L27.86 16.86L29.11 12.8L34.58 12.8L35.51 16.86L38.63 17.95L39.57 17.8L42.69 15.61L4"
|
|
47
|
+
+ "6.6 19.67L46.28 20.92L44.88 22.95L44.88 23.73L46.28 26.38L50.65 27.16L50.65 32.47L47.69 33"
|
|
48
|
+
+ ".4L47.06 34.03L42.54 32.0L42.54 29.97L41.91 27.63L40.82 25.6L38.63 23.26L34.73 21.39L32.55"
|
|
49
|
+
+ " 21.07L29.11 21.39L26.3 22.48L23.96 24.35L22.4 26.54L21.31 29.97L21.31 33.25L22.87 37.46L2"
|
|
50
|
+
+ "4.12 39.18L27.4 41.83L24.59 42.77L19.9 42.93L16.0 41.68L15.22 40.12L17.72 37.31L17.09 34.8"
|
|
51
|
+
+ "1ZM36.76 35.59L35.51 32.62L35.82 32.0L50.19 39.34L49.25 41.99L43.32 51.2L42.54 51.04L41.6 "
|
|
52
|
+
+ "46.2L35.51 49.64L31.3 50.58L28.49 50.58L21.93 49.01L18.97 47.3L16.47 45.11L16.78 44.8L19.1"
|
|
53
|
+
+ "2 45.58L22.09 45.74L25.21 45.42L28.18 44.33L29.74 44.49L33.48 42.46L37.39 39.18L37.85 37.9"
|
|
54
|
+
+ "3L36.76 35.59Z";
|
|
55
|
+
|
|
56
|
+
const duroxideTheme = createTheme({
|
|
57
|
+
id: "duroxide",
|
|
58
|
+
label: "Duroxide",
|
|
59
|
+
description: "Iron oxide pressed into black felt - the durable-execution runtime's own mark, traced from the banner.",
|
|
60
|
+
group: THEME_GROUP_OTHER,
|
|
61
|
+
// Traced from the banner artwork, not redrawn: the hexagon, the eight-tooth
|
|
62
|
+
// gear, and the replay arrow sweeping its lower arc. Subpaths are wound so
|
|
63
|
+
// the default nonzero fill rule punches the holes - ThemeIconMark renders
|
|
64
|
+
// a bare <path d fill> with no fill-rule to lean on.
|
|
65
|
+
icon: {
|
|
66
|
+
viewBox: "0 0 64 64",
|
|
67
|
+
// ONE path, not four. ThemeIconMark renders each entry as its own
|
|
68
|
+
// <path>, and the nonzero fill rule can only cancel windings WITHIN a
|
|
69
|
+
// single element - split across four, the hexagon ring and the gear
|
|
70
|
+
// hole both filled solid and the mark came out as a plain blob.
|
|
71
|
+
paths: [
|
|
72
|
+
{ d: DUROXIDE_MARK, fill: OXIDE },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
page: {
|
|
76
|
+
// Deeper than the banner so panels sit ON the sheet rather than in it.
|
|
77
|
+
background: "#201e1f",
|
|
78
|
+
foreground: "#e5e0dc",
|
|
79
|
+
overlayBackground: "#2e2c2d",
|
|
80
|
+
overlayForeground: "#e5e0dc",
|
|
81
|
+
hintColor: "#a89f99",
|
|
82
|
+
modalBackdrop: "rgba(23, 21, 23, 0.86)",
|
|
83
|
+
modalBackground: "#2e2c2d",
|
|
84
|
+
modalBorder: "#4d4643",
|
|
85
|
+
modalForeground: "#e5e0dc",
|
|
86
|
+
modalMuted: "#a89f99",
|
|
87
|
+
// Selection is the oxide at its deepest measured shadow, not a tint of
|
|
88
|
+
// the accent - the colour the pressed edge of the mark actually goes.
|
|
89
|
+
modalSelectedBackground: "#7c3f26",
|
|
90
|
+
modalSelectedBorder: "#a35838",
|
|
91
|
+
modalSelectedForeground: "#ffeee4",
|
|
92
|
+
},
|
|
93
|
+
terminal: {
|
|
94
|
+
// The banner tone, exactly.
|
|
95
|
+
background: "#282627",
|
|
96
|
+
foreground: "#e5e0dc",
|
|
97
|
+
cursor: "#d2764a",
|
|
98
|
+
cursorAccent: "#201e1f",
|
|
99
|
+
selectionBackground: "#7c3f26",
|
|
100
|
+
black: "#201e1f",
|
|
101
|
+
// rustc's levels, desaturated into the brand's register.
|
|
102
|
+
red: "#e86a5f", // error[E0382]
|
|
103
|
+
green: "#7fb069", // Compiling / Finished
|
|
104
|
+
yellow: "#d9a441", // warning:
|
|
105
|
+
blue: "#6a9fd4", // the --> gutter rail and help:
|
|
106
|
+
magenta: "#b98bc4",
|
|
107
|
+
// The accent slot. The portal derives --ps-accent from tui.cyan, so the
|
|
108
|
+
// interactive colour travels here regardless of the slot's name.
|
|
109
|
+
cyan: "#d2764a",
|
|
110
|
+
white: "#e5e0dc",
|
|
111
|
+
brightBlack: "#a89f99",
|
|
112
|
+
brightRed: "#f4897f",
|
|
113
|
+
brightGreen: "#9bc888",
|
|
114
|
+
brightYellow: "#efc069",
|
|
115
|
+
brightBlue: "#8fbce4",
|
|
116
|
+
brightMagenta: "#d0a8d9",
|
|
117
|
+
// The banner's lit edge, measured where the press catches the light.
|
|
118
|
+
brightCyan: "#ba714c",
|
|
119
|
+
brightWhite: "#fdf7f2",
|
|
120
|
+
},
|
|
121
|
+
tui: {
|
|
122
|
+
border: "#46403d",
|
|
123
|
+
// The human speaks in the warning register, as in the Rust theme: the
|
|
124
|
+
// accent stays reserved for the machine and what it produces.
|
|
125
|
+
userChat: "#e0b070",
|
|
126
|
+
userChatLabel: "#f0cf9a",
|
|
127
|
+
selectionBackground: "#7c3f26",
|
|
128
|
+
selectionForeground: "#ffeee4",
|
|
129
|
+
activeHighlightBackground: "#7c3f26",
|
|
130
|
+
activeHighlightForeground: "#ffeee4",
|
|
131
|
+
promptCursorBackground: "#d2764a",
|
|
132
|
+
promptCursorForeground: "#201e1f",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export default duroxideTheme;
|
|
@@ -17,6 +17,7 @@ import msDosTheme from "./ms-dos.js";
|
|
|
17
17
|
import doomTheme from "./doom.js";
|
|
18
18
|
import winampTheme from "./winamp.js";
|
|
19
19
|
import rustTheme from "./rust.js";
|
|
20
|
+
import duroxideTheme from "./duroxide.js";
|
|
20
21
|
import spongebobTheme from "./spongebob.js";
|
|
21
22
|
|
|
22
23
|
// Themes are ordered by GROUP first, then alphabetically inside it — the
|
|
@@ -54,6 +55,7 @@ const THEMES = Object.freeze([
|
|
|
54
55
|
doomTheme,
|
|
55
56
|
winampTheme,
|
|
56
57
|
rustTheme,
|
|
58
|
+
duroxideTheme,
|
|
57
59
|
spongebobTheme,
|
|
58
60
|
].sort(byGroupThenLabel));
|
|
59
61
|
|