pilotswarm 0.5.61 → 0.5.63
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/resources/models.d.ts.map +1 -1
- package/mcp/dist/src/resources/models.js +6 -1
- package/mcp/dist/src/resources/models.js.map +1 -1
- package/mcp/dist/src/tools/models.d.ts.map +1 -1
- package/mcp/dist/src/tools/models.js +11 -1
- package/mcp/dist/src/tools/models.js.map +1 -1
- package/mcp/dist/src/tools/sessions.d.ts.map +1 -1
- package/mcp/dist/src/tools/sessions.js +8 -2
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/package.json +2 -2
- package/tui/src/node-sdk-transport.js +1 -1
- package/ui/core/src/chat-activity.js +94 -0
- package/ui/core/src/controller.js +87 -19
- package/ui/core/src/history.js +15 -0
- package/ui/core/src/moa.js +41 -11
- package/ui/core/src/reducer.js +25 -10
- package/ui/core/src/selectors.js +25 -5
- package/ui/core/src/session-errors.js +10 -2
- package/ui/core/src/state.js +6 -2
- package/ui/core/src/system-titles.js +2 -0
- package/ui/react/src/components.js +2 -1
- package/ui/react/src/web-app.js +110 -35
- package/web/dist/assets/{abnfDiagram-VRR7QNED-CCVkGWsr.js → abnfDiagram-VRR7QNED-C8iAmIqt.js} +1 -1
- package/web/dist/assets/{arc-y040btO3.js → arc-Cg8qs17j.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-24Yt5esy.js → architectureDiagram-ZJ3FMSHR-fYr0dRm3.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-BlbS-_GI.js → blockDiagram-677ZJIJ3-Bpj79Dzq.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-_mamn0Fv.js → c4Diagram-LMCZKHZV-b26Lsm6m.js} +1 -1
- package/web/dist/assets/channel-C-ASbcRr.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-TCbIB2Ni.js → chunk-2Q5K7J3B-VLYQfjgG.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-C9q5KNKG.js → chunk-32BRIVSS-mVx--2Hg.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BvoqfU8P.js → chunk-5VM5RSS4-jwnJmked.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-zFJjZeNd.js → chunk-EX3LRPZG-DdfzAT1n.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-Ce_6C5M-.js → chunk-JWPE2WC7-BHm6Udd8.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-CPjpLM65.js → chunk-MOJQB5TN-Dn5qSVAS.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-CsmVUSDH.js → chunk-RYQCIY6F-D4IKMROn.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-CgrmPPOd.js → chunk-V7JOEXUC-BhME3pRf.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-BxAHDR7h.js → chunk-VR4S4FIN-CHG_NBff.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-BGfFoO5_.js → chunk-XXDRQBXY-yw4QCgiL.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-D2vlJIb1.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-D2vlJIb1.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-B6KcngJW.js → cose-bilkent-JH36ORCC-DqwPj9Kx.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DaY7l4Ce.js → cynefin-VYW2F7L2-CVokaaE_.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BldfGsoa.js → cynefinDiagram-TSTJHNR4-Bkhj9fuY.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-C4T9X5Bs.js → dagre-VKFMJZFB-DVv-jnjb.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-DCVA_t-e.js → diagram-FQU43EPY-DDlZjkrF.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-pbTZGgE5.js → diagram-G47NLZAW-CNDUgqYa.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-P8Iks122.js → diagram-NH7WQ7WH-DywF8u8i.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-hEjZPipp.js → diagram-OA4YK3LP-ZfN8nvLw.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-CVAzVCGu.js → diagram-WEI45ONY-S94A6Avm.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-BLL398qt.js → ebnfDiagram-CCIWWBDH-Mpt3QUnw.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-CLTADyUG.js → erDiagram-Q63AITRT-Bh-eWbdV.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-BAdlWAt0.js → flowDiagram-23GEKE2U-BwRtDI6V.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-C7Z2wavr.js → ganttDiagram-NO4QXBWP-CrZoBmcB.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BI546pYu.js → gitGraphDiagram-IHSO6WYX-giXv1ynk.js} +1 -1
- package/web/dist/assets/index-Br-gGWmc.js +24 -0
- package/web/dist/assets/{index-CvW-4eXF.css → index-DRFDn87d.css} +1 -1
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-8C7iQydo.js → infoDiagram-FWYZ7A6U-H_5oc4-7.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-Co4STVxQ.js → ishikawaDiagram-FXEZZL3T-BydiPDX0.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-DbUt11q-.js → journeyDiagram-5HDEW3XC-DS1HXms0.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-DUNcFmme.js → kanban-definition-HUTT4EX6-BjY5SyYu.js} +1 -1
- package/web/dist/assets/{linear-Cjaxj94d.js → linear-BvOotjKL.js} +1 -1
- package/web/dist/assets/{mermaid.core-B3cPFf4f.js → mermaid.core-0iRu7sp-.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-DaZtxB4N.js → mindmap-definition-LN4V7U3C-DB93zsvL.js} +1 -1
- package/web/dist/assets/{papaparse.min-vfdso2fs.js → papaparse.min-f5Xncedc.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-DkBpYV7m.js → pegDiagram-2B236MQR-Dh_U1cFA.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-By3tI-vc.js → pieDiagram-ENE6RG2P-Csv1qxMm.js} +1 -1
- package/web/dist/assets/pilotswarm-DQNSWfNK.js +105 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-DZmVm0xK.js → quadrantDiagram-ABIIQ3AL-Bo0pcrBM.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-BC5Mbz7q.js → railroadDiagram-RFXS5EU6-CHPvHNWG.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-CyZNX_O1.js → requirementDiagram-TGXJPOKE-DgX8l-6H.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-7603NEmu.js → sankeyDiagram-HTMAVEWB-xR6MTs5a.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CXJ-i2Xj.js → sequenceDiagram-DBY2YBRQ-CD_45diB.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CKT0ATnS.js → sizeCapture-X5ZJPWSS-BJVEXU0K.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-BIDPX4YU.js → stateDiagram-2N3HPSRC-Bwu6kd23.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-aypsN5jW.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-CPoy6vnO.js → swimlanes-5IMT3BWC-BmhEzevT.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-Duf5C4YK.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-BGdNkplk.js → timeline-definition-FHXFAJF6-DFPI_uTI.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-ByFzarUb.js → vennDiagram-L72KCM5P-DDTpTkBC.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CAk0SYuv.js → wardleyDiagram-EHGQE667-bV2_YVIL.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-Np7h6_A0.js → xychartDiagram-FW5EYKEG-C7bDIXAV.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +1 -0
- package/web/dist/assets/channel-COFs9dqk.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-B5-KmsCx.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-B5-KmsCx.js +0 -1
- package/web/dist/assets/index-DsPaDrf9.js +0 -24
- package/web/dist/assets/pilotswarm-BL_NbZug.js +0 -98
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-Dr8oD9T6.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BV0EQRlB.js +0 -8
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DEFAULT_HISTORY_EVENT_LIMIT,
|
|
12
12
|
dedupeChatMessages,
|
|
13
13
|
getNextHistoryEventLimit,
|
|
14
|
+
parseAskedAndAnsweredExchange,
|
|
14
15
|
} from "./history.js";
|
|
15
16
|
import { applySessionUsageEvent, cloneContextUsageSnapshot } from "./context-usage.js";
|
|
16
17
|
import { validateCanvasAction, formatCanvasActionPrompt, createCanvasActionLimiter } from "./canvas-actions.js";
|
|
@@ -5490,6 +5491,7 @@ export class PilotSwarmUiController {
|
|
|
5490
5491
|
/**
|
|
5491
5492
|
* Reconcile outbox state against a single CMS event. Removes outbox items
|
|
5492
5493
|
* whose `clientMessageIds` were observed as a durable `user.message`
|
|
5494
|
+
* (or a legacy merged `system.message` carrying those exact identities)
|
|
5493
5495
|
* (acknowledged → drop the local optimistic item) or as a
|
|
5494
5496
|
* `pending_messages.cancelled` (cancel confirmed → drop the cancelling
|
|
5495
5497
|
* item). Pure side-effect helper that is safe to call repeatedly.
|
|
@@ -5501,16 +5503,20 @@ export class PilotSwarmUiController {
|
|
|
5501
5503
|
*/
|
|
5502
5504
|
reconcileOutboxAgainstEvent(sessionId, event) {
|
|
5503
5505
|
if (!sessionId || !event) return;
|
|
5504
|
-
if (event.eventType === "user.message") {
|
|
5506
|
+
if (event.eventType === "user.message" || event.eventType === "system.message") {
|
|
5505
5507
|
const content = event?.data?.content;
|
|
5506
5508
|
const clientMessageIds = Array.isArray(event?.data?.clientMessageIds)
|
|
5507
5509
|
? event.data.clientMessageIds.filter((id) => typeof id === "string")
|
|
5508
5510
|
: (typeof event?.data?.clientMessageId === "string" ? [event.data.clientMessageId] : []);
|
|
5509
5511
|
if (clientMessageIds.length > 0) {
|
|
5510
5512
|
for (const id of clientMessageIds) {
|
|
5511
|
-
|
|
5513
|
+
// Older workers classified a merged internal + user turn
|
|
5514
|
+
// from its leading protocol marker. The preserved IDs are
|
|
5515
|
+
// still an exact receipt. Never text-match a system event:
|
|
5516
|
+
// its content can quote unrelated pending user input.
|
|
5517
|
+
this.acknowledgeOutboxPrompt(sessionId, event.eventType === "user.message" ? content : undefined, id);
|
|
5512
5518
|
}
|
|
5513
|
-
} else if (typeof content === "string" && content.trim()) {
|
|
5519
|
+
} else if (event.eventType === "user.message" && typeof content === "string" && content.trim()) {
|
|
5514
5520
|
// Fallback: text-match acknowledgement until clientMessageId is
|
|
5515
5521
|
// plumbed through every layer.
|
|
5516
5522
|
this.acknowledgeOutboxPrompt(sessionId, content);
|
|
@@ -5832,10 +5838,11 @@ export class PilotSwarmUiController {
|
|
|
5832
5838
|
}
|
|
5833
5839
|
const existing = this.getState().history.bySessionId.get(sessionId)
|
|
5834
5840
|
|| { chat: [], activity: [], events: [], lastSeq: 0 };
|
|
5841
|
+
const liveHistory = this.preservePausedChatWindow(sessionId, existing);
|
|
5835
5842
|
this.dispatch({
|
|
5836
5843
|
type: "history/set",
|
|
5837
5844
|
sessionId,
|
|
5838
|
-
history: applyLiveTurnToHistory(
|
|
5845
|
+
history: applyLiveTurnToHistory(liveHistory, event.data, {
|
|
5839
5846
|
sessionId,
|
|
5840
5847
|
seq: event.liveSeq,
|
|
5841
5848
|
createdAt: Date.now(),
|
|
@@ -5872,7 +5879,10 @@ export class PilotSwarmUiController {
|
|
|
5872
5879
|
this._liveTurnIdleTimers.delete(sessionId);
|
|
5873
5880
|
}
|
|
5874
5881
|
const state = this.getState();
|
|
5875
|
-
const existing =
|
|
5882
|
+
const existing = this.preservePausedChatWindow(
|
|
5883
|
+
sessionId,
|
|
5884
|
+
state.history.bySessionId.get(sessionId) || { chat: [], activity: [], events: [], lastSeq: 0 },
|
|
5885
|
+
);
|
|
5876
5886
|
if (event.seq <= (existing.lastSeq || 0)) return false;
|
|
5877
5887
|
this.dispatch({
|
|
5878
5888
|
type: "history/set",
|
|
@@ -5887,7 +5897,11 @@ export class PilotSwarmUiController {
|
|
|
5887
5897
|
// misrouted into the outbox queue, where it would sit until a later send
|
|
5888
5898
|
// flushed it. The eventual detail-sync reconciles to the authoritative
|
|
5889
5899
|
// customStatus.
|
|
5890
|
-
|
|
5900
|
+
const questionSession = this.getState().sessions.byId[sessionId];
|
|
5901
|
+
const questionEventTime = timestampMs(event.createdAt);
|
|
5902
|
+
const currentSessionTime = timestampMs(questionSession?.updatedAt);
|
|
5903
|
+
if (event.eventType === "session.input_required_started" && event.data?.question
|
|
5904
|
+
&& (!questionEventTime || !currentSessionTime || questionEventTime >= currentSessionTime)) {
|
|
5891
5905
|
this.dispatch({
|
|
5892
5906
|
type: "sessions/merged",
|
|
5893
5907
|
session: {
|
|
@@ -5895,12 +5909,27 @@ export class PilotSwarmUiController {
|
|
|
5895
5909
|
status: "input_required",
|
|
5896
5910
|
pendingQuestion: {
|
|
5897
5911
|
question: event.data.question,
|
|
5912
|
+
...(questionEventTime ? { askedAt: questionEventTime } : {}),
|
|
5913
|
+
...(Number.isSafeInteger(event.data.questionIteration) ? { iteration: event.data.questionIteration } : {}),
|
|
5898
5914
|
choices: Array.isArray(event.data.choices) ? event.data.choices : undefined,
|
|
5899
5915
|
allowFreeform: event.data.allowFreeform ?? true,
|
|
5900
5916
|
},
|
|
5901
5917
|
},
|
|
5902
5918
|
});
|
|
5903
5919
|
}
|
|
5920
|
+
// Catch-up events also include answers from other windows/writers.
|
|
5921
|
+
// Retire the matching question before another send can use sendAnswer.
|
|
5922
|
+
if (event.eventType === "user.message" && questionSession?.pendingQuestion) {
|
|
5923
|
+
const exchange = parseAskedAndAnsweredExchange(event.data?.content);
|
|
5924
|
+
const pendingSince = Math.max(currentSessionTime, timestampMs(questionSession.pendingQuestion.askedAt));
|
|
5925
|
+
if (exchange?.question === questionSession.pendingQuestion.question
|
|
5926
|
+
&& (!questionEventTime || !pendingSince || questionEventTime >= pendingSince)) {
|
|
5927
|
+
this.dispatch({ type: "sessions/merged", session: {
|
|
5928
|
+
sessionId, pendingQuestion: null,
|
|
5929
|
+
resolvedInputQuestion: { question: exchange.question, answeredAt: questionEventTime },
|
|
5930
|
+
} });
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5904
5933
|
// show_artifact: the agent is presenting something to look at, so the
|
|
5905
5934
|
// inspector switches to Files and opens that preview live.
|
|
5906
5935
|
//
|
|
@@ -5954,6 +5983,31 @@ export class PilotSwarmUiController {
|
|
|
5954
5983
|
return true;
|
|
5955
5984
|
}
|
|
5956
5985
|
|
|
5986
|
+
/**
|
|
5987
|
+
* Do not evict the first rendered item while its active chat is paused.
|
|
5988
|
+
* A fixed-size history window normally drops one old item for each live
|
|
5989
|
+
* append. Keeping the same numeric scrollTop then moves the actual text the
|
|
5990
|
+
* user is reading. Grow the in-memory window just enough for arrivals made
|
|
5991
|
+
* during that pause; normal bottom-follow appends resume bounded trimming.
|
|
5992
|
+
*/
|
|
5993
|
+
preservePausedChatWindow(sessionId, history) {
|
|
5994
|
+
const state = this.getState();
|
|
5995
|
+
if (state.sessions.activeSessionId !== sessionId || state.ui.followBottom?.chat !== false) {
|
|
5996
|
+
return history;
|
|
5997
|
+
}
|
|
5998
|
+
const currentLimit = Math.max(
|
|
5999
|
+
DEFAULT_HISTORY_EVENT_LIMIT,
|
|
6000
|
+
Number(history?.loadedEventLimit ?? DEFAULT_HISTORY_EVENT_LIMIT) || DEFAULT_HISTORY_EVENT_LIMIT,
|
|
6001
|
+
);
|
|
6002
|
+
const requiredLimit = Math.max(
|
|
6003
|
+
currentLimit,
|
|
6004
|
+
(Array.isArray(history?.events) ? history.events.length : 0) + 1,
|
|
6005
|
+
(Array.isArray(history?.chat) ? history.chat.length : 0) + 4,
|
|
6006
|
+
(Array.isArray(history?.activity) ? history.activity.length : 0) + 1,
|
|
6007
|
+
);
|
|
6008
|
+
return requiredLimit === currentLimit ? history : { ...history, loadedEventLimit: requiredLimit };
|
|
6009
|
+
}
|
|
6010
|
+
|
|
5957
6011
|
async syncSessionEvents(sessionId) {
|
|
5958
6012
|
if (!sessionId || typeof this.transport.getSessionEvents !== "function") return;
|
|
5959
6013
|
const existing = this.getState().history.bySessionId.get(sessionId);
|
|
@@ -8685,7 +8739,10 @@ export class PilotSwarmUiController {
|
|
|
8685
8739
|
});
|
|
8686
8740
|
this.dispatch({ type: "ui/status", text: "Sending answer..." });
|
|
8687
8741
|
try {
|
|
8688
|
-
await this.transport.sendAnswer(sessionId, prompt
|
|
8742
|
+
await this.transport.sendAnswer(sessionId, prompt, { expectedQuestion: {
|
|
8743
|
+
question: activePendingQuestion.question,
|
|
8744
|
+
...(Number.isSafeInteger(activePendingQuestion.iteration) ? { iteration: activePendingQuestion.iteration } : {}),
|
|
8745
|
+
} });
|
|
8689
8746
|
this.dispatch({
|
|
8690
8747
|
type: "sessions/merged",
|
|
8691
8748
|
session: {
|
|
@@ -9150,6 +9207,7 @@ export class PilotSwarmUiController {
|
|
|
9150
9207
|
}
|
|
9151
9208
|
|
|
9152
9209
|
paneUsesStickyBottomFollow(pane, state = this.getState()) {
|
|
9210
|
+
if (pane === "chat") return true;
|
|
9153
9211
|
if (pane === "activity") return true;
|
|
9154
9212
|
if (pane === "inspector") return state.ui.inspectorTab === "logs";
|
|
9155
9213
|
return false;
|
|
@@ -9223,6 +9281,15 @@ export class PilotSwarmUiController {
|
|
|
9223
9281
|
const current = this.getPaneVisualScrollOffset(pane, state);
|
|
9224
9282
|
const nextOffset = Math.max(0, Math.min(current - delta, maxOffset));
|
|
9225
9283
|
this.applyPaneVisualScrollOffset(pane, nextOffset, { followBottom: nextOffset >= maxOffset }, state);
|
|
9284
|
+
if (pane === "chat" && delta > 0) {
|
|
9285
|
+
if (current <= 0) {
|
|
9286
|
+
this.handleChatTopHistoryScrollIntent(nextOffset).catch(() => {});
|
|
9287
|
+
} else if (nextOffset <= 0) {
|
|
9288
|
+
this.armChatTopHistoryLoad();
|
|
9289
|
+
}
|
|
9290
|
+
} else if (pane === "chat" && delta < 0) {
|
|
9291
|
+
this.disarmChatTopHistoryLoad();
|
|
9292
|
+
}
|
|
9226
9293
|
return;
|
|
9227
9294
|
}
|
|
9228
9295
|
const current = Math.max(0, Math.min(Number(state.ui.scroll?.[pane]) || 0, maxOffset));
|
|
@@ -9245,6 +9312,10 @@ export class PilotSwarmUiController {
|
|
|
9245
9312
|
const nextOffset = Math.max(0, Math.min(Number(offset) || 0, maxOffset));
|
|
9246
9313
|
if (this.paneUsesStickyBottomFollow(pane, state)) {
|
|
9247
9314
|
this.applyPaneVisualScrollOffset(pane, nextOffset, { followBottom: nextOffset >= maxOffset }, state);
|
|
9315
|
+
if (pane === "chat") {
|
|
9316
|
+
if (maxOffset > 0 && nextOffset <= 0) this.armChatTopHistoryLoad();
|
|
9317
|
+
else this.disarmChatTopHistoryLoad();
|
|
9318
|
+
}
|
|
9248
9319
|
return;
|
|
9249
9320
|
}
|
|
9250
9321
|
this.dispatch({ type: "ui/scroll", pane, offset: nextOffset });
|
|
@@ -9280,6 +9351,7 @@ export class PilotSwarmUiController {
|
|
|
9280
9351
|
|
|
9281
9352
|
await this.maybeAutoExpandActiveHistory(requestedScrollOffset, {
|
|
9282
9353
|
pages: AUTO_HISTORY_SCROLL_PAGE_COUNT,
|
|
9354
|
+
preserveDomAnchor: options.preserveDomAnchor === true,
|
|
9283
9355
|
// Always bypass the offset gate. Every caller of this method fires
|
|
9284
9356
|
// only when the DOM scroller is already AT the top — a direct
|
|
9285
9357
|
// measurement. The gate is a second, weaker guess that compares a
|
|
@@ -9341,7 +9413,7 @@ export class PilotSwarmUiController {
|
|
|
9341
9413
|
const pane = this.getScrollablePaneForFocus();
|
|
9342
9414
|
if (!pane) return;
|
|
9343
9415
|
if (this.paneUsesStickyBottomFollow(pane)) {
|
|
9344
|
-
this.
|
|
9416
|
+
this.scrollPaneTo(pane, 0);
|
|
9345
9417
|
return;
|
|
9346
9418
|
}
|
|
9347
9419
|
const inspectorUsesBottomScroll = pane === "inspector" && this.inspectorUsesBottomScroll();
|
|
@@ -9621,6 +9693,7 @@ export class PilotSwarmUiController {
|
|
|
9621
9693
|
requestedScrollOffset: targetOffset,
|
|
9622
9694
|
autoTriggered: true,
|
|
9623
9695
|
pages: options.pages,
|
|
9696
|
+
preserveDomAnchor: options.preserveDomAnchor === true,
|
|
9624
9697
|
// Chat-driven pull: page backward over renderable message types
|
|
9625
9698
|
// only, so each page is transcript instead of raw event noise.
|
|
9626
9699
|
eventTypes: CHAT_HISTORY_EVENT_TYPES,
|
|
@@ -9757,9 +9830,12 @@ export class PilotSwarmUiController {
|
|
|
9757
9830
|
pane: "chat",
|
|
9758
9831
|
offset: 0,
|
|
9759
9832
|
});
|
|
9760
|
-
} else if (preserveChatView &&
|
|
9761
|
-
//
|
|
9762
|
-
//
|
|
9833
|
+
} else if (preserveChatView && options.autoTriggered && !options.preserveDomAnchor) {
|
|
9834
|
+
// Terminal panes store paused chat as a top offset. Prepending
|
|
9835
|
+
// must advance that offset by the rendered rows inserted above
|
|
9836
|
+
// it, preserving the same visible message. Browser panes use
|
|
9837
|
+
// measured DOM height because rich cards and wrapping do not
|
|
9838
|
+
// match terminal row metrics.
|
|
9763
9839
|
const nextState = this.getState();
|
|
9764
9840
|
const nextRenderedLines = this.getActiveChatRenderMetrics(nextState).totalLines;
|
|
9765
9841
|
const addedLines = Math.max(0, nextRenderedLines - previousRenderedLines);
|
|
@@ -9771,14 +9847,6 @@ export class PilotSwarmUiController {
|
|
|
9771
9847
|
});
|
|
9772
9848
|
}
|
|
9773
9849
|
}
|
|
9774
|
-
// AUTO-triggered (scrolling reached the top): change NOTHING. The
|
|
9775
|
-
// offset is distance-from-BOTTOM, which prepended content cannot
|
|
9776
|
-
// move — leaving it alone keeps the viewport glued to the exact
|
|
9777
|
-
// messages the user was reading, and they scroll on into the new
|
|
9778
|
-
// page naturally. Adding the delta here teleported the view to
|
|
9779
|
-
// the TOP of a 3.3x-larger window — observed as "loading history
|
|
9780
|
-
// takes you way back instead of a few pages up".
|
|
9781
|
-
|
|
9782
9850
|
const stateLabel = history.hasOlderEvents
|
|
9783
9851
|
? options.autoTriggered
|
|
9784
9852
|
? `Loaded older history page from CMS (${history.loadedEventCount} events loaded)`
|
package/ui/core/src/history.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isCanvasActionContent, parseCanvasActionContent } from "./canvas-action
|
|
|
3
3
|
import { formatCompactionActivityRuns } from "./context-usage.js";
|
|
4
4
|
import { canonicalSystemTitle } from "./system-titles.js";
|
|
5
5
|
import { buildSessionWarning } from "./session-errors.js";
|
|
6
|
+
import { appendChatCall, CHAT_CALL_EVENT_TYPES } from "./chat-activity.js";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_HISTORY_EVENT_LIMIT = 300;
|
|
8
9
|
export const HISTORY_EVENT_LIMIT_STEPS = [
|
|
@@ -17,6 +18,7 @@ export const HISTORY_EVENT_LIMIT_STEPS = [
|
|
|
17
18
|
// noisy sessions (thousands of tool/orchestration events between messages)
|
|
18
19
|
// load transcript pages instead of raw-stream pages.
|
|
19
20
|
export const CHAT_HISTORY_EVENT_TYPES = [
|
|
21
|
+
...CHAT_CALL_EVENT_TYPES,
|
|
20
22
|
"session.error",
|
|
21
23
|
"user.message",
|
|
22
24
|
"assistant.message",
|
|
@@ -354,6 +356,7 @@ function buildSessionMessageChatCard(event, text) {
|
|
|
354
356
|
time: formatTimestamp(event.createdAt),
|
|
355
357
|
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
356
358
|
cardTitle: expectsResponse ? "Session Request" : "Session Message",
|
|
359
|
+
agentCallPreview: body,
|
|
357
360
|
cardTitleColor: "cyan",
|
|
358
361
|
cardBorderColor: "cyan",
|
|
359
362
|
};
|
|
@@ -381,6 +384,7 @@ function buildSessionMessageChatCard(event, text) {
|
|
|
381
384
|
time: formatTimestamp(event.createdAt),
|
|
382
385
|
createdAt: event.createdAt instanceof Date ? event.createdAt.getTime() : new Date(event.createdAt).getTime(),
|
|
383
386
|
cardTitle: "Session Reply",
|
|
387
|
+
agentCallPreview: body,
|
|
384
388
|
cardTitleColor: "green",
|
|
385
389
|
cardBorderColor: "green",
|
|
386
390
|
};
|
|
@@ -405,6 +409,13 @@ function sharesClientMessageId(left, right) {
|
|
|
405
409
|
function areMessagesEquivalent(left, right) {
|
|
406
410
|
if (!left || !right) return false;
|
|
407
411
|
if (left.role !== right.role) return false;
|
|
412
|
+
// Different agents can send the same short acknowledgement. Only a
|
|
413
|
+
// durable identity or a redelivered queue envelope can merge those calls.
|
|
414
|
+
if (left.sender?.kind === "agent" || right.sender?.kind === "agent") {
|
|
415
|
+
return left.sender?.kind === right.sender?.kind
|
|
416
|
+
&& left.sender?.sessionId === right.sender?.sessionId
|
|
417
|
+
&& (left.id === right.id || sharesClientMessageId(left, right) === true);
|
|
418
|
+
}
|
|
408
419
|
// Identical text in separate model messages still represents separate
|
|
409
420
|
// preview slots (and one may later become the final answer).
|
|
410
421
|
if (left.role === "assistant" && left.messageId && right.messageId) {
|
|
@@ -415,6 +426,7 @@ function areMessagesEquivalent(left, right) {
|
|
|
415
426
|
// link) — the redelivery time-window below would eat rev N-1 on every
|
|
416
427
|
// reload, making the transcript disagree with what live append showed.
|
|
417
428
|
if (left.kind === "canvas-update" || right.kind === "canvas-update"
|
|
429
|
+
|| left.kind === "chat-call" || right.kind === "chat-call"
|
|
418
430
|
|| left.kind === "session-warning" || right.kind === "session-warning") {
|
|
419
431
|
return left.kind === right.kind && left.id === right.id;
|
|
420
432
|
}
|
|
@@ -1214,6 +1226,7 @@ export function buildHistoryModel(events = [], options = {}) {
|
|
|
1214
1226
|
|
|
1215
1227
|
for (const event of events) {
|
|
1216
1228
|
storedEvents.push(event);
|
|
1229
|
+
appendChatCall(chat, event);
|
|
1217
1230
|
if (event.eventType === "session.error") {
|
|
1218
1231
|
const warning = buildSessionWarning(event);
|
|
1219
1232
|
if (warning) chat.push(warning);
|
|
@@ -1322,6 +1335,8 @@ export function appendEventToHistory(history, event) {
|
|
|
1322
1335
|
}
|
|
1323
1336
|
}
|
|
1324
1337
|
|
|
1338
|
+
appendChatCall(next.chat, event);
|
|
1339
|
+
|
|
1325
1340
|
if (["user.message", "session.turn_completed", "session.turn_stopped", "session.epoch_committed"].includes(event.eventType)) {
|
|
1326
1341
|
settleAssistantResponses(next.chat, event);
|
|
1327
1342
|
}
|
package/ui/core/src/moa.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Personal dashboards contain references and geometry only, never session data.
|
|
2
|
+
export const MOA_MAX_DASHBOARDS = 5;
|
|
2
3
|
export const MOA_MAX_PANELS = 16;
|
|
3
4
|
export const MOA_BREAKPOINT = 920;
|
|
4
5
|
const object = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
@@ -24,17 +25,46 @@ export function normalizeMoaLayout(value) {
|
|
|
24
25
|
return { name: typeof value.name === "string" ? value.name.trim().slice(0, 64) || "Untitled MoA" : "Untitled MoA", tree: value.tree == null ? null : walk(value.tree) };
|
|
25
26
|
}
|
|
26
27
|
export function normalizeMoa(value) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const safeLayout = (layout, dashboardId = "moa-1", name = "MoA 1") => {
|
|
29
|
+
let tree = null;
|
|
30
|
+
try { tree = normalizeMoaLayout(layout).tree; } catch {}
|
|
31
|
+
const leaves = moaLeaves(tree);
|
|
32
|
+
return { id: dashboardId, name: typeof layout?.name === "string" ? layout.name.trim().slice(0, 64) || name : name, tree,
|
|
33
|
+
...(Number.isFinite(layout?.aspectRatio) && layout.aspectRatio >= .2 && layout.aspectRatio <= 8 ? { aspectRatio: layout.aspectRatio } : {}),
|
|
34
|
+
focusedPanelId: leaves.some(p => p.id === layout?.focusedPanelId) ? layout.focusedPanelId : leaves[0]?.id || null };
|
|
35
|
+
};
|
|
36
|
+
if (value?.version === 3 && Array.isArray(value.dashboards)) {
|
|
37
|
+
const used = new Set();
|
|
38
|
+
const dashboards = value.dashboards.slice(0, MOA_MAX_DASHBOARDS).map((dashboard, index) => {
|
|
39
|
+
let key = id(dashboard?.id) && !used.has(dashboard.id) ? dashboard.id : `moa-${index + 1}`;
|
|
40
|
+
while (used.has(key)) key += "-copy";
|
|
41
|
+
used.add(key);
|
|
42
|
+
return safeLayout(dashboard, key, `MoA ${index + 1}`);
|
|
43
|
+
});
|
|
44
|
+
if (!dashboards.length) dashboards.push(safeLayout(null));
|
|
45
|
+
return { version: 3, activeDashboardId: dashboards.some(d => d.id === value.activeDashboardId) ? value.activeDashboardId : dashboards[0].id, dashboards };
|
|
31
46
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
let source = value;
|
|
48
|
+
if (!(value?.version === 2 || (object(value) && Object.hasOwn(value, "tree")))) {
|
|
49
|
+
// Match the old single-workspace migration; do not resurrect discarded tabs.
|
|
50
|
+
const slots = Array.isArray(value?.slots) ? value.slots.slice(0, 5) : [];
|
|
51
|
+
const active = Number.isInteger(value?.activeSlot) ? Math.max(0, Math.min(4, value.activeSlot)) : 0;
|
|
52
|
+
source = safeLayout(slots[active]).tree ? slots[active] : slots.find(slot => safeLayout(slot).tree);
|
|
53
|
+
}
|
|
54
|
+
return { version: 3, activeDashboardId: "moa-1", dashboards: [safeLayout(source)] };
|
|
55
|
+
}
|
|
56
|
+
export const activeMoaDashboard = value => value.dashboards.find(d => d.id === value.activeDashboardId) || value.dashboards[0];
|
|
57
|
+
export function updateMoaDashboard(value, dashboardId, patch) {
|
|
58
|
+
return normalizeMoa({ ...value, dashboards: value.dashboards.map(d => d.id === dashboardId ? { ...d, ...patch } : d) });
|
|
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 };
|
|
38
68
|
}
|
|
39
69
|
export function replaceMoaNode(tree, nodeId, next) {
|
|
40
70
|
if (!tree) return null;
|
package/ui/core/src/reducer.js
CHANGED
|
@@ -379,7 +379,7 @@ function mergeDefinedSessionFields(previousSession = {}, nextSession = {}) {
|
|
|
379
379
|
// An absent/invalid counter must not erase the last known server version.
|
|
380
380
|
if (key === "statusVersion" && previousVersion != null
|
|
381
381
|
&& sessionStatusVersion(nextSession) == null) continue;
|
|
382
|
-
if (key === "pendingQuestion" && isAnsweredPendingQuestion(previousSession, value)) {
|
|
382
|
+
if (key === "pendingQuestion" && isAnsweredPendingQuestion(previousSession, value, nextSession)) {
|
|
383
383
|
if (merged === previousSession) {
|
|
384
384
|
merged = { ...(previousSession || {}) };
|
|
385
385
|
}
|
|
@@ -555,10 +555,18 @@ function normalizedPendingQuestionText(pendingQuestion) {
|
|
|
555
555
|
return String(pendingQuestion?.question || "").trim();
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
-
function isAnsweredPendingQuestion(previousSession, pendingQuestion) {
|
|
559
|
-
const answeredQuestion = normalizedPendingQuestionText(previousSession?.answeredPendingQuestion);
|
|
558
|
+
function isAnsweredPendingQuestion(previousSession, pendingQuestion, nextSession) {
|
|
560
559
|
const incomingQuestion = normalizedPendingQuestionText(pendingQuestion);
|
|
561
|
-
|
|
560
|
+
if (!incomingQuestion) return false;
|
|
561
|
+
const incomingTime = sessionUpdateTimestampMs({ updatedAt: pendingQuestion?.askedAt })
|
|
562
|
+
|| sessionUpdateTimestampMs(nextSession);
|
|
563
|
+
return [previousSession?.answeredPendingQuestion, previousSession?.resolvedInputQuestion].some(answered => {
|
|
564
|
+
if (normalizedPendingQuestionText(answered) !== incomingQuestion) return false;
|
|
565
|
+
const answeredAt = sessionUpdateTimestampMs({ updatedAt: answered?.answeredAt });
|
|
566
|
+
// Preserve the answer fence across stale detail polls, but let a new
|
|
567
|
+
// instance of the same question through when it was asked later.
|
|
568
|
+
return !incomingTime || !answeredAt || incomingTime <= answeredAt;
|
|
569
|
+
});
|
|
562
570
|
}
|
|
563
571
|
|
|
564
572
|
function pickDefaultActiveSessionId(sessions = []) {
|
|
@@ -638,6 +646,7 @@ function updateUiForSessionSelection(state, nextActiveSessionId) {
|
|
|
638
646
|
},
|
|
639
647
|
followBottom: {
|
|
640
648
|
...(state.ui.followBottom || {}),
|
|
649
|
+
chat: true,
|
|
641
650
|
inspector: true,
|
|
642
651
|
activity: true,
|
|
643
652
|
},
|
|
@@ -1473,7 +1482,7 @@ function baseReducer(state, action) {
|
|
|
1473
1482
|
};
|
|
1474
1483
|
|
|
1475
1484
|
case "ui/followBottom": {
|
|
1476
|
-
if (action.pane !== "inspector" && action.pane !== "activity") {
|
|
1485
|
+
if (action.pane !== "chat" && action.pane !== "inspector" && action.pane !== "activity") {
|
|
1477
1486
|
return state;
|
|
1478
1487
|
}
|
|
1479
1488
|
const nextFollowBottom = Boolean(action.followBottom);
|
|
@@ -1868,14 +1877,15 @@ function baseReducer(state, action) {
|
|
|
1868
1877
|
|
|
1869
1878
|
case "sessions/selected": {
|
|
1870
1879
|
state = { ...state, sessions: { ...state.sessions, listDeselected: false } };
|
|
1871
|
-
// Per-session chat scroll memory: stash the outgoing
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
1874
|
-
// latest — "latest chat, or where you left it".
|
|
1880
|
+
// Per-session chat scroll memory: stash both the outgoing offset
|
|
1881
|
+
// and whether it follows the bottom. A paused reading position
|
|
1882
|
+
// stays paused as new messages arrive and across session switches.
|
|
1875
1883
|
const previousActiveId = state.sessions.activeSessionId;
|
|
1876
1884
|
const savedChatScroll = { ...(state.ui.chatScrollBySession || {}) };
|
|
1885
|
+
const savedChatFollowBottom = { ...(state.ui.chatFollowBottomBySession || {}) };
|
|
1877
1886
|
if (previousActiveId && previousActiveId !== action.sessionId) {
|
|
1878
1887
|
savedChatScroll[previousActiveId] = Number(state.ui.scroll?.chat) || 0;
|
|
1888
|
+
savedChatFollowBottom[previousActiveId] = state.ui.followBottom?.chat !== false;
|
|
1879
1889
|
}
|
|
1880
1890
|
// Per-session prompt drafts: a half-written message belongs to the
|
|
1881
1891
|
// session it was written in. Stash the outgoing draft (text +
|
|
@@ -1928,6 +1938,7 @@ function baseReducer(state, action) {
|
|
|
1928
1938
|
ui: {
|
|
1929
1939
|
...state.ui,
|
|
1930
1940
|
chatScrollBySession: savedChatScroll,
|
|
1941
|
+
chatFollowBottomBySession: savedChatFollowBottom,
|
|
1931
1942
|
promptDraftBySession: savedDrafts,
|
|
1932
1943
|
prompt: nextPrompt,
|
|
1933
1944
|
promptCursor: nextPromptCursor,
|
|
@@ -1946,6 +1957,7 @@ function baseReducer(state, action) {
|
|
|
1946
1957
|
},
|
|
1947
1958
|
followBottom: {
|
|
1948
1959
|
...(state.ui.followBottom || {}),
|
|
1960
|
+
chat: savedChatFollowBottom[action.sessionId] !== false,
|
|
1949
1961
|
inspector: true,
|
|
1950
1962
|
activity: true,
|
|
1951
1963
|
},
|
|
@@ -2193,7 +2205,7 @@ function baseReducer(state, action) {
|
|
|
2193
2205
|
...state.history,
|
|
2194
2206
|
bySessionId: nextHistory,
|
|
2195
2207
|
},
|
|
2196
|
-
ui: activeChatUpdated
|
|
2208
|
+
ui: activeChatUpdated && state.ui.followBottom?.chat !== false
|
|
2197
2209
|
? {
|
|
2198
2210
|
...state.ui,
|
|
2199
2211
|
scroll: {
|
|
@@ -2214,6 +2226,8 @@ function baseReducer(state, action) {
|
|
|
2214
2226
|
for (const id of ids) delete nextOutbox[id];
|
|
2215
2227
|
const nextChatScroll = { ...(state.ui.chatScrollBySession || {}) };
|
|
2216
2228
|
for (const id of ids) delete nextChatScroll[id];
|
|
2229
|
+
const nextChatFollowBottom = { ...(state.ui.chatFollowBottomBySession || {}) };
|
|
2230
|
+
for (const id of ids) delete nextChatFollowBottom[id];
|
|
2217
2231
|
const nextDrafts = { ...(state.ui.promptDraftBySession || {}) };
|
|
2218
2232
|
for (const id of ids) delete nextDrafts[id];
|
|
2219
2233
|
return {
|
|
@@ -2229,6 +2243,7 @@ function baseReducer(state, action) {
|
|
|
2229
2243
|
ui: {
|
|
2230
2244
|
...state.ui,
|
|
2231
2245
|
chatScrollBySession: nextChatScroll,
|
|
2246
|
+
chatFollowBottomBySession: nextChatFollowBottom,
|
|
2232
2247
|
promptDraftBySession: nextDrafts,
|
|
2233
2248
|
},
|
|
2234
2249
|
};
|
package/ui/core/src/selectors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { INSPECTOR_TABS, FOCUS_REGIONS } from "./commands.js";
|
|
2
|
+
import { chatCallLine, firstCallLine } from "./chat-activity.js";
|
|
2
3
|
import { canvasKey as canvasSlotKey, parseCanvasKey } from "./state.js";
|
|
3
4
|
import { isManuallyOrderableSession } from "./session-tree.js";
|
|
4
5
|
import {
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
getContextHeaderBadge,
|
|
29
30
|
} from "./context-usage.js";
|
|
30
31
|
import { canonicalSystemTitle } from "./system-titles.js";
|
|
31
|
-
import { withSessionWarnings } from "./session-errors.js";
|
|
32
|
+
import { withSessionWarnings, isActivityOnlySessionError } from "./session-errors.js";
|
|
32
33
|
import {
|
|
33
34
|
BUDGET_PERIODS,
|
|
34
35
|
BUDGET_SERIES_DAYS,
|
|
@@ -1645,7 +1646,7 @@ function chatAlreadyContainsPendingQuestion(chat, question) {
|
|
|
1645
1646
|
|
|
1646
1647
|
function buildSessionErrorMessage(session, events = []) {
|
|
1647
1648
|
const errorText = String(session?.error || "").trim();
|
|
1648
|
-
if (!errorText) return null;
|
|
1649
|
+
if (!errorText || isActivityOnlySessionError(errorText)) return null;
|
|
1649
1650
|
|
|
1650
1651
|
const errorKind = getSessionErrorVisualKind(session);
|
|
1651
1652
|
if (!errorKind) return null;
|
|
@@ -2251,6 +2252,7 @@ function chatMessageSpacingKind(message) {
|
|
|
2251
2252
|
|
|
2252
2253
|
function shouldInsertChatSpacer(currentMessage, nextMessage) {
|
|
2253
2254
|
if (!currentMessage || !nextMessage) return false;
|
|
2255
|
+
if (currentMessage.kind === "chat-call" || nextMessage.kind === "chat-call") return false;
|
|
2254
2256
|
return chatMessageSpacingKind(currentMessage) !== chatMessageSpacingKind(nextMessage);
|
|
2255
2257
|
}
|
|
2256
2258
|
|
|
@@ -2287,7 +2289,7 @@ function buildCollapsedSystemNoticeLine(text, timestamp = "") {
|
|
|
2287
2289
|
};
|
|
2288
2290
|
}
|
|
2289
2291
|
|
|
2290
|
-
function buildSystemNoticeLine({ title, summary, body, timestamp = "", color = "gray" }) {
|
|
2292
|
+
function buildSystemNoticeLine({ title, summary, body, timestamp = "", color = "gray", callPreview, callKey }) {
|
|
2291
2293
|
const normalizedTitle = String(title || "System").trim() || "System";
|
|
2292
2294
|
const normalizedSummary = String(summary || "").replace(/\s+/g, " ").trim();
|
|
2293
2295
|
return {
|
|
@@ -2296,6 +2298,7 @@ function buildSystemNoticeLine({ title, summary, body, timestamp = "", color = "
|
|
|
2296
2298
|
summary: normalizedSummary,
|
|
2297
2299
|
body: decorateArtifactLinksForChat(body || normalizedSummary),
|
|
2298
2300
|
color,
|
|
2301
|
+
...(callPreview !== undefined ? { callPreview: `${normalizedTitle} — ${firstCallLine(callPreview)}`, callKey } : {}),
|
|
2299
2302
|
};
|
|
2300
2303
|
}
|
|
2301
2304
|
|
|
@@ -2401,6 +2404,8 @@ function buildSystemNoticeCardLines(systemText, message, maxWidth) {
|
|
|
2401
2404
|
].filter((line, index) => index === 2 || line).join("\n");
|
|
2402
2405
|
return [buildSystemNoticeLine({
|
|
2403
2406
|
title: `Sub-agent Response — ${shortId}`,
|
|
2407
|
+
callPreview: subAgent.result || subAgent.status,
|
|
2408
|
+
callKey: `${message.id}:agent:${shortId}`,
|
|
2404
2409
|
summary: subAgent.status || "completed",
|
|
2405
2410
|
timestamp,
|
|
2406
2411
|
body,
|
|
@@ -2420,6 +2425,8 @@ function buildSystemNoticeCardLines(systemText, message, maxWidth) {
|
|
|
2420
2425
|
.join(" · ");
|
|
2421
2426
|
return [buildSystemNoticeLine({
|
|
2422
2427
|
title: `Sub-agent Responses (${subAgent.agents.length})`,
|
|
2428
|
+
callPreview: subAgent.agents.map(agent => firstCallLine(agent.result) || agent.status).join(" · "),
|
|
2429
|
+
callKey: `${message.id}:agents`,
|
|
2423
2430
|
summary,
|
|
2424
2431
|
timestamp,
|
|
2425
2432
|
body: sections.join("\n\n---\n\n"),
|
|
@@ -2505,6 +2512,17 @@ function buildChatMessageLines(message, maxWidth, options = {}) {
|
|
|
2505
2512
|
}
|
|
2506
2513
|
|
|
2507
2514
|
function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
|
|
2515
|
+
if (message?.kind === "chat-call") {
|
|
2516
|
+
return options.tableMode === "sentinel" ? [chatCallLine(message)] : [];
|
|
2517
|
+
}
|
|
2518
|
+
if (options.tableMode === "sentinel" && (message?.sender?.kind === "agent" || message?.agentCallPreview !== undefined)) {
|
|
2519
|
+
const label = message.cardTitle || message.sender?.display || "Agent message";
|
|
2520
|
+
return [{
|
|
2521
|
+
kind: "chatCall", callKey: message.id, category: "Agent",
|
|
2522
|
+
text: `${label} — ${firstCallLine(message.agentCallPreview ?? message.text)}`,
|
|
2523
|
+
body: message.text, time: message.time,
|
|
2524
|
+
}];
|
|
2525
|
+
}
|
|
2508
2526
|
if (message?.splash) {
|
|
2509
2527
|
// Swap in the narrow-viewport variant when the main art would
|
|
2510
2528
|
// overflow the pane (mobile portal, narrow terminals). When the
|
|
@@ -2574,7 +2592,9 @@ function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
|
|
|
2574
2592
|
const askedAndAnswered = parseAskedAndAnsweredExchange(message?.text || "");
|
|
2575
2593
|
if (askedAndAnswered) {
|
|
2576
2594
|
return [
|
|
2577
|
-
|
|
2595
|
+
// Older runtimes inserted this placeholder for late answers.
|
|
2596
|
+
// Keep the user's answer, without displaying a fabricated card.
|
|
2597
|
+
...(askedAndAnswered.question === "a question" ? [] : buildMessageCardLines({
|
|
2578
2598
|
title: "Question",
|
|
2579
2599
|
timestamp: formatTimestamp(message?.createdAt || message?.time),
|
|
2580
2600
|
body: askedAndAnswered.question,
|
|
@@ -2582,7 +2602,7 @@ function buildChatMessageLinesUncached(message, maxWidth, options = {}) {
|
|
|
2582
2602
|
titleColor: USER_CHAT_COLOR,
|
|
2583
2603
|
borderColor: USER_CHAT_COLOR,
|
|
2584
2604
|
tableMode: options.tableMode,
|
|
2585
|
-
}),
|
|
2605
|
+
})),
|
|
2586
2606
|
...buildChatMessageLines({
|
|
2587
2607
|
...message,
|
|
2588
2608
|
text: askedAndAnswered.answer,
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Copilot emits this diagnostic for an accepted turn without visible text.
|
|
2
|
+
// Keep its durable event and Activity entry, but do not turn it into a chat
|
|
3
|
+
// warning. Match only this message and the runtime's known retry wrapper.
|
|
4
|
+
export function isActivityOnlySessionError(text) {
|
|
5
|
+
return /^(?:Execution failed:\s*)?No response was returned\. Send your message again to retry\.(?: \(retry \d+\/\d+ in \d+s\))?$/.test(String(text || "").trim());
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
export function shouldKeepSessionWarning(previousSession, nextSession) {
|
|
2
9
|
if (!String(previousSession?.error || "").trim()) return false;
|
|
3
10
|
if (nextSession?.error !== undefined) return false;
|
|
@@ -26,7 +33,7 @@ const sameError = (statusText, eventText) => Boolean(eventText) && statusText.in
|
|
|
26
33
|
|
|
27
34
|
export function buildSessionWarning(event) {
|
|
28
35
|
const text = errorText(event);
|
|
29
|
-
if (!text) return null;
|
|
36
|
+
if (!text || isActivityOnlySessionError(text)) return null;
|
|
30
37
|
const failed = event?.data?.fatal === true;
|
|
31
38
|
return {
|
|
32
39
|
id: `${event.sessionId}:${event.seq}:warning`,
|
|
@@ -79,8 +86,9 @@ export function retainSessionWarnings(previous, next, events = [], now = Date.no
|
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
export function withSessionWarnings(chat, session, events = []) {
|
|
82
|
-
const messages =
|
|
89
|
+
const messages = chat.filter(message => message.kind !== "session-warning" || !isActivityOnlySessionError(message.text));
|
|
83
90
|
for (const warning of session?.chatWarnings || []) {
|
|
91
|
+
if (isActivityOnlySessionError(warning.text)) continue;
|
|
84
92
|
// Status often wins the race against its durable event. Match only
|
|
85
93
|
// that boundary, never a later failure after a new user/agent message.
|
|
86
94
|
const following = events.filter(e => Number(e.seq) > warning.afterSeq);
|
package/ui/core/src/state.js
CHANGED
|
@@ -448,10 +448,14 @@ export function createInitialState({ mode = "local", branding = null, docs = nul
|
|
|
448
448
|
activity: 0,
|
|
449
449
|
filePreview: 0,
|
|
450
450
|
},
|
|
451
|
-
// Per-session chat scroll memory
|
|
452
|
-
//
|
|
451
|
+
// Per-session chat scroll memory. While following, the offset is
|
|
452
|
+
// distance from the bottom; once the reader scrolls up it becomes
|
|
453
|
+
// a stable viewport offset from the top. The paired follow flag
|
|
454
|
+
// disambiguates the two modes across session switches.
|
|
453
455
|
chatScrollBySession: {},
|
|
456
|
+
chatFollowBottomBySession: {},
|
|
454
457
|
followBottom: {
|
|
458
|
+
chat: true,
|
|
455
459
|
inspector: true,
|
|
456
460
|
activity: true,
|
|
457
461
|
},
|
|
@@ -5,12 +5,14 @@ export function canonicalSystemTitle(session, brandingTitle = "PilotSwarm") {
|
|
|
5
5
|
if (agentId === "sweeper") return "Sweeper Agent";
|
|
6
6
|
if (agentId === "resourcemgr") return "Resource Manager Agent";
|
|
7
7
|
if (agentId === "facts-manager") return "Facts Manager";
|
|
8
|
+
if (agentId === "agent-manager") return "Agent Smith";
|
|
8
9
|
|
|
9
10
|
const rawTitle = String(session?.title || "").trim();
|
|
10
11
|
if (/^pilotswarm(?: agent)?$/i.test(rawTitle)) return brandedRootTitle;
|
|
11
12
|
if (/^sweeper agent$/i.test(rawTitle) || /^sweeper$/i.test(rawTitle)) return "Sweeper Agent";
|
|
12
13
|
if (/^resource manager agent$/i.test(rawTitle) || /^resourcemgr$/i.test(rawTitle)) return "Resource Manager Agent";
|
|
13
14
|
if (/^facts manager$/i.test(rawTitle) || /^facts-manager$/i.test(rawTitle)) return "Facts Manager";
|
|
15
|
+
if (/^agent manager$/i.test(rawTitle)) return "Agent Smith";
|
|
14
16
|
return rawTitle || "System Agent";
|
|
15
17
|
}
|
|
16
18
|
|