pilotswarm-web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +144 -0
  2. package/auth/authz/engine.js +139 -0
  3. package/auth/config.js +110 -0
  4. package/auth/index.js +153 -0
  5. package/auth/normalize/entra.js +22 -0
  6. package/auth/providers/entra.js +76 -0
  7. package/auth/providers/none.js +24 -0
  8. package/auth.js +10 -0
  9. package/bin/serve.js +53 -0
  10. package/config.js +20 -0
  11. package/dist/app.js +469 -0
  12. package/dist/assets/index-BSVg-lGb.css +1 -0
  13. package/dist/assets/index-BXD5YP7A.js +24 -0
  14. package/dist/assets/msal-CytV9RFv.js +7 -0
  15. package/dist/assets/pilotswarm-WX3NED6m.js +40 -0
  16. package/dist/assets/react-jg0oazEi.js +1 -0
  17. package/dist/index.html +16 -0
  18. package/node_modules/pilotswarm-ui-core/README.md +6 -0
  19. package/node_modules/pilotswarm-ui-core/package.json +32 -0
  20. package/node_modules/pilotswarm-ui-core/src/commands.js +72 -0
  21. package/node_modules/pilotswarm-ui-core/src/context-usage.js +212 -0
  22. package/node_modules/pilotswarm-ui-core/src/controller.js +3613 -0
  23. package/node_modules/pilotswarm-ui-core/src/formatting.js +872 -0
  24. package/node_modules/pilotswarm-ui-core/src/history.js +571 -0
  25. package/node_modules/pilotswarm-ui-core/src/index.js +13 -0
  26. package/node_modules/pilotswarm-ui-core/src/layout.js +196 -0
  27. package/node_modules/pilotswarm-ui-core/src/reducer.js +1027 -0
  28. package/node_modules/pilotswarm-ui-core/src/selectors.js +2786 -0
  29. package/node_modules/pilotswarm-ui-core/src/session-tree.js +109 -0
  30. package/node_modules/pilotswarm-ui-core/src/state.js +80 -0
  31. package/node_modules/pilotswarm-ui-core/src/store.js +23 -0
  32. package/node_modules/pilotswarm-ui-core/src/system-titles.js +24 -0
  33. package/node_modules/pilotswarm-ui-core/src/themes/catppuccin-mocha.js +56 -0
  34. package/node_modules/pilotswarm-ui-core/src/themes/cobalt2.js +56 -0
  35. package/node_modules/pilotswarm-ui-core/src/themes/dark-high-contrast.js +56 -0
  36. package/node_modules/pilotswarm-ui-core/src/themes/dracula.js +56 -0
  37. package/node_modules/pilotswarm-ui-core/src/themes/github-dark.js +56 -0
  38. package/node_modules/pilotswarm-ui-core/src/themes/gruvbox-dark.js +56 -0
  39. package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-matrix.js +56 -0
  40. package/node_modules/pilotswarm-ui-core/src/themes/hacker-x-orion-prime.js +56 -0
  41. package/node_modules/pilotswarm-ui-core/src/themes/helpers.js +77 -0
  42. package/node_modules/pilotswarm-ui-core/src/themes/index.js +42 -0
  43. package/node_modules/pilotswarm-ui-core/src/themes/noctis-viola.js +56 -0
  44. package/node_modules/pilotswarm-ui-core/src/themes/noctis.js +56 -0
  45. package/node_modules/pilotswarm-ui-core/src/themes/nord.js +56 -0
  46. package/node_modules/pilotswarm-ui-core/src/themes/solarized-dark.js +56 -0
  47. package/node_modules/pilotswarm-ui-core/src/themes/tokyo-night.js +56 -0
  48. package/node_modules/pilotswarm-ui-react/README.md +5 -0
  49. package/node_modules/pilotswarm-ui-react/package.json +36 -0
  50. package/node_modules/pilotswarm-ui-react/src/components.js +1316 -0
  51. package/node_modules/pilotswarm-ui-react/src/index.js +4 -0
  52. package/node_modules/pilotswarm-ui-react/src/platform.js +15 -0
  53. package/node_modules/pilotswarm-ui-react/src/use-controller-state.js +38 -0
  54. package/node_modules/pilotswarm-ui-react/src/web-app.js +2661 -0
  55. package/package.json +64 -0
  56. package/runtime.js +146 -0
  57. package/server.js +311 -0
@@ -0,0 +1,1027 @@
1
+ import { buildSessionTree } from "./session-tree.js";
2
+ import { FOCUS_REGIONS } from "./commands.js";
3
+ import { DEFAULT_HISTORY_EVENT_LIMIT, dedupeChatMessages } from "./history.js";
4
+ import { getPromptInputRows } from "./layout.js";
5
+
6
+ function cloneHistoryMap(historyMap) {
7
+ return new Map(historyMap);
8
+ }
9
+
10
+ function cloneCollapsedIds(collapsedIds) {
11
+ return new Set(collapsedIds);
12
+ }
13
+
14
+ function cloneOrderById(orderById) {
15
+ return { ...(orderById || {}) };
16
+ }
17
+
18
+ function cloneFilesBySessionId(bySessionId) {
19
+ return { ...(bySessionId || {}) };
20
+ }
21
+
22
+ function cloneOrchestrationBySessionId(bySessionId) {
23
+ return { ...(bySessionId || {}) };
24
+ }
25
+
26
+ function normalizeFilesFilter(filter) {
27
+ return {
28
+ scope: filter?.scope === "allSessions" ? "allSessions" : "selectedSession",
29
+ query: typeof filter?.query === "string" ? filter.query : "",
30
+ };
31
+ }
32
+
33
+ function normalizeLogEntries(entries) {
34
+ const list = Array.isArray(entries) ? entries.filter(Boolean) : [];
35
+ return list.slice(-1000);
36
+ }
37
+
38
+ function normalizeFullscreenPane(fullscreenPane) {
39
+ return [
40
+ FOCUS_REGIONS.SESSIONS,
41
+ FOCUS_REGIONS.CHAT,
42
+ FOCUS_REGIONS.INSPECTOR,
43
+ FOCUS_REGIONS.ACTIVITY,
44
+ ].includes(fullscreenPane)
45
+ ? fullscreenPane
46
+ : null;
47
+ }
48
+
49
+ function clampHistoryItems(items, maxItems) {
50
+ const list = Array.isArray(items) ? items.filter(Boolean) : [];
51
+ const safeMax = Math.max(DEFAULT_HISTORY_EVENT_LIMIT, Number(maxItems) || DEFAULT_HISTORY_EVENT_LIMIT);
52
+ return list.length > safeMax ? list.slice(-safeMax) : list;
53
+ }
54
+
55
+ function areStructuredValuesEqual(left, right) {
56
+ if (Object.is(left, right)) return true;
57
+ if (Array.isArray(left) || Array.isArray(right)) {
58
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
59
+ for (let index = 0; index < left.length; index += 1) {
60
+ if (!areStructuredValuesEqual(left[index], right[index])) return false;
61
+ }
62
+ return true;
63
+ }
64
+ if (!left || !right || typeof left !== "object" || typeof right !== "object") {
65
+ return false;
66
+ }
67
+ const leftKeys = Object.keys(left);
68
+ const rightKeys = Object.keys(right);
69
+ if (leftKeys.length !== rightKeys.length) return false;
70
+ for (const key of leftKeys) {
71
+ if (!Object.prototype.hasOwnProperty.call(right, key)) return false;
72
+ if (!areStructuredValuesEqual(left[key], right[key])) return false;
73
+ }
74
+ return true;
75
+ }
76
+
77
+ function mergeDefinedSessionFields(previousSession = {}, nextSession = {}) {
78
+ let merged = previousSession || {};
79
+ for (const [key, value] of Object.entries(nextSession || {})) {
80
+ if (value === undefined) continue;
81
+ if (areStructuredValuesEqual(previousSession?.[key], value)) continue;
82
+ if (merged === previousSession) {
83
+ merged = { ...(previousSession || {}) };
84
+ }
85
+ merged[key] = value;
86
+ }
87
+ return merged;
88
+ }
89
+
90
+ function pickDefaultActiveSessionId(sessions = []) {
91
+ const firstNonSystem = (sessions || []).find((session) => session?.sessionId && !session.isSystem);
92
+ return firstNonSystem?.sessionId || null;
93
+ }
94
+
95
+ function assignStableSessionOrder(previousOrderById = {}, nextOrderOrdinal = 0, sessions = []) {
96
+ const orderById = cloneOrderById(previousOrderById);
97
+ let orderOrdinal = Number.isFinite(nextOrderOrdinal) ? nextOrderOrdinal : 0;
98
+
99
+ for (const session of sessions || []) {
100
+ const sessionId = session?.sessionId;
101
+ if (!sessionId) continue;
102
+ if (typeof orderById[sessionId] === "number") continue;
103
+ orderById[sessionId] = orderOrdinal;
104
+ orderOrdinal += 1;
105
+ }
106
+
107
+ return {
108
+ orderById,
109
+ nextOrderOrdinal: orderOrdinal,
110
+ };
111
+ }
112
+
113
+ function clampPromptCursor(prompt, cursor, fallback = null) {
114
+ const text = String(prompt || "");
115
+ const preferred = Number.isFinite(cursor)
116
+ ? cursor
117
+ : (Number.isFinite(fallback) ? fallback : text.length);
118
+ return Math.max(0, Math.min(preferred, text.length));
119
+ }
120
+
121
+ function normalizePromptAttachments(prompt, attachments) {
122
+ const safePrompt = String(prompt || "");
123
+ const list = Array.isArray(attachments) ? attachments.filter(Boolean) : [];
124
+ return list.filter((attachment) => {
125
+ const token = String(attachment?.token || "").trim();
126
+ return token && safePrompt.includes(token);
127
+ });
128
+ }
129
+
130
+ export function appReducer(state, action) {
131
+ switch (action.type) {
132
+ case "connection/ready":
133
+ return {
134
+ ...state,
135
+ connection: {
136
+ ...state.connection,
137
+ connected: true,
138
+ workersOnline: action.workersOnline ?? state.connection.workersOnline,
139
+ error: null,
140
+ },
141
+ ui: {
142
+ ...state.ui,
143
+ statusText: action.statusText ?? state.ui.statusText ?? "Ready",
144
+ },
145
+ };
146
+
147
+ case "connection/error":
148
+ return {
149
+ ...state,
150
+ connection: {
151
+ ...state.connection,
152
+ connected: false,
153
+ error: action.error,
154
+ },
155
+ ui: {
156
+ ...state.ui,
157
+ statusText: action.statusText || action.error || "Connection error",
158
+ },
159
+ };
160
+
161
+ case "ui/status":
162
+ return {
163
+ ...state,
164
+ ui: {
165
+ ...state.ui,
166
+ statusText: action.text,
167
+ },
168
+ };
169
+
170
+ case "ui/theme":
171
+ return {
172
+ ...state,
173
+ ui: {
174
+ ...state.ui,
175
+ themeId: action.themeId || state.ui.themeId,
176
+ },
177
+ };
178
+
179
+ case "ui/modal":
180
+ return {
181
+ ...state,
182
+ ui: {
183
+ ...state.ui,
184
+ modal: action.modal ?? null,
185
+ },
186
+ };
187
+
188
+ case "ui/viewport": {
189
+ const currentWidth = state.ui.layout?.viewportWidth ?? 120;
190
+ const currentHeight = state.ui.layout?.viewportHeight ?? 40;
191
+ const nextWidth = Math.max(40, action.width ?? currentWidth);
192
+ const nextHeight = Math.max(18, action.height ?? currentHeight);
193
+ if (nextWidth === currentWidth && nextHeight === currentHeight) {
194
+ return state;
195
+ }
196
+ return {
197
+ ...state,
198
+ ui: {
199
+ ...state.ui,
200
+ layout: {
201
+ ...(state.ui.layout || {}),
202
+ viewportWidth: nextWidth,
203
+ viewportHeight: nextHeight,
204
+ },
205
+ },
206
+ };
207
+ }
208
+
209
+ case "ui/paneAdjust":
210
+ return {
211
+ ...state,
212
+ ui: {
213
+ ...state.ui,
214
+ layout: {
215
+ ...(state.ui.layout || {}),
216
+ paneAdjust: Number(action.paneAdjust) || 0,
217
+ },
218
+ },
219
+ };
220
+
221
+ case "ui/sessionPaneAdjust":
222
+ return {
223
+ ...state,
224
+ ui: {
225
+ ...state.ui,
226
+ layout: {
227
+ ...(state.ui.layout || {}),
228
+ sessionPaneAdjust: Number(action.sessionPaneAdjust) || 0,
229
+ },
230
+ },
231
+ };
232
+
233
+ case "ui/focus":
234
+ return {
235
+ ...state,
236
+ ui: {
237
+ ...state.ui,
238
+ focusRegion: action.focusRegion,
239
+ },
240
+ };
241
+
242
+ case "sessions/filterQuery":
243
+ return {
244
+ ...state,
245
+ sessions: {
246
+ ...state.sessions,
247
+ filterQuery: typeof action.query === "string" ? action.query : "",
248
+ },
249
+ };
250
+
251
+ case "ui/modalSelection": {
252
+ const modal = state.ui.modal;
253
+ if (!modal || !Array.isArray(modal.items) || modal.items.length === 0) {
254
+ return state;
255
+ }
256
+ const nextIndex = Math.max(0, Math.min(action.index ?? 0, modal.items.length - 1));
257
+ return {
258
+ ...state,
259
+ ui: {
260
+ ...state.ui,
261
+ modal: {
262
+ ...modal,
263
+ selectedIndex: nextIndex,
264
+ },
265
+ },
266
+ };
267
+ }
268
+
269
+ case "ui/scroll":
270
+ return {
271
+ ...state,
272
+ ui: {
273
+ ...state.ui,
274
+ scroll: {
275
+ ...state.ui.scroll,
276
+ [action.pane]: Math.max(0, action.offset ?? 0),
277
+ },
278
+ },
279
+ };
280
+
281
+ case "ui/inspectorTab":
282
+ return {
283
+ ...state,
284
+ ui: {
285
+ ...state.ui,
286
+ inspectorTab: action.inspectorTab,
287
+ fullscreenPane: action.inspectorTab === "files" && state.ui.fullscreenPane === FOCUS_REGIONS.INSPECTOR
288
+ ? null
289
+ : state.ui.fullscreenPane,
290
+ scroll: {
291
+ ...state.ui.scroll,
292
+ inspector: 0,
293
+ },
294
+ },
295
+ files: action.inspectorTab === "files"
296
+ ? state.files
297
+ : {
298
+ ...state.files,
299
+ fullscreen: false,
300
+ },
301
+ };
302
+
303
+ case "ui/prompt":
304
+ return {
305
+ ...state,
306
+ ui: {
307
+ ...state.ui,
308
+ prompt: action.prompt,
309
+ promptCursor: clampPromptCursor(action.prompt, action.promptCursor, state.ui.promptCursor),
310
+ promptRows: getPromptInputRows(action.prompt),
311
+ promptAttachments: normalizePromptAttachments(action.prompt, state.ui.promptAttachments),
312
+ },
313
+ };
314
+
315
+ case "ui/promptAttachments":
316
+ return {
317
+ ...state,
318
+ ui: {
319
+ ...state.ui,
320
+ promptAttachments: normalizePromptAttachments(
321
+ state.ui.prompt,
322
+ action.attachments,
323
+ ),
324
+ },
325
+ };
326
+
327
+ case "sessions/loaded": {
328
+ const byId = {};
329
+ let anyChanged = false;
330
+ for (const session of action.sessions) {
331
+ const previous = state.sessions.byId[session.sessionId];
332
+ const merged = mergeDefinedSessionFields(previous, session);
333
+ byId[session.sessionId] = merged;
334
+ if (!anyChanged && merged !== previous) anyChanged = true;
335
+ }
336
+ if (
337
+ state.sessions.activeSessionId
338
+ && state.sessions.byId[state.sessions.activeSessionId]
339
+ && !byId[state.sessions.activeSessionId]
340
+ ) {
341
+ byId[state.sessions.activeSessionId] = {
342
+ ...state.sessions.byId[state.sessions.activeSessionId],
343
+ };
344
+ anyChanged = true;
345
+ }
346
+ // Check if session set changed (added/removed)
347
+ const prevIds = Object.keys(state.sessions.byId);
348
+ const nextIds = Object.keys(byId);
349
+ if (prevIds.length !== nextIds.length) anyChanged = true;
350
+ if (!anyChanged) {
351
+ for (const id of nextIds) {
352
+ if (!state.sessions.byId[id]) { anyChanged = true; break; }
353
+ }
354
+ }
355
+ if (!anyChanged) return state;
356
+ const mergedSessions = Object.values(byId);
357
+ const {
358
+ orderById,
359
+ nextOrderOrdinal,
360
+ } = assignStableSessionOrder(
361
+ state.sessions.orderById,
362
+ state.sessions.nextOrderOrdinal,
363
+ mergedSessions,
364
+ );
365
+ const collapsedIds = cloneCollapsedIds(state.sessions.collapsedIds);
366
+ const previousParentIds = new Set(
367
+ Object.values(state.sessions.byId)
368
+ .map((session) => session.parentSessionId)
369
+ .filter((sessionId) => Boolean(sessionId)),
370
+ );
371
+ const parentIds = new Set(
372
+ mergedSessions
373
+ .map((session) => session.parentSessionId)
374
+ .filter((sessionId) => Boolean(sessionId)),
375
+ );
376
+ for (const sessionId of parentIds) {
377
+ if (!previousParentIds.has(sessionId)) {
378
+ collapsedIds.add(sessionId);
379
+ }
380
+ }
381
+ const flat = buildSessionTree(mergedSessions, collapsedIds, orderById);
382
+ const activeSessionId = state.sessions.activeSessionId && flat.some((entry) => entry.sessionId === state.sessions.activeSessionId)
383
+ ? state.sessions.activeSessionId
384
+ : pickDefaultActiveSessionId(mergedSessions);
385
+ return {
386
+ ...state,
387
+ sessions: {
388
+ ...state.sessions,
389
+ byId,
390
+ collapsedIds,
391
+ flat,
392
+ activeSessionId,
393
+ orderById,
394
+ nextOrderOrdinal,
395
+ },
396
+ };
397
+ }
398
+
399
+ case "sessions/merged": {
400
+ if (!action.session?.sessionId) return state;
401
+ const previousSession = state.sessions.byId[action.session.sessionId];
402
+ const mergedSession = mergeDefinedSessionFields(previousSession, action.session);
403
+ if (mergedSession === previousSession) return state;
404
+ const byId = {
405
+ ...state.sessions.byId,
406
+ [action.session.sessionId]: mergedSession,
407
+ };
408
+ const {
409
+ orderById,
410
+ nextOrderOrdinal,
411
+ } = assignStableSessionOrder(
412
+ state.sessions.orderById,
413
+ state.sessions.nextOrderOrdinal,
414
+ Object.values(byId),
415
+ );
416
+ return {
417
+ ...state,
418
+ sessions: {
419
+ ...state.sessions,
420
+ byId,
421
+ flat: buildSessionTree(Object.values(byId), state.sessions.collapsedIds, orderById),
422
+ orderById,
423
+ nextOrderOrdinal,
424
+ },
425
+ };
426
+ }
427
+
428
+ case "sessions/selected":
429
+ return {
430
+ ...state,
431
+ sessions: {
432
+ ...state.sessions,
433
+ activeSessionId: action.sessionId,
434
+ },
435
+ ui: {
436
+ ...state.ui,
437
+ scroll: {
438
+ ...state.ui.scroll,
439
+ chat: 0,
440
+ inspector: 0,
441
+ activity: 0,
442
+ },
443
+ },
444
+ };
445
+
446
+ case "ui/fullscreenPane": {
447
+ const fullscreenPane = normalizeFullscreenPane(action.fullscreenPane);
448
+ return {
449
+ ...state,
450
+ files: fullscreenPane
451
+ ? {
452
+ ...state.files,
453
+ fullscreen: false,
454
+ }
455
+ : state.files,
456
+ ui: {
457
+ ...state.ui,
458
+ fullscreenPane,
459
+ focusRegion: fullscreenPane || state.ui.focusRegion,
460
+ },
461
+ };
462
+ }
463
+
464
+ case "sessions/collapse": {
465
+ const collapsedIds = cloneCollapsedIds(state.sessions.collapsedIds);
466
+ collapsedIds.add(action.sessionId);
467
+ return {
468
+ ...state,
469
+ sessions: {
470
+ ...state.sessions,
471
+ collapsedIds,
472
+ flat: buildSessionTree(Object.values(state.sessions.byId), collapsedIds, state.sessions.orderById),
473
+ },
474
+ };
475
+ }
476
+
477
+ case "sessions/expand": {
478
+ const collapsedIds = cloneCollapsedIds(state.sessions.collapsedIds);
479
+ collapsedIds.delete(action.sessionId);
480
+ return {
481
+ ...state,
482
+ sessions: {
483
+ ...state.sessions,
484
+ collapsedIds,
485
+ flat: buildSessionTree(Object.values(state.sessions.byId), collapsedIds, state.sessions.orderById),
486
+ },
487
+ };
488
+ }
489
+
490
+ case "history/set": {
491
+ const previousHistory = state.history.bySessionId.get(action.sessionId) || null;
492
+ const previousChat = previousHistory?.chat || [];
493
+ const loadedEventLimit = Math.max(
494
+ DEFAULT_HISTORY_EVENT_LIMIT,
495
+ Number(action.history?.loadedEventLimit ?? previousHistory?.loadedEventLimit ?? DEFAULT_HISTORY_EVENT_LIMIT) || DEFAULT_HISTORY_EVENT_LIMIT,
496
+ );
497
+ const nextChat = clampHistoryItems(dedupeChatMessages(action.history?.chat || []), loadedEventLimit);
498
+ const previousLastChatId = previousChat[previousChat.length - 1]?.id || null;
499
+ const nextLastChatId = nextChat[nextChat.length - 1]?.id || null;
500
+ const activeChatUpdated = action.sessionId === state.sessions.activeSessionId
501
+ && nextLastChatId !== previousLastChatId;
502
+ const nextHistory = cloneHistoryMap(state.history.bySessionId);
503
+ nextHistory.set(action.sessionId, {
504
+ ...(action.history || {}),
505
+ chat: nextChat,
506
+ activity: clampHistoryItems(action.history?.activity || [], loadedEventLimit),
507
+ events: clampHistoryItems(action.history?.events || [], loadedEventLimit),
508
+ loadedEventLimit,
509
+ });
510
+ return {
511
+ ...state,
512
+ history: {
513
+ ...state.history,
514
+ bySessionId: nextHistory,
515
+ },
516
+ ui: activeChatUpdated
517
+ ? {
518
+ ...state.ui,
519
+ scroll: {
520
+ ...state.ui.scroll,
521
+ chat: 0,
522
+ },
523
+ }
524
+ : state.ui,
525
+ };
526
+ }
527
+
528
+ case "history/evict": {
529
+ const ids = Array.isArray(action.sessionIds) ? action.sessionIds : [];
530
+ if (ids.length === 0) return state;
531
+ const nextHistory = cloneHistoryMap(state.history.bySessionId);
532
+ for (const id of ids) nextHistory.delete(id);
533
+ return {
534
+ ...state,
535
+ history: {
536
+ ...state.history,
537
+ bySessionId: nextHistory,
538
+ },
539
+ };
540
+ }
541
+
542
+ case "orchestration/statsLoading": {
543
+ const bySessionId = cloneOrchestrationBySessionId(state.orchestration.bySessionId);
544
+ bySessionId[action.sessionId] = {
545
+ ...(bySessionId[action.sessionId] || {}),
546
+ loading: true,
547
+ error: null,
548
+ };
549
+ return {
550
+ ...state,
551
+ orchestration: {
552
+ ...state.orchestration,
553
+ bySessionId,
554
+ },
555
+ };
556
+ }
557
+
558
+ case "orchestration/statsLoaded": {
559
+ const bySessionId = cloneOrchestrationBySessionId(state.orchestration.bySessionId);
560
+ bySessionId[action.sessionId] = {
561
+ loading: false,
562
+ error: null,
563
+ fetchedAt: action.fetchedAt || Date.now(),
564
+ stats: action.stats || null,
565
+ };
566
+ return {
567
+ ...state,
568
+ orchestration: {
569
+ ...state.orchestration,
570
+ bySessionId,
571
+ },
572
+ };
573
+ }
574
+
575
+ case "orchestration/statsError": {
576
+ const bySessionId = cloneOrchestrationBySessionId(state.orchestration.bySessionId);
577
+ bySessionId[action.sessionId] = {
578
+ ...(bySessionId[action.sessionId] || {}),
579
+ loading: false,
580
+ error: action.error || "Failed to load orchestration stats",
581
+ fetchedAt: action.fetchedAt || Date.now(),
582
+ };
583
+ return {
584
+ ...state,
585
+ orchestration: {
586
+ ...state.orchestration,
587
+ bySessionId,
588
+ },
589
+ };
590
+ }
591
+
592
+ case "orchestration/evict": {
593
+ const ids = Array.isArray(action.sessionIds) ? action.sessionIds : [];
594
+ if (ids.length === 0) return state;
595
+ const bySessionId = cloneOrchestrationBySessionId(state.orchestration.bySessionId);
596
+ for (const id of ids) delete bySessionId[id];
597
+ return {
598
+ ...state,
599
+ orchestration: {
600
+ ...state.orchestration,
601
+ bySessionId,
602
+ },
603
+ };
604
+ }
605
+
606
+ case "executionHistory/loading": {
607
+ const bySessionId = { ...(state.executionHistory?.bySessionId || {}) };
608
+ bySessionId[action.sessionId] = {
609
+ ...(bySessionId[action.sessionId] || {}),
610
+ loading: true,
611
+ error: null,
612
+ };
613
+ return {
614
+ ...state,
615
+ executionHistory: { ...state.executionHistory, bySessionId },
616
+ };
617
+ }
618
+
619
+ case "executionHistory/loaded": {
620
+ const bySessionId = { ...(state.executionHistory?.bySessionId || {}) };
621
+ const rawEvents = action.events || [];
622
+ const MAX_EXECUTION_HISTORY_EVENTS = 1000;
623
+ const clampedEvents = rawEvents.length > MAX_EXECUTION_HISTORY_EVENTS
624
+ ? rawEvents.slice(-MAX_EXECUTION_HISTORY_EVENTS)
625
+ : rawEvents;
626
+ bySessionId[action.sessionId] = {
627
+ loading: false,
628
+ error: null,
629
+ fetchedAt: action.fetchedAt || Date.now(),
630
+ events: clampedEvents,
631
+ };
632
+ return {
633
+ ...state,
634
+ executionHistory: { ...state.executionHistory, bySessionId },
635
+ };
636
+ }
637
+
638
+ case "executionHistory/evict": {
639
+ const ids = Array.isArray(action.sessionIds) ? action.sessionIds : [];
640
+ if (ids.length === 0) return state;
641
+ const bySessionId = { ...(state.executionHistory?.bySessionId || {}) };
642
+ for (const id of ids) delete bySessionId[id];
643
+ return {
644
+ ...state,
645
+ executionHistory: { ...state.executionHistory, bySessionId },
646
+ };
647
+ }
648
+
649
+ case "executionHistory/error": {
650
+ const bySessionId = { ...(state.executionHistory?.bySessionId || {}) };
651
+ bySessionId[action.sessionId] = {
652
+ ...(bySessionId[action.sessionId] || {}),
653
+ loading: false,
654
+ error: action.error || "Failed to load execution history",
655
+ fetchedAt: action.fetchedAt || Date.now(),
656
+ };
657
+ return {
658
+ ...state,
659
+ executionHistory: { ...state.executionHistory, bySessionId },
660
+ };
661
+ }
662
+
663
+ case "executionHistory/format": {
664
+ return {
665
+ ...state,
666
+ executionHistory: {
667
+ ...state.executionHistory,
668
+ format: action.format || "pretty",
669
+ },
670
+ };
671
+ }
672
+
673
+ case "files/evictPreviews": {
674
+ const ids = Array.isArray(action.sessionIds) ? action.sessionIds : [];
675
+ if (ids.length === 0) return state;
676
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
677
+ for (const id of ids) {
678
+ if (bySessionId[id]) {
679
+ // Keep entries list (lightweight), drop heavy preview content
680
+ bySessionId[id] = {
681
+ ...bySessionId[id],
682
+ previews: {},
683
+ };
684
+ }
685
+ }
686
+ return {
687
+ ...state,
688
+ files: {
689
+ ...state.files,
690
+ bySessionId,
691
+ },
692
+ };
693
+ }
694
+
695
+ case "files/sessionLoading": {
696
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
697
+ const current = bySessionId[action.sessionId] || {
698
+ entries: [],
699
+ previews: {},
700
+ downloads: {},
701
+ selectedFilename: null,
702
+ };
703
+ bySessionId[action.sessionId] = {
704
+ ...current,
705
+ loading: true,
706
+ error: null,
707
+ };
708
+ return {
709
+ ...state,
710
+ files: {
711
+ ...state.files,
712
+ bySessionId,
713
+ },
714
+ };
715
+ }
716
+
717
+ case "files/sessionLoaded": {
718
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
719
+ const current = bySessionId[action.sessionId] || {
720
+ previews: {},
721
+ downloads: {},
722
+ };
723
+ const entries = Array.isArray(action.entries) ? [...action.entries] : [];
724
+ const selectedFilename = current.selectedFilename && entries.includes(current.selectedFilename)
725
+ ? current.selectedFilename
726
+ : (action.selectedFilename && entries.includes(action.selectedFilename)
727
+ ? action.selectedFilename
728
+ : (entries[0] || null));
729
+ bySessionId[action.sessionId] = {
730
+ ...current,
731
+ entries,
732
+ selectedFilename,
733
+ loading: false,
734
+ loaded: true,
735
+ error: null,
736
+ };
737
+ return {
738
+ ...state,
739
+ files: {
740
+ ...state.files,
741
+ bySessionId,
742
+ },
743
+ };
744
+ }
745
+
746
+ case "files/sessionError": {
747
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
748
+ const current = bySessionId[action.sessionId] || {
749
+ entries: [],
750
+ previews: {},
751
+ downloads: {},
752
+ selectedFilename: null,
753
+ };
754
+ bySessionId[action.sessionId] = {
755
+ ...current,
756
+ loading: false,
757
+ loaded: true,
758
+ error: action.error || "Failed to load files",
759
+ };
760
+ return {
761
+ ...state,
762
+ files: {
763
+ ...state.files,
764
+ bySessionId,
765
+ },
766
+ };
767
+ }
768
+
769
+ case "files/select": {
770
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
771
+ const current = bySessionId[action.sessionId] || {
772
+ entries: [],
773
+ previews: {},
774
+ downloads: {},
775
+ selectedFilename: null,
776
+ };
777
+ bySessionId[action.sessionId] = {
778
+ ...current,
779
+ selectedFilename: action.filename || null,
780
+ };
781
+ return {
782
+ ...state,
783
+ files: {
784
+ ...state.files,
785
+ bySessionId,
786
+ },
787
+ ui: {
788
+ ...state.ui,
789
+ scroll: {
790
+ ...state.ui.scroll,
791
+ filePreview: 0,
792
+ },
793
+ },
794
+ };
795
+ }
796
+
797
+ case "files/selectGlobal":
798
+ return {
799
+ ...state,
800
+ files: {
801
+ ...state.files,
802
+ selectedArtifactId: action.artifactId || null,
803
+ },
804
+ ui: {
805
+ ...state.ui,
806
+ scroll: {
807
+ ...state.ui.scroll,
808
+ filePreview: 0,
809
+ },
810
+ },
811
+ };
812
+
813
+ case "files/previewLoading": {
814
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
815
+ const current = bySessionId[action.sessionId] || {
816
+ entries: [],
817
+ previews: {},
818
+ downloads: {},
819
+ selectedFilename: action.filename || null,
820
+ };
821
+ const previews = {
822
+ ...(current.previews || {}),
823
+ [action.filename]: {
824
+ ...(current.previews?.[action.filename] || {}),
825
+ loading: true,
826
+ error: null,
827
+ },
828
+ };
829
+ bySessionId[action.sessionId] = {
830
+ ...current,
831
+ previews,
832
+ };
833
+ return {
834
+ ...state,
835
+ files: {
836
+ ...state.files,
837
+ bySessionId,
838
+ },
839
+ };
840
+ }
841
+
842
+ case "files/previewLoaded": {
843
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
844
+ const current = bySessionId[action.sessionId] || {
845
+ entries: [],
846
+ previews: {},
847
+ downloads: {},
848
+ selectedFilename: action.filename || null,
849
+ };
850
+ const previews = {
851
+ ...(current.previews || {}),
852
+ [action.filename]: {
853
+ loading: false,
854
+ error: null,
855
+ content: action.content || "",
856
+ contentType: action.contentType || "text/plain",
857
+ renderMode: action.renderMode || "text",
858
+ },
859
+ };
860
+ bySessionId[action.sessionId] = {
861
+ ...current,
862
+ previews,
863
+ };
864
+ return {
865
+ ...state,
866
+ files: {
867
+ ...state.files,
868
+ bySessionId,
869
+ },
870
+ };
871
+ }
872
+
873
+ case "files/previewError": {
874
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
875
+ const current = bySessionId[action.sessionId] || {
876
+ entries: [],
877
+ previews: {},
878
+ downloads: {},
879
+ selectedFilename: action.filename || null,
880
+ };
881
+ const previews = {
882
+ ...(current.previews || {}),
883
+ [action.filename]: {
884
+ ...(current.previews?.[action.filename] || {}),
885
+ loading: false,
886
+ error: action.error || "Failed to load file preview",
887
+ },
888
+ };
889
+ bySessionId[action.sessionId] = {
890
+ ...current,
891
+ previews,
892
+ };
893
+ return {
894
+ ...state,
895
+ files: {
896
+ ...state.files,
897
+ bySessionId,
898
+ },
899
+ };
900
+ }
901
+
902
+ case "files/downloaded": {
903
+ const bySessionId = cloneFilesBySessionId(state.files.bySessionId);
904
+ const current = bySessionId[action.sessionId] || {
905
+ entries: [],
906
+ previews: {},
907
+ downloads: {},
908
+ selectedFilename: action.filename || null,
909
+ };
910
+ const downloads = {
911
+ ...(current.downloads || {}),
912
+ [action.filename]: {
913
+ localPath: action.localPath || null,
914
+ downloadedAt: action.downloadedAt || Date.now(),
915
+ },
916
+ };
917
+ bySessionId[action.sessionId] = {
918
+ ...current,
919
+ downloads,
920
+ };
921
+ return {
922
+ ...state,
923
+ files: {
924
+ ...state.files,
925
+ bySessionId,
926
+ },
927
+ };
928
+ }
929
+
930
+ case "files/fullscreen":
931
+ return {
932
+ ...state,
933
+ files: {
934
+ ...state.files,
935
+ fullscreen: Boolean(action.fullscreen),
936
+ },
937
+ ui: Boolean(action.fullscreen)
938
+ ? {
939
+ ...state.ui,
940
+ focusRegion: "inspector",
941
+ fullscreenPane: null,
942
+ }
943
+ : state.ui,
944
+ };
945
+
946
+ case "files/filter":
947
+ return {
948
+ ...state,
949
+ files: {
950
+ ...state.files,
951
+ filter: {
952
+ ...normalizeFilesFilter(state.files.filter),
953
+ ...normalizeFilesFilter(action.filter),
954
+ },
955
+ ...(normalizeFilesFilter(action.filter).scope === "selectedSession"
956
+ ? {}
957
+ : {
958
+ selectedArtifactId: state.files.selectedArtifactId || null,
959
+ }),
960
+ },
961
+ ui: {
962
+ ...state.ui,
963
+ scroll: {
964
+ ...state.ui.scroll,
965
+ filePreview: 0,
966
+ },
967
+ },
968
+ };
969
+
970
+ case "logs/config":
971
+ return {
972
+ ...state,
973
+ logs: {
974
+ ...state.logs,
975
+ available: Boolean(action.available),
976
+ availabilityReason: action.availabilityReason || state.logs.availabilityReason,
977
+ },
978
+ };
979
+
980
+ case "logs/tailing":
981
+ return {
982
+ ...state,
983
+ logs: {
984
+ ...state.logs,
985
+ tailing: Boolean(action.tailing),
986
+ },
987
+ };
988
+
989
+ case "logs/filter":
990
+ return {
991
+ ...state,
992
+ logs: {
993
+ ...state.logs,
994
+ filter: {
995
+ ...state.logs.filter,
996
+ ...(action.filter || {}),
997
+ },
998
+ },
999
+ };
1000
+
1001
+ case "logs/set":
1002
+ return {
1003
+ ...state,
1004
+ logs: {
1005
+ ...state.logs,
1006
+ entries: normalizeLogEntries(action.entries),
1007
+ },
1008
+ };
1009
+
1010
+ case "logs/append": {
1011
+ const newEntries = (Array.isArray(action.entries) ? action.entries : [action.entry]).filter(Boolean);
1012
+ if (newEntries.length === 0) return state;
1013
+ const combined = [...(state.logs.entries || []), ...newEntries];
1014
+ const capped = combined.length > 1000 ? combined.slice(-1000) : combined;
1015
+ return {
1016
+ ...state,
1017
+ logs: {
1018
+ ...state.logs,
1019
+ entries: capped,
1020
+ },
1021
+ };
1022
+ }
1023
+
1024
+ default:
1025
+ return state;
1026
+ }
1027
+ }