pilotswarm 0.5.63 → 0.5.64

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 (90) hide show
  1. package/mcp/dist/src/server.d.ts.map +1 -1
  2. package/mcp/dist/src/server.js +2 -0
  3. package/mcp/dist/src/server.js.map +1 -1
  4. package/mcp/dist/src/tools/features.d.ts +4 -0
  5. package/mcp/dist/src/tools/features.d.ts.map +1 -0
  6. package/mcp/dist/src/tools/features.js +35 -0
  7. package/mcp/dist/src/tools/features.js.map +1 -0
  8. package/package.json +2 -2
  9. package/tui/src/node-sdk-transport.js +13 -0
  10. package/ui/core/src/controller.js +276 -0
  11. package/ui/core/src/history.js +89 -14
  12. package/ui/core/src/native-tasks.js +197 -0
  13. package/ui/core/src/question-display.js +13 -0
  14. package/ui/core/src/reducer.js +18 -1
  15. package/ui/core/src/selectors.js +46 -13
  16. package/ui/core/src/session-errors.js +13 -6
  17. package/ui/core/src/session-warning.js +14 -0
  18. package/ui/core/src/state.js +1 -0
  19. package/ui/react/src/feature-flags-panel.js +186 -0
  20. package/ui/react/src/native-task-card.js +59 -0
  21. package/ui/react/src/web-app.js +105 -15
  22. package/web/dist/assets/{abnfDiagram-VRR7QNED-C8iAmIqt.js → abnfDiagram-VRR7QNED-gIPHF2gV.js} +1 -1
  23. package/web/dist/assets/{arc-Cg8qs17j.js → arc-DMW_q5Ml.js} +1 -1
  24. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-fYr0dRm3.js → architectureDiagram-ZJ3FMSHR-DAKsrxcB.js} +1 -1
  25. package/web/dist/assets/{blockDiagram-677ZJIJ3-Bpj79Dzq.js → blockDiagram-677ZJIJ3-BUjSLpP3.js} +1 -1
  26. package/web/dist/assets/{c4Diagram-LMCZKHZV-b26Lsm6m.js → c4Diagram-LMCZKHZV-Dw2nPoes.js} +1 -1
  27. package/web/dist/assets/channel-CcOa1LtY.js +1 -0
  28. package/web/dist/assets/{chunk-2Q5K7J3B-VLYQfjgG.js → chunk-2Q5K7J3B-BAL28Rv0.js} +1 -1
  29. package/web/dist/assets/{chunk-32BRIVSS-mVx--2Hg.js → chunk-32BRIVSS-DDlt9K8s.js} +1 -1
  30. package/web/dist/assets/{chunk-5VM5RSS4-jwnJmked.js → chunk-5VM5RSS4-BLQGJGoP.js} +1 -1
  31. package/web/dist/assets/{chunk-EX3LRPZG-DdfzAT1n.js → chunk-EX3LRPZG-XFMb3HRy.js} +1 -1
  32. package/web/dist/assets/{chunk-JWPE2WC7-BHm6Udd8.js → chunk-JWPE2WC7-DWnKTQ72.js} +1 -1
  33. package/web/dist/assets/{chunk-MOJQB5TN-Dn5qSVAS.js → chunk-MOJQB5TN-MltzhnwP.js} +1 -1
  34. package/web/dist/assets/{chunk-RYQCIY6F-D4IKMROn.js → chunk-RYQCIY6F-Ce3q0-mJ.js} +1 -1
  35. package/web/dist/assets/{chunk-V7JOEXUC-BhME3pRf.js → chunk-V7JOEXUC-ThXLYY88.js} +1 -1
  36. package/web/dist/assets/{chunk-VR4S4FIN-CHG_NBff.js → chunk-VR4S4FIN-DbhAykiW.js} +1 -1
  37. package/web/dist/assets/{chunk-XXDRQBXY-yw4QCgiL.js → chunk-XXDRQBXY-BpkNB0mD.js} +1 -1
  38. package/web/dist/assets/classDiagram-OUVF2IWQ-B2VhHRSV.js +1 -0
  39. package/web/dist/assets/classDiagram-v2-EOCWNBFH-B2VhHRSV.js +1 -0
  40. package/web/dist/assets/{cose-bilkent-JH36ORCC-DqwPj9Kx.js → cose-bilkent-JH36ORCC-rctqAPvj.js} +1 -1
  41. package/web/dist/assets/{cynefin-VYW2F7L2-CVokaaE_.js → cynefin-VYW2F7L2-Dg1cltg6.js} +1 -1
  42. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Bkhj9fuY.js → cynefinDiagram-TSTJHNR4-Ci23Sv4Z.js} +1 -1
  43. package/web/dist/assets/{dagre-VKFMJZFB-DVv-jnjb.js → dagre-VKFMJZFB-BpWTGhpv.js} +1 -1
  44. package/web/dist/assets/{diagram-FQU43EPY-DDlZjkrF.js → diagram-FQU43EPY-B69JpiAe.js} +1 -1
  45. package/web/dist/assets/{diagram-G47NLZAW-CNDUgqYa.js → diagram-G47NLZAW-BHMk1TOO.js} +1 -1
  46. package/web/dist/assets/{diagram-NH7WQ7WH-DywF8u8i.js → diagram-NH7WQ7WH-CtvZIIsk.js} +1 -1
  47. package/web/dist/assets/{diagram-OA4YK3LP-ZfN8nvLw.js → diagram-OA4YK3LP-BFvKh800.js} +1 -1
  48. package/web/dist/assets/{diagram-WEI45ONY-S94A6Avm.js → diagram-WEI45ONY-CgB8sWPe.js} +1 -1
  49. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-Mpt3QUnw.js → ebnfDiagram-CCIWWBDH-DyqPNydM.js} +1 -1
  50. package/web/dist/assets/{erDiagram-Q63AITRT-Bh-eWbdV.js → erDiagram-Q63AITRT-CwpvZQ3Q.js} +1 -1
  51. package/web/dist/assets/{flowDiagram-23GEKE2U-BwRtDI6V.js → flowDiagram-23GEKE2U-EBKSKxfC.js} +1 -1
  52. package/web/dist/assets/{ganttDiagram-NO4QXBWP-CrZoBmcB.js → ganttDiagram-NO4QXBWP-q5tMPAuC.js} +1 -1
  53. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-giXv1ynk.js → gitGraphDiagram-IHSO6WYX-DCRbi4KE.js} +1 -1
  54. package/web/dist/assets/{index-Br-gGWmc.js → index-BfF7e08d.js} +6 -6
  55. package/web/dist/assets/index-uhc851H3.css +1 -0
  56. package/web/dist/assets/{infoDiagram-FWYZ7A6U-H_5oc4-7.js → infoDiagram-FWYZ7A6U-Bex5pSIX.js} +1 -1
  57. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-BydiPDX0.js → ishikawaDiagram-FXEZZL3T-Dj0li_TY.js} +1 -1
  58. package/web/dist/assets/{journeyDiagram-5HDEW3XC-DS1HXms0.js → journeyDiagram-5HDEW3XC-BtEGEeVr.js} +1 -1
  59. package/web/dist/assets/{kanban-definition-HUTT4EX6-BjY5SyYu.js → kanban-definition-HUTT4EX6-BTFfZvYt.js} +1 -1
  60. package/web/dist/assets/{linear-BvOotjKL.js → linear-C3SQCsip.js} +1 -1
  61. package/web/dist/assets/{mermaid.core-0iRu7sp-.js → mermaid.core-DALDJDu8.js} +4 -4
  62. package/web/dist/assets/{mindmap-definition-LN4V7U3C-DB93zsvL.js → mindmap-definition-LN4V7U3C-BZf3uw0u.js} +1 -1
  63. package/web/dist/assets/{papaparse.min-f5Xncedc.js → papaparse.min-Bh9MZ-fk.js} +1 -1
  64. package/web/dist/assets/{pegDiagram-2B236MQR-Dh_U1cFA.js → pegDiagram-2B236MQR-PuOleiMg.js} +1 -1
  65. package/web/dist/assets/{pieDiagram-ENE6RG2P-Csv1qxMm.js → pieDiagram-ENE6RG2P-CcFORUaW.js} +1 -1
  66. package/web/dist/assets/pilotswarm-B9rLtXpI.js +109 -0
  67. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-Bo0pcrBM.js → quadrantDiagram-ABIIQ3AL-CEm0mp4y.js} +1 -1
  68. package/web/dist/assets/{railroadDiagram-RFXS5EU6-CHPvHNWG.js → railroadDiagram-RFXS5EU6-DeH-uX0_.js} +1 -1
  69. package/web/dist/assets/{requirementDiagram-TGXJPOKE-DgX8l-6H.js → requirementDiagram-TGXJPOKE-CMbJVMc1.js} +1 -1
  70. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-xR6MTs5a.js → sankeyDiagram-HTMAVEWB-C9gL5w_b.js} +1 -1
  71. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CD_45diB.js → sequenceDiagram-DBY2YBRQ-BUo8TT74.js} +1 -1
  72. package/web/dist/assets/{sizeCapture-X5ZJPWSS-BJVEXU0K.js → sizeCapture-X5ZJPWSS-C5lqG0ei.js} +1 -1
  73. package/web/dist/assets/{stateDiagram-2N3HPSRC-Bwu6kd23.js → stateDiagram-2N3HPSRC-DyeISrEU.js} +1 -1
  74. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-5FZbaloZ.js +1 -0
  75. package/web/dist/assets/{swimlanes-5IMT3BWC-BmhEzevT.js → swimlanes-5IMT3BWC-DawuBk_a.js} +2 -2
  76. package/web/dist/assets/swimlanesDiagram-G3AALYLV-DL8PgtmM.js +8 -0
  77. package/web/dist/assets/{timeline-definition-FHXFAJF6-DFPI_uTI.js → timeline-definition-FHXFAJF6-BZJ2YJcm.js} +1 -1
  78. package/web/dist/assets/{vennDiagram-L72KCM5P-DDTpTkBC.js → vennDiagram-L72KCM5P-BMXBVCfe.js} +1 -1
  79. package/web/dist/assets/{wardleyDiagram-EHGQE667-bV2_YVIL.js → wardleyDiagram-EHGQE667-DWedKsrQ.js} +1 -1
  80. package/web/dist/assets/{xychartDiagram-FW5EYKEG-C7bDIXAV.js → xychartDiagram-FW5EYKEG--XdfYceC.js} +1 -1
  81. package/web/dist/index.html +3 -3
  82. package/web/runtime.js +19 -0
  83. package/web/server.js +2 -2
  84. package/web/dist/assets/channel-C-ASbcRr.js +0 -1
  85. package/web/dist/assets/classDiagram-OUVF2IWQ-D2vlJIb1.js +0 -1
  86. package/web/dist/assets/classDiagram-v2-EOCWNBFH-D2vlJIb1.js +0 -1
  87. package/web/dist/assets/index-DRFDn87d.css +0 -1
  88. package/web/dist/assets/pilotswarm-DQNSWfNK.js +0 -105
  89. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-aypsN5jW.js +0 -1
  90. package/web/dist/assets/swimlanesDiagram-G3AALYLV-Duf5C4YK.js +0 -8
@@ -0,0 +1,197 @@
1
+ // Native Copilot work is local to one parent turn. Durable spawn_agent
2
+ // sessions deliberately do not enter this projection.
3
+ const TERMINAL = new Set(["completed", "failed", "cancelled", "interrupted"]);
4
+ const clip = (value, max = 240) => typeof value === "string" ? value.trim().slice(0, max) : "";
5
+ const timeMs = value => {
6
+ const ms = typeof value === "number" ? value : new Date(value).getTime();
7
+ return Number.isFinite(ms) && ms > 0 ? ms : null;
8
+ };
9
+ const resultText = value => typeof value === "string" ? value
10
+ : value?.detailedContent || value?.content || value?.textResultForLlm || value?.message || value?.error?.message || "";
11
+
12
+ export const NATIVE_TASK_LABELS = {
13
+ starting: "Starting", running: "Running", waiting: "Waiting", completed: "Done",
14
+ failed: "Failed", cancelled: "Cancelled", interrupted: "Interrupted",
15
+ };
16
+
17
+ function findTask(chat, task) {
18
+ for (let groupIndex = chat.length - 1; groupIndex >= 0; groupIndex--) {
19
+ const group = chat[groupIndex];
20
+ if (group.kind !== "native-task-group") continue;
21
+ const taskIndex = group.tasks.findLastIndex(item =>
22
+ (task.toolCallId && item.toolCallId === task.toolCallId)
23
+ || (task.agentId && item.agentId === task.agentId && (!task.toolCallId || !item.toolCallId))
24
+ || (task.id && item.id === task.id));
25
+ if (taskIndex >= 0) return { groupIndex, taskIndex };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function updateTask(chat, patch, event, { authoritative = false } = {}) {
31
+ const match = findTask(chat, patch);
32
+ const previous = match ? chat[match.groupIndex].tasks[match.taskIndex] : null;
33
+ if (previous?.ownerId && patch.ownerId && previous.ownerId !== patch.ownerId) return;
34
+ if (patch.revision != null && previous?.revision != null && patch.revision < previous.revision) return;
35
+ const next = {
36
+ id: patch.toolCallId || patch.agentId || patch.id,
37
+ title: "Native task", status: "starting", toolCalls: 0,
38
+ startedAt: timeMs(patch.startedAt) || timeMs(event.createdAt),
39
+ ...previous,
40
+ ...Object.fromEntries(Object.entries(patch).filter(([,value]) => value !== undefined)),
41
+ telemetryStale: false,
42
+ };
43
+ if (!next.id) return;
44
+ next.id = previous?.id || next.id;
45
+ next.startedAt = previous?.startedAt || timeMs(patch.startedAt) || timeMs(event.createdAt);
46
+ next.completedAt = timeMs(next.completedAt);
47
+ // Cleanup and late progress must not replace a confirmed outcome. A
48
+ // cancelled:true completion is handled before it becomes terminal.
49
+ if (previous && TERMINAL.has(previous.status)
50
+ && (!previous.inferredTerminal || !TERMINAL.has(patch.status))) {
51
+ next.status = previous.status;
52
+ next.error = previous.error;
53
+ next.completedAt = previous.completedAt;
54
+ next.durationMs = previous.durationMs;
55
+ if (!TERMINAL.has(patch.status) || patch.status === "cancelled") next.toolCalls = previous.toolCalls;
56
+ if (!patch.result && !patch.error) next.preview = previous.preview;
57
+ } else if (TERMINAL.has(patch.status) && !patch.inferredTerminal) {
58
+ next.inferredTerminal = false;
59
+ // A late real outcome replaces the explanatory text of an inferred
60
+ // interruption, rather than displaying that stale error under Done.
61
+ if (previous?.inferredTerminal) next.error = patch.error || "";
62
+ }
63
+ next.title = clip(next.title, 160) || "Native task";
64
+ next.preview = clip(next.preview);
65
+ next.result = clip(next.result, 4000);
66
+ next.error = clip(next.error, 1000);
67
+ next.toolCalls = Math.max(Number(previous?.toolCalls) || 0, Number(next.toolCalls) || 0);
68
+ next.recentActivity = Array.isArray(next.recentActivity)
69
+ ? next.recentActivity.slice(-5).map(a => ({ message: clip(a.message), timestamp: a.timestamp })) : [];
70
+ if (match) {
71
+ const group = chat[match.groupIndex];
72
+ const tasks = [...group.tasks];
73
+ tasks[match.taskIndex] = next;
74
+ chat[match.groupIndex] = { ...group, tasks };
75
+ return;
76
+ }
77
+ // A retained snapshot can arrive after newer chat. Insert at its original
78
+ // start time instead of attaching it permanently to the transcript tail.
79
+ const later = next.startedAt ? chat.findIndex(item => Number(item.createdAt) > next.startedAt) : -1;
80
+ const insertion = later < 0 ? chat.length : later;
81
+ const prior = chat[insertion - 1];
82
+ if (prior?.kind === "native-task-group" && prior.tasks.length < 32) {
83
+ chat[insertion - 1] = { ...prior, tasks: [...prior.tasks, next] };
84
+ } else {
85
+ chat.splice(insertion, 0, {
86
+ id: `native-tasks:${event.sessionId}:${next.id}`, kind: "native-task-group", role: "system",
87
+ createdAt: next.startedAt, tasks: [next],
88
+ });
89
+ }
90
+ }
91
+
92
+ export function appendNativeTaskEvent(chat, event) {
93
+ const type = event?.eventType || "";
94
+ const data = event?.data || {};
95
+ if (type === "native.task_updated") {
96
+ if (data.id && NATIVE_TASK_LABELS[data.status]) updateTask(chat, data, event, { authoritative: true });
97
+ return;
98
+ }
99
+ if (type === "session.turn_completed" || type === "session.turn_stopped") {
100
+ for (let i = 0; i < chat.length; i++) {
101
+ if (chat[i].kind !== "native-task-group") continue;
102
+ chat[i] = { ...chat[i], tasks: chat[i].tasks.map(task => TERMINAL.has(task.status) ? task : {
103
+ ...task, status: "interrupted", inferredTerminal: true, completedAt: timeMs(event.createdAt),
104
+ error: type === "session.turn_stopped" ? "The parent turn was stopped." : "The parent turn ended before a task result was confirmed.",
105
+ }) };
106
+ }
107
+ return;
108
+ }
109
+ const lifecycle = type.startsWith("subagent.");
110
+ const child = type.startsWith("native.");
111
+ const taskTool = !child && !data.nativeAgentId && (data.toolName || data.name) === "task"
112
+ && ["tool.execution_start", "tool.execution_complete"].includes(type);
113
+ if (!lifecycle && !child && !taskTool) return;
114
+ const args = data.arguments || data.args || {};
115
+ const toolCallId = child ? data.parentToolCallId : data.toolCallId;
116
+ const agentId = data.nativeAgentId || data.agentId;
117
+ const patch = { toolCallId, agentId };
118
+ const found = findTask(chat, patch);
119
+ const previous = found ? chat[found.groupIndex].tasks[found.taskIndex] : null;
120
+ if (!previous && !lifecycle && !taskTool) return;
121
+ if (!previous || taskTool || type === "subagent.started") Object.assign(patch, {
122
+ title: args.description || data.agentDisplayName || args.name || data.agentName || "Native task",
123
+ profile: args.agent_type || data.agentName || data.agentType,
124
+ model: data.model,
125
+ });
126
+ if (type === "subagent.configured") {
127
+ Object.assign(patch, { model: data.model, reasoningEffort: data.reasoningEffort });
128
+ } else if (type === "subagent.started") {
129
+ patch.status = "running";
130
+ } else if (type === "subagent.completed" || type === "subagent.failed") {
131
+ Object.assign(patch, {
132
+ status: data.cancelled ? "cancelled" : type === "subagent.failed" ? "failed" : "completed",
133
+ completedAt: event.createdAt, durationMs: data.durationMs, toolCalls: data.totalToolCalls,
134
+ ...(data.cancelled ? { error: "Task was cancelled before returning a result." }
135
+ : type === "subagent.failed" ? { error: clip(resultText(data.error) || data.message || "Native task failed.", 1000) } : {}),
136
+ });
137
+ } else if (type === "tool.execution_start") {
138
+ patch.status = "starting";
139
+ } else if (type === "tool.execution_complete") {
140
+ const failed = data.success === false || data.result?.success === false || Boolean(data.error)
141
+ || ["failure", "failed", "rejected", "denied"].includes(data.result?.resultType ?? data.resultType);
142
+ const text = resultText(data.result) || resultText(data.error);
143
+ Object.assign(patch, { status: failed ? "failed" : "completed", completedAt: event.createdAt,
144
+ ...(failed ? { error: clip(text, 1000) || "Native task request failed." } : { result: clip(text, 4000) }) });
145
+ } else if (type === "native.tool.execution_start") {
146
+ const ids = previous.toolCallIds || [];
147
+ 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);
149
+ patch.preview = args.description || (data.toolName ? `Using ${data.toolName}` : "Working");
150
+ } else if (type === "native.assistant.message") {
151
+ // Do not expose reasoning, tool arguments, or the whole child transcript.
152
+ patch.preview = clip(data.content);
153
+ } else return;
154
+ updateTask(chat, patch, event);
155
+ }
156
+
157
+ export function applyNativeTaskSnapshot(history, payload, meta = {}) {
158
+ if (payload?.phase === "unavailable" && history) return { ...history,
159
+ nativeTaskSnapshot: history.nativeTaskSnapshot ? { ...history.nativeTaskSnapshot, liveSeq: undefined } : undefined,
160
+ chat: history.chat.map(group => group.kind !== "native-task-group" ? group : { ...group,
161
+ tasks: group.tasks.map(task => TERMINAL.has(task.status) ? task : { ...task, telemetryStale: true }) }) };
162
+ if (payload?.version !== 1 || !Array.isArray(payload.tasks) || !payload.ownerId) return history;
163
+ const current = history || { chat: [], activity: [], events: [], lastSeq: 0 };
164
+ const latest = current.nativeTaskSnapshot;
165
+ const newestRecordedOwner = Math.max(0, ...current.chat
166
+ .filter(group => group.kind === "native-task-group")
167
+ .flatMap(group => group.tasks.map(task => Number(task.ownerStartedAt) || 0)));
168
+ // The retained live row can belong to an old publisher that finished
169
+ // writing after a newer turn. On reconnect, durable summary owners fence
170
+ // that row even when this viewer has not received a live snapshot yet.
171
+ if (Number.isFinite(payload.ownerStartedAt) && payload.ownerStartedAt < newestRecordedOwner) {
172
+ if (latest?.ownerStartedAt >= newestRecordedOwner) return current;
173
+ return { ...current, chat: current.chat.map(group => group.kind !== "native-task-group" ? group : { ...group,
174
+ tasks: group.tasks.map(task => !TERMINAL.has(task.status) && task.ownerStartedAt === newestRecordedOwner
175
+ ? { ...task, telemetryStale: true } : task) }) };
176
+ }
177
+ if (Number.isFinite(latest?.ownerStartedAt) && Number.isFinite(payload.ownerStartedAt)
178
+ && payload.ownerStartedAt < latest.ownerStartedAt) return current;
179
+ if (latest?.ownerId === payload.ownerId && latest.revision >= payload.revision) return current;
180
+ if (latest?.ownerId === payload.ownerId && Number.isFinite(meta.seq)
181
+ && Number.isFinite(latest?.liveSeq) && meta.seq <= latest.liveSeq) return current;
182
+ const chat = [...current.chat];
183
+ for (const task of payload.tasks.slice(0, 32)) {
184
+ if (!task?.id || !NATIVE_TASK_LABELS[task.status]) continue;
185
+ const ended = current.events?.some(e => ["session.turn_completed", "session.turn_stopped"].includes(e.eventType)
186
+ && timeMs(e.createdAt) > timeMs(task.startedAt));
187
+ const patch = { ...task, ownerId: payload.ownerId, ownerStartedAt: payload.ownerStartedAt, revision: payload.revision };
188
+ if (ended && !TERMINAL.has(patch.status)) {
189
+ patch.status = "interrupted";
190
+ patch.inferredTerminal = true;
191
+ patch.error = "The parent turn ended before a task result was confirmed.";
192
+ }
193
+ updateTask(chat, patch, { sessionId: meta.sessionId, createdAt: task.startedAt }, { authoritative: true });
194
+ }
195
+ return { ...current, chat, nativeTaskSnapshot: { ownerId: payload.ownerId, ownerStartedAt: payload.ownerStartedAt,
196
+ revision: payload.revision, liveSeq: meta.seq } };
197
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Some model-authored questions contain literal newline escapes. Decode those
3
+ * for presentation before markdown parsing, including mixed real/escaped lines.
4
+ * Keep stored question/choice identity intact for answers and history matching.
5
+ * Code spans/fences, Windows paths, quoted escape literals and doubled slashes
6
+ * are literal content, not formatting. Do not run a general JSON unescape here.
7
+ */
8
+ export function normalizeQuestionForDisplay(value) {
9
+ const source = String(value ?? "");
10
+ if (!source.includes("\\n")) return source;
11
+ const tokens = /(`+)[\s\S]*?\1(?!`)|(~{3,})[\s\S]*?\2|(["'])[A-Za-z]:\\[^\r\n]*?\3|(?<!\w)(?:[A-Za-z]:\\|\\\\)(?:(?!\\n\\n|\\r\\n\\r\\n)[^\s"'`])+|(["'])(?:\\r\\n|\\[nr])\4|\\\\|\\r\\n|\\n/g;
12
+ return source.replace(tokens, token => token === "\\n" || token === "\\r\\n" ? "\n" : token);
13
+ }
@@ -41,6 +41,15 @@ function packageResponseIsStale(packages, action) {
41
41
  return false;
42
42
  }
43
43
 
44
+ function clearFeatureIdentity(features) {
45
+ return { mode: "mine", userId: null, data: null, users: [], drafts: {}, loading: false, saving: false,
46
+ error: null, generation: (features?.generation || 0) + 1, fetchedAt: null, userQuery: "" };
47
+ }
48
+
49
+ function featurePrincipalKey(principal) {
50
+ return JSON.stringify([principal?.provider, principal?.subject]);
51
+ }
52
+
44
53
  /** A provider name, or null. Names are cluster-unique and never blank. */
45
54
  function normalizeProviderName(value) {
46
55
  const name = String(value ?? "").trim();
@@ -965,6 +974,9 @@ function baseReducer(state, action) {
965
974
  case "auth/context":
966
975
  return {
967
976
  ...state,
977
+ admin: featurePrincipalKey(state.auth?.principal) !== featurePrincipalKey(action.principal)
978
+ || state.auth?.authorization?.role !== action.authorization?.role
979
+ ? { ...state.admin, features: clearFeatureIdentity(state.admin.features) } : state.admin,
968
980
  auth: {
969
981
  principal: action.principal ?? null,
970
982
  authorization: action.authorization ?? null,
@@ -2513,6 +2525,9 @@ function baseReducer(state, action) {
2513
2525
  loading: false,
2514
2526
  loadError: null,
2515
2527
  profile: action.profile || null,
2528
+ features: featurePrincipalKey(state.admin.profile) !== featurePrincipalKey(action.profile)
2529
+ || Boolean(state.admin.profile?.isAdmin) !== Boolean(action.profile?.isAdmin)
2530
+ ? clearFeatureIdentity(state.admin.features) : state.admin.features,
2516
2531
  ghcpKey: {
2517
2532
  ...state.admin.ghcpKey,
2518
2533
  // A successful load doesn't clobber an in-progress
@@ -2964,9 +2979,11 @@ function baseReducer(state, action) {
2964
2979
  };
2965
2980
  }
2966
2981
  case "admin/section": {
2967
- const section = ["packages", "workers", "ghcp"].includes(action.section) ? action.section : "ghcp";
2982
+ const section = ["providers", "packages", "workers", "features", "ghcp"].includes(action.section) ? action.section : "ghcp";
2968
2983
  return { ...state, admin: { ...state.admin, section } };
2969
2984
  }
2985
+ case "admin/features":
2986
+ return { ...state, admin: { ...state.admin, features: { ...state.admin.features, ...action.patch } } };
2970
2987
  case "admin/workers/attempt": {
2971
2988
  const workers = state.admin.workers || {};
2972
2989
  return { ...state, admin: { ...state.admin, workers: { ...workers, attempts: (workers.attempts || 0) + 1, lastAttemptAt: Date.now(), lastSkip: action.skip || null } } };
@@ -29,6 +29,8 @@ import {
29
29
  getContextHeaderBadge,
30
30
  } from "./context-usage.js";
31
31
  import { canonicalSystemTitle } from "./system-titles.js";
32
+ import { matchesSessionError } from "./session-warning.js";
33
+ import { normalizeQuestionForDisplay } from "./question-display.js";
32
34
  import { withSessionWarnings, isActivityOnlySessionError } from "./session-errors.js";
33
35
  import {
34
36
  BUDGET_PERIODS,
@@ -1543,11 +1545,11 @@ export function canStopSessionTurn(session) {
1543
1545
  // raised them. They used to stamp session.updatedAt, which moves on EVERY
1544
1546
  // poll and status tick — so the card's clock jumped forward with each update
1545
1547
  // and read as flicker.
1546
- function latestEventCreatedAtMs(events = [], eventTypes = []) {
1548
+ function latestEventCreatedAtMs(events = [], eventTypes = [], predicate = () => true) {
1547
1549
  const wanted = new Set(eventTypes);
1548
1550
  for (let index = (events || []).length - 1; index >= 0; index -= 1) {
1549
1551
  const event = events[index];
1550
- if (!event || !wanted.has(event.eventType)) continue;
1552
+ if (!event || !wanted.has(event.eventType) || !predicate(event)) continue;
1551
1553
  const createdAt = event.createdAt;
1552
1554
  const ms = createdAt instanceof Date
1553
1555
  ? createdAt.getTime()
@@ -1561,7 +1563,7 @@ function buildPendingQuestionMessage(session, events = []) {
1561
1563
  const pendingQuestion = session?.pendingQuestion;
1562
1564
  if (!pendingQuestion?.question) return null;
1563
1565
 
1564
- const body = [String(pendingQuestion.question).trim()];
1566
+ const body = [normalizeQuestionForDisplay(pendingQuestion.question).trim()];
1565
1567
  const choices = Array.isArray(pendingQuestion.choices)
1566
1568
  ? pendingQuestion.choices.filter((choice) => typeof choice === "string" && choice.trim())
1567
1569
  : [];
@@ -1569,7 +1571,7 @@ function buildPendingQuestionMessage(session, events = []) {
1569
1571
  if (choices.length > 0) {
1570
1572
  body.push("", "Choices:");
1571
1573
  for (const choice of choices) {
1572
- body.push(`- ${choice}`);
1574
+ body.push(`- ${normalizeQuestionForDisplay(choice)}`);
1573
1575
  }
1574
1576
  }
1575
1577
 
@@ -1661,10 +1663,11 @@ function buildSessionErrorMessage(session, events = []) {
1661
1663
  role: "system",
1662
1664
  text: body,
1663
1665
  time: "",
1664
- // The error event when one was recorded, else the end of the turn
1665
- // that produced the error; never the session's rolling updatedAt.
1666
- createdAt: latestEventCreatedAtMs(events, ["session.error"])
1667
- ?? latestEventCreatedAtMs(events, ["session.turn_completed", "assistant.turn_end"]),
1666
+ // Only an actual failure can anchor this card. A later successful
1667
+ // turn must never move an earlier warning's timestamp forward.
1668
+ createdAt: latestEventCreatedAtMs(events, ["session.error", "session.turn_completed"],
1669
+ (event) => matchesSessionError(event.eventType === "session.error" ? event.data?.message
1670
+ : event.data?.resultType === "error" ? event.data?.errorMessage : null, errorText)),
1668
1671
  cardTitle: isFailed ? "Error" : "Warning",
1669
1672
  cardTitleColor: isFailed ? "red" : "yellow",
1670
1673
  cardBorderColor: isFailed ? "red" : "yellow",
@@ -1912,8 +1915,24 @@ export function selectActiveChat(state) {
1912
1915
  messages.push(answeredQuestionMessage);
1913
1916
  }
1914
1917
  if (sessionErrorMessage) {
1915
- const index = messages.findIndex(message => sessionErrorMessage.createdAt != null && message.createdAt > sessionErrorMessage.createdAt);
1916
- messages.splice(index < 0 ? messages.length : index, 0, sessionErrorMessage);
1918
+ // History owns the warning's position and identity. Status may add
1919
+ // current retry/failure details, but must not append the same warning
1920
+ // after the newer conversation on every poll.
1921
+ const warningIndex = messages.findLastIndex((message) => message.kind === "session-warning"
1922
+ && matchesSessionError(message.text, session.error));
1923
+ if (warningIndex < 0) {
1924
+ const index = messages.findIndex(message => sessionErrorMessage.createdAt != null && message.createdAt > sessionErrorMessage.createdAt);
1925
+ messages.splice(index < 0 ? messages.length : index, 0, sessionErrorMessage);
1926
+ } else if (warningIndex === messages.length - 1 || sessionErrorMessage.cardTitle === "Error") {
1927
+ const warning = messages[warningIndex];
1928
+ messages[warningIndex] = {
1929
+ ...warning,
1930
+ ...sessionErrorMessage,
1931
+ id: warning.id,
1932
+ createdAt: warning.createdAt,
1933
+ time: warning.time,
1934
+ };
1935
+ }
1917
1936
  }
1918
1937
  return messages;
1919
1938
  }
@@ -2597,7 +2616,7 @@ function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
2597
2616
  ...(askedAndAnswered.question === "a question" ? [] : buildMessageCardLines({
2598
2617
  title: "Question",
2599
2618
  timestamp: formatTimestamp(message?.createdAt || message?.time),
2600
- body: askedAndAnswered.question,
2619
+ body: normalizeQuestionForDisplay(askedAndAnswered.question),
2601
2620
  width: Math.max(20, maxWidth),
2602
2621
  titleColor: USER_CHAT_COLOR,
2603
2622
  borderColor: USER_CHAT_COLOR,
@@ -3069,7 +3088,20 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
3069
3088
  };
3070
3089
  const lines = [];
3071
3090
  for (const [index, message] of messages.entries()) {
3072
- if (message?.kind === "epoch-divider") {
3091
+ if (message?.kind === "native-task-group") {
3092
+ if (options.tableMode === "sentinel") {
3093
+ lines.push({ kind: "nativeTasks", group: message });
3094
+ } else {
3095
+ const labels = { starting: "Starting", running: "Running", waiting: "Waiting", completed: "Done",
3096
+ failed: "Failed", cancelled: "Cancelled", interrupted: "Interrupted" };
3097
+ appendChatBlockLines(lines, buildChatMessageLines({ ...message, cardTitle: "Native tasks",
3098
+ cardTitleColor: "cyan", cardBorderColor: "cyan",
3099
+ text: message.tasks.map(task => `[${labels[task.status] || task.status}] ${task.title}`
3100
+ + (task.toolCalls ? ` · ${task.toolCalls} calls` : "")
3101
+ + (task.error || task.result || task.preview ? `\n ${String(task.error || task.result || task.preview).replace(/\s+/g, " ").slice(0, 240)}` : "")).join("\n\n"),
3102
+ }, maxWidth, buildOptions));
3103
+ }
3104
+ } else if (message?.kind === "epoch-divider") {
3073
3105
  lines.push(buildEpochDividerLine(message, maxWidth));
3074
3106
  } else if (message?.kind === "regen-refused") {
3075
3107
  lines.push(buildRegenRefusedLine(message, maxWidth));
@@ -4422,11 +4454,12 @@ export function selectAdminConsole(state) {
4422
4454
  // Settings tree — the session-list-slot navigation. Rendered by both
4423
4455
  // hosts; `kind` drives affordances (section rows switch panes, package
4424
4456
  // rows select a package).
4425
- const section = ["providers", "packages", "workers"].includes(admin.section) ? admin.section : "providers";
4457
+ const section = ["providers", "packages", "workers", "features"].includes(admin.section) ? admin.section : "providers";
4426
4458
  const settingsTree = [
4427
4459
  { id: "providers", kind: "section", depth: 0, label: "Model Providers", selected: false },
4428
4460
  { id: "myProviders", kind: "subsection", depth: 1, label: "My Providers", selected: section === "providers" && providerPage === "mine" },
4429
4461
  ...(isAdmin ? [{ id: "sharedProviders", kind: "subsection", depth: 1, label: "Shared Providers", selected: section === "providers" && providerPage === "shared" }] : []),
4462
+ { id: "features", kind: "section", depth: 0, label: "Feature flags", selected: section === "features" },
4430
4463
  { id: "agents", kind: "section", depth: 0, label: "Agents", selected: section === "packages" && !pkgState.selectedName },
4431
4464
  { id: "group:shared", kind: "group", depth: 1, label: "Shared", count: sharedRows.length },
4432
4465
  ...sharedRows.map((row) => ({ id: `pkg:shared:${row.name}`, kind: "package", depth: 2, label: row.name, ...row })),
@@ -1,3 +1,5 @@
1
+ import { matchesSessionError } from "./session-warning.js";
2
+
1
3
  // Copilot emits this diagnostic for an accepted turn without visible text.
2
4
  // Keep its durable event and Activity entry, but do not turn it into a chat
3
5
  // warning. Match only this message and the runtime's known retry wrapper.
@@ -27,9 +29,13 @@ export function shouldKeepSessionWarning(previousSession, nextSession) {
27
29
 
28
30
  const timeMs = value => value instanceof Date ? value.getTime()
29
31
  : typeof value === "number" ? value : Date.parse(value || "");
30
- const errorText = event => typeof event?.data === "string" ? event.data
32
+ const isWarningEvent = event => event?.eventType === "session.error"
33
+ || event?.eventType === "session.turn_completed" && event.data?.resultType === "error";
34
+ const errorText = event => event?.eventType === "session.turn_completed"
35
+ ? String(event.data?.resultType === "error" ? event.data.errorMessage || "" : "").trim()
36
+ : typeof event?.data === "string" ? event.data
31
37
  : String(event?.data?.message || event?.data?.error || event?.data?.text || "").trim();
32
- const sameError = (statusText, eventText) => Boolean(eventText) && statusText.includes(eventText);
38
+ const sameError = (statusText, eventText) => matchesSessionError(eventText, statusText);
33
39
 
34
40
  export function buildSessionWarning(event) {
35
41
  const text = errorText(event);
@@ -62,7 +68,7 @@ export function retainSessionWarnings(previous, next, events = [], now = Date.no
62
68
  return { ...next, chatWarnings: [...warnings.slice(0, -1), { ...last, active: false }] };
63
69
  }
64
70
  const since = last?.createdAt || 0;
65
- const event = events.findLast(e => e.eventType === "session.error"
71
+ const event = events.findLast(e => isWarningEvent(e)
66
72
  && timeMs(e.createdAt) > since && sameError(text, errorText(e)));
67
73
  // A retained error field can outlive a terminal/wait state. A later
68
74
  // running snapshot alone is not evidence of another failure episode.
@@ -92,7 +98,7 @@ export function withSessionWarnings(chat, session, events = []) {
92
98
  // Status often wins the race against its durable event. Match only
93
99
  // that boundary, never a later failure after a new user/agent message.
94
100
  const following = events.filter(e => Number(e.seq) > warning.afterSeq);
95
- const boundary = following.find(e => ["session.error", "user.message", "assistant.message"].includes(e.eventType));
101
+ const boundary = following.find(e => isWarningEvent(e) || ["user.message", "assistant.message"].includes(e.eventType));
96
102
  // On initial load the catalog can arrive before any history at all.
97
103
  // Find its matching older event by the captured failure time, without
98
104
  // reusing an event from a previous error episode with identical text.
@@ -103,8 +109,9 @@ export function withSessionWarnings(chat, session, events = []) {
103
109
  && timeMs(m.createdAt) <= warning.createdAt
104
110
  && sameError(warning.text, m.text));
105
111
  const eventSeq = warning.eventSeq ?? recorded?.warningSeq
106
- ?? (boundary?.eventType === "session.error" && sameError(warning.text, errorText(boundary)) ? Number(boundary.seq) : null);
107
- const index = messages.findIndex(m => m.kind === "session-warning" && m.warningSeq === eventSeq);
112
+ ?? (isWarningEvent(boundary) && sameError(warning.text, errorText(boundary)) ? Number(boundary.seq) : null);
113
+ const index = messages.findIndex(m => m.kind === "session-warning"
114
+ && (m.warningSeq === eventSeq || eventSeq != null && m.turnCompletedSeq === eventSeq));
108
115
  const notice = {
109
116
  id: warning.id, kind: "session-warning", role: "system", text: warning.text,
110
117
  createdAt: warning.createdAt,
@@ -0,0 +1,14 @@
1
+ function canonicalErrorText(value) {
2
+ // ManagedSession adds this wrapper when promoting an SDK session.error
3
+ // to a failed turn. The SDK event itself retains the original message.
4
+ return String(value || "").trim().replace(/^Execution failed:\s*/, "");
5
+ }
6
+
7
+ export function matchesSessionError(recorded, current) {
8
+ const error = canonicalErrorText(recorded);
9
+ const statusError = canonicalErrorText(current);
10
+ // Orchestration status can append retry details to the recorded failure.
11
+ // Match only the whole message or its detail suffix, never an arbitrary
12
+ // substring shared by different errors.
13
+ return Boolean(error) && (statusError === error || statusError.startsWith(`${error} (`));
14
+ }
@@ -771,6 +771,7 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
771
771
  // replaces the legacy GitHub-key editor in new UI; the legacy
772
772
  // state remains available for rollback compatibility only.
773
773
  section: "providers",
774
+ features: { mode: "mine", userId: null, data: null, users: [], drafts: {}, loading: false, saving: false, error: null, generation: 0, fetchedAt: null, userQuery: "" },
774
775
  // Agent packages (docs/proposals/agent-packages.md) — registry
775
776
  // list, selected package detail, and the workspace viewer.
776
777
  packages: {