pilotswarm 0.5.62 → 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 +330 -14
- package/ui/core/src/history.js +89 -14
- package/ui/core/src/moa.js +9 -0
- package/ui/core/src/native-tasks.js +197 -0
- package/ui/core/src/question-display.js +13 -0
- package/ui/core/src/reducer.js +31 -7
- 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 +7 -2
- package/ui/react/src/components.js +2 -1
- 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 +173 -33
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DAiG41W-.js → abnfDiagram-VRR7QNED-gIPHF2gV.js} +1 -1
- package/web/dist/assets/{arc-CN_GY3DG.js → arc-DMW_q5Ml.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DD9qiBOj.js → architectureDiagram-ZJ3FMSHR-DAKsrxcB.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-CZg2R8i6.js → blockDiagram-677ZJIJ3-BUjSLpP3.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-DuZxUZYd.js → c4Diagram-LMCZKHZV-Dw2nPoes.js} +1 -1
- package/web/dist/assets/channel-CcOa1LtY.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-BX7lDvmf.js → chunk-2Q5K7J3B-BAL28Rv0.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-COupwzAw.js → chunk-32BRIVSS-DDlt9K8s.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-C_IyERib.js → chunk-5VM5RSS4-BLQGJGoP.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-CyfIuYKn.js → chunk-EX3LRPZG-XFMb3HRy.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-Cj5lvEfN.js → chunk-JWPE2WC7-DWnKTQ72.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-l907LpMA.js → chunk-MOJQB5TN-MltzhnwP.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-PLru1fOZ.js → chunk-RYQCIY6F-Ce3q0-mJ.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-C6GCxiE5.js → chunk-V7JOEXUC-ThXLYY88.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-BGelrIxQ.js → chunk-VR4S4FIN-DbhAykiW.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-BmJstjEv.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-CRabxOOK.js → cose-bilkent-JH36ORCC-rctqAPvj.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-CminxBoG.js → cynefin-VYW2F7L2-Dg1cltg6.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-yBazsQf4.js → cynefinDiagram-TSTJHNR4-Ci23Sv4Z.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DcZuZV2n.js → dagre-VKFMJZFB-BpWTGhpv.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-CHoFzsw6.js → diagram-FQU43EPY-B69JpiAe.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-B8leqMRl.js → diagram-G47NLZAW-BHMk1TOO.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-DNoVxpa3.js → diagram-NH7WQ7WH-CtvZIIsk.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-gC3Q2X6-.js → diagram-OA4YK3LP-BFvKh800.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-CABqyjDl.js → diagram-WEI45ONY-CgB8sWPe.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-cSguXaDC.js → ebnfDiagram-CCIWWBDH-DyqPNydM.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-CoWX37V3.js → erDiagram-Q63AITRT-CwpvZQ3Q.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-I9DHYjhG.js → flowDiagram-23GEKE2U-EBKSKxfC.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-DaFnfYTt.js → ganttDiagram-NO4QXBWP-q5tMPAuC.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-CwiXVM_O.js → gitGraphDiagram-IHSO6WYX-DCRbi4KE.js} +1 -1
- package/web/dist/assets/index-BfF7e08d.js +24 -0
- package/web/dist/assets/index-uhc851H3.css +1 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-DtzIrIpW.js → infoDiagram-FWYZ7A6U-Bex5pSIX.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-B9nk829n.js → ishikawaDiagram-FXEZZL3T-Dj0li_TY.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-xLaPm-ow.js → journeyDiagram-5HDEW3XC-BtEGEeVr.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-BnDqLYLC.js → kanban-definition-HUTT4EX6-BTFfZvYt.js} +1 -1
- package/web/dist/assets/{linear-Djc7nVo8.js → linear-C3SQCsip.js} +1 -1
- package/web/dist/assets/{mermaid.core-B5elhUtV.js → mermaid.core-DALDJDu8.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-N-B7EJGs.js → mindmap-definition-LN4V7U3C-BZf3uw0u.js} +1 -1
- package/web/dist/assets/{papaparse.min-BqM-LJkq.js → papaparse.min-Bh9MZ-fk.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-BIollSMB.js → pegDiagram-2B236MQR-PuOleiMg.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-DkZY6iCd.js → pieDiagram-ENE6RG2P-CcFORUaW.js} +1 -1
- package/web/dist/assets/pilotswarm-B9rLtXpI.js +109 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-iUDmQIyL.js → quadrantDiagram-ABIIQ3AL-CEm0mp4y.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-BSCGJgV7.js → railroadDiagram-RFXS5EU6-DeH-uX0_.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-9yqcGRUA.js → requirementDiagram-TGXJPOKE-CMbJVMc1.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-SPh1plT9.js → sankeyDiagram-HTMAVEWB-C9gL5w_b.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-C0nv5x71.js → sequenceDiagram-DBY2YBRQ-BUo8TT74.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-DMXfUt1o.js → sizeCapture-X5ZJPWSS-C5lqG0ei.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-vjhtquua.js → stateDiagram-2N3HPSRC-DyeISrEU.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-5FZbaloZ.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-Bqewmkt5.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-DYHsdXUL.js → timeline-definition-FHXFAJF6-BZJ2YJcm.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-Cb7cOHSa.js → vennDiagram-L72KCM5P-BMXBVCfe.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-BHpO1byL.js → wardleyDiagram-EHGQE667-DWedKsrQ.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DHDvGPtH.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-B50-C7oJ.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-CIJlKS0m.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CIJlKS0m.js +0 -1
- package/web/dist/assets/index-CHlrgp0s.css +0 -1
- package/web/dist/assets/index-bf7RCrAf.js +0 -24
- package/web/dist/assets/pilotswarm-Di5s6c69.js +0 -105
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CYIeYmPN.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BeKhH9C9.js +0 -8
package/ui/core/src/history.js
CHANGED
|
@@ -2,6 +2,8 @@ import { formatHumanDurationSeconds, formatTimestamp, shortModelName, shortSessi
|
|
|
2
2
|
import { isCanvasActionContent, parseCanvasActionContent } from "./canvas-actions.js";
|
|
3
3
|
import { formatCompactionActivityRuns } from "./context-usage.js";
|
|
4
4
|
import { canonicalSystemTitle } from "./system-titles.js";
|
|
5
|
+
import { matchesSessionError } from "./session-warning.js";
|
|
6
|
+
import { appendNativeTaskEvent } from "./native-tasks.js";
|
|
5
7
|
import { buildSessionWarning } from "./session-errors.js";
|
|
6
8
|
import { appendChatCall, CHAT_CALL_EVENT_TYPES } from "./chat-activity.js";
|
|
7
9
|
|
|
@@ -19,13 +21,20 @@ export const HISTORY_EVENT_LIMIT_STEPS = [
|
|
|
19
21
|
// load transcript pages instead of raw-stream pages.
|
|
20
22
|
export const CHAT_HISTORY_EVENT_TYPES = [
|
|
21
23
|
...CHAT_CALL_EVENT_TYPES,
|
|
22
|
-
"session.error",
|
|
23
24
|
"user.message",
|
|
24
25
|
"assistant.message",
|
|
25
26
|
// Needed to distinguish interim assistant output from the final answer
|
|
26
27
|
// when an older transcript page is loaded.
|
|
27
28
|
"session.turn_completed",
|
|
28
29
|
"session.turn_stopped",
|
|
30
|
+
// Warnings belong at the error's position in the transcript, including
|
|
31
|
+
// after recovery and when paging backward through a noisy session.
|
|
32
|
+
"session.error",
|
|
33
|
+
"subagent.started",
|
|
34
|
+
"subagent.configured",
|
|
35
|
+
"subagent.completed",
|
|
36
|
+
"subagent.failed",
|
|
37
|
+
"native.task_updated",
|
|
29
38
|
"system.message",
|
|
30
39
|
// Session regeneration boundary — rendered as an inline epoch divider in
|
|
31
40
|
// the transcript, so it must survive backward chat-history paging.
|
|
@@ -90,6 +99,63 @@ function buildRegenFailedItem(event) {
|
|
|
90
99
|
};
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
function buildSessionWarningItem(event) {
|
|
103
|
+
const completed = event.eventType === "session.turn_completed" && event.data?.resultType === "error";
|
|
104
|
+
if (event.eventType !== "session.error" && !completed) return null;
|
|
105
|
+
const warning = buildSessionWarning(event);
|
|
106
|
+
if (!warning) return null;
|
|
107
|
+
return {
|
|
108
|
+
...warning,
|
|
109
|
+
errorText: warning.text,
|
|
110
|
+
sourceEventType: event.eventType,
|
|
111
|
+
...(completed ? { turnCompletedSeq: event.seq } : {}),
|
|
112
|
+
time: formatTimestamp(event.createdAt),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function appendSessionWarning(chat, event) {
|
|
117
|
+
const warning = buildSessionWarningItem(event);
|
|
118
|
+
if (!warning) return;
|
|
119
|
+
if (chat.some((item) => item.id === warning.id
|
|
120
|
+
|| (item.turnCompletedSeq != null && item.turnCompletedSeq === event.seq))) return;
|
|
121
|
+
// Tool disclosures can arrive between the SDK error and the failed-turn
|
|
122
|
+
// summary. They do not start a different conversation/failure episode.
|
|
123
|
+
const previousIndex = chat.findLastIndex(item => item.kind !== "chat-call" && item.kind !== "native-task-group");
|
|
124
|
+
const previous = chat[previousIndex];
|
|
125
|
+
// A runtime error and its failed-turn summary can describe one failure.
|
|
126
|
+
// Retain the original anchor; subsequent failed turns remain distinct.
|
|
127
|
+
if (previous?.kind === "session-warning"
|
|
128
|
+
&& previous.sourceEventType === "session.error"
|
|
129
|
+
&& previous.turnCompletedSeq == null
|
|
130
|
+
&& event.eventType === "session.turn_completed"
|
|
131
|
+
&& matchesSessionError(previous.errorText, warning.errorText)) {
|
|
132
|
+
chat[previousIndex] = { ...previous, turnCompletedSeq: event.seq };
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
chat.push(warning);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const parentChatCallTypes = new Set(CHAT_CALL_EVENT_TYPES);
|
|
139
|
+
function appendParentChatCall(chat, event) {
|
|
140
|
+
if (!parentChatCallTypes.has(event?.eventType)) return;
|
|
141
|
+
const data = event?.data || {};
|
|
142
|
+
// Native tasks have their own abridged disclosure. Generic call cards
|
|
143
|
+
// would duplicate them and reveal the full native prompt/arguments.
|
|
144
|
+
const name = data.toolName || data.name;
|
|
145
|
+
const namespace = `${event.sessionId}:${data.durableSessionId || ""}`;
|
|
146
|
+
const keys = [data.toolCallId && `call:${data.toolCallId}`, data.requestId && `request:${data.requestId}`].filter(Boolean);
|
|
147
|
+
// An empty diagnostic completion has neither a call to update nor useful
|
|
148
|
+
// content. Keep it in Activity without evicting real conversation cards.
|
|
149
|
+
const hasContent = name || data.arguments != null || data.args != null || data.result != null || data.output != null
|
|
150
|
+
|| data.error || data.partialOutput || data.progressMessage || data.childSessionId || data.agentId || data.task;
|
|
151
|
+
if (!keys.length && !hasContent) return;
|
|
152
|
+
const parentCall = !name && chat.some(item => item.kind === "chat-call" && item.namespace === namespace
|
|
153
|
+
&& item.callKeys.some(key => keys.includes(key)));
|
|
154
|
+
const nativeCall = name === "task" || Boolean(!name && !parentCall && data.toolCallId && chat.some(item =>
|
|
155
|
+
item.kind === "native-task-group" && item.tasks.some(task => task.toolCallId === data.toolCallId)));
|
|
156
|
+
if (!nativeCall) appendChatCall(chat, event);
|
|
157
|
+
}
|
|
158
|
+
|
|
93
159
|
function clampHistoryItems(items, maxItems) {
|
|
94
160
|
const list = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
95
161
|
const safeMax = Math.max(DEFAULT_HISTORY_EVENT_LIMIT, Number(maxItems) || DEFAULT_HISTORY_EVENT_LIMIT);
|
|
@@ -881,7 +947,21 @@ function formatActivity(event) {
|
|
|
881
947
|
const body = formatEventSnippet(event);
|
|
882
948
|
let runs = null;
|
|
883
949
|
|
|
950
|
+
// Native child transcripts remain inspectable as events, but only their
|
|
951
|
+
// lifecycle belongs in the parent's activity feed.
|
|
952
|
+
if (event.eventType?.startsWith("native.") || event.eventType === "session.background_tasks_changed") return null;
|
|
884
953
|
switch (event.eventType) {
|
|
954
|
+
case "subagent.started":
|
|
955
|
+
case "subagent.completed":
|
|
956
|
+
case "subagent.failed": {
|
|
957
|
+
const phase = event.eventType.slice("subagent.".length);
|
|
958
|
+
const name = event.data?.agentDisplayName || event.data?.agentName || "agent";
|
|
959
|
+
const duration = Number.isFinite(event.data?.durationMs)
|
|
960
|
+
? ` (${formatHumanDurationSeconds(event.data.durationMs / 1000)})` : "";
|
|
961
|
+
runs = buildLabeledActivityRuns(time, "[native agent]", "cyan", `${name} ${phase}${duration}`, phase === "failed" ? "red" : "white");
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
case "subagent.configured":
|
|
885
965
|
case "assistant.usage":
|
|
886
966
|
case "session.info":
|
|
887
967
|
case "session.idle":
|
|
@@ -1226,11 +1306,9 @@ export function buildHistoryModel(events = [], options = {}) {
|
|
|
1226
1306
|
|
|
1227
1307
|
for (const event of events) {
|
|
1228
1308
|
storedEvents.push(event);
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if (warning) chat.push(warning);
|
|
1233
|
-
}
|
|
1309
|
+
appendNativeTaskEvent(chat, event);
|
|
1310
|
+
appendParentChatCall(chat, event);
|
|
1311
|
+
appendSessionWarning(chat, event);
|
|
1234
1312
|
if (["user.message", "session.turn_completed", "session.turn_stopped", "session.epoch_committed"].includes(event.eventType)) {
|
|
1235
1313
|
settleAssistantResponses(chat, event);
|
|
1236
1314
|
}
|
|
@@ -1309,6 +1387,7 @@ export function appendEventToHistory(history, event) {
|
|
|
1309
1387
|
? existingEvents
|
|
1310
1388
|
: [...existingEvents, event].slice(-loadedEventLimit);
|
|
1311
1389
|
const next = {
|
|
1390
|
+
nativeTaskSnapshot: history?.nativeTaskSnapshot,
|
|
1312
1391
|
closedLiveKeys: history?.closedLiveKeys || [],
|
|
1313
1392
|
closedLiveStreams: history?.closedLiveStreams || [],
|
|
1314
1393
|
chat: clampHistoryItems(history?.chat || [], loadedEventLimit),
|
|
@@ -1328,14 +1407,10 @@ export function appendEventToHistory(history, event) {
|
|
|
1328
1407
|
stoppedMessageIds: Array.isArray(history?.stoppedMessageIds) ? history.stoppedMessageIds : [],
|
|
1329
1408
|
};
|
|
1330
1409
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
appendChatCall(next.chat, event);
|
|
1410
|
+
appendNativeTaskEvent(next.chat, event);
|
|
1411
|
+
appendParentChatCall(next.chat, event);
|
|
1412
|
+
appendSessionWarning(next.chat, event);
|
|
1413
|
+
next.chat = clampHistoryItems(next.chat, loadedEventLimit);
|
|
1339
1414
|
|
|
1340
1415
|
if (["user.message", "session.turn_completed", "session.turn_stopped", "session.epoch_committed"].includes(event.eventType)) {
|
|
1341
1416
|
settleAssistantResponses(next.chat, event);
|
package/ui/core/src/moa.js
CHANGED
|
@@ -57,6 +57,15 @@ export const activeMoaDashboard = value => value.dashboards.find(d => d.id === v
|
|
|
57
57
|
export function updateMoaDashboard(value, dashboardId, patch) {
|
|
58
58
|
return normalizeMoa({ ...value, dashboards: value.dashboards.map(d => d.id === dashboardId ? { ...d, ...patch } : d) });
|
|
59
59
|
}
|
|
60
|
+
export function moveMoaDashboard(value, dashboardId, toIndex) {
|
|
61
|
+
const normalized = normalizeMoa(value);
|
|
62
|
+
const fromIndex = normalized.dashboards.findIndex(d => d.id === dashboardId);
|
|
63
|
+
if (fromIndex < 0 || !Number.isInteger(toIndex)) return normalized;
|
|
64
|
+
const dashboards = [...normalized.dashboards];
|
|
65
|
+
const [dashboard] = dashboards.splice(fromIndex, 1);
|
|
66
|
+
dashboards.splice(Math.max(0, Math.min(dashboards.length, toIndex)), 0, dashboard);
|
|
67
|
+
return { ...normalized, dashboards };
|
|
68
|
+
}
|
|
60
69
|
export function replaceMoaNode(tree, nodeId, next) {
|
|
61
70
|
if (!tree) return null;
|
|
62
71
|
if (tree.id === nodeId) return next;
|
|
@@ -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();
|
|
@@ -646,6 +655,7 @@ function updateUiForSessionSelection(state, nextActiveSessionId) {
|
|
|
646
655
|
},
|
|
647
656
|
followBottom: {
|
|
648
657
|
...(state.ui.followBottom || {}),
|
|
658
|
+
chat: true,
|
|
649
659
|
inspector: true,
|
|
650
660
|
activity: true,
|
|
651
661
|
},
|
|
@@ -964,6 +974,9 @@ function baseReducer(state, action) {
|
|
|
964
974
|
case "auth/context":
|
|
965
975
|
return {
|
|
966
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,
|
|
967
980
|
auth: {
|
|
968
981
|
principal: action.principal ?? null,
|
|
969
982
|
authorization: action.authorization ?? null,
|
|
@@ -1481,7 +1494,7 @@ function baseReducer(state, action) {
|
|
|
1481
1494
|
};
|
|
1482
1495
|
|
|
1483
1496
|
case "ui/followBottom": {
|
|
1484
|
-
if (action.pane !== "inspector" && action.pane !== "activity") {
|
|
1497
|
+
if (action.pane !== "chat" && action.pane !== "inspector" && action.pane !== "activity") {
|
|
1485
1498
|
return state;
|
|
1486
1499
|
}
|
|
1487
1500
|
const nextFollowBottom = Boolean(action.followBottom);
|
|
@@ -1876,14 +1889,15 @@ function baseReducer(state, action) {
|
|
|
1876
1889
|
|
|
1877
1890
|
case "sessions/selected": {
|
|
1878
1891
|
state = { ...state, sessions: { ...state.sessions, listDeselected: false } };
|
|
1879
|
-
// Per-session chat scroll memory: stash the outgoing
|
|
1880
|
-
//
|
|
1881
|
-
//
|
|
1882
|
-
// latest — "latest chat, or where you left it".
|
|
1892
|
+
// Per-session chat scroll memory: stash both the outgoing offset
|
|
1893
|
+
// and whether it follows the bottom. A paused reading position
|
|
1894
|
+
// stays paused as new messages arrive and across session switches.
|
|
1883
1895
|
const previousActiveId = state.sessions.activeSessionId;
|
|
1884
1896
|
const savedChatScroll = { ...(state.ui.chatScrollBySession || {}) };
|
|
1897
|
+
const savedChatFollowBottom = { ...(state.ui.chatFollowBottomBySession || {}) };
|
|
1885
1898
|
if (previousActiveId && previousActiveId !== action.sessionId) {
|
|
1886
1899
|
savedChatScroll[previousActiveId] = Number(state.ui.scroll?.chat) || 0;
|
|
1900
|
+
savedChatFollowBottom[previousActiveId] = state.ui.followBottom?.chat !== false;
|
|
1887
1901
|
}
|
|
1888
1902
|
// Per-session prompt drafts: a half-written message belongs to the
|
|
1889
1903
|
// session it was written in. Stash the outgoing draft (text +
|
|
@@ -1936,6 +1950,7 @@ function baseReducer(state, action) {
|
|
|
1936
1950
|
ui: {
|
|
1937
1951
|
...state.ui,
|
|
1938
1952
|
chatScrollBySession: savedChatScroll,
|
|
1953
|
+
chatFollowBottomBySession: savedChatFollowBottom,
|
|
1939
1954
|
promptDraftBySession: savedDrafts,
|
|
1940
1955
|
prompt: nextPrompt,
|
|
1941
1956
|
promptCursor: nextPromptCursor,
|
|
@@ -1954,6 +1969,7 @@ function baseReducer(state, action) {
|
|
|
1954
1969
|
},
|
|
1955
1970
|
followBottom: {
|
|
1956
1971
|
...(state.ui.followBottom || {}),
|
|
1972
|
+
chat: savedChatFollowBottom[action.sessionId] !== false,
|
|
1957
1973
|
inspector: true,
|
|
1958
1974
|
activity: true,
|
|
1959
1975
|
},
|
|
@@ -2201,7 +2217,7 @@ function baseReducer(state, action) {
|
|
|
2201
2217
|
...state.history,
|
|
2202
2218
|
bySessionId: nextHistory,
|
|
2203
2219
|
},
|
|
2204
|
-
ui: activeChatUpdated
|
|
2220
|
+
ui: activeChatUpdated && state.ui.followBottom?.chat !== false
|
|
2205
2221
|
? {
|
|
2206
2222
|
...state.ui,
|
|
2207
2223
|
scroll: {
|
|
@@ -2222,6 +2238,8 @@ function baseReducer(state, action) {
|
|
|
2222
2238
|
for (const id of ids) delete nextOutbox[id];
|
|
2223
2239
|
const nextChatScroll = { ...(state.ui.chatScrollBySession || {}) };
|
|
2224
2240
|
for (const id of ids) delete nextChatScroll[id];
|
|
2241
|
+
const nextChatFollowBottom = { ...(state.ui.chatFollowBottomBySession || {}) };
|
|
2242
|
+
for (const id of ids) delete nextChatFollowBottom[id];
|
|
2225
2243
|
const nextDrafts = { ...(state.ui.promptDraftBySession || {}) };
|
|
2226
2244
|
for (const id of ids) delete nextDrafts[id];
|
|
2227
2245
|
return {
|
|
@@ -2237,6 +2255,7 @@ function baseReducer(state, action) {
|
|
|
2237
2255
|
ui: {
|
|
2238
2256
|
...state.ui,
|
|
2239
2257
|
chatScrollBySession: nextChatScroll,
|
|
2258
|
+
chatFollowBottomBySession: nextChatFollowBottom,
|
|
2240
2259
|
promptDraftBySession: nextDrafts,
|
|
2241
2260
|
},
|
|
2242
2261
|
};
|
|
@@ -2506,6 +2525,9 @@ function baseReducer(state, action) {
|
|
|
2506
2525
|
loading: false,
|
|
2507
2526
|
loadError: null,
|
|
2508
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,
|
|
2509
2531
|
ghcpKey: {
|
|
2510
2532
|
...state.admin.ghcpKey,
|
|
2511
2533
|
// A successful load doesn't clobber an in-progress
|
|
@@ -2957,9 +2979,11 @@ function baseReducer(state, action) {
|
|
|
2957
2979
|
};
|
|
2958
2980
|
}
|
|
2959
2981
|
case "admin/section": {
|
|
2960
|
-
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";
|
|
2961
2983
|
return { ...state, admin: { ...state.admin, section } };
|
|
2962
2984
|
}
|
|
2985
|
+
case "admin/features":
|
|
2986
|
+
return { ...state, admin: { ...state.admin, features: { ...state.admin.features, ...action.patch } } };
|
|
2963
2987
|
case "admin/workers/attempt": {
|
|
2964
2988
|
const workers = state.admin.workers || {};
|
|
2965
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 })),
|