pilotswarm 0.5.69 → 0.5.71

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 (80) hide show
  1. package/package.json +2 -2
  2. package/ui/core/src/controller.js +3 -1
  3. package/ui/core/src/history.js +3 -1
  4. package/ui/core/src/moa.js +2 -2
  5. package/ui/core/src/native-tasks.js +46 -2
  6. package/ui/core/src/reducer.js +14 -2
  7. package/ui/core/src/selectors.js +41 -16
  8. package/ui/core/src/session-sort.js +45 -0
  9. package/ui/core/src/state.js +3 -0
  10. package/ui/react/src/chat-call-line.js +17 -0
  11. package/ui/react/src/native-task-card.js +53 -23
  12. package/ui/react/src/web-app.js +100 -48
  13. package/web/dist/assets/{abnfDiagram-VRR7QNED-DCEEpIGf.js → abnfDiagram-VRR7QNED-C1caNr4U.js} +1 -1
  14. package/web/dist/assets/{arc-_RJw0dJ3.js → arc-BufdFJYK.js} +1 -1
  15. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Bze_PsYG.js → architectureDiagram-ZJ3FMSHR-C0vQblUK.js} +1 -1
  16. package/web/dist/assets/{blockDiagram-677ZJIJ3-CZaMO9H-.js → blockDiagram-677ZJIJ3-B6PYWI9u.js} +1 -1
  17. package/web/dist/assets/{c4Diagram-LMCZKHZV-Dzr58V1d.js → c4Diagram-LMCZKHZV-DFoaVeMm.js} +1 -1
  18. package/web/dist/assets/channel-CDzPwSI9.js +1 -0
  19. package/web/dist/assets/{chunk-2Q5K7J3B-1IO7uTFx.js → chunk-2Q5K7J3B-CxFBb-8y.js} +1 -1
  20. package/web/dist/assets/{chunk-32BRIVSS-DKdpRP0l.js → chunk-32BRIVSS-DO1G8AVc.js} +1 -1
  21. package/web/dist/assets/{chunk-5VM5RSS4-CxyhZOh5.js → chunk-5VM5RSS4-spE4bKSd.js} +1 -1
  22. package/web/dist/assets/{chunk-EX3LRPZG-DWY7moCs.js → chunk-EX3LRPZG-Bc_eUP5F.js} +1 -1
  23. package/web/dist/assets/{chunk-JWPE2WC7-By7oiTbd.js → chunk-JWPE2WC7-CGM2GAMX.js} +1 -1
  24. package/web/dist/assets/{chunk-MOJQB5TN-CS-m-Pv3.js → chunk-MOJQB5TN-CHdrVT1z.js} +1 -1
  25. package/web/dist/assets/{chunk-RYQCIY6F-Cxykz4-E.js → chunk-RYQCIY6F-BWJK7-AW.js} +1 -1
  26. package/web/dist/assets/{chunk-V7JOEXUC-Dwl0aU9m.js → chunk-V7JOEXUC-D8tgpQwK.js} +1 -1
  27. package/web/dist/assets/{chunk-VR4S4FIN-BERWiLR7.js → chunk-VR4S4FIN-BP86JfVu.js} +1 -1
  28. package/web/dist/assets/{chunk-XXDRQBXY-CZ0e721z.js → chunk-XXDRQBXY-Ctq_LUrs.js} +1 -1
  29. package/web/dist/assets/classDiagram-OUVF2IWQ-DdcABn-U.js +1 -0
  30. package/web/dist/assets/classDiagram-v2-EOCWNBFH-DdcABn-U.js +1 -0
  31. package/web/dist/assets/{cose-bilkent-JH36ORCC-RLgoKX1J.js → cose-bilkent-JH36ORCC-DNvC4thT.js} +1 -1
  32. package/web/dist/assets/{cynefin-VYW2F7L2-CUvcvCFo.js → cynefin-VYW2F7L2-3zVj2GvI.js} +1 -1
  33. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-D2ML_npa.js → cynefinDiagram-TSTJHNR4-DQbITfsJ.js} +1 -1
  34. package/web/dist/assets/{dagre-VKFMJZFB-BHG6-avE.js → dagre-VKFMJZFB-BkGAcDyj.js} +1 -1
  35. package/web/dist/assets/{diagram-FQU43EPY-ynOb8q0h.js → diagram-FQU43EPY-D3lsgelm.js} +1 -1
  36. package/web/dist/assets/{diagram-G47NLZAW-BLHAADd0.js → diagram-G47NLZAW-C2rO9e3d.js} +1 -1
  37. package/web/dist/assets/{diagram-NH7WQ7WH-DwOMd4DW.js → diagram-NH7WQ7WH-DJBwtT8q.js} +1 -1
  38. package/web/dist/assets/{diagram-OA4YK3LP-DZWnpqT9.js → diagram-OA4YK3LP-C-SAwEHZ.js} +1 -1
  39. package/web/dist/assets/{diagram-WEI45ONY-BMcvO3Pf.js → diagram-WEI45ONY-BpXMlmKz.js} +1 -1
  40. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-CRX9h0gP.js → ebnfDiagram-CCIWWBDH-DwxUgEmE.js} +1 -1
  41. package/web/dist/assets/{erDiagram-Q63AITRT-Con2S52S.js → erDiagram-Q63AITRT-DNZMCmYz.js} +1 -1
  42. package/web/dist/assets/{flowDiagram-23GEKE2U-wg4unvdR.js → flowDiagram-23GEKE2U-CUlGtnWV.js} +1 -1
  43. package/web/dist/assets/{ganttDiagram-NO4QXBWP-CAg65Xfj.js → ganttDiagram-NO4QXBWP-DCj6zkyt.js} +1 -1
  44. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-5u02gmfN.js → gitGraphDiagram-IHSO6WYX-Q6UP_dgi.js} +1 -1
  45. package/web/dist/assets/index-23y0f1it.js +24 -0
  46. package/web/dist/assets/index-BXR-II2L.css +1 -0
  47. package/web/dist/assets/{infoDiagram-FWYZ7A6U-DW830L4J.js → infoDiagram-FWYZ7A6U-Cwn8XNQT.js} +1 -1
  48. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-6Qr30UBj.js → ishikawaDiagram-FXEZZL3T-BaMvGBAo.js} +1 -1
  49. package/web/dist/assets/{journeyDiagram-5HDEW3XC-BHCjdAv1.js → journeyDiagram-5HDEW3XC-DK95FJ0t.js} +1 -1
  50. package/web/dist/assets/{kanban-definition-HUTT4EX6-PGpXvhY7.js → kanban-definition-HUTT4EX6-C6W6RJLm.js} +1 -1
  51. package/web/dist/assets/{linear-CuN5Qi_I.js → linear-Un37Oyxr.js} +1 -1
  52. package/web/dist/assets/{mermaid.core-BEG_GOf7.js → mermaid.core-i8uqvLPa.js} +4 -4
  53. package/web/dist/assets/{mindmap-definition-LN4V7U3C-Bbi2gb0f.js → mindmap-definition-LN4V7U3C-d_cgq1KW.js} +1 -1
  54. package/web/dist/assets/{papaparse.min-LDhOsJ62.js → papaparse.min-BiJEkQ8f.js} +1 -1
  55. package/web/dist/assets/{pegDiagram-2B236MQR-IZ3AowOt.js → pegDiagram-2B236MQR-5QRWM5AP.js} +1 -1
  56. package/web/dist/assets/{pieDiagram-ENE6RG2P-QnG3Rlhe.js → pieDiagram-ENE6RG2P-VIZXsjFV.js} +1 -1
  57. package/web/dist/assets/pilotswarm-D7Q48waO.js +109 -0
  58. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-BIB7xhGf.js → quadrantDiagram-ABIIQ3AL-DMOx841b.js} +1 -1
  59. package/web/dist/assets/{railroadDiagram-RFXS5EU6-CHbggWfd.js → railroadDiagram-RFXS5EU6-lTWzjdcd.js} +1 -1
  60. package/web/dist/assets/{requirementDiagram-TGXJPOKE-CgNGJVBn.js → requirementDiagram-TGXJPOKE-BE5SpiNy.js} +1 -1
  61. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-B1GfGCwn.js → sankeyDiagram-HTMAVEWB-C3LYWQTA.js} +1 -1
  62. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-D-Bfj-RU.js → sequenceDiagram-DBY2YBRQ-BTCp88tQ.js} +1 -1
  63. package/web/dist/assets/{sizeCapture-X5ZJPWSS-Ca-mVfMN.js → sizeCapture-X5ZJPWSS-Ppzj8n5l.js} +1 -1
  64. package/web/dist/assets/{stateDiagram-2N3HPSRC-5gIoJ_8A.js → stateDiagram-2N3HPSRC-Ch_iZ_cQ.js} +1 -1
  65. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-BbJN7t8_.js +1 -0
  66. package/web/dist/assets/{swimlanes-5IMT3BWC-HxN6_3nA.js → swimlanes-5IMT3BWC-BX4-Oeby.js} +2 -2
  67. package/web/dist/assets/swimlanesDiagram-G3AALYLV-0UA1VebA.js +8 -0
  68. package/web/dist/assets/{timeline-definition-FHXFAJF6-CSkNwYwY.js → timeline-definition-FHXFAJF6-B1l-6l6u.js} +1 -1
  69. package/web/dist/assets/{vennDiagram-L72KCM5P-o6qvAiNl.js → vennDiagram-L72KCM5P-Grpa6wFZ.js} +1 -1
  70. package/web/dist/assets/{wardleyDiagram-EHGQE667-HcUO3YZZ.js → wardleyDiagram-EHGQE667-B5bXnFlL.js} +1 -1
  71. package/web/dist/assets/{xychartDiagram-FW5EYKEG-DmNJJPi3.js → xychartDiagram-FW5EYKEG-BkELQtgc.js} +1 -1
  72. package/web/dist/index.html +3 -3
  73. package/web/dist/assets/channel-CHAZ3LVB.js +0 -1
  74. package/web/dist/assets/classDiagram-OUVF2IWQ-CXv3ioyD.js +0 -1
  75. package/web/dist/assets/classDiagram-v2-EOCWNBFH-CXv3ioyD.js +0 -1
  76. package/web/dist/assets/index-BT3MiUPE.css +0 -1
  77. package/web/dist/assets/index-DzjecAo8.js +0 -24
  78. package/web/dist/assets/pilotswarm-BNTnbu9f.js +0 -109
  79. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-C_IKdqOV.js +0 -1
  80. package/web/dist/assets/swimlanesDiagram-G3AALYLV-B8jNOz1z.js +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pilotswarm",
3
- "version": "0.5.69",
3
+ "version": "0.5.71",
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.69",
84
+ "pilotswarm-sdk": "0.5.71",
85
85
  "react": "^19.2.4",
86
86
  "react-dom": "^19.2.4",
87
87
  "ws": "^8.18.2"
@@ -7545,6 +7545,7 @@ export class PilotSwarmUiController {
7545
7545
  force = true,
7546
7546
  fullscreen = false,
7547
7547
  pane = false,
7548
+ preserveSession = false,
7548
7549
  } = {}) {
7549
7550
  const resolvedSessionId = sessionId || this.getState().sessions.activeSessionId;
7550
7551
  const name = String(filename || "").trim();
@@ -7558,7 +7559,7 @@ export class PilotSwarmUiController {
7558
7559
  restoreArtifactId: this.getState().files.selectedArtifactId || null,
7559
7560
  });
7560
7561
 
7561
- if (this.getState().sessions.activeSessionId !== resolvedSessionId) {
7562
+ if (!preserveSession && this.getState().sessions.activeSessionId !== resolvedSessionId) {
7562
7563
  await this.loadSession(resolvedSessionId).catch(() => null);
7563
7564
  }
7564
7565
  // The reader REPLACES the inspector, so it has no business changing
@@ -8996,6 +8997,7 @@ export class PilotSwarmUiController {
8996
8997
  return;
8997
8998
  }
8998
8999
 
9000
+ this.dispatch({ type: "sessions/used", sessionId });
8999
9001
  const activePendingQuestion = activeSession?.pendingQuestion || null;
9000
9002
  const answeringPendingQuestion = Boolean(activePendingQuestion?.question);
9001
9003
  const promptEdit = this.getPromptEditSessionMatch(sessionId);
@@ -3,7 +3,7 @@ import { isCanvasActionContent, parseCanvasActionContent } from "./canvas-action
3
3
  import { formatCompactionActivityRuns } from "./context-usage.js";
4
4
  import { canonicalSystemTitle } from "./system-titles.js";
5
5
  import { matchesSessionError } from "./session-warning.js";
6
- import { appendNativeTaskEvent } from "./native-tasks.js";
6
+ import { appendNativeTaskEvent, appendNativeTaskCall } from "./native-tasks.js";
7
7
  import { buildSessionWarning } from "./session-errors.js";
8
8
  import { appendChatCall, CHAT_CALL_EVENT_TYPES } from "./chat-activity.js";
9
9
 
@@ -21,6 +21,7 @@ export const HISTORY_EVENT_LIMIT_STEPS = [
21
21
  // load transcript pages instead of raw-stream pages.
22
22
  export const CHAT_HISTORY_EVENT_TYPES = [
23
23
  ...CHAT_CALL_EVENT_TYPES,
24
+ ...CHAT_CALL_EVENT_TYPES.filter(type => type !== "session.agent_spawned").map(type => `native.${type}`),
24
25
  "user.message",
25
26
  "assistant.message",
26
27
  // Needed to distinguish interim assistant output from the final answer
@@ -137,6 +138,7 @@ function appendSessionWarning(chat, event) {
137
138
 
138
139
  const parentChatCallTypes = new Set(CHAT_CALL_EVENT_TYPES);
139
140
  function appendParentChatCall(chat, event) {
141
+ if (appendNativeTaskCall(chat, event)) return;
140
142
  if (!parentChatCallTypes.has(event?.eventType)) return;
141
143
  const data = event?.data || {};
142
144
  // Native tasks have their own abridged disclosure. Generic call cards
@@ -42,7 +42,7 @@ export function normalizeMoa(value) {
42
42
  return safeLayout(dashboard, key, `MoA ${index + 1}`);
43
43
  });
44
44
  if (!dashboards.length) dashboards.push(safeLayout(null));
45
- return { version: 3, activeDashboardId: dashboards.some(d => d.id === value.activeDashboardId) ? value.activeDashboardId : dashboards[0].id, dashboards };
45
+ return { version: 3, composerMode: value?.composerMode === "shared" ? "shared" : "per-chat", activeDashboardId: dashboards.some(d => d.id === value.activeDashboardId) ? value.activeDashboardId : dashboards[0].id, dashboards };
46
46
  }
47
47
  let source = value;
48
48
  if (!(value?.version === 2 || (object(value) && Object.hasOwn(value, "tree")))) {
@@ -51,7 +51,7 @@ export function normalizeMoa(value) {
51
51
  const active = Number.isInteger(value?.activeSlot) ? Math.max(0, Math.min(4, value.activeSlot)) : 0;
52
52
  source = safeLayout(slots[active]).tree ? slots[active] : slots.find(slot => safeLayout(slot).tree);
53
53
  }
54
- return { version: 3, activeDashboardId: "moa-1", dashboards: [safeLayout(source)] };
54
+ return { version: 3, composerMode: value?.composerMode === "shared" ? "shared" : "per-chat", activeDashboardId: "moa-1", dashboards: [safeLayout(source)] };
55
55
  }
56
56
  export const activeMoaDashboard = value => value.dashboards.find(d => d.id === value.activeDashboardId) || value.dashboards[0];
57
57
  export function updateMoaDashboard(value, dashboardId, patch) {
@@ -1,3 +1,5 @@
1
+ import { appendChatCall, CHAT_CALL_EVENT_TYPES } from "./chat-activity.js";
2
+
1
3
  // Native Copilot work is local to one parent turn. Durable spawn_agent
2
4
  // sessions deliberately do not enter this projection.
3
5
  const TERMINAL = new Set(["completed", "failed", "cancelled", "interrupted"]);
@@ -61,6 +63,7 @@ function updateTask(chat, patch, event, { authoritative = false } = {}) {
61
63
  if (previous?.inferredTerminal) next.error = patch.error || "";
62
64
  }
63
65
  next.title = clip(next.title, 160) || "Native task";
66
+ if (next.title === "Native task" && previous?.title) next.title = previous.title;
64
67
  next.preview = clip(next.preview);
65
68
  next.result = clip(next.result, 4000);
66
69
  next.error = clip(next.error, 1000);
@@ -89,6 +92,47 @@ function updateTask(chat, patch, event, { authoritative = false } = {}) {
89
92
  }
90
93
  }
91
94
 
95
+ // Correlate by runtime identity, never by tool name or arrival time. External
96
+ // callbacks may arrive before the child event; move that existing disclosure
97
+ // under its owner as soon as the explicit relationship becomes available.
98
+ export function appendNativeTaskCall(chat, event) {
99
+ const type = (event?.eventType || "").replace(/^native\./, "");
100
+ if (!CHAT_CALL_EVENT_TYPES.includes(type) || type === "session.agent_spawned") return false;
101
+ const data = event.data || {};
102
+ const explicit = Boolean(data.nativeAgentId || data.parentToolCallId || event.eventType.startsWith("native."));
103
+ const keys = [data.toolCallId && `call:${data.toolCallId}`, data.requestId && `request:${data.requestId}`].filter(Boolean);
104
+ let match = explicit ? findTask(chat, { toolCallId: data.parentToolCallId, agentId: data.nativeAgentId }) : null;
105
+ const namespace = `${event.sessionId}:${data.durableSessionId || ""}`;
106
+ if (!match && !explicit) {
107
+ for (let g = chat.length - 1; g >= 0 && !match; g--) {
108
+ if (chat[g].kind !== "native-task-group") continue;
109
+ const t = chat[g].tasks.findIndex(task => task.calls?.some(call => call.namespace === namespace
110
+ && call.callKeys.some(key => keys.includes(key))));
111
+ if (t >= 0) match = { groupIndex: g, taskIndex: t };
112
+ }
113
+ }
114
+ if (!match && explicit && (data.parentToolCallId || data.nativeAgentId)) {
115
+ updateTask(chat, { toolCallId: data.parentToolCallId, agentId: data.nativeAgentId, status: "running" }, event);
116
+ match = findTask(chat, { toolCallId: data.parentToolCallId, agentId: data.nativeAgentId });
117
+ }
118
+ if (!match) return explicit; // Unknown child activity must not impersonate the parent.
119
+ const group = chat[match.groupIndex];
120
+ const task = group.tasks[match.taskIndex];
121
+ const calls = [...(task.calls || [])];
122
+ for (let i = chat.length - 1; i >= 0; i--) {
123
+ const call = chat[i];
124
+ if (call.kind !== "chat-call" || call.namespace !== namespace || !call.callKeys.some(key => keys.includes(key))) continue;
125
+ if (!calls.some(item => item.callKeys.some(key => call.callKeys.includes(key)))) calls.push(call);
126
+ chat.splice(i, 1);
127
+ }
128
+ appendChatCall(calls, { ...event, eventType: type });
129
+ const tasks = [...group.tasks];
130
+ tasks[match.taskIndex] = { ...task, calls, toolCalls: Math.max(task.toolCalls || 0, calls.length) };
131
+ // Removing a parent disclosure can shift the group's index.
132
+ chat[chat.indexOf(group)] = { ...group, tasks };
133
+ return true;
134
+ }
135
+
92
136
  export function appendNativeTaskEvent(chat, event) {
93
137
  const type = event?.eventType || "";
94
138
  const data = event?.data || {};
@@ -119,7 +163,7 @@ export function appendNativeTaskEvent(chat, event) {
119
163
  const previous = found ? chat[found.groupIndex].tasks[found.taskIndex] : null;
120
164
  if (!previous && !lifecycle && !taskTool) return;
121
165
  if (!previous || taskTool || type === "subagent.started") Object.assign(patch, {
122
- title: args.description || data.agentDisplayName || args.name || data.agentName || "Native task",
166
+ title: args.description || args.name || (previous?.title !== "Native task" ? previous?.title : null) || data.agentDisplayName || "Native task",
123
167
  profile: args.agent_type || data.agentName || data.agentType,
124
168
  model: data.model,
125
169
  });
@@ -145,7 +189,7 @@ export function appendNativeTaskEvent(chat, event) {
145
189
  } else if (type === "native.tool.execution_start") {
146
190
  const ids = previous.toolCallIds || [];
147
191
  patch.toolCallIds = data.toolCallId && !ids.includes(data.toolCallId) ? [...ids, data.toolCallId].slice(-500) : ids;
148
- patch.toolCalls = (previous.toolCalls || 0) + (patch.toolCallIds !== ids ? 1 : 0);
192
+ patch.toolCalls = Math.max(previous.toolCalls || 0, patch.toolCallIds.length);
149
193
  patch.preview = args.description || (data.toolName ? `Using ${data.toolName}` : "Working");
150
194
  } else if (type === "native.assistant.message") {
151
195
  // Do not expose reasoning, tool arguments, or the whole child transcript.
@@ -1,3 +1,4 @@
1
+ import { normalizeSessionSortMode, normalizeSessionUsage, reconcileSessionSort } from "./session-sort.js";
1
2
  import { normalizeMoa } from "./moa.js";
2
3
  import { retainSessionWarnings } from "./session-errors.js";
3
4
  import { buildSessionTree, isManuallyOrderableSession } from "./session-tree.js";
@@ -846,7 +847,7 @@ export function appReducer(state, action) {
846
847
  if (sessionId && contentUpdate && state.sessions?.goneIds?.includes(sessionId)) return state;
847
848
  const next = baseReducer(state, action);
848
849
  if (next === state) return next;
849
- return reconcileSessionView(state, next, action);
850
+ return reconcileSessionView(state, reconcileSessionSort(state, next, action), action);
850
851
  }
851
852
 
852
853
  // Per-session desktop views, kept in step with the ui slice.
@@ -1101,6 +1102,7 @@ function baseReducer(state, action) {
1101
1102
  const nextActiveSessionId = hasActive
1102
1103
  ? normalizeStoredActiveSessionId(settings.activeSessionId)
1103
1104
  : state.sessions.activeSessionId;
1105
+ const remoteUsage = normalizeSessionUsage(settings.sessionUsedAt);
1104
1106
  const nextSessions = {
1105
1107
  ...state.sessions,
1106
1108
  ...(hasOwnerFilter
@@ -1117,6 +1119,9 @@ function baseReducer(state, action) {
1117
1119
  : {}),
1118
1120
  pinnedIds: nextPinnedIds,
1119
1121
  manualOrder: nextManualOrder,
1122
+ sortMode: Object.hasOwn(settings, "sessionSortMode") ? normalizeSessionSortMode(settings.sessionSortMode) : state.sessions.sortMode,
1123
+ usedAt: Object.fromEntries([...new Set([...Object.keys(remoteUsage), ...Object.keys(state.sessions.usedAt || {})])]
1124
+ .map(id => [id, Math.max(remoteUsage[id] || 0, state.sessions.usedAt?.[id] || 0)])),
1120
1125
  collapsedIds: nextCollapsedIds,
1121
1126
  collapsedIdsExplicit: hasCollapsed ? true : state.sessions.collapsedIdsExplicit,
1122
1127
  activeSessionId: nextActiveSessionId,
@@ -1891,8 +1896,15 @@ function baseReducer(state, action) {
1891
1896
  };
1892
1897
  }
1893
1898
 
1899
+ case "sessions/sortMode":
1900
+ return { ...state, sessions: { ...state.sessions, sortMode: normalizeSessionSortMode(action.mode) } };
1901
+ case "sessions/refreshSort":
1902
+ return { ...state, sessions: { ...state.sessions } };
1903
+ case "sessions/used":
1904
+ if (!action.sessionId) return state;
1905
+ return { ...state, sessions: { ...state.sessions, usedAt: { ...state.sessions.usedAt, [action.sessionId]: action.at || Date.now() } } };
1894
1906
  case "sessions/selected": {
1895
- state = { ...state, sessions: { ...state.sessions, listDeselected: false, localSelectionMade: true } };
1907
+ state = { ...state, sessions: { ...state.sessions, listDeselected: false, localSelectionMade: true, usedAt: action.sessionId ? { ...state.sessions.usedAt, [action.sessionId]: Date.now() } : state.sessions.usedAt } };
1896
1908
  // Per-session chat scroll memory: stash both the outgoing offset
1897
1909
  // and whether it follows the bottom. A paused reading position
1898
1910
  // stays paused as new messages arrive and across session switches.
@@ -638,11 +638,20 @@ function distinctHumanOwnerCount(state, stopAt = Infinity) {
638
638
  return owners.size;
639
639
  }
640
640
 
641
+ const sessionOwnerDecorationCache = new WeakMap();
641
642
  function shouldDecorateSessionOwners(state) {
642
643
  // One rule: the chip earns its place when the list holds more than one
643
644
  // distinct human owner. Otherwise it is noise on every row — you are
644
645
  // always "you".
645
- return distinctHumanOwnerCount(state, 2) > 1;
646
+ const byId = state?.sessions?.byId;
647
+ if (!byId) return false;
648
+ const filter = state.sessions.ownerFilter;
649
+ const auth = state.auth;
650
+ const cached = sessionOwnerDecorationCache.get(byId);
651
+ if (cached && cached.filter === filter && cached.auth === auth) return cached.value;
652
+ const value = distinctHumanOwnerCount(state, 2) > 1;
653
+ sessionOwnerDecorationCache.set(byId, { filter, auth, value });
654
+ return value;
646
655
  }
647
656
 
648
657
  function groupMemberSessions(group, byId = {}) {
@@ -1293,6 +1302,7 @@ function matchesSearchQuery(value, query) {
1293
1302
  }
1294
1303
 
1295
1304
  const searchedSessionFlatCache = new WeakMap();
1305
+ const sessionSearchIndexCache = new WeakMap();
1296
1306
 
1297
1307
  function sessionGroupTitle(session, byId) {
1298
1308
  if (session?.isGroup) return session.title || "";
@@ -1320,20 +1330,17 @@ function sessionSearchOwner(session, byId) {
1320
1330
  return null;
1321
1331
  }
1322
1332
 
1323
- function buildSearchedSessionFlat(state, query) {
1324
- const existingFlat = Array.isArray(state.sessions?.flat) ? state.sessions.flat : [];
1325
- if (!query) return existingFlat;
1326
- const cached = searchedSessionFlatCache.get(existingFlat);
1327
- if (cached?.query === query) return cached.result;
1328
-
1333
+ function sessionSearchIndex(state) {
1329
1334
  const byId = state.sessions?.byId || {};
1330
- const parsed = parseSessionSearchQuery(query);
1335
+ const deps = [state.sessions?.orderById, state.sessions?.pinnedIds, (state.sessions?.sortMode && state.sessions.sortMode !== "saved" ? state.sessions.sortSnapshot : state.sessions?.manualOrder)];
1336
+ const cached = sessionSearchIndexCache.get(byId);
1337
+ if (cached && sameRowDeps(cached.deps, deps)) return cached;
1331
1338
  const expanded = buildSessionTree(
1332
1339
  Object.values(byId),
1333
1340
  new Set(),
1334
1341
  state.sessions?.orderById,
1335
1342
  state.sessions?.pinnedIds,
1336
- state.sessions?.manualOrder,
1343
+ (state.sessions?.sortMode && state.sessions.sortMode !== "saved" ? state.sessions.sortSnapshot : state.sessions?.manualOrder),
1337
1344
  );
1338
1345
  const parentById = new Map();
1339
1346
  const entryById = new Map();
@@ -1348,16 +1355,34 @@ function buildSearchedSessionFlat(state, query) {
1348
1355
  baseIndex.set(entry.sessionId, index);
1349
1356
  }
1350
1357
 
1358
+ const documents = new Map(expanded.map(entry => {
1359
+ const session = byId[entry.sessionId] || entry.standIn;
1360
+ return [entry.sessionId, buildSessionSearchDocument(session, {
1361
+ owner: sessionSearchOwner(session, byId), groupTitle: sessionGroupTitle(session, byId),
1362
+ })];
1363
+ }));
1364
+ const index = { deps, expanded, parentById, entryById, baseIndex, documents };
1365
+ sessionSearchIndexCache.set(byId, index);
1366
+ return index;
1367
+ }
1368
+
1369
+ function buildSearchedSessionFlat(state, query) {
1370
+ const existingFlat = Array.isArray(state.sessions?.flat) ? state.sessions.flat : [];
1371
+ if (!query) return existingFlat;
1372
+ const cached = searchedSessionFlatCache.get(existingFlat);
1373
+ const deps = [state.sessions?.byId, state.sessions?.ownerFilter, state.auth, state.sessions?.orderById,
1374
+ state.sessions?.pinnedIds, (state.sessions?.sortMode && state.sessions.sortMode !== "saved" ? state.sessions.sortSnapshot : state.sessions?.manualOrder), state.sessions?.filterExceptionId];
1375
+ if (cached?.query === query && sameRowDeps(cached.deps, deps)) return cached.result;
1376
+
1377
+ const byId = state.sessions?.byId || {};
1378
+ const parsed = parseSessionSearchQuery(query);
1379
+ const { expanded, parentById, entryById, baseIndex, documents } = sessionSearchIndex(state);
1380
+
1351
1381
  const directScores = new Map();
1352
1382
  for (const entry of expanded) {
1353
1383
  const session = byId[entry.sessionId] || entry.standIn;
1354
1384
  if (!session || !matchesOwnerFilter(session, state.sessions?.ownerFilter, state.auth || {}, byId)) continue;
1355
- const owner = sessionSearchOwner(session, byId);
1356
- const document = buildSessionSearchDocument(session, {
1357
- owner,
1358
- groupTitle: sessionGroupTitle(session, byId),
1359
- });
1360
- const score = scoreSessionSearchDocument(document, parsed);
1385
+ const score = scoreSessionSearchDocument(documents.get(entry.sessionId), parsed);
1361
1386
  if (score > 0) directScores.set(entry.sessionId, score);
1362
1387
  }
1363
1388
  const matchedIds = new Set(directScores.keys());
@@ -1414,7 +1439,7 @@ function buildSearchedSessionFlat(state, query) {
1414
1439
  // A user can type an unbounded number of distinct queries while the
1415
1440
  // catalog array keeps the same identity. Keep only the latest result for
1416
1441
  // that catalog rather than retaining every intermediate keystroke.
1417
- searchedSessionFlatCache.set(existingFlat, { query, result });
1442
+ searchedSessionFlatCache.set(existingFlat, { query, deps, result });
1418
1443
  return result;
1419
1444
  }
1420
1445
 
@@ -0,0 +1,45 @@
1
+ import { buildSessionTree } from "./session-tree.js";
2
+
3
+ export const normalizeSessionSortMode = mode => ["used", "updated"].includes(mode) ? mode : "saved";
4
+ export function normalizeSessionUsage(value) {
5
+ return Object.fromEntries(Object.entries(value || {}).filter(([id, at]) => id && Number.isFinite(at) && at > 0));
6
+ }
7
+ export function snapshotSessionOrder(sessions, mode) {
8
+ const saved = buildSessionTree(Object.values(sessions.byId), new Set(), sessions.orderById, sessions.pinnedIds, sessions.manualOrder);
9
+ const rank = new Map(saved.map((row, index) => [row.sessionId, index]));
10
+ const timestamp = session => {
11
+ if (mode === "used") return sessions.usedAt?.[session.sessionId] || 0;
12
+ const raw = session.updatedAt ?? session.summaryUpdatedAt ?? session.latestSummaryUpdatedAt;
13
+ const value = typeof raw === "number" ? raw : Date.parse(raw || "");
14
+ return Number.isFinite(value) ? value : 0;
15
+ };
16
+ return Object.values(sessions.byId).sort((a, b) => timestamp(b) - timestamp(a)
17
+ || (rank.get(a.sessionId) ?? Infinity) - (rank.get(b.sessionId) ?? Infinity))
18
+ .map(session => session.sessionId);
19
+ }
20
+
21
+ // Live catalog and usage changes update row contents, never existing positions.
22
+ // A mode change or explicit refresh captures the next order. New rows append.
23
+ export function reconcileSessionSort(previous, next, action) {
24
+ const sessions = next.sessions;
25
+ if (!sessions) return next;
26
+ const mode = normalizeSessionSortMode(sessions.sortMode);
27
+ const modeChanged = mode !== normalizeSessionSortMode(previous.sessions?.sortMode);
28
+ const refresh = action.type === "sessions/refreshSort";
29
+ const rebuild = sessions.flat !== previous.sessions?.flat || sessions.byId !== previous.sessions?.byId || modeChanged || refresh;
30
+ if (!rebuild) return next;
31
+ let sortSnapshot = sessions.sortSnapshot;
32
+ if (mode !== "saved") {
33
+ if (!sessions.listingSeen) sortSnapshot = [];
34
+ else if (modeChanged || refresh || !sortSnapshot?.length) sortSnapshot = snapshotSessionOrder(sessions, mode);
35
+ else {
36
+ const known = new Set(sortSnapshot);
37
+ const added = Object.keys(sessions.byId).filter(id => !known.has(id));
38
+ if (added.length) sortSnapshot = [...sortSnapshot, ...added];
39
+ }
40
+ }
41
+ const flat = mode === "saved" && !modeChanged ? sessions.flat : buildSessionTree(
42
+ Object.values(sessions.byId), sessions.collapsedIds, sessions.orderById,
43
+ sessions.pinnedIds, mode === "saved" ? sessions.manualOrder : sortSnapshot);
44
+ return { ...next, sessions: { ...sessions, sortSnapshot, flat } };
45
+ }
@@ -480,6 +480,9 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
480
480
  pinnedIds: normalizeStoredPinnedSessionIds(pinnedSessionIds),
481
481
  // Explicit user placement (desktop drag); read by every surface.
482
482
  manualOrder: normalizeStoredSessionOrder(sessionOrder),
483
+ sortMode: "saved",
484
+ sortSnapshot: [],
485
+ usedAt: {},
483
486
  // Whether a listing of each kind has been processed. "This row is
484
487
  // new" is only answerable against a previous listing OF THE SAME
485
488
  // KIND — inferring it from byId being non-empty was wrong, because
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+
3
+ export function ChatCallLine({ line }) {
4
+ const [open, setOpen] = React.useState(false);
5
+ return React.createElement("details", {
6
+ className: "ps-system-notice ps-chat-call",
7
+ "data-call-id": line.callKey,
8
+ onToggle: event => setOpen(event.currentTarget.open),
9
+ },
10
+ React.createElement("summary", { className: "ps-system-notice-summary ps-chat-call-summary" },
11
+ React.createElement("span", { className: "ps-chat-call-tag" }, line.category || "Tool"),
12
+ React.createElement("span", { className: "ps-system-notice-summary-text" }, line.text),
13
+ line.status ? React.createElement("span", { className: `ps-chat-call-status${line.status === "Failed" ? " is-failed" : ""}` }, line.status) : null),
14
+ open ? React.createElement("div", { className: "ps-system-notice-body" },
15
+ line.time ? React.createElement("div", { className: "ps-chat-call-time" }, line.time) : null,
16
+ React.createElement("pre", { className: "ps-chat-call-payload" }, line.body)) : null);
17
+ }
@@ -1,4 +1,6 @@
1
1
  import React from "react";
2
+ import { ChatCallLine } from "./chat-call-line.js";
3
+ import { chatCallLine } from "../../core/src/chat-activity.js";
2
4
  import { NATIVE_TASK_LABELS } from "../../core/src/native-tasks.js";
3
5
 
4
6
  const TERMINAL = new Set(["completed", "failed", "cancelled", "interrupted"]);
@@ -31,29 +33,57 @@ export const NativeTaskCard = React.memo(function NativeTaskCard({ group, colors
31
33
  React.createElement("span", { className: "ps-native-tasks-title" },
32
34
  React.createElement("span", { className: "ps-native-task-branch", "aria-hidden": true }, "⑂"), "Native tasks"),
33
35
  React.createElement("span", { className: "ps-native-tasks-count", "aria-live": "polite" }, countLabel)),
34
- tasks.map(task => {
35
- const color = colors[task.status] || colors.running;
36
- const preview = task.error || task.result || task.preview || (task.status === "completed" ? "Task completed." : "");
37
- const time = duration(task, now);
38
- return React.createElement("details", { key: task.id, className: "ps-native-task", "data-task-id": task.id, "data-status": task.status,
39
- style: { "--ps-native-task-accent": color } },
40
- React.createElement("summary", null,
41
- React.createElement("span", { className: "ps-native-task-mark", "aria-hidden": true }, MARKS[task.status] || "?"),
42
- React.createElement("span", { className: "ps-native-task-main" },
43
- React.createElement("span", { className: "ps-native-task-title" }, task.title),
44
- preview ? React.createElement("span", { className: "ps-native-task-preview" }, preview.replace(/\s+/g, " ").slice(0, 240)) : null),
45
- React.createElement("span", { className: "ps-native-task-meta" },
46
- React.createElement("span", { className: "ps-native-task-status" }, task.telemetryStale ? "Reconnecting" : NATIVE_TASK_LABELS[task.status] || task.status),
47
- React.createElement("span", null, [time, `${task.toolCalls || 0} calls`].filter(Boolean).join(" · "))),
48
- React.createElement("span", { className: "ps-native-task-chevron", "aria-hidden": true }, "›")),
49
- React.createElement("div", { className: "ps-native-task-detail" },
50
- React.createElement("div", { className: "ps-native-task-scope" }, [task.profile, task.model, task.reasoningEffort, "Same worker"].filter(Boolean).join(" · ")),
51
- task.result && !task.error ? React.createElement("div", { className: "ps-native-task-scope" }, "Result excerpt") : null,
52
- React.createElement("p", { className: "ps-native-task-result" }, task.error || task.result || task.preview
53
- || (task.status === "completed" ? "Task completed. A result excerpt is not available in this recording." : "No result available yet.")),
54
- task.recentActivity?.length ? React.createElement("ul", { className: "ps-native-task-activity" },
55
- task.recentActivity.map((activity, i) => React.createElement("li", { key: i }, activity.message))) : null));
56
- }),
36
+ tasks.map(task => React.createElement(NativeTask, { key: task.id, task, colors, now })),
57
37
  group.tasks.length > 3 ? React.createElement("button", { type: "button", className: "ps-native-tasks-more", "aria-expanded": showAll,
58
38
  onClick: () => setShowAll(value => !value) }, showAll ? "Show fewer tasks" : `Show ${group.tasks.length - 3} more tasks`) : null);
59
39
  });
40
+
41
+ function NativeTask({ task, colors, now }) {
42
+ const failed = ["failed", "cancelled", "interrupted"].includes(task.status);
43
+ const [open, setOpen] = React.useState(!TERMINAL.has(task.status) || failed);
44
+ const inspected = React.useRef(false);
45
+ const viewport = React.useRef(null);
46
+ const follow = React.useRef(true);
47
+ const calls = React.useMemo(() => (task.calls || []).map(chatCallLine), [task.calls]);
48
+ React.useLayoutEffect(() => {
49
+ if (open && viewport.current && follow.current) viewport.current.scrollTop = viewport.current.scrollHeight;
50
+ }, [open, calls.length]);
51
+ React.useEffect(() => {
52
+ if (failed || !inspected.current) setOpen(!TERMINAL.has(task.status) || failed);
53
+ }, [task.status, failed]);
54
+ const preview = task.error || task.result || task.preview || (task.status === "completed" ? "Task completed." : "");
55
+ const time = duration(task, now);
56
+ const profile = task.profile === "swarm-explore" ? "Explore" : task.profile === "swarm-task" ? "Task" : "Native task";
57
+ return React.createElement("details", {
58
+ className: "ps-native-task", "data-task-id": task.id, "data-status": task.status, open,
59
+ onToggle: event => { if (event.target === event.currentTarget) setOpen(event.currentTarget.open); },
60
+ style: { "--ps-native-task-accent": colors[task.status] || colors.running },
61
+ },
62
+ React.createElement("summary", {
63
+ onClick: () => { inspected.current = true; },
64
+ onKeyDown: event => { if (["Enter", " "].includes(event.key)) inspected.current = true; },
65
+ },
66
+ React.createElement("span", { className: "ps-native-task-mark", "aria-hidden": true }, MARKS[task.status] || "?"),
67
+ React.createElement("span", { className: "ps-native-task-main" },
68
+ React.createElement("span", { className: "ps-native-task-title" },
69
+ React.createElement("span", { className: "ps-native-task-profile" }, profile),
70
+ task.title === "Native task" ? "Working on delegated request" : task.title),
71
+ preview ? React.createElement("span", { className: "ps-native-task-preview" }, preview.replace(/\s+/g, " ").slice(0, 240)) : null),
72
+ React.createElement("span", { className: "ps-native-task-meta" },
73
+ React.createElement("span", { className: "ps-native-task-status" }, task.telemetryStale ? "Reconnecting" : NATIVE_TASK_LABELS[task.status] || task.status),
74
+ React.createElement("span", null, [time, `${task.toolCalls || 0} ${task.toolCalls === 1 ? "call" : "calls"}`].filter(Boolean).join(" · "))),
75
+ React.createElement("span", { className: "ps-native-task-chevron", "aria-hidden": true }, "›")),
76
+ React.createElement("div", { className: "ps-native-task-detail", onClick: () => { inspected.current = true; } },
77
+ React.createElement("div", { className: "ps-native-task-scope" }, [task.model, task.reasoningEffort, "Same worker"].filter(Boolean).join(" · ")),
78
+ calls.length ? React.createElement("div", {
79
+ ref: viewport, className: "ps-native-task-calls", role: "region", "aria-label": `Tool calls for ${task.title}`, tabIndex: 0,
80
+ onScroll: event => { const node = event.currentTarget; follow.current = node.scrollHeight - node.scrollTop - node.clientHeight <= 24; },
81
+ onWheel: event => { inspected.current = true; if (event.deltaY < 0) follow.current = false; },
82
+ onTouchStart: () => { inspected.current = true; follow.current = false; },
83
+ onFocus: () => { inspected.current = true; },
84
+ }, calls.map(line => React.createElement(ChatCallLine, { key: line.callKey, line }))) :
85
+ React.createElement("p", { className: "ps-native-task-scope" }, task.toolCalls ? "Tool details are not available in this recording." : "Waiting for task activity…"),
86
+ task.result || task.error ? React.createElement("p", { className: "ps-native-task-result" }, task.error || task.result) : null,
87
+ task.recentActivity?.length && !task.calls?.length ? React.createElement("ul", { className: "ps-native-task-activity" },
88
+ task.recentActivity.map((activity, i) => React.createElement("li", { key: i }, activity.message))) : null));
89
+ }