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.
- package/mcp/dist/src/server.d.ts.map +1 -1
- package/mcp/dist/src/server.js +2 -0
- package/mcp/dist/src/server.js.map +1 -1
- package/mcp/dist/src/tools/features.d.ts +4 -0
- package/mcp/dist/src/tools/features.d.ts.map +1 -0
- package/mcp/dist/src/tools/features.js +35 -0
- package/mcp/dist/src/tools/features.js.map +1 -0
- package/package.json +2 -2
- package/tui/src/node-sdk-transport.js +13 -0
- package/ui/core/src/controller.js +276 -0
- package/ui/core/src/history.js +89 -14
- package/ui/core/src/native-tasks.js +197 -0
- package/ui/core/src/question-display.js +13 -0
- package/ui/core/src/reducer.js +18 -1
- package/ui/core/src/selectors.js +46 -13
- package/ui/core/src/session-errors.js +13 -6
- package/ui/core/src/session-warning.js +14 -0
- package/ui/core/src/state.js +1 -0
- package/ui/react/src/feature-flags-panel.js +186 -0
- package/ui/react/src/native-task-card.js +59 -0
- package/ui/react/src/web-app.js +105 -15
- package/web/dist/assets/{abnfDiagram-VRR7QNED-C8iAmIqt.js → abnfDiagram-VRR7QNED-gIPHF2gV.js} +1 -1
- package/web/dist/assets/{arc-Cg8qs17j.js → arc-DMW_q5Ml.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-fYr0dRm3.js → architectureDiagram-ZJ3FMSHR-DAKsrxcB.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-Bpj79Dzq.js → blockDiagram-677ZJIJ3-BUjSLpP3.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-b26Lsm6m.js → c4Diagram-LMCZKHZV-Dw2nPoes.js} +1 -1
- package/web/dist/assets/channel-CcOa1LtY.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-VLYQfjgG.js → chunk-2Q5K7J3B-BAL28Rv0.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-mVx--2Hg.js → chunk-32BRIVSS-DDlt9K8s.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-jwnJmked.js → chunk-5VM5RSS4-BLQGJGoP.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DdfzAT1n.js → chunk-EX3LRPZG-XFMb3HRy.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-BHm6Udd8.js → chunk-JWPE2WC7-DWnKTQ72.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-Dn5qSVAS.js → chunk-MOJQB5TN-MltzhnwP.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-D4IKMROn.js → chunk-RYQCIY6F-Ce3q0-mJ.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-BhME3pRf.js → chunk-V7JOEXUC-ThXLYY88.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-CHG_NBff.js → chunk-VR4S4FIN-DbhAykiW.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-yw4QCgiL.js → chunk-XXDRQBXY-BpkNB0mD.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B2VhHRSV.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B2VhHRSV.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-DqwPj9Kx.js → cose-bilkent-JH36ORCC-rctqAPvj.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CVokaaE_.js → cynefin-VYW2F7L2-Dg1cltg6.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Bkhj9fuY.js → cynefinDiagram-TSTJHNR4-Ci23Sv4Z.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DVv-jnjb.js → dagre-VKFMJZFB-BpWTGhpv.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-DDlZjkrF.js → diagram-FQU43EPY-B69JpiAe.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CNDUgqYa.js → diagram-G47NLZAW-BHMk1TOO.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-DywF8u8i.js → diagram-NH7WQ7WH-CtvZIIsk.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-ZfN8nvLw.js → diagram-OA4YK3LP-BFvKh800.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-S94A6Avm.js → diagram-WEI45ONY-CgB8sWPe.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-Mpt3QUnw.js → ebnfDiagram-CCIWWBDH-DyqPNydM.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-Bh-eWbdV.js → erDiagram-Q63AITRT-CwpvZQ3Q.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-BwRtDI6V.js → flowDiagram-23GEKE2U-EBKSKxfC.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-CrZoBmcB.js → ganttDiagram-NO4QXBWP-q5tMPAuC.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-giXv1ynk.js → gitGraphDiagram-IHSO6WYX-DCRbi4KE.js} +1 -1
- package/web/dist/assets/{index-Br-gGWmc.js → index-BfF7e08d.js} +6 -6
- package/web/dist/assets/index-uhc851H3.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-H_5oc4-7.js → infoDiagram-FWYZ7A6U-Bex5pSIX.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-BydiPDX0.js → ishikawaDiagram-FXEZZL3T-Dj0li_TY.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-DS1HXms0.js → journeyDiagram-5HDEW3XC-BtEGEeVr.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-BjY5SyYu.js → kanban-definition-HUTT4EX6-BTFfZvYt.js} +1 -1
- package/web/dist/assets/{linear-BvOotjKL.js → linear-C3SQCsip.js} +1 -1
- package/web/dist/assets/{mermaid.core-0iRu7sp-.js → mermaid.core-DALDJDu8.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-DB93zsvL.js → mindmap-definition-LN4V7U3C-BZf3uw0u.js} +1 -1
- package/web/dist/assets/{papaparse.min-f5Xncedc.js → papaparse.min-Bh9MZ-fk.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-Dh_U1cFA.js → pegDiagram-2B236MQR-PuOleiMg.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-Csv1qxMm.js → pieDiagram-ENE6RG2P-CcFORUaW.js} +1 -1
- package/web/dist/assets/pilotswarm-B9rLtXpI.js +109 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-Bo0pcrBM.js → quadrantDiagram-ABIIQ3AL-CEm0mp4y.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-CHPvHNWG.js → railroadDiagram-RFXS5EU6-DeH-uX0_.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-DgX8l-6H.js → requirementDiagram-TGXJPOKE-CMbJVMc1.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-xR6MTs5a.js → sankeyDiagram-HTMAVEWB-C9gL5w_b.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CD_45diB.js → sequenceDiagram-DBY2YBRQ-BUo8TT74.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-BJVEXU0K.js → sizeCapture-X5ZJPWSS-C5lqG0ei.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-Bwu6kd23.js → stateDiagram-2N3HPSRC-DyeISrEU.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-5FZbaloZ.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-BmhEzevT.js → swimlanes-5IMT3BWC-DawuBk_a.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DL8PgtmM.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-DFPI_uTI.js → timeline-definition-FHXFAJF6-BZJ2YJcm.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-DDTpTkBC.js → vennDiagram-L72KCM5P-BMXBVCfe.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-bV2_YVIL.js → wardleyDiagram-EHGQE667-DWedKsrQ.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-C7bDIXAV.js → xychartDiagram-FW5EYKEG--XdfYceC.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +19 -0
- package/web/server.js +2 -2
- package/web/dist/assets/channel-C-ASbcRr.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-D2vlJIb1.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-D2vlJIb1.js +0 -1
- package/web/dist/assets/index-DRFDn87d.css +0 -1
- package/web/dist/assets/pilotswarm-DQNSWfNK.js +0 -105
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-aypsN5jW.js +0 -1
- 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
|
+
}
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -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 } } };
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -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 = [
|
|
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
|
-
//
|
|
1665
|
-
//
|
|
1666
|
-
createdAt: latestEventCreatedAtMs(events, ["session.error"]
|
|
1667
|
-
|
|
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
|
-
|
|
1916
|
-
|
|
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 === "
|
|
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
|
|
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) =>
|
|
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
|
|
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 => ["
|
|
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
|
|
107
|
-
const index = messages.findIndex(m => m.kind === "session-warning"
|
|
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
|
+
}
|
package/ui/core/src/state.js
CHANGED
|
@@ -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: {
|