@qilin-agent/client-ui-chat 3.1.4 → 3.1.6

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/lib/client.js CHANGED
@@ -906,7 +906,7 @@ window.__ModuleLoader__.load({
906
906
  const location = node?.location;
907
907
  if (location?.kind !== "turn" && location?.kind !== "step") return false;
908
908
  const reason = location.turn.end?.data.reason.kind;
909
- return location.turn.status === "open" || reason === "aborted" || reason === "error";
909
+ return location.turn.status === "open" || reason === "aborted" || reason === "interrupted" || reason === "error";
910
910
  }
911
911
  //#endregion
912
912
  //#region lib/types/client/stores.js
@@ -997,9 +997,10 @@ window.__ModuleLoader__.load({
997
997
  //#endregion
998
998
  //#region lib/types/client/chat/flow-motion.js
999
999
  /**
1000
- * Fade-then-close transitions owned by one Chat viewport. Rows fade before their boxes close;
1001
- * the owner reserves removed height and holds scrolling until that viewport's folds finish.
1002
- * Motion is opt-in under `[data-chat-motion]`, respects reduced motion, and clears temporary styles.
1000
+ * Fold transitions owned by one Chat viewport. Every batch containing a closing row moves its boxes and
1001
+ * the scrollport on one frame clock; a batch that only reveals rows, or a viewport without a reading
1002
+ * policy, uses CSS transitions. Motion is opt-in under `[data-chat-motion]`, respects reduced motion,
1003
+ * and clears temporary styles.
1003
1004
  */
1004
1005
  const FADE_OUT_MS = 70;
1005
1006
  /** The box starts closing while the fade finishes; by then the row is nearly transparent. */
@@ -1025,18 +1026,159 @@ window.__ModuleLoader__.load({
1025
1026
  return element.hasAttribute("hidden") || element.dataset.chatMotion === "collapse";
1026
1027
  }
1027
1028
  /**
1029
+ * Opacity of a revealing box at batch progress, continuing from the share its own transition reached.
1030
+ * @param box - revealing box.
1031
+ * @param progress - shared eased progress from zero to one.
1032
+ * @returns opacity from zero to one.
1033
+ */
1034
+ function revealedShare(box, progress) {
1035
+ const start = box.height === 0 ? 0 : box.fromHeight / box.height;
1036
+ return start + (1 - start) * progress;
1037
+ }
1038
+ /**
1028
1039
  * Create animation coordination without cross-viewport broadcasts.
1029
- * @param reserve - reserve the deduplicated removed height in this viewport.
1040
+ * @param driver - the viewport's driver for the next batch; null while detached.
1030
1041
  * @param foldingChanged - disable or restore this viewport's native scroll anchoring.
1031
1042
  * @returns stable callbacks and cleanup for this viewport's animations.
1032
1043
  */
1033
- function createFlowMotion(reserve, foldingChanged) {
1044
+ function createFlowMotion(driver, foldingChanged) {
1034
1045
  const running = /* @__PURE__ */ new Map();
1035
1046
  let folding = 0;
1036
1047
  const foldIdleListeners = /* @__PURE__ */ new Set();
1037
- const pendingReserve = /* @__PURE__ */ new Set();
1038
- let pendingGrowth = 0;
1039
- let reserveScheduled = false;
1048
+ let batchScheduled = false;
1049
+ const boxes = /* @__PURE__ */ new Map();
1050
+ let frameRequest = null;
1051
+ let batch = false;
1052
+ let cancelled = false;
1053
+ let clearEpoch = 0;
1054
+ let batchCleanup = null;
1055
+ let cancelEmptyBatch = null;
1056
+ /** The batch is committing hidden rows and its final geometry; idle listeners wait until both land. */
1057
+ let settling = false;
1058
+ let anchoringDisabled = false;
1059
+ function cancelInput() {
1060
+ if (batch) cancelled = true;
1061
+ }
1062
+ function resumeInput() {
1063
+ if (batch) cancelled = false;
1064
+ }
1065
+ /** Rows inside a closing ancestor change with it and never join a batch on their own. */
1066
+ function batchMember(box) {
1067
+ return box.element.parentElement?.closest("[data-chat-motion=\"collapse\"]") === null;
1068
+ }
1069
+ function runBatch(current) {
1070
+ for (const box of [...boxes.values()]) {
1071
+ if (!batchMember(box)) continue;
1072
+ const height = box.element.offsetHeight;
1073
+ const marginTop = Number.parseFloat(getComputedStyle(box.element).marginTop) || 0;
1074
+ if (!box.closing) {
1075
+ box.fromHeight = Math.min(height, box.height);
1076
+ box.fromMarginTop = Math.min(marginTop, box.marginTop);
1077
+ continue;
1078
+ }
1079
+ box.height = height;
1080
+ box.marginTop = marginTop;
1081
+ if (height === 0 && marginTop === 0) box.finish();
1082
+ }
1083
+ const owned = [...boxes.values()];
1084
+ const members = owned.filter(batchMember).sort((left, right) => left.element.compareDocumentPosition(right.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
1085
+ if (!members.some((box) => box.closing)) return;
1086
+ batch = true;
1087
+ cancelled = false;
1088
+ const travelling = current.begin(members.map((box) => box.closing ? box : {
1089
+ element: box.element,
1090
+ height: box.height - box.fromHeight,
1091
+ marginTop: box.marginTop - box.fromMarginTop,
1092
+ closing: false
1093
+ }));
1094
+ batchCleanup = () => {
1095
+ current.finish(true);
1096
+ };
1097
+ for (const box of owned) box.take();
1098
+ for (const box of members) {
1099
+ box.element.style.transition = "none";
1100
+ box.element.style.height = `${box.closing ? box.height : box.fromHeight}px`;
1101
+ box.element.style.marginTop = `${box.closing ? box.marginTop : box.fromMarginTop}px`;
1102
+ box.element.style.opacity = box.closing ? "1" : String(revealedShare(box, 0));
1103
+ }
1104
+ const memberSet = new Set(members);
1105
+ const settledTravel = /* @__PURE__ */ new Set();
1106
+ const freezeTravelling = () => {
1107
+ for (const element of travelling) {
1108
+ if (settledTravel.has(element)) continue;
1109
+ settledTravel.add(element);
1110
+ element.style.translate = "";
1111
+ for (const box of owned) {
1112
+ if (memberSet.has(box) || !element.contains(box.element)) continue;
1113
+ box.element.style.transition = "none";
1114
+ box.element.style.height = `${box.height}px`;
1115
+ box.element.style.marginTop = `${box.marginTop}px`;
1116
+ box.element.style.opacity = "1";
1117
+ }
1118
+ }
1119
+ };
1120
+ freezeTravelling();
1121
+ const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1122
+ let started;
1123
+ let displacement = 0;
1124
+ const settle = (finishCancelled) => {
1125
+ batch = false;
1126
+ batchCleanup = null;
1127
+ cancelEmptyBatch = null;
1128
+ settling = true;
1129
+ try {
1130
+ for (const box of owned) if (boxes.get(box.element) === box) box.finish();
1131
+ current.finish(finishCancelled);
1132
+ } finally {
1133
+ settling = false;
1134
+ }
1135
+ if (folding === 0) notifyIdle();
1136
+ if ([...boxes.values()].some((box) => box.held)) flushBatch();
1137
+ };
1138
+ cancelEmptyBatch = () => {
1139
+ if (members.some((box) => box.closing && boxes.get(box.element) === box)) return;
1140
+ if (frameRequest !== null) cancelAnimationFrame(frameRequest);
1141
+ frameRequest = null;
1142
+ settle(true);
1143
+ };
1144
+ const tick = (now) => {
1145
+ frameRequest = null;
1146
+ started ??= now;
1147
+ const elapsed = reduced ? 160 : now - started;
1148
+ const linear = Math.min(1, Math.max(0, (elapsed - CLOSE_DELAY_MS) / CLOSE_MS));
1149
+ const progress = linear * linear * (3 - 2 * linear);
1150
+ current.prepare(progress, cancelled);
1151
+ freezeTravelling();
1152
+ for (const box of members) {
1153
+ if (boxes.get(box.element) !== box) continue;
1154
+ if (box.closing) {
1155
+ box.element.style.height = `${box.height * (1 - progress)}px`;
1156
+ box.element.style.marginTop = `${box.marginTop * (1 - progress)}px`;
1157
+ box.element.style.opacity = travelling.has(box.element) ? "1" : String(Math.max(0, 1 - elapsed / FADE_OUT_MS));
1158
+ } else {
1159
+ box.element.style.height = `${box.fromHeight + (box.height - box.fromHeight) * progress}px`;
1160
+ box.element.style.marginTop = `${box.fromMarginTop + (box.marginTop - box.fromMarginTop) * progress}px`;
1161
+ box.element.style.opacity = String(revealedShare(box, progress));
1162
+ }
1163
+ }
1164
+ displacement = current.frame(progress, cancelled);
1165
+ let removedBefore = 0;
1166
+ for (const box of members) {
1167
+ if (boxes.get(box.element) !== box) continue;
1168
+ if (box.closing) {
1169
+ if (!travelling.has(box.element)) box.element.style.translate = `0 ${displacement + removedBefore}px`;
1170
+ removedBefore += (box.height + box.marginTop) * progress;
1171
+ } else removedBefore -= (box.height - box.fromHeight + box.marginTop - box.fromMarginTop) * progress;
1172
+ }
1173
+ if (linear < 1) {
1174
+ frameRequest = requestAnimationFrame(tick);
1175
+ return;
1176
+ }
1177
+ settle(cancelled);
1178
+ };
1179
+ if (reduced) tick(0);
1180
+ else frameRequest = requestAnimationFrame(tick);
1181
+ }
1040
1182
  function foldActive() {
1041
1183
  return folding > 0;
1042
1184
  }
@@ -1047,30 +1189,41 @@ window.__ModuleLoader__.load({
1047
1189
  foldIdleListeners.delete(listener);
1048
1190
  };
1049
1191
  }
1050
- function flushReserve() {
1051
- reserveScheduled = false;
1052
- let total = -pendingGrowth;
1053
- pendingGrowth = 0;
1054
- for (const element of pendingReserve) {
1055
- if (element.parentElement?.closest("[data-chat-motion=\"collapse\"]") !== null) continue;
1056
- total += element.offsetHeight + (Number.parseFloat(getComputedStyle(element).marginTop) || 0);
1192
+ function flushBatch() {
1193
+ batchScheduled = false;
1194
+ if (batch) return;
1195
+ const current = [...boxes.values()].some((box) => box.closing) ? driver() : null;
1196
+ if (current !== null) {
1197
+ runBatch(current);
1198
+ return;
1057
1199
  }
1058
- pendingReserve.clear();
1059
- if (total > 0) reserve(total);
1200
+ const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1201
+ for (const box of [...boxes.values()]) if (reduced || box.held && box.closing) box.finish();
1202
+ else if (box.held) box.release();
1060
1203
  }
1061
- function scheduleReserve() {
1062
- if (reserveScheduled) return;
1063
- reserveScheduled = true;
1064
- queueMicrotask(flushReserve);
1204
+ function scheduleBatch() {
1205
+ if (batchScheduled) return;
1206
+ batchScheduled = true;
1207
+ const epoch = clearEpoch;
1208
+ queueMicrotask(() => {
1209
+ if (epoch === clearEpoch) flushBatch();
1210
+ });
1065
1211
  }
1066
1212
  function foldStarted() {
1067
1213
  folding += 1;
1068
- if (folding === 1) foldingChanged(true);
1214
+ if (folding !== 1 || anchoringDisabled) return;
1215
+ anchoringDisabled = true;
1216
+ foldingChanged(true);
1069
1217
  }
1070
1218
  function foldEnded() {
1071
1219
  folding = Math.max(0, folding - 1);
1072
- if (folding > 0) return;
1073
- foldingChanged(false);
1220
+ if (folding === 0 && !settling) notifyIdle();
1221
+ }
1222
+ function notifyIdle() {
1223
+ if (anchoringDisabled) {
1224
+ anchoringDisabled = false;
1225
+ foldingChanged(false);
1226
+ }
1074
1227
  const listeners = [...foldIdleListeners];
1075
1228
  foldIdleListeners.clear();
1076
1229
  for (const listener of listeners) listener();
@@ -1079,17 +1232,19 @@ window.__ModuleLoader__.load({
1079
1232
  element.removeEventListener("transitionend", onEnd);
1080
1233
  clearTimeout(timer);
1081
1234
  running.delete(element);
1235
+ boxes.delete(element);
1082
1236
  const { style } = element;
1083
1237
  style.height = "";
1084
1238
  style.marginTop = "";
1085
1239
  style.opacity = "";
1086
1240
  style.overflow = "";
1087
1241
  style.transition = "";
1242
+ style.translate = "";
1088
1243
  delete element.dataset.chatMotion;
1089
1244
  }
1090
1245
  function cancelMotion(element) {
1091
- pendingReserve.delete(element);
1092
1246
  running.get(element)?.();
1247
+ cancelEmptyBatch?.();
1093
1248
  }
1094
1249
  function collapse(element, done) {
1095
1250
  cancelMotion(element);
@@ -1100,19 +1255,21 @@ window.__ModuleLoader__.load({
1100
1255
  return;
1101
1256
  }
1102
1257
  const { style } = element;
1258
+ const held = batch;
1103
1259
  element.dataset.chatMotion = "collapse";
1104
1260
  foldStarted();
1105
- pendingReserve.add(element);
1106
- scheduleReserve();
1261
+ scheduleBatch();
1107
1262
  style.transition = "none";
1108
1263
  style.overflow = "hidden";
1109
1264
  style.height = `${height}px`;
1110
1265
  style.marginTop = `${marginTop}px`;
1111
- element.offsetHeight;
1112
- style.transition = `opacity ${FADE_OUT_MS}ms ${EASING}, height ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms, margin-top ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms`;
1113
- style.opacity = "0";
1114
- style.height = "0px";
1115
- style.marginTop = "0px";
1266
+ if (!held) {
1267
+ element.offsetHeight;
1268
+ style.transition = `opacity ${FADE_OUT_MS}ms ${EASING}, height ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms, margin-top ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms`;
1269
+ style.opacity = "0";
1270
+ style.height = "0px";
1271
+ style.marginTop = "0px";
1272
+ }
1116
1273
  const finish = () => {
1117
1274
  if (running.get(element) !== cancel) return;
1118
1275
  clearMotion(element, onEnd, timer);
@@ -1122,21 +1279,35 @@ window.__ModuleLoader__.load({
1122
1279
  const onEnd = (event) => {
1123
1280
  if (event.target === element && event.propertyName === "height") finish();
1124
1281
  };
1125
- const timer = setTimeout(finish, 240);
1282
+ const timer = held ? void 0 : setTimeout(finish, 240);
1126
1283
  const cancel = () => {
1127
1284
  clearMotion(element, onEnd, timer);
1128
1285
  foldEnded();
1129
1286
  };
1130
- element.addEventListener("transitionend", onEnd);
1287
+ if (!held) element.addEventListener("transitionend", onEnd);
1131
1288
  running.set(element, cancel);
1289
+ boxes.set(element, {
1290
+ element,
1291
+ height,
1292
+ marginTop,
1293
+ fromHeight: height,
1294
+ fromMarginTop: marginTop,
1295
+ closing: true,
1296
+ held,
1297
+ finish,
1298
+ release: () => {},
1299
+ take: () => {
1300
+ element.removeEventListener("transitionend", onEnd);
1301
+ clearTimeout(timer);
1302
+ }
1303
+ });
1132
1304
  }
1133
1305
  function reveal(element) {
1134
1306
  cancelMotion(element);
1135
1307
  const height = element.offsetHeight;
1136
1308
  if (height === 0) return;
1137
1309
  const marginTop = Number.parseFloat(getComputedStyle(element).marginTop) || 0;
1138
- pendingGrowth += height + marginTop;
1139
- scheduleReserve();
1310
+ scheduleBatch();
1140
1311
  const { style } = element;
1141
1312
  element.dataset.chatMotion = "reveal";
1142
1313
  style.transition = "none";
@@ -1144,11 +1315,6 @@ window.__ModuleLoader__.load({
1144
1315
  style.opacity = "0";
1145
1316
  style.height = "0px";
1146
1317
  style.marginTop = "0px";
1147
- element.offsetHeight;
1148
- style.transition = `height ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms, margin-top ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms, opacity ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms`;
1149
- style.height = `${height}px`;
1150
- style.marginTop = `${marginTop}px`;
1151
- style.opacity = "1";
1152
1318
  const finish = () => {
1153
1319
  if (running.get(element) !== finish) return;
1154
1320
  clearMotion(element, onEnd, timer);
@@ -1156,14 +1322,50 @@ window.__ModuleLoader__.load({
1156
1322
  const onEnd = (event) => {
1157
1323
  if (event.target === element && event.propertyName === "height") finish();
1158
1324
  };
1159
- const timer = setTimeout(finish, 240);
1160
- element.addEventListener("transitionend", onEnd);
1325
+ let timer;
1326
+ const startTransition = () => {
1327
+ element.offsetHeight;
1328
+ style.transition = `height ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms, margin-top ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms, opacity ${CLOSE_MS}ms ${EASING} ${CLOSE_DELAY_MS}ms`;
1329
+ style.height = `${height}px`;
1330
+ style.marginTop = `${marginTop}px`;
1331
+ style.opacity = "1";
1332
+ timer = setTimeout(finish, 240);
1333
+ element.addEventListener("transitionend", onEnd);
1334
+ };
1335
+ const held = batch;
1336
+ if (!held) startTransition();
1161
1337
  running.set(element, finish);
1338
+ const box = {
1339
+ element,
1340
+ height,
1341
+ marginTop,
1342
+ fromHeight: 0,
1343
+ fromMarginTop: 0,
1344
+ closing: false,
1345
+ held,
1346
+ finish,
1347
+ release: () => {
1348
+ box.held = false;
1349
+ startTransition();
1350
+ },
1351
+ take: () => {
1352
+ element.removeEventListener("transitionend", onEnd);
1353
+ clearTimeout(timer);
1354
+ }
1355
+ };
1356
+ boxes.set(element, box);
1162
1357
  }
1163
1358
  function clear() {
1359
+ clearEpoch++;
1360
+ batchScheduled = false;
1361
+ batch = false;
1362
+ cancelled = false;
1363
+ if (frameRequest !== null) cancelAnimationFrame(frameRequest);
1364
+ frameRequest = null;
1365
+ batchCleanup?.();
1366
+ batchCleanup = null;
1367
+ cancelEmptyBatch = null;
1164
1368
  foldIdleListeners.clear();
1165
- pendingReserve.clear();
1166
- pendingGrowth = 0;
1167
1369
  for (const cancel of [...running.values()]) cancel();
1168
1370
  }
1169
1371
  return {
@@ -1172,7 +1374,10 @@ window.__ModuleLoader__.load({
1172
1374
  cancelMotion,
1173
1375
  foldActive,
1174
1376
  onFoldIdle,
1175
- clear
1377
+ cancelInput,
1378
+ resumeInput,
1379
+ clear,
1380
+ inputActive: () => batch || batchScheduled && [...boxes.values()].some((box) => box.closing && batchMember(box))
1176
1381
  };
1177
1382
  }
1178
1383
  function useFlowTransition(motion, grow) {
@@ -1182,11 +1387,12 @@ window.__ModuleLoader__.load({
1182
1387
  current.current = null;
1183
1388
  }, [motion]);
1184
1389
  const transition = (0, react.useCallback)((element, hidden, commit) => {
1185
- const animate = current.current === element && motionEnabled(element);
1390
+ const mounted = current.current === element;
1391
+ const animate = mounted && motionEnabled(element);
1186
1392
  current.current = element;
1187
1393
  if (hidden) {
1188
- if (element.hasAttribute("hidden")) return;
1189
- if (animate && motion !== void 0) motion.collapse(element, commit);
1394
+ if (hiddenOrCollapsing(element)) return;
1395
+ if (mounted && element.closest(MOTION_SCOPE) !== null && motion !== void 0) motion.collapse(element, commit);
1190
1396
  else commit();
1191
1397
  return;
1192
1398
  }
@@ -1201,7 +1407,7 @@ window.__ModuleLoader__.load({
1201
1407
  * Apply searchable hiding with the containing Chat viewport's optional fold animation.
1202
1408
  * @param hidden - desired visibility.
1203
1409
  * @param reveal - browser-find and focus-protection callback.
1204
- * @param motion - owning viewport's row callbacks; absent in standalone rendering.
1410
+ * @param motion - owning viewport's row callbacks; undefined applies visibility synchronously.
1205
1411
  * @returns the stable subtree ref.
1206
1412
  */
1207
1413
  function useFlowHidden(hidden, reveal, motion) {
@@ -1230,7 +1436,7 @@ window.__ModuleLoader__.load({
1230
1436
  }
1231
1437
  //#endregion
1232
1438
  //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-chat/src/client/chat/ChatView.module.css.mjs
1233
- const css$17 = ".JJ8Zsq_frame{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative;container-type:inline-size}.JJ8Zsq_root{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative;overflow:visible clip}.JJ8Zsq_scroll{min-height:0;padding:16px calc(var(--qilin-composer-side-clearance) + 16px);flex:auto;overflow-y:auto;container-type:inline-size}.JJ8Zsq_root[data-chat-following-tail] .JJ8Zsq_scroll,[data-conversation-scroll]:has(.JJ8Zsq_root[data-chat-following-tail]){overflow-anchor:none}[data-conversation-scroll] .JJ8Zsq_frame,[data-conversation-scroll] .JJ8Zsq_root{flex:none;height:auto;min-height:auto}[data-conversation-scroll] .JJ8Zsq_scroll{flex:none;min-height:auto;overflow:visible}.JJ8Zsq_column{max-width:var(--qilin-chat-content-width);flex-direction:column;width:100%;margin:0 auto;display:flex}:is(.JJ8Zsq_column,[data-slot=\"conversation.chat.flow\"])>:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty)))~:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty))),.JJ8Zsq_column:has(>.JJ8Zsq_older)>[data-slot=\"conversation.chat.flow\"]>:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty))){margin-top:var(--qilin-chat-flow-gap,6px)}.JJ8Zsq_flowItem[data-chat-group-part=response],:is(.JJ8Zsq_column,[data-slot=\"conversation.chat.flow\"])>[data-chat-group-part=response]:not([hidden]):not(:empty)+:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty))):not(.JJ8Zsq_running){--qilin-chat-flow-gap:12px}.JJ8Zsq_flowItem{min-width:0}.JJ8Zsq_turnSpacer{height:0}.JJ8Zsq_column[data-chat-motion]>:not([data-chat-motion]),.JJ8Zsq_column[data-chat-motion]>[data-slot=\"conversation.chat.flow\"]>:not([data-chat-motion]){transition:margin-top .16s cubic-bezier(.4,0,.2,1)}@media (prefers-reduced-motion:reduce){.JJ8Zsq_column[data-chat-motion]>:not([data-chat-motion]),.JJ8Zsq_column[data-chat-motion]>[data-slot=\"conversation.chat.flow\"]>:not([data-chat-motion]){transition:none}}.JJ8Zsq_flowItem[data-chat-flow-kind=turn-process],.JJ8Zsq_flowItem[data-turn-process-answer],:is(.JJ8Zsq_column,[data-slot=\"conversation.chat.flow\"])>[data-chat-flow-kind=turn-process]+:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty))):not(.JJ8Zsq_running){--qilin-chat-flow-gap:16px}.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty)){height:0}.JJ8Zsq_callRow{border-radius:var(--dsw-radius-sm)}.JJ8Zsq_hint{color:var(--dsw-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(18px + var(--qilin-content-font-delta-secondary,0px))}.JJ8Zsq_running{--qilin-chat-flow-gap:12px;--dsw-alias-label-shimmer:var(--dsw-alias-label-deep-diving-shimmer);color:var(--dsw-alias-label-deep-diving);font-size:calc(var(--qilin-content-font-size,14px) - 2px);line-height:calc(22px + var(--qilin-content-font-delta,0px));flex-direction:column;align-items:flex-start;display:flex}.JJ8Zsq_modalAction{min-width:72px}.JJ8Zsq_running{--qilin-chat-flow-gap:12px;--qilin-alias-label-shimmer:var(--qilin-alias-label-deep-diving-shimmer);color:var(--qilin-alias-label-deep-diving,var(--qilin-alias-label-tertiary));font-size:calc(var(--qilin-content-font-size,14px) - 2px);line-height:calc(22px + var(--qilin-content-font-delta,0px));flex-direction:column;align-items:flex-start;display:flex}:is(.JJ8Zsq_column,[data-slot=\"conversation.chat.flow\"])>:nth-last-child(2 of :not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty)))):is(.JJ8Zsq_flowItem:not([data-chat-flow-kind=user]):not([data-chat-flow-kind=steering]):not([data-chat-flow-kind=turn-trigger]),[data-chat-group-key])~.JJ8Zsq_running>.JJ8Zsq_runningDivider{display:block}.JJ8Zsq_runningDivider{background:color-mix(in srgb, var(--qilin-alias-border-l1) 75%, var(--qilin-alias-border-l2));pointer-events:none;width:100%;height:.5px;margin:8px 0 10px;display:none}.JJ8Zsq_runningContent{align-items:center;gap:6px;min-width:0;display:inline-flex}.JJ8Zsq_toBottomSlot{z-index:8;justify-content:center;height:0;display:flex;position:absolute;bottom:16px;left:0;right:0}[data-conversation-scroll] .JJ8Zsq_toBottomSlot{bottom:calc(var(--qilin-composer-height,152px) + 16px);position:sticky}.JJ8Zsq_toBottom{--qilin-elevation-stroke-color:var(--qilin-alias-border-l3);corner-shape:round;width:34px;height:34px;color:var(--qilin-alias-label-primary);background:var(--qilin-alias-button-floating-fill);box-shadow:var(--qilin-shadow-lv1);cursor:pointer;pointer-events:auto;border:0;border-radius:100px;justify-content:center;align-items:center;margin-top:-34px;padding:0;display:flex}.JJ8Zsq_toBottom:hover{background:var(--qilin-alias-button-floating-hover)}";
1439
+ const css$17 = ".JJ8Zsq_frame{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative;container-type:inline-size}.JJ8Zsq_root{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative;overflow:visible clip}.JJ8Zsq_scroll{min-height:0;padding:16px calc(var(--qilin-composer-side-clearance) + 16px);flex:auto;overflow-y:auto;container-type:inline-size}.JJ8Zsq_root[data-chat-following-tail] .JJ8Zsq_scroll,[data-conversation-scroll]:has(.JJ8Zsq_root[data-chat-following-tail]){overflow-anchor:none}[data-conversation-scroll] .JJ8Zsq_frame,[data-conversation-scroll] .JJ8Zsq_root{flex:none;height:auto;min-height:auto}[data-conversation-scroll] .JJ8Zsq_scroll{flex:none;min-height:auto;overflow:visible}.JJ8Zsq_column{max-width:var(--qilin-chat-content-width);flex-direction:column;width:100%;margin:0 auto;display:flex}:is(.JJ8Zsq_column,[data-slot=\"conversation.chat.flow\"])>:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty)))~:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty))),.JJ8Zsq_column:has(>.JJ8Zsq_older)>[data-slot=\"conversation.chat.flow\"]>:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty))){margin-top:var(--qilin-chat-flow-gap,6px)}.JJ8Zsq_older{justify-content:center;display:flex}.JJ8Zsq_older button{border-radius:var(--qilin-radius-sm);color:var(--qilin-alias-label-secondary);background:var(--qilin-alias-interactive-bg-hover-solid);cursor:pointer;border:none;padding:4px 12px;font-size:12px}.JJ8Zsq_older button:disabled{cursor:default;opacity:.6}.JJ8Zsq_flowItem[data-chat-group-part=response],:is(.JJ8Zsq_column,[data-slot=\"conversation.chat.flow\"])>[data-chat-group-part=response]:not([hidden]):not(:empty)+:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty))):not(.JJ8Zsq_running){--qilin-chat-flow-gap:12px}.JJ8Zsq_flowItem{min-width:0}.JJ8Zsq_turnSpacer{height:0}.JJ8Zsq_column[data-chat-motion]>:not([data-chat-motion]),.JJ8Zsq_column[data-chat-motion]>[data-slot=\"conversation.chat.flow\"]>:not([data-chat-motion]){transition:margin-top .16s cubic-bezier(.4,0,.2,1)}@media (prefers-reduced-motion:reduce){.JJ8Zsq_column[data-chat-motion]>:not([data-chat-motion]),.JJ8Zsq_column[data-chat-motion]>[data-slot=\"conversation.chat.flow\"]>:not([data-chat-motion]){transition:none}}.JJ8Zsq_flowItem[data-chat-flow-kind=turn-process],.JJ8Zsq_flowItem[data-turn-process-answer],:is(.JJ8Zsq_column,[data-slot=\"conversation.chat.flow\"])>[data-chat-flow-kind=turn-process]+:not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty))):not(.JJ8Zsq_running){--qilin-chat-flow-gap:16px}.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty)){height:0}.JJ8Zsq_callRow{border-radius:var(--dsw-radius-sm)}.JJ8Zsq_hint{color:var(--dsw-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(18px + var(--qilin-content-font-delta-secondary,0px))}.JJ8Zsq_running{--qilin-chat-flow-gap:12px;--dsw-alias-label-shimmer:var(--dsw-alias-label-deep-diving-shimmer);color:var(--dsw-alias-label-deep-diving);font-size:calc(var(--qilin-content-font-size,14px) - 2px);line-height:calc(22px + var(--qilin-content-font-delta,0px));flex-direction:column;align-items:flex-start;display:flex}.JJ8Zsq_modalAction{min-width:72px}.JJ8Zsq_running{--qilin-chat-flow-gap:12px;--qilin-alias-label-shimmer:var(--qilin-alias-label-running-shimmer);color:var(--qilin-alias-label-running,var(--qilin-alias-label-tertiary));font-size:calc(var(--qilin-content-font-size,14px) - 2px);line-height:calc(22px + var(--qilin-content-font-delta,0px));flex-direction:column;align-items:flex-start;display:flex}:is(.JJ8Zsq_column,[data-slot=\"conversation.chat.flow\"])>:nth-last-child(2 of :not([hidden]):not(.JJ8Zsq_flowItem:is(:empty,:has(>[data-slot=\"conversation.chat.node\"]:empty)))):is(.JJ8Zsq_flowItem:not([data-chat-flow-kind=user]):not([data-chat-flow-kind=steering]):not([data-chat-flow-kind=turn-trigger]),[data-chat-group-key])~.JJ8Zsq_running>.JJ8Zsq_runningDivider{display:block}.JJ8Zsq_runningDivider{background:color-mix(in srgb, var(--qilin-alias-border-l1) 75%, var(--qilin-alias-border-l2));pointer-events:none;width:100%;height:.5px;margin:8px 0 10px;display:none}.JJ8Zsq_runningContent{align-items:center;gap:6px;min-width:0;display:inline-flex}.JJ8Zsq_toBottomSlot{z-index:8;justify-content:center;height:0;display:flex;position:absolute;bottom:16px;left:0;right:0}[data-conversation-scroll] .JJ8Zsq_toBottomSlot{bottom:calc(var(--qilin-composer-height,152px) + 16px);position:sticky}.JJ8Zsq_toBottom{--qilin-elevation-stroke-color:var(--qilin-alias-border-l3);corner-shape:round;width:34px;height:34px;color:var(--qilin-alias-label-primary);background:var(--qilin-alias-button-floating-fill);box-shadow:var(--qilin-shadow-lv1);cursor:pointer;pointer-events:auto;border:0;border-radius:100px;justify-content:center;align-items:center;margin-top:-34px;padding:0;display:flex}.JJ8Zsq_toBottom:hover{background:var(--qilin-alias-button-floating-hover)}";
1234
1440
  const tagId$17 = "@qilin-agent/client-ui-chat/ChatView.module.css";
1235
1441
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$17) + "]") === null) {
1236
1442
  const tag = document.createElement("style");
@@ -1271,7 +1477,7 @@ window.__ModuleLoader__.load({
1271
1477
  * Policy reads select this seat's own conclusion, so a mode change re-renders
1272
1478
  * only seats whose visibility actually changes.
1273
1479
  */
1274
- const ChatNodeSeat = (0, react.memo)(function ChatNodeSeat({ nodeKey, groupPart, useChatNode, useChatNodeProcess, usePresentation, cwd, openFile, openSkill, inspectCall, forkAt, editUserMessage, loadImage, renderMessageImages, fileMentions, useStore, actions, renderSlot, t, useChatNodeBottom, deferCollapse, useGroupAction }) {
1480
+ const ChatNodeSeat = (0, react.memo)(function ChatNodeSeat({ nodeKey, groupPart, depth, useChatNode, useChatNodeProcess, usePresentation, cwd, openFile, openSkill, inspectCall, forkAt, editUserMessage, loadImage, renderMessageImages, fileMentions, useStore, actions, renderSlot, t, useChatNodeBottom, deferCollapse, useGroupAction }) {
1275
1481
  const bottom = useChatNodeBottom(nodeKey);
1276
1482
  const node = useChatNode(nodeKey);
1277
1483
  const routedNode = node;
@@ -1374,6 +1580,7 @@ window.__ModuleLoader__.load({
1374
1580
  "data-chat-flow-key": flowKey,
1375
1581
  "data-chat-paging-anchor": routedNode.kind !== "turn-process" || void 0,
1376
1582
  "data-chat-node-key": routedNode.key,
1583
+ "data-chat-depth": depth,
1377
1584
  "data-chat-group-part": groupPart,
1378
1585
  "data-chat-flow-kind": routedNode.kind,
1379
1586
  "data-chat-turn": turn,
@@ -1769,7 +1976,7 @@ window.__ModuleLoader__.load({
1769
1976
  }
1770
1977
  //#endregion
1771
1978
  //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css.mjs
1772
- const css$16 = ".nsI7mW_root{min-width:0}.nsI7mW_title{max-width:100%;color:var(--qilin-alias-label-secondary);font:inherit;font-size:var(--dsh-content-font-size,14px);text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;gap:6px;padding:0;transition:color .1s;display:flex}.nsI7mW_title:hover{color:var(--qilin-alias-label-primary)}.nsI7mW_leading{width:calc(16px + var(--qilin-content-font-delta,0px));height:calc(16px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex;position:relative}.nsI7mW_leading svg{width:calc(14px + var(--qilin-content-font-delta,0px));height:calc(14px + var(--qilin-content-font-delta,0px))}.nsI7mW_leading svg[width=\"16\"]{width:calc(16px + var(--qilin-content-font-delta,0px));height:calc(16px + var(--qilin-content-font-delta,0px))}.nsI7mW_activityIcon,.nsI7mW_chevron{justify-content:center;align-items:center;transition:opacity .1s;display:inline-flex;position:absolute;inset:0}.nsI7mW_activityIcon{opacity:1}.nsI7mW_chevron,.nsI7mW_title:is(:hover,:focus-visible) .nsI7mW_activityIcon{opacity:0}.nsI7mW_title:is(:hover,:focus-visible) .nsI7mW_chevron{opacity:1}.nsI7mW_title[aria-expanded=true] .nsI7mW_activityIcon{opacity:0}.nsI7mW_title[aria-expanded=true] .nsI7mW_chevron{opacity:1}.nsI7mW_title[aria-expanded=true]{padding-bottom:8px}.nsI7mW_body{--dsh-chat-flow-gap:6px;overscroll-behavior-y:auto;scrollbar-gutter:stable;max-height:min(400px,50vh);overflow-y:auto}.nsI7mW_label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.nsI7mW_fadeTop{mask-image:linear-gradient(#0000 0,#000 24px 100%)}.nsI7mW_fadeBottom{mask-image:linear-gradient(#000 0 calc(100% - 24px),#0000 100%)}.nsI7mW_fadeTop.nsI7mW_fadeBottom{mask-image:linear-gradient(#0000 0,#000 24px calc(100% - 24px),#0000 100%)}@media (prefers-reduced-motion:reduce){.nsI7mW_title,.nsI7mW_activityIcon,.nsI7mW_chevron{transition:none}}.nsI7mW_content{flex-direction:column;display:flex}.nsI7mW_content>*{flex-shrink:0}.nsI7mW_content>:not([hidden]):not(:empty)~:not([hidden]):not(:empty){margin-top:var(--dsh-chat-flow-gap,6px)}.nsI7mW_expandedBody{--dsh-chat-flow-gap:16px;scrollbar-gutter:auto;max-height:none;overflow:visible}[data-chat-motion] .nsI7mW_content>:not([data-chat-motion]){transition:margin-top .16s cubic-bezier(.4,0,.2,1)}@media (prefers-reduced-motion:reduce){[data-chat-motion] .nsI7mW_content>:not([data-chat-motion]){transition:none}}";
1979
+ const css$16 = ".nsI7mW_root{min-width:0}.nsI7mW_title{max-width:100%;color:var(--qilin-alias-label-secondary);font:inherit;font-size:var(--dsh-content-font-size,14px);text-align:left;cursor:pointer;background:0 0;border:0;align-items:center;gap:6px;padding:0;transition:color .1s;display:flex}.nsI7mW_title:hover{color:var(--qilin-alias-label-primary)}.nsI7mW_leading{width:calc(16px + var(--qilin-content-font-delta,0px));height:calc(16px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex;position:relative}.nsI7mW_leading svg{width:calc(14px + var(--qilin-content-font-delta,0px));height:calc(14px + var(--qilin-content-font-delta,0px))}.nsI7mW_leading svg[width=\"16\"]{width:calc(16px + var(--qilin-content-font-delta,0px));height:calc(16px + var(--qilin-content-font-delta,0px))}.nsI7mW_activityIcon,.nsI7mW_chevron{justify-content:center;align-items:center;transition:opacity .1s;display:inline-flex;position:absolute;inset:0}.nsI7mW_activityIcon{opacity:1}.nsI7mW_chevron,.nsI7mW_title:is(:hover,:focus-visible) .nsI7mW_activityIcon{opacity:0}.nsI7mW_title:is(:hover,:focus-visible) .nsI7mW_chevron{opacity:1}.nsI7mW_title[aria-expanded=true] .nsI7mW_activityIcon{opacity:0}.nsI7mW_title[aria-expanded=true] .nsI7mW_chevron{opacity:1}.nsI7mW_title[aria-expanded=true]{padding-bottom:8px}.nsI7mW_body{--dsh-chat-flow-gap:6px;overscroll-behavior-y:auto;scrollbar-gutter:stable;max-height:min(400px,50vh);overflow-y:auto}.nsI7mW_root:not([data-group-expanded-mode]) .nsI7mW_body{--dsh-chat-process-indent:24px}.nsI7mW_root:not([data-group-expanded-mode]) .nsI7mW_content>[data-chat-depth=\"1\"]{padding-inline-start:var(--dsh-chat-process-indent)}.nsI7mW_root:not([data-group-expanded-mode]) .nsI7mW_content>[data-chat-depth=\"2\"]{padding-inline-start:calc(var(--dsh-chat-process-indent) * 2)}.nsI7mW_label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.nsI7mW_fadeTop{mask-image:linear-gradient(#0000 0,#000 24px 100%)}.nsI7mW_fadeBottom{mask-image:linear-gradient(#000 0 calc(100% - 24px),#0000 100%)}.nsI7mW_fadeTop.nsI7mW_fadeBottom{mask-image:linear-gradient(#0000 0,#000 24px calc(100% - 24px),#0000 100%)}@media (prefers-reduced-motion:reduce){.nsI7mW_title,.nsI7mW_activityIcon,.nsI7mW_chevron{transition:none}}.nsI7mW_content{flex-direction:column;display:flex}.nsI7mW_content>*{flex-shrink:0}.nsI7mW_content>:not([hidden]):not(:empty)~:not([hidden]):not(:empty){margin-top:var(--dsh-chat-flow-gap,6px)}.nsI7mW_expandedBody{--dsh-chat-flow-gap:16px;scrollbar-gutter:auto;max-height:none;overflow:visible}[data-chat-motion] .nsI7mW_content>:not([data-chat-motion]){transition:margin-top .16s cubic-bezier(.4,0,.2,1)}@media (prefers-reduced-motion:reduce){[data-chat-motion] .nsI7mW_content>:not([data-chat-motion]){transition:none}}";
1773
1980
  const tagId$16 = "@qilin-agent/client-ui-chat/ChatGroupSeat.module.css";
1774
1981
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$16) + "]") === null) {
1775
1982
  const tag = document.createElement("style");
@@ -1845,11 +2052,21 @@ window.__ModuleLoader__.load({
1845
2052
  ]);
1846
2053
  return active ? displayed : desired;
1847
2054
  }
2055
+ /**
2056
+ * Nesting level of one member: a Step's own row reads one level in, and the
2057
+ * reasoning and Tool rows that belong to it read two.
2058
+ * @param member - group member reference.
2059
+ * @returns the member's depth below the group header.
2060
+ */
2061
+ function memberDepth(member) {
2062
+ return member.groupPart === "response" ? 1 : 2;
2063
+ }
1848
2064
  const GroupMembers = (0, react.memo)(function GroupMembers({ members, ...props }) {
1849
2065
  return members.map((member) => (0, react.createElement)(ChatNodeSeat, {
1850
2066
  ...props,
1851
2067
  key: chatRenderKey(member),
1852
2068
  nodeKey: member.key,
2069
+ depth: memberDepth(member),
1853
2070
  ...member.groupPart === void 0 ? {} : { groupPart: member.groupPart }
1854
2071
  }));
1855
2072
  });
@@ -1911,7 +2128,7 @@ window.__ModuleLoader__.load({
1911
2128
  });
1912
2129
  const grouped = props.usePresentation((policy) => policy.stepGrouping === "collapsed" || policy.stepGrouping === "history" && turnLocation?.status !== "open" && !deferCollapse);
1913
2130
  const reason = turnLocation?.end?.data.reason.kind;
1914
- const alwaysOpen = presentation?.turnClosed === false || presentation?.hasInterleavedInput === true || reason === "aborted" || reason === "error";
2131
+ const alwaysOpen = presentation?.turnClosed === false || presentation?.hasInterleavedInput === true || reason === "aborted" || reason === "interrupted" || reason === "error";
1915
2132
  const spec = presentation?.spec;
1916
2133
  const selectOpen = (0, react.useCallback)((state) => spec !== void 0 && turnProcessOpen(state, spec, deferCollapse), [spec, deferCollapse]);
1917
2134
  const storedOpen = props.useStore(selectOpen);
@@ -2023,7 +2240,7 @@ window.__ModuleLoader__.load({
2023
2240
  }
2024
2241
  //#endregion
2025
2242
  //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-chat/src/client/chat/MessageItem.module.css.mjs
2026
- const css$15 = ".Icvj1a_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.Icvj1a_userStack{min-width:0;max-width:min(calc(var(--qilin-chat-content-width,748px) * .702), 82%);flex-direction:column;align-items:flex-end;gap:8px;display:flex}.Icvj1a_bubble{background:var(--qilin-specific-bubble);max-width:100%;font-size:var(--qilin-content-font-size,14px);line-height:calc(22px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-primary);white-space:pre-wrap;word-break:break-word;border-radius:22px;padding:10px 16px}.Icvj1a_referenceSummary{color:var(--qilin-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(18px + var(--qilin-content-font-delta-secondary,0px))}.Icvj1a_contextRow{padding:2px 0}.Icvj1a_compactionRow{--qilin-compaction-header-height:calc(24px + var(--qilin-content-font-delta,0px));padding:2px 0}.Icvj1a_compactionButton{width:100%;height:var(--qilin-compaction-header-height);min-width:0;color:inherit;font:inherit;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;padding:0;display:flex}.Icvj1a_compactionRow:has(.Icvj1a_compactionBody) .Icvj1a_compactionButton{z-index:7;background:var(--qilin-alias-bg-base);border-radius:0;position:sticky;top:0}.Icvj1a_compactionBody :has(>[data-code-block-banner]){top:var(--qilin-compaction-header-height)}.Icvj1a_compactionRow:has(.Icvj1a_compactionBody) .Icvj1a_compactionButton:hover{background:var(--qilin-alias-interactive-bg-hover-solid)}.Icvj1a_compactionButton:not(:disabled){cursor:pointer}.Icvj1a_compactionButton:not(:disabled):hover{background:var(--qilin-alias-interactive-bg-hover)}.Icvj1a_compactionLeading{width:calc(16px + var(--qilin-content-font-delta,0px));height:calc(16px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-secondary);flex:none;place-items:center;margin-right:6px;display:inline-grid}.Icvj1a_compactionLeading svg{width:calc(14px + var(--qilin-content-font-delta,0px));height:calc(14px + var(--qilin-content-font-delta,0px))}.Icvj1a_compactionContextIcon,.Icvj1a_compactionDisclosureIcon{grid-area:1/1;justify-content:center;align-items:center;display:inline-flex}.Icvj1a_compactionDisclosureIcon,.Icvj1a_compactionButton:not(:disabled):hover .Icvj1a_compactionContextIcon,.Icvj1a_compactionButton:not(:disabled):focus-visible .Icvj1a_compactionContextIcon{opacity:0}.Icvj1a_compactionButton:not(:disabled):hover .Icvj1a_compactionDisclosureIcon,.Icvj1a_compactionButton:not(:disabled):focus-visible .Icvj1a_compactionDisclosureIcon{opacity:1}.Icvj1a_compactionTitle{font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(24px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-primary-dimmed);flex:none}.Icvj1a_compactionSep{background:var(--qilin-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.Icvj1a_compactionSummary{min-width:0;color:var(--qilin-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(24px + var(--qilin-content-font-delta,0px));text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.Icvj1a_compactionBody{padding:4px 0 4px calc(22px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(24px + var(--qilin-content-font-delta,0px))}.Icvj1a_retryRow{color:var(--qilin-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(20px + var(--qilin-content-font-delta-secondary,0px))}.Icvj1a_retrySummary{width:fit-content;color:inherit;cursor:pointer;user-select:none;border-radius:3px;align-items:center;gap:7px;padding:2px 0;list-style:none;display:inline-flex}.Icvj1a_retrySummary::-webkit-details-marker{display:none}.Icvj1a_retrySummary:after{content:\"\";opacity:.8;border-bottom:1.5px solid;border-right:1.5px solid;width:6px;height:6px;transition:transform .12s;transform:rotate(-45deg)}.Icvj1a_retrySummary:hover{color:var(--qilin-alias-label-secondary)}.Icvj1a_retrySummary:focus-visible{outline:1.5px solid var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline-offset:2px}.Icvj1a_retryText{color:inherit}.Icvj1a_retryRow[data-active] .Icvj1a_retryText{background:linear-gradient(90deg, var(--qilin-alias-label-tertiary) 0%, var(--qilin-alias-label-tertiary) 40%, var(--qilin-alias-label-secondary) 50%, var(--qilin-alias-label-tertiary) 60%, var(--qilin-alias-label-tertiary) 100%);color:#0000;background-position:100%;background-size:200% 100%;background-clip:text;animation:1.6s ease-in-out infinite Icvj1a_retry-shimmer}.Icvj1a_retryRow[open] .Icvj1a_retrySummary:after{transform:rotate(45deg)}.Icvj1a_retryDetails{overflow-wrap:anywhere;font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(18px + var(--qilin-content-font-delta-secondary,0px));gap:2px;margin-top:3px;padding-left:14px;display:grid}.Icvj1a_retryDetailLabel{color:var(--qilin-alias-label-secondary)}.Icvj1a_turnErrorRow{font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(20px + var(--qilin-content-font-delta-secondary,0px));grid-template-columns:10px minmax(0,1fr) auto;align-items:start;gap:8px;padding:2px 0;display:grid}.Icvj1a_turnErrorDot{margin-top:5px}.Icvj1a_turnErrorCopy{overflow-wrap:anywhere;min-width:0}.Icvj1a_turnErrorTitle{color:var(--qilin-alias-state-error-primary);margin-right:6px;font-weight:600}.Icvj1a_turnErrorMessage{color:var(--qilin-alias-label-secondary)}.Icvj1a_turnErrorCode{color:var(--qilin-alias-label-tertiary);font:var(--qilin-font-markdown-code-block-small)}.Icvj1a_maxTokensTitle{color:var(--qilin-alias-state-warn-primary);margin-right:6px;font-weight:600}@keyframes Icvj1a_retry-shimmer{0%{background-position:100%}to{background-position:0}}@media (prefers-reduced-motion:reduce){.Icvj1a_retryRow[data-active] .Icvj1a_retryText{color:inherit;background:0 0;animation:none}}.Icvj1a_attachmentRow{flex-wrap:wrap;justify-content:flex-end;gap:8px;max-width:100%;display:flex}.Icvj1a_fileCard{border:.5px solid var(--qilin-alias-border-l2,#0000001f);background:var(--qilin-specific-input-major,transparent);box-sizing:border-box;border-radius:16px;flex:0 0 240px;align-items:center;gap:10px;width:240px;min-height:64px;padding:8px 12px;display:inline-flex}.Icvj1a_fileIcon{flex:none;width:28px;height:28px}.Icvj1a_fileContent{flex-direction:column;flex:1;min-width:0;display:flex}.Icvj1a_fileName{white-space:nowrap;text-overflow:ellipsis;color:var(--qilin-alias-label-primary);font-size:14px;font-weight:500;line-height:22px;overflow:hidden}.Icvj1a_fileMeta{white-space:nowrap;text-overflow:ellipsis;color:var(--qilin-alias-label-tertiary,#00000073);font-size:12px;line-height:15px;overflow:hidden}";
2243
+ const css$15 = ".Icvj1a_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.Icvj1a_userStack{min-width:0;max-width:min(calc(var(--qilin-chat-content-width,748px) * .702), 82%);flex-direction:column;align-items:flex-end;gap:8px;display:flex}.Icvj1a_bubble{background:var(--qilin-specific-bubble);max-width:100%;font-size:var(--qilin-content-font-size,14px);line-height:calc(22px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-primary);white-space:pre-wrap;word-break:break-word;border-radius:22px;padding:10px 16px}.Icvj1a_referenceSummary{color:var(--qilin-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(18px + var(--qilin-content-font-delta-secondary,0px))}.Icvj1a_contextRow{padding:2px 0}.Icvj1a_compactionRow{--qilin-compaction-header-height:calc(24px + var(--qilin-content-font-delta,0px));padding:2px 0}.Icvj1a_compactionButton{width:100%;height:var(--qilin-compaction-header-height);min-width:0;color:inherit;font:inherit;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;padding:0;display:flex}.Icvj1a_compactionRow:has(.Icvj1a_compactionBody) .Icvj1a_compactionButton{z-index:7;background:var(--qilin-alias-bg-base);border-radius:0;position:sticky;top:0}.Icvj1a_compactionBody :has(>[data-code-block-banner]){top:var(--qilin-compaction-header-height)}.Icvj1a_compactionRow:has(.Icvj1a_compactionBody) .Icvj1a_compactionButton:hover{background:var(--qilin-alias-interactive-bg-hover-solid)}.Icvj1a_compactionButton:not(:disabled){cursor:pointer}.Icvj1a_compactionButton:not(:disabled):hover{background:var(--qilin-alias-interactive-bg-hover)}.Icvj1a_compactionLeading{width:calc(16px + var(--qilin-content-font-delta,0px));height:calc(16px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-secondary);flex:none;place-items:center;margin-right:6px;display:inline-grid}.Icvj1a_compactionLeading svg{width:calc(14px + var(--qilin-content-font-delta,0px));height:calc(14px + var(--qilin-content-font-delta,0px))}.Icvj1a_compactionContextIcon,.Icvj1a_compactionDisclosureIcon{grid-area:1/1;justify-content:center;align-items:center;display:inline-flex}.Icvj1a_compactionDisclosureIcon,.Icvj1a_compactionButton:not(:disabled):hover .Icvj1a_compactionContextIcon,.Icvj1a_compactionButton:not(:disabled):focus-visible .Icvj1a_compactionContextIcon{opacity:0}.Icvj1a_compactionButton:not(:disabled):hover .Icvj1a_compactionDisclosureIcon,.Icvj1a_compactionButton:not(:disabled):focus-visible .Icvj1a_compactionDisclosureIcon{opacity:1}.Icvj1a_compactionTitle{font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(24px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-primary-dimmed);flex:none}.Icvj1a_compactionSep{background:var(--qilin-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.Icvj1a_compactionSummary{min-width:0;color:var(--qilin-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(24px + var(--qilin-content-font-delta,0px));text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.Icvj1a_compactionBody{padding:4px 0 4px calc(22px + var(--qilin-content-font-delta,0px));color:var(--qilin-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(24px + var(--qilin-content-font-delta,0px))}.Icvj1a_retryRow{color:var(--qilin-alias-label-tertiary);font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(20px + var(--qilin-content-font-delta-secondary,0px))}.Icvj1a_retrySummary{width:fit-content;color:inherit;cursor:pointer;user-select:none;border-radius:3px;align-items:center;gap:7px;padding:2px 0;list-style:none;display:inline-flex}.Icvj1a_retrySummary::-webkit-details-marker{display:none}.Icvj1a_retrySummary:after{content:\"\";opacity:.8;border-bottom:1.5px solid;border-right:1.5px solid;width:6px;height:6px;transition:transform .12s;transform:rotate(-45deg)}.Icvj1a_retrySummary:hover{color:var(--qilin-alias-label-secondary)}.Icvj1a_retrySummary:focus-visible{outline:1.5px solid var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline-offset:2px}.Icvj1a_retryText{color:inherit}.Icvj1a_retryRow[data-active] .Icvj1a_retryText{background:linear-gradient(90deg, var(--qilin-alias-label-tertiary) 0%, var(--qilin-alias-label-tertiary) 40%, var(--qilin-alias-label-secondary) 50%, var(--qilin-alias-label-tertiary) 60%, var(--qilin-alias-label-tertiary) 100%);color:#0000;background-position:100%;background-size:200% 100%;background-clip:text;animation:1.6s ease-in-out infinite Icvj1a_retry-shimmer}.Icvj1a_retryRow[open] .Icvj1a_retrySummary:after{transform:rotate(45deg)}.Icvj1a_retryDetails{overflow-wrap:anywhere;font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(18px + var(--qilin-content-font-delta-secondary,0px));gap:2px;margin-top:3px;padding-left:14px;display:grid}.Icvj1a_retryDetailLabel{color:var(--qilin-alias-label-secondary)}.Icvj1a_turnErrorRow{font-size:var(--qilin-content-font-size-secondary,13px);line-height:calc(20px + var(--qilin-content-font-delta-secondary,0px));grid-template-columns:10px minmax(0,1fr) auto;align-items:start;gap:8px;padding:2px 0;display:grid}.Icvj1a_turnErrorDot{margin-top:5px}.Icvj1a_turnErrorCopy{overflow-wrap:anywhere;min-width:0}.Icvj1a_turnErrorTitle{color:var(--qilin-alias-state-error-primary);margin-right:6px;font-weight:600}.Icvj1a_turnErrorMessage{color:var(--qilin-alias-label-secondary)}.Icvj1a_turnErrorCode{color:var(--qilin-alias-label-tertiary);font:var(--qilin-font-markdown-code-block-small)}.Icvj1a_maxTokensTitle{color:var(--qilin-alias-state-warn-primary);margin-right:6px;font-weight:600}@keyframes Icvj1a_retry-shimmer{0%{background-position:100%}to{background-position:0}}@media (prefers-reduced-motion:reduce){.Icvj1a_retryRow[data-active] .Icvj1a_retryText{color:inherit;background:0 0;animation:none}}.Icvj1a_attachmentRow{flex-wrap:wrap;justify-content:flex-end;gap:8px;max-width:100%;display:flex}.Icvj1a_fileCard{border:.5px solid var(--qilin-alias-border-l2,#0000001f);border-radius:var(--qilin-radius-lg);background:var(--qilin-specific-input-major,transparent);box-sizing:border-box;flex:0 0 240px;align-items:center;gap:10px;width:240px;min-height:64px;padding:8px 12px;display:inline-flex}.Icvj1a_fileIcon{flex:none;width:28px;height:28px}.Icvj1a_fileContent{flex-direction:column;flex:1;min-width:0;display:flex}.Icvj1a_fileName{white-space:nowrap;text-overflow:ellipsis;color:var(--qilin-alias-label-primary);font-size:14px;font-weight:500;line-height:22px;overflow:hidden}.Icvj1a_fileMeta{white-space:nowrap;text-overflow:ellipsis;color:var(--qilin-alias-label-tertiary,#00000073);font-size:12px;line-height:15px;overflow:hidden}";
2027
2244
  const tagId$15 = "@qilin-agent/client-ui-chat/MessageItem.module.css";
2028
2245
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$15) + "]") === null) {
2029
2246
  const tag = document.createElement("style");
@@ -3036,6 +3253,10 @@ window.__ModuleLoader__.load({
3036
3253
  return Math.max(1, Math.ceil(milliseconds / 1e3));
3037
3254
  }
3038
3255
  function failureMessage(message, code, t) {
3256
+ if (code === "ACCOUNT_SIGNED_OUT") return t("message.failure.accountSignedOut");
3257
+ if (code === "INTERRUPTED") return t("message.failure.interrupted");
3258
+ if (code === "ACCOUNT_SIGN_IN_REQUIRED") return t("message.failure.accountSignInRequired");
3259
+ if (code === "QUOTA" || code === "ACCOUNT_QUOTA") return t("message.failure.quota");
3039
3260
  return code === "AUTH" ? t("message.failure.auth") : message;
3040
3261
  }
3041
3262
  function ModelRetryItem({ node, active, t }) {
@@ -3108,13 +3329,13 @@ window.__ModuleLoader__.load({
3108
3329
  className: MessageItem_module_css_default.turnErrorCopy,
3109
3330
  children: [(0, react_jsx_runtime.jsx)("span", {
3110
3331
  className: MessageItem_module_css_default.turnErrorTitle,
3111
- children: t("message.turnError")
3332
+ children: node.code === "ACCOUNT_SIGNED_OUT" || node.code === "INTERRUPTED" ? t("message.taskStopped") : t("message.turnError")
3112
3333
  }), (0, react_jsx_runtime.jsx)("span", {
3113
3334
  className: MessageItem_module_css_default.turnErrorMessage,
3114
3335
  children: failureMessage(node.message, node.code, t)
3115
3336
  })]
3116
3337
  }),
3117
- node.code !== void 0 && (0, react_jsx_runtime.jsx)("code", {
3338
+ node.code !== void 0 && node.code !== "INTERRUPTED" && (0, react_jsx_runtime.jsx)("code", {
3118
3339
  className: MessageItem_module_css_default.turnErrorCode,
3119
3340
  children: node.code
3120
3341
  })
@@ -3182,7 +3403,10 @@ window.__ModuleLoader__.load({
3182
3403
  }),
3183
3404
  showBubble && (0, react_jsx_runtime.jsxs)("div", {
3184
3405
  className: MessageItem_module_css_default.bubble,
3185
- children: [(0, _qilin_agent_client_ui_primitives.projectUserText)(text, referenceLabels, skillNames, "skill", references), rest.map((block, i) => (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.JsonBlock, {
3406
+ children: [(0, _qilin_agent_client_ui_primitives.projectUserText)(text, referenceLabels, skillNames, "skill", references, {
3407
+ reveal: t("secret.reveal"),
3408
+ hide: t("secret.hide")
3409
+ }), rest.map((block, i) => (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.JsonBlock, {
3186
3410
  label: t("message.extraBlock"),
3187
3411
  payload: block,
3188
3412
  truncatedLabel: truncated
@@ -4666,7 +4890,7 @@ window.__ModuleLoader__.load({
4666
4890
  }
4667
4891
  //#endregion
4668
4892
  //#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-chat/src/client/chat/TurnNavigator.module.css.mjs
4669
- const css$10 = ".GYXNwG_slot{z-index:7;height:0;padding-inline:calc(var(--qilin-composer-side-clearance) + 16px);pointer-events:none;position:absolute;top:0;left:0;right:0;container-type:inline-size}[data-conversation-scroll] .GYXNwG_slot{position:sticky}.GYXNwG_frame{--turn-rail-band:calc(var(--qilin-conversation-viewport-height,100dvh) - var(--qilin-composer-height,152px));--turn-preview-height:100px;top:calc(var(--turn-rail-band) / 2);width:28px;max-height:min(max(0px, calc(var(--turn-rail-band) - 64px)), 420px);contain:layout;cursor:pointer;pointer-events:auto;position:absolute;right:12px;transform:translateY(-50%)}.GYXNwG_scroller{max-height:inherit;overscroll-behavior:contain;scrollbar-width:none;position:relative;overflow-y:auto}.GYXNwG_scroller::-webkit-scrollbar{display:none}.GYXNwG_fadeTop{mask-image:linear-gradient(#0000 0,#000 24px 100%)}.GYXNwG_fadeBottom{mask-image:linear-gradient(#000 0 calc(100% - 24px),#0000 100%)}.GYXNwG_fadeTop.GYXNwG_fadeBottom{mask-image:linear-gradient(#0000 0,#000 24px calc(100% - 24px),#0000 100%)}.GYXNwG_marks{position:relative}.GYXNwG_mark{cursor:pointer;background:0 0;border:0;border-radius:8px;height:10px;padding:0;position:absolute;top:0;left:0;right:0}.GYXNwG_mark:before{background:var(--qilin-alias-border-l4);content:\"\";transform-origin:100%;border-radius:2px;width:20px;height:2px;transition:transform .14s,background-color .14s;position:absolute;top:50%;right:0;transform:translateY(-50%)scaleX(.6)}.GYXNwG_markUnloaded:before{opacity:.6;transform:translateY(-50%)scaleX(.4)}.GYXNwG_markPreview:before{background:var(--qilin-alias-label-tertiary);transform:translateY(-50%)scaleX(.9)}.GYXNwG_markBusy:before{animation:1s ease-in-out infinite GYXNwG_qilin-turn-mark-busy}.GYXNwG_markActive:before{background:var(--qilin-alias-label-primary);transform:translateY(-50%)scaleX(1)}.GYXNwG_mark:focus-visible:before{background:var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));transform:translateY(-50%)scaleX(1)}.GYXNwG_mark:focus-visible{outline:none}.GYXNwG_mark:focus-visible:after{border-radius:inherit;outline:1px solid var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline-offset:-1px;content:\"\";width:20px;position:absolute;inset:0 0 0 auto}.GYXNwG_preview{top:clamp(0px, calc(var(--turn-preview-center) - var(--turn-preview-height) / 2), calc(100% - var(--turn-preview-height)));box-sizing:border-box;width:min(300px,100cqw - 120px);max-height:var(--turn-preview-height);border-radius:var(--qilin-radius-lg);color:var(--qilin-alias-label-primary);background:var(--qilin-alias-bg-layer-1);box-shadow:var(--qilin-elevation-panel);pointer-events:none;border:0;padding:10px 12px;transition:top .14s cubic-bezier(.2,.8,.2,1);animation:.12s ease-out GYXNwG_qilin-turn-preview-enter;position:absolute;right:calc(100% + 10px);overflow:hidden}.GYXNwG_previewPrompt,.GYXNwG_previewResponse{-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.GYXNwG_previewPrompt{font:var(--qilin-font-xs-strong-13);-webkit-line-clamp:1}.GYXNwG_previewResponse{color:var(--qilin-alias-label-caption);font:var(--qilin-font-xxs-12);-webkit-line-clamp:3;margin-top:4px}@keyframes GYXNwG_qilin-turn-preview-enter{0%{opacity:0;transform:translate(4px)}to{opacity:1;transform:translate(0)}}@keyframes GYXNwG_qilin-turn-mark-busy{0%,to{opacity:1}50%{opacity:.35}}@container (width<=900px){.GYXNwG_frame{display:none}}@media (prefers-reduced-motion:reduce){.GYXNwG_frame,.GYXNwG_scroller,.GYXNwG_mark:before,.GYXNwG_markBusy:before,.GYXNwG_preview{scroll-behavior:auto;transition:none;animation:none}}";
4893
+ const css$10 = ".GYXNwG_slot{z-index:7;height:0;padding-inline:calc(var(--qilin-composer-side-clearance) + 16px);pointer-events:none;position:absolute;top:0;left:0;right:0;container-type:inline-size}[data-conversation-scroll] .GYXNwG_slot{position:sticky}.GYXNwG_frame{--turn-rail-band:calc(var(--qilin-conversation-viewport-height,100dvh) - var(--qilin-composer-height,152px));--turn-preview-height:100px;top:calc(var(--turn-rail-band) / 2);width:28px;max-height:min(max(0px, calc(var(--turn-rail-band) - 64px)), 420px);contain:layout;cursor:pointer;pointer-events:auto;position:absolute;left:12px;transform:translateY(-50%)}.GYXNwG_scroller{max-height:inherit;overscroll-behavior:contain;scrollbar-width:none;position:relative;overflow-y:auto}.GYXNwG_scroller::-webkit-scrollbar{display:none}.GYXNwG_fadeTop{mask-image:linear-gradient(#0000 0,#000 24px 100%)}.GYXNwG_fadeBottom{mask-image:linear-gradient(#000 0 calc(100% - 24px),#0000 100%)}.GYXNwG_fadeTop.GYXNwG_fadeBottom{mask-image:linear-gradient(#0000 0,#000 24px calc(100% - 24px),#0000 100%)}.GYXNwG_marks{position:relative}.GYXNwG_mark{cursor:pointer;background:0 0;border:0;border-radius:8px;height:10px;padding:0;position:absolute;top:0;left:0;right:0}.GYXNwG_mark:before{background:var(--qilin-alias-border-l4);content:\"\";transform-origin:0;border-radius:2px;width:20px;height:2px;transition:transform .14s,background-color .14s;position:absolute;top:50%;left:0;transform:translateY(-50%)scaleX(.6)}.GYXNwG_markUnloaded:before{opacity:.6;transform:translateY(-50%)scaleX(.4)}.GYXNwG_markPreview:before{background:var(--qilin-alias-label-tertiary);transform:translateY(-50%)scaleX(.9)}.GYXNwG_markBusy:before{animation:1s ease-in-out infinite GYXNwG_qilin-turn-mark-busy}.GYXNwG_markActive:before{background:var(--qilin-alias-label-primary);transform:translateY(-50%)scaleX(1)}.GYXNwG_mark:focus-visible:before{background:var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));transform:translateY(-50%)scaleX(1)}.GYXNwG_mark:focus-visible{outline:none}.GYXNwG_mark:focus-visible:after{border-radius:inherit;outline:1px solid var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline-offset:-1px;content:\"\";width:20px;position:absolute;inset:0 auto 0 0}.GYXNwG_preview{top:clamp(0px, calc(var(--turn-preview-center) - var(--turn-preview-height) / 2), calc(100% - var(--turn-preview-height)));box-sizing:border-box;width:min(300px,100cqw - 120px);max-height:var(--turn-preview-height);border-radius:var(--qilin-radius-lg);color:var(--qilin-alias-label-primary);background:var(--qilin-alias-bg-layer-1);box-shadow:var(--qilin-elevation-panel);pointer-events:none;border:0;padding:10px 12px;transition:top .14s cubic-bezier(.2,.8,.2,1);animation:.12s ease-out GYXNwG_qilin-turn-preview-enter;position:absolute;left:calc(100% + 10px);overflow:hidden}.GYXNwG_previewPrompt,.GYXNwG_previewResponse{-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.GYXNwG_previewPrompt{font:var(--qilin-font-xs-strong-13);-webkit-line-clamp:1}.GYXNwG_previewResponse{color:var(--qilin-alias-label-caption);font:var(--qilin-font-xxs-12);-webkit-line-clamp:3;margin-top:4px}@keyframes GYXNwG_qilin-turn-preview-enter{0%{opacity:0;transform:translate(-4px)}to{opacity:1;transform:translate(0)}}@keyframes GYXNwG_qilin-turn-mark-busy{0%,to{opacity:1}50%{opacity:.35}}@container (width<=900px){.GYXNwG_frame{display:none}}@media (prefers-reduced-motion:reduce){.GYXNwG_frame,.GYXNwG_scroller,.GYXNwG_mark:before,.GYXNwG_markBusy:before,.GYXNwG_preview{scroll-behavior:auto;transition:none;animation:none}}";
4670
4894
  const tagId$10 = "@qilin-agent/client-ui-chat/TurnNavigator.module.css";
4671
4895
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$10) + "]") === null) {
4672
4896
  const tag = document.createElement("style");
@@ -5254,9 +5478,9 @@ window.__ModuleLoader__.load({
5254
5478
  follow;
5255
5479
  sampleTimer = null;
5256
5480
  probeFrame = null;
5481
+ sampled = null;
5257
5482
  /** Settled reader row and offset, re-aligned when a layout change moves it. */
5258
5483
  readerPosition = null;
5259
- sampled = null;
5260
5484
  constructor(viewport, store, state, onChange, follow) {
5261
5485
  this.viewport = viewport;
5262
5486
  this.store = store;
@@ -5319,6 +5543,28 @@ window.__ModuleLoader__.load({
5319
5543
  this.cancelPending();
5320
5544
  this.commit(landing, true, this.viewport.latestTurn);
5321
5545
  }
5546
+ /**
5547
+ * Follow the tail on the active fold's animation clock.
5548
+ * @returns whether a fold batch owns the tail movement instead of a separate scroll animation.
5549
+ */
5550
+ followInputFold() {
5551
+ this.cancelPending();
5552
+ return this.viewport.followInputFold(this.follow);
5553
+ }
5554
+ /**
5555
+ * Let fold batches read follow intent and commit their tail landings.
5556
+ * @returns a disposer that disconnects this reading policy from the viewport.
5557
+ */
5558
+ bindFolds() {
5559
+ return this.viewport.bindReader({
5560
+ follow: this.follow,
5561
+ landedAtTail: (landing) => {
5562
+ this.cancelPending();
5563
+ this.commit(landing, true, this.viewport.latestTurn);
5564
+ },
5565
+ pending: () => this.pending
5566
+ });
5567
+ }
5322
5568
  /** Stop a native follow animation where it is; the next sampled position decides follow intent. */
5323
5569
  interruptFollow() {
5324
5570
  this.viewport.interruptFollow(this.follow);
@@ -5364,7 +5610,7 @@ window.__ModuleLoader__.load({
5364
5610
  */
5365
5611
  onScroll = (scroll) => {
5366
5612
  if (this.follow.animating) return;
5367
- if (!scroll.movedByReader && this.state.followingTail || scroll.movedByReader && scroll.metrics.top >= scroll.metrics.floor) {
5613
+ if (!scroll.movedByReader && this.state.followingTail || scroll.movedByReader && Math.abs(scroll.metrics.top - scroll.metrics.floor) < 1) {
5368
5614
  this.followTail();
5369
5615
  this.sampled?.({
5370
5616
  position: null,
@@ -5432,9 +5678,9 @@ window.__ModuleLoader__.load({
5432
5678
  else this.probeFrame = requestAnimationFrame(this.probe);
5433
5679
  }
5434
5680
  commit(landing, followingTail, activeTurn, initialized = true) {
5681
+ this.readerPosition = followingTail ? null : landing.position;
5435
5682
  if (followingTail) this.store.save(null);
5436
5683
  else if (landing.position !== null) this.store.save(landing.position);
5437
- this.readerPosition = followingTail ? null : landing.position;
5438
5684
  this.publish({
5439
5685
  initialized,
5440
5686
  followingTail,
@@ -5520,6 +5766,7 @@ window.__ModuleLoader__.load({
5520
5766
  (0, react.useLayoutEffect)(() => {
5521
5767
  reading.setStore(store);
5522
5768
  }, [reading, store]);
5769
+ (0, react.useLayoutEffect)(() => reading.bindFolds(), [reading]);
5523
5770
  (0, react.useLayoutEffect)(() => () => {
5524
5771
  reading.dispose();
5525
5772
  }, [reading]);
@@ -5557,13 +5804,18 @@ window.__ModuleLoader__.load({
5557
5804
  top: 0,
5558
5805
  landing: null
5559
5806
  };
5807
+ paging = null;
5808
+ reader = null;
5809
+ /** A transcript resize observed since the previous sample. */
5560
5810
  resized = false;
5811
+ /** Reader input observed since the previous sample. */
5561
5812
  readerIntent = false;
5562
- paging = null;
5813
+ /** The pending or running fold batch lands at the tail. */
5814
+ tailRequested = false;
5815
+ /** Switches the running batch to a tail landing; null while no batch runs. */
5816
+ retarget = null;
5563
5817
  /** Transition state and row callbacks owned by this viewport's attachment. */
5564
- motion = createFlowMotion((px) => {
5565
- this.reserveBelow(px);
5566
- }, (active) => {
5818
+ motion = createFlowMotion(() => this.foldDriver(), (active) => {
5567
5819
  if (this.elements !== null) this.elements.scroller.style.overflowAnchor = active ? "none" : "";
5568
5820
  });
5569
5821
  /**
@@ -5597,8 +5849,8 @@ window.__ModuleLoader__.load({
5597
5849
  if (typeof ResizeObserver !== "undefined") {
5598
5850
  this.observer = new ResizeObserver(() => {
5599
5851
  if (this.elements !== elements) return;
5600
- this.reclaimBelow();
5601
5852
  this.resized = true;
5853
+ this.reclaimBelow();
5602
5854
  this.invalidate();
5603
5855
  this.events?.resize();
5604
5856
  });
@@ -5619,13 +5871,13 @@ window.__ModuleLoader__.load({
5619
5871
  this.observer = null;
5620
5872
  this.elements = null;
5621
5873
  this.events = null;
5874
+ this.tailRequested = false;
5875
+ this.retarget = null;
5622
5876
  this.turns = [];
5623
5877
  this.observation = {
5624
5878
  top: 0,
5625
5879
  landing: null
5626
5880
  };
5627
- this.resized = false;
5628
- this.readerIntent = false;
5629
5881
  }
5630
5882
  /**
5631
5883
  * Connect business policy without changing DOM listener ownership.
@@ -5667,13 +5919,271 @@ window.__ModuleLoader__.load({
5667
5919
  if (spacer.style.height !== next) spacer.style.height = next;
5668
5920
  }
5669
5921
  /**
5670
- * Add room below the flow before rows above the reader close, so the scrollport never clamps:
5671
- * the closing gap pulls the content below it upward while everything above stays put.
5672
- * @param px - height about to leave the flow.
5922
+ * Connect the reading policy that fold batches consult.
5923
+ * @param reader - follow intent and the tail-landing commit.
5924
+ * @returns a disposer that disconnects only this reader.
5925
+ */
5926
+ bindReader(reader) {
5927
+ this.reader = reader;
5928
+ return () => {
5929
+ if (this.reader === reader) this.reader = null;
5930
+ };
5931
+ }
5932
+ /**
5933
+ * Land the pending or running fold batch at the tail instead of starting a separate scroll.
5934
+ * @param follow - follow controller whose native animation yields to the batch.
5935
+ * @returns whether a fold owns the tail movement.
5936
+ */
5937
+ followInputFold(follow) {
5938
+ if (this.elements === null || this.reader === null || !this.motion.inputActive()) return false;
5939
+ this.interruptFollow(follow);
5940
+ this.tailRequested = true;
5941
+ this.retarget?.();
5942
+ return true;
5943
+ }
5944
+ /**
5945
+ * Drive one fold batch on its frame clock. A tail landing ends at the floor, never above the reader's
5946
+ * compensated position; a reader landing keeps the visible content still. Both install only the room
5947
+ * the landing lacks.
5948
+ * @returns the batch driver, or null while detached or without a reading policy.
5949
+ */
5950
+ foldDriver() {
5951
+ const elements = this.elements;
5952
+ const reader = this.reader;
5953
+ if (elements === null || reader === null) return null;
5954
+ const { follow } = reader;
5955
+ let tail = false;
5956
+ let start = 0;
5957
+ let base = 0;
5958
+ let target = 0;
5959
+ let removed = 0;
5960
+ let growth = 0;
5961
+ let initialHeight = 0;
5962
+ let finalRoom = 0;
5963
+ let previousColumnHeight = 0;
5964
+ let previousProgress = 0;
5965
+ let segmentProgress = 0;
5966
+ let segmentTop = 0;
5967
+ let written = 0;
5968
+ const closing = [];
5969
+ const travelling = /* @__PURE__ */ new Set();
5970
+ let reference = null;
5971
+ let referenceTop = 0;
5972
+ let viewportTop = 0;
5973
+ const screenTop = (element) => element.getBoundingClientRect().top - viewportTop;
5974
+ const aim = () => {
5975
+ const finalHeight = initialHeight - removed + growth;
5976
+ const height = elements.scroller.clientHeight;
5977
+ target = tail ? Math.max(base, finalHeight - height, 0) : base;
5978
+ finalRoom = target === 0 ? 0 : Math.max(0, target + height - finalHeight);
5979
+ };
5980
+ const restart = () => {
5981
+ segmentTop = elements.scroller.scrollTop;
5982
+ segmentProgress = previousProgress;
5983
+ };
5984
+ const automatic = (cancelled) => !(cancelled && tail);
5985
+ const position = (progress, cancelled) => {
5986
+ if (!automatic(cancelled)) return elements.scroller.scrollTop;
5987
+ const remaining = segmentProgress === 1 ? 1 : (progress - segmentProgress) / (1 - segmentProgress);
5988
+ return segmentTop + (target - segmentTop) * remaining;
5989
+ };
5990
+ const room = (top, progress, cancelled) => Math.max(automatic(cancelled) ? finalRoom : 0, top + elements.scroller.clientHeight - (initialHeight + (growth - removed) * progress));
5991
+ return {
5992
+ begin: (boxes) => {
5993
+ tail = this.tailRequested || follow.active && !reader.pending();
5994
+ this.tailRequested = false;
5995
+ viewportTop = elements.scroller.getBoundingClientRect().top;
5996
+ start = elements.scroller.scrollTop;
5997
+ const originalRoom = this.spacerHeight();
5998
+ initialHeight = Math.max(elements.column.offsetHeight, elements.scroller.scrollHeight - originalRoom);
5999
+ let above = 0;
6000
+ const below = [];
6001
+ for (const box of boxes) {
6002
+ const amount = box.height + box.marginTop;
6003
+ const top = box.element.getBoundingClientRect().top - box.marginTop;
6004
+ if (box.closing) {
6005
+ removed += amount;
6006
+ above += Math.max(0, Math.min(amount, viewportTop - top));
6007
+ if (top >= viewportTop) below.push(box.element);
6008
+ closing.push(box);
6009
+ } else {
6010
+ growth += amount;
6011
+ if (top < viewportTop) above -= amount;
6012
+ }
6013
+ }
6014
+ if (!tail) reference = this.readingReference();
6015
+ const referenceBefore = reference === null ? 0 : screenTop(reference);
6016
+ elements.column.dataset.inputFold = "";
6017
+ if (reference !== null) {
6018
+ referenceTop = screenTop(reference);
6019
+ above -= referenceTop - referenceBefore;
6020
+ }
6021
+ base = Math.max(0, start - above);
6022
+ segmentTop = start;
6023
+ written = start;
6024
+ previousColumnHeight = elements.column.offsetHeight;
6025
+ aim();
6026
+ this.setSpacer(Math.max(originalRoom, finalRoom, removed));
6027
+ if (tail) follow.setFollowing(true);
6028
+ this.retarget = () => {
6029
+ tail = true;
6030
+ follow.setFollowing(true);
6031
+ this.motion.resumeInput();
6032
+ aim();
6033
+ restart();
6034
+ if (target <= segmentTop) return;
6035
+ for (const box of closing) {
6036
+ const shift = Number.parseFloat(box.element.style.translate.split(" ")[1] ?? "") || 0;
6037
+ if (box.element.getBoundingClientRect().top - shift - box.marginTop >= viewportTop) travelling.add(box.element);
6038
+ }
6039
+ };
6040
+ if (tail && target > start) for (const element of below) travelling.add(element);
6041
+ return travelling;
6042
+ },
6043
+ prepare: (progress, cancelled) => {
6044
+ if (this.elements !== elements) return;
6045
+ if (!tail) {
6046
+ const drift = elements.scroller.scrollTop - written;
6047
+ if (Math.abs(drift) > .5) {
6048
+ base = Math.max(0, base + drift);
6049
+ segmentTop += drift;
6050
+ referenceTop -= drift;
6051
+ aim();
6052
+ }
6053
+ }
6054
+ const added = elements.column.offsetHeight - previousColumnHeight;
6055
+ if (added !== 0) {
6056
+ initialHeight += added;
6057
+ aim();
6058
+ restart();
6059
+ }
6060
+ const top = position(progress, cancelled);
6061
+ this.setSpacer(Math.max(this.spacerHeight(), room(top, progress, cancelled)));
6062
+ },
6063
+ frame: (progress, cancelled) => {
6064
+ if (this.elements !== elements) return 0;
6065
+ const top = position(progress, cancelled);
6066
+ elements.scroller.scrollTop = top;
6067
+ this.setSpacer(room(top, progress, cancelled));
6068
+ written = elements.scroller.scrollTop;
6069
+ if (reference !== null && progress === 1) referenceTop = screenTop(reference);
6070
+ if (automatic(cancelled)) this.observation = {
6071
+ top: written,
6072
+ landing: null
6073
+ };
6074
+ previousColumnHeight = elements.column.offsetHeight;
6075
+ previousProgress = progress;
6076
+ return written - start;
6077
+ },
6078
+ finish: (cancelled) => {
6079
+ this.retarget = null;
6080
+ this.tailRequested = false;
6081
+ if (this.elements !== elements) return;
6082
+ elements.column.offsetHeight;
6083
+ delete elements.column.dataset.inputFold;
6084
+ if (!tail || cancelled) {
6085
+ if (!tail && reference?.isConnected === true && reference.closest("[hidden]") === null) {
6086
+ const shift = screenTop(reference) - referenceTop;
6087
+ if (Math.abs(shift) > .5) {
6088
+ const top = elements.scroller.scrollTop + shift;
6089
+ const content = elements.scroller.scrollHeight - this.spacerHeight();
6090
+ this.setSpacer(Math.max(this.spacerHeight(), top + elements.scroller.clientHeight - content));
6091
+ elements.scroller.scrollTop = top;
6092
+ }
6093
+ }
6094
+ const metrics = scrollMetrics(elements.scroller);
6095
+ const height = elements.scroller.scrollHeight - this.spacerHeight();
6096
+ this.setSpacer(metrics.top === 0 ? 0 : Math.max(0, metrics.top + metrics.height - height));
6097
+ if (tail) this.invalidate();
6098
+ else this.observation = {
6099
+ top: elements.scroller.scrollTop,
6100
+ landing: null
6101
+ };
6102
+ return;
6103
+ }
6104
+ const content = elements.scroller.scrollHeight - this.spacerHeight();
6105
+ const clientHeight = elements.scroller.clientHeight;
6106
+ const top = Math.max(target, content - clientHeight, 0);
6107
+ this.setSpacer(top === 0 ? 0 : Math.max(0, top + clientHeight - content));
6108
+ elements.scroller.scrollTop = top;
6109
+ const metrics = scrollMetrics(elements.scroller);
6110
+ const landing = {
6111
+ metrics,
6112
+ position: null,
6113
+ turn: this.latestTurn
6114
+ };
6115
+ this.observation = {
6116
+ top: metrics.top,
6117
+ landing
6118
+ };
6119
+ reader.landedAtTail(landing);
6120
+ }
6121
+ };
6122
+ }
6123
+ /**
6124
+ * Find the first visible top-level row at or below the scrollport top that holds no batch box.
6125
+ * Binary search over the ordered top-level rows reads O(log n) boxes.
6126
+ * @returns the reference row, or null when every visible row folds.
6127
+ */
6128
+ readingReference() {
6129
+ const elements = this.elements;
6130
+ if (elements === null) return null;
6131
+ const viewport = elements.scroller.getBoundingClientRect();
6132
+ const rows = (elements.column.querySelector(":scope > [data-slot=\"conversation.chat.flow\"]") ?? elements.column).children;
6133
+ let low = 0;
6134
+ let high = rows.length;
6135
+ while (low < high) {
6136
+ const middle = low + high >>> 1;
6137
+ if (rows[middle].getBoundingClientRect().bottom > viewport.top) high = middle;
6138
+ else low = middle + 1;
6139
+ }
6140
+ for (let index = low; index < rows.length; index++) {
6141
+ const row = rows[index];
6142
+ if (row.hasAttribute("hidden")) continue;
6143
+ const rect = row.getBoundingClientRect();
6144
+ if (rect.top >= viewport.bottom) return null;
6145
+ if (rect.height === 0 || rect.bottom <= viewport.top) continue;
6146
+ if (row.hasAttribute("data-chat-motion") || row.querySelector("[data-chat-motion]") !== null) continue;
6147
+ return row;
6148
+ }
6149
+ return null;
6150
+ }
6151
+ /**
6152
+ * Consume the pending transcript resize. A delivery that arrives with one is the
6153
+ * browser's layout compensation, not a reader choice, so it must not re-anchor the
6154
+ * semantic position.
6155
+ * @returns whether the transcript resized since the previous sample.
6156
+ */
6157
+ consumeResize() {
6158
+ const resized = this.resized;
6159
+ this.resized = false;
6160
+ return resized;
6161
+ }
6162
+ /**
6163
+ * Consume reader input recorded since the previous sample. Geometry alone cannot
6164
+ * separate a reader scroll from the browser's layout compensation, so a transcript
6165
+ * that moved with reader input present is the reader's own choice.
6166
+ * @returns whether wheel, touch, pointer, key, or find-in-page input arrived.
5673
6167
  */
5674
- reserveBelow(px) {
5675
- if (px <= 0) return;
5676
- this.setSpacer(this.spacerHeight() + px);
6168
+ consumeReaderIntent() {
6169
+ const intent = this.readerIntent;
6170
+ this.readerIntent = false;
6171
+ return intent;
6172
+ }
6173
+ /**
6174
+ * Re-align the reader's own settled row after a layout change moved it. A missing row
6175
+ * leaves the scrollport untouched: the raw position is only meaningful for the row it
6176
+ * was sampled against.
6177
+ * @param position - semantic position sampled while the reader owned the transcript.
6178
+ * @returns the actual landing, or null when the row is absent or still aligned.
6179
+ */
6180
+ restoreReaderRow(position) {
6181
+ if (position === null || this.elements === null) return null;
6182
+ const row = this.anchor(position.anchorKey);
6183
+ if (row === null) return null;
6184
+ const top = row.getBoundingClientRect().top - this.elements.scroller.getBoundingClientRect().top;
6185
+ if (Math.abs(top - position.anchorTop) < .5) return null;
6186
+ return this.align(row, position.anchorTop, null);
5677
6187
  }
5678
6188
  /** Clear fold reservations after a display-mode change, allowing the scroll range to shrink. */
5679
6189
  resetBelow() {
@@ -5681,26 +6191,27 @@ window.__ModuleLoader__.load({
5681
6191
  this.invalidate();
5682
6192
  }
5683
6193
  /**
5684
- * Give reserved room back only as content grows into it, never by scrolling back down: growth fills
5685
- * the blank before it moves the floor. Holds still while rows are closing, because trimming mid-close
5686
- * would let the next frame clamp.
6194
+ * Give reserved room back after acknowledged scrolling or content growth without clamping the reader.
6195
+ * Holds still during folds and unsampled movement; following includes the room that remains.
5687
6196
  */
5688
6197
  reclaimBelow() {
5689
6198
  const elements = this.elements;
5690
6199
  if (elements === null || elements.spacer === null) return;
5691
6200
  const current = this.spacerHeight();
5692
6201
  if (current === 0 || this.motion.foldActive()) return;
5693
- const { scroller } = elements;
5694
- const floorWithoutSpacer = scroller.scrollHeight - current - scroller.clientHeight;
5695
- this.setSpacer(Math.min(current, Math.max(0, scroller.scrollTop - floorWithoutSpacer)));
6202
+ const scroll = this.readScroll();
6203
+ if (scroll === null || scroll.movedByReader) return;
6204
+ const floorWithoutSpacer = scroll.metrics.floor - current;
6205
+ this.setSpacer(Math.min(current, Math.max(0, scroll.metrics.top - floorWithoutSpacer)));
5696
6206
  }
5697
6207
  /**
5698
6208
  * Stop an outstanding native follow animation at its current position before a reading gesture.
5699
6209
  * @param follow - follow controller bound to this scrollport.
5700
6210
  */
5701
6211
  interruptFollow(follow) {
6212
+ if (!follow.animating) return;
5702
6213
  const metrics = this.metrics();
5703
- if (metrics === null || this.elements === null || !follow.animating) return;
6214
+ if (metrics === null || this.elements === null) return;
5704
6215
  follow.interrupt(this.elements.scroller, metrics);
5705
6216
  this.observation = {
5706
6217
  top: metrics.top,
@@ -5716,29 +6227,7 @@ window.__ModuleLoader__.load({
5716
6227
  top: metrics.top,
5717
6228
  landing: null
5718
6229
  };
5719
- }
5720
- /**
5721
- * Consume the pending transcript resize.
5722
- * A delivery that arrives with one is the browser's layout compensation,
5723
- * not a reader choice, so it must not re-anchor the semantic position.
5724
- * @returns whether the transcript resized since the previous sample.
5725
- */
5726
- consumeResize() {
5727
- const resized = this.resized;
5728
- this.resized = false;
5729
- return resized;
5730
- }
5731
- /**
5732
- * Consume reader input recorded since the previous sample.
5733
- * Geometry alone cannot separate a reader scroll from the browser's layout
5734
- * compensation, so a transcript that moved with reader input present is the
5735
- * reader's own choice.
5736
- * @returns whether wheel, touch, pointer, key, or find-in-page input arrived.
5737
- */
5738
- consumeReaderIntent() {
5739
- const intent = this.readerIntent;
5740
- this.readerIntent = false;
5741
- return intent;
6230
+ this.reclaimBelow();
5742
6231
  }
5743
6232
  /**
5744
6233
  * Compare the current scroll geometry with the last acknowledged position.
@@ -5822,7 +6311,7 @@ window.__ModuleLoader__.load({
5822
6311
  const first = this.turns[0];
5823
6312
  if (elements === null || metrics === null || first === void 0) return null;
5824
6313
  const line = elements.scroller.getBoundingClientRect().top + Math.min(96, metrics.height * .2);
5825
- const rows = elements.column.children;
6314
+ const rows = (elements.column.querySelector(":scope > [data-slot=\"conversation.chat.flow\"]") ?? elements.column).children;
5826
6315
  let low = 0;
5827
6316
  let high = rows.length;
5828
6317
  let reading = first.turn;
@@ -5875,21 +6364,6 @@ window.__ModuleLoader__.load({
5875
6364
  const metrics = this.metrics();
5876
6365
  return metrics === null ? null : this.write(position.scrollTop, metrics, null);
5877
6366
  }
5878
- /**
5879
- * Re-align the reader's own settled row after a layout change moved it.
5880
- * A missing row leaves the scrollport untouched: the raw position is only
5881
- * meaningful for the row it was sampled against.
5882
- * @param position - semantic position sampled while the reader owned the transcript.
5883
- * @returns the actual landing, or null when the row is absent or still aligned.
5884
- */
5885
- restoreReaderRow(position) {
5886
- if (position === null || this.elements === null) return null;
5887
- const row = this.anchor(position.anchorKey);
5888
- if (row === null) return null;
5889
- const top = row.getBoundingClientRect().top - this.elements.scroller.getBoundingClientRect().top;
5890
- if (Math.abs(top - position.anchorTop) < .5) return null;
5891
- return this.align(row, position.anchorTop, null);
5892
- }
5893
6367
  /** Retain the first eligible transcript seat in DOM order; selection reads no geometry. */
5894
6368
  beginPaging() {
5895
6369
  this.stopPreserving();
@@ -5992,7 +6466,7 @@ window.__ModuleLoader__.load({
5992
6466
  });
5993
6467
  }
5994
6468
  /**
5995
- * Align the scrollport with its current floor.
6469
+ * Align with the physical floor, including fold room that newer content has not yet filled.
5996
6470
  * @param follow - independent follow intent and scrolling controller.
5997
6471
  * @param behavior - immediate positioning, or one native animation for a reader-caused tail change.
5998
6472
  * @returns the actual floor landing, or null while detached. A smooth landing reports the starting
@@ -6001,8 +6475,13 @@ window.__ModuleLoader__.load({
6001
6475
  scrollToBottom(follow, behavior = "instant") {
6002
6476
  const metrics = this.metrics();
6003
6477
  if (metrics === null || this.elements === null) return null;
6478
+ this.observation = {
6479
+ top: metrics.top,
6480
+ landing: null
6481
+ };
6482
+ this.reclaimBelow();
6004
6483
  const landing = {
6005
- metrics: follow.toBottom(this.elements.scroller, metrics, behavior),
6484
+ metrics: follow.toBottom(this.elements.scroller, scrollMetrics(this.elements.scroller), behavior),
6006
6485
  position: null,
6007
6486
  turn: this.latestTurn
6008
6487
  };
@@ -6056,8 +6535,10 @@ window.__ModuleLoader__.load({
6056
6535
  if (scroll !== null) this.events?.scroll(scroll);
6057
6536
  };
6058
6537
  onScrollEnd = (event) => {
6059
- if (event.target === this.elements?.scroller) this.events?.scrollEnd(true);
6060
- else if (event.target instanceof HTMLElement && event.target.hasAttribute("data-step-process-body")) this.events?.scrollEnd(false);
6538
+ if (event.target === this.elements?.scroller) {
6539
+ this.events?.scrollEnd(true);
6540
+ this.reclaimBelow();
6541
+ } else if (event.target instanceof HTMLElement && event.target.hasAttribute("data-step-process-body")) this.events?.scrollEnd(false);
6061
6542
  };
6062
6543
  onIntent = (event) => {
6063
6544
  if (event.type === "keydown" || event.type === "pointerdown") {
@@ -6125,26 +6606,9 @@ window.__ModuleLoader__.load({
6125
6606
  applied: null,
6126
6607
  opened: false
6127
6608
  });
6128
- const cancelFollow = (0, react.useRef)(null);
6129
- const clearPendingFollow = (0, react.useCallback)(() => {
6130
- cancelFollow.current?.();
6131
- cancelFollow.current = null;
6132
- }, []);
6133
6609
  const cancelPendingFollow = (0, react.useCallback)(() => {
6134
- clearPendingFollow();
6135
- if (viewport.motion.foldActive()) cancelFollow.current = viewport.motion.onFoldIdle(() => {
6136
- cancelFollow.current = null;
6137
- });
6138
- }, [clearPendingFollow, viewport]);
6139
- const followAfterFold = (0, react.useCallback)(() => {
6140
- if (cancelFollow.current !== null) return;
6141
- reading.interruptFollow();
6142
- cancelFollow.current = viewport.motion.onFoldIdle(() => {
6143
- cancelFollow.current = null;
6144
- viewport.reclaimBelow();
6145
- if (content.current.input.deferCompletedTurns) reading.followTail("smooth");
6146
- });
6147
- }, [reading, viewport]);
6610
+ viewport.motion.cancelInput();
6611
+ }, [viewport]);
6148
6612
  const processContent = (0, react.useCallback)(() => {
6149
6613
  const current = content.current.input;
6150
6614
  if (!current.ready && !content.current.opened) return;
@@ -6159,10 +6623,9 @@ window.__ModuleLoader__.load({
6159
6623
  return;
6160
6624
  }
6161
6625
  if (ownInput) {
6162
- clearPendingFollow();
6163
6626
  navigation.cancel();
6164
- if (current.deferCompletedTurns && viewport.motion.foldActive()) followAfterFold();
6165
- else reading.followTail(current.deferCompletedTurns ? "smooth" : "instant");
6627
+ if (current.deferCompletedTurns && reading.followInputFold()) return;
6628
+ reading.followTail(current.deferCompletedTurns ? "smooth" : "instant");
6166
6629
  return;
6167
6630
  }
6168
6631
  if (navigation.contentCommitted()) {
@@ -6171,16 +6634,10 @@ window.__ModuleLoader__.load({
6171
6634
  }
6172
6635
  if ((previous === null || current.ready !== previous.ready || current.firstSeq !== previous.firstSeq || current.lastKey !== previous.lastKey || current.order.length !== previous.order.length || current.running !== previous.running || current.steeringId !== previous.steeringId || current.submissionId !== previous.submissionId) && reading.followingTail) {
6173
6636
  navigation.cancel();
6174
- if (current.deferCompletedTurns && viewport.motion.foldActive()) followAfterFold();
6175
- else reading.followTail();
6637
+ if (current.deferCompletedTurns && reading.followInputFold()) return;
6638
+ reading.followTail();
6176
6639
  } else navigation.reconcile();
6177
- }, [
6178
- viewport,
6179
- reading,
6180
- navigation,
6181
- followAfterFold,
6182
- clearPendingFollow
6183
- ]);
6640
+ }, [reading, navigation]);
6184
6641
  (0, react.useLayoutEffect)(() => {
6185
6642
  const disconnectViewport = viewport.connect({
6186
6643
  scroll: (event) => {
@@ -6205,9 +6662,7 @@ window.__ModuleLoader__.load({
6205
6662
  navigation.reconcile();
6206
6663
  return;
6207
6664
  }
6208
- if (content.current.input.deferCompletedTurns && viewport.motion.foldActive()) {
6209
- if (!reading.pending && reading.followingTail) followAfterFold();
6210
- } else reading.onResize();
6665
+ if (!content.current.input.deferCompletedTurns || !viewport.motion.foldActive()) reading.onResize();
6211
6666
  navigation.reconcile();
6212
6667
  }
6213
6668
  });
@@ -6216,7 +6671,6 @@ window.__ModuleLoader__.load({
6216
6671
  processContent();
6217
6672
  });
6218
6673
  return () => {
6219
- clearPendingFollow();
6220
6674
  disconnectViewport();
6221
6675
  disconnectReading();
6222
6676
  content.current.opened = false;
@@ -6227,9 +6681,7 @@ window.__ModuleLoader__.load({
6227
6681
  reading,
6228
6682
  navigation,
6229
6683
  processContent,
6230
- followAfterFold,
6231
- cancelPendingFollow,
6232
- clearPendingFollow
6684
+ cancelPendingFollow
6233
6685
  ]);
6234
6686
  (0, react.useLayoutEffect)(() => {
6235
6687
  const previous = content.current.input;
@@ -6331,8 +6783,9 @@ window.__ModuleLoader__.load({
6331
6783
  if (node === void 0 || node.kind !== "user" && node.kind !== "steering" && node.kind !== "turn-trigger") continue;
6332
6784
  if (node.location.kind === "turn" || node.location.kind === "step") lastInputTurn = node.location.turn.turn;
6333
6785
  if (node.kind === "turn-trigger") continue;
6334
- const source = node.data.source;
6335
- if (source?.kind === "user" && typeof source.rpcId === "string") observed.add(source.rpcId);
6786
+ const data = node.data;
6787
+ const source = typeof data === "object" && data !== null && "source" in data ? data.source : void 0;
6788
+ if (typeof source === "object" && source !== null && "kind" in source && source.kind === "user" && "rpcId" in source && typeof source.rpcId === "string") observed.add(source.rpcId);
6336
6789
  }
6337
6790
  for (const { source } of [...inbox?.["next-turn"] ?? [], ...inbox?.["next-step"] ?? []]) if (source.kind === "user" && "rpcId" in source) observed.add(source.rpcId);
6338
6791
  return {
@@ -6627,6 +7080,8 @@ window.__ModuleLoader__.load({
6627
7080
  "fileOpen.title": "无法打开文件",
6628
7081
  "fileOpen.unknown": "无法打开此文件",
6629
7082
  "message.extraBlock": "附加内容块",
7083
+ "secret.reveal": "显示密钥",
7084
+ "secret.hide": "隐藏密钥",
6630
7085
  "message.systemPrompt": "系统提示词",
6631
7086
  "message.systemPromptUpdate": "系统提示词更新",
6632
7087
  "message.contextInjection": "上下文注入",
@@ -6739,6 +7194,11 @@ window.__ModuleLoader__.load({
6739
7194
  "message.retry.delay": "重试延迟:",
6740
7195
  "message.retry.failure": "失败原因:",
6741
7196
  "message.failure.auth": "API 密钥无效",
7197
+ "message.taskStopped": "任务已停止",
7198
+ "message.failure.accountSignedOut": "任务已因退出 DeepSeek 登录而停止。",
7199
+ "message.failure.interrupted": "QiLin 进程在本轮完成前退出(例如被强制结束、崩溃或关闭超时),重新启动后本轮已中止。未完成的工具调用结果未知。",
7200
+ "message.failure.accountSignInRequired": "请先登录 DeepSeek,并确认请求地址支持账号认证。",
7201
+ "message.failure.quota": "当前请求的额度已用尽",
6742
7202
  "message.turnError": "本轮运行失败",
6743
7203
  "message.maxTokens": "已达到输出 token 上限",
6744
7204
  "message.maxTokens.hint": "回答被截断,已有输出保留在对话中。发送“继续”可让模型接着输出。",
@@ -6817,6 +7277,8 @@ window.__ModuleLoader__.load({
6817
7277
  "fileOpen.title": "Couldn’t open file",
6818
7278
  "fileOpen.unknown": "Couldn’t open this file",
6819
7279
  "message.extraBlock": "Extra content block",
7280
+ "secret.reveal": "Show secret",
7281
+ "secret.hide": "Hide secret",
6820
7282
  "message.systemPrompt": "System prompt",
6821
7283
  "message.systemPromptUpdate": "System prompt update",
6822
7284
  "message.contextInjection": "Context injection",
@@ -6929,6 +7391,11 @@ window.__ModuleLoader__.load({
6929
7391
  "message.retry.delay": "Retry delay: ",
6930
7392
  "message.retry.failure": "Failure reason: ",
6931
7393
  "message.failure.auth": "API key is invalid",
7394
+ "message.taskStopped": "Task stopped",
7395
+ "message.failure.accountSignedOut": "Stopped because you signed out of DeepSeek.",
7396
+ "message.failure.interrupted": "The QiLin process exited before this turn finished (for example, it was killed, crashed, or timed out while shutting down), so the turn was closed when QiLin restarted. Unfinished tool calls have unknown outcomes.",
7397
+ "message.failure.accountSignInRequired": "Sign in to DeepSeek and ensure the request destination supports account authentication.",
7398
+ "message.failure.quota": "Request quota exhausted.",
6932
7399
  "message.turnError": "This turn failed",
6933
7400
  "message.maxTokens": "Output token limit reached",
6934
7401
  "message.maxTokens.hint": "The reply was cut off; earlier output is preserved in the conversation. Send \"continue\" to let the model resume.",
@@ -7448,10 +7915,11 @@ window.__ModuleLoader__.load({
7448
7915
  if (turn?.status !== "closed") return null;
7449
7916
  const canCollapse = turnProcess.foldable && turnProcess.hasContent && !turnProcessAlwaysOpen(node);
7450
7917
  const reason = turn.end?.data.reason.kind;
7918
+ const stopped = reason === "aborted" || reason === "interrupted";
7451
7919
  const elapsedMs = turn.start === void 0 || turn.end === void 0 ? void 0 : Math.max(1e3, turn.end.time - turn.start.time);
7452
- const duration = elapsedMs === void 0 || reason === "aborted" || reason === "error" ? void 0 : formatRunDuration(elapsedMs, t);
7453
- const label = reason === "aborted" ? t("message.stopped") : reason === "error" ? t("message.turnProcess.failed") : duration === void 0 ? t("message.turnProcess.worked") : t("message.turnProcess.took");
7454
- const announcement = reason === "aborted" ? t("message.stopped") : reason === "error" ? t("message.turnProcess.failed") : t("message.turnProcess.worked");
7920
+ const duration = elapsedMs === void 0 || stopped || reason === "error" ? void 0 : formatRunDuration(elapsedMs, t);
7921
+ const label = stopped ? t("message.stopped") : reason === "error" ? t("message.turnProcess.failed") : duration === void 0 ? t("message.turnProcess.worked") : t("message.turnProcess.took");
7922
+ const announcement = stopped ? t("message.stopped") : reason === "error" ? t("message.turnProcess.failed") : t("message.turnProcess.worked");
7455
7923
  return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("span", {
7456
7924
  className: accessibility_module_css_default.visuallyHidden,
7457
7925
  role: "status",
@@ -8977,7 +9445,7 @@ window.__ModuleLoader__.load({
8977
9445
  * @returns the item, or undefined when the Turn has no visible loaded node.
8978
9446
  */
8979
9447
  function turnNavigationItem(turn, locations, nodes) {
8980
- const loaded = locations.getTurn(turn).map((key) => nodes.get(key)).filter((node) => node !== void 0 && node.visibility === "visible");
9448
+ const loaded = locations.getTurn(turn).map((key) => nodes.get(key)).filter((node) => node !== void 0 && isVisibleChatNode(node));
8981
9449
  const user = loaded.find((node) => node.kind === "user");
8982
9450
  const anchor = user ?? loaded[0];
8983
9451
  if (anchor === void 0) return void 0;
@@ -9438,7 +9906,7 @@ window.__ModuleLoader__.load({
9438
9906
  * @returns visible Nodes in presentation order.
9439
9907
  */
9440
9908
  function orderedVisibleChatNodes(nodes) {
9441
- const visible = nodes.filter((node) => node.visibility === "visible");
9909
+ const visible = nodes.filter((node) => isVisibleChatNode(node));
9442
9910
  const presentations = turnProcessPresentations(visible);
9443
9911
  return visible.sort((left, right) => {
9444
9912
  const leftPosition = presentationPosition(left, presentations);
@@ -11046,9 +11514,28 @@ window.__ModuleLoader__.load({
11046
11514
  if (location?.kind !== "turn" && location?.kind !== "step") return 0;
11047
11515
  return location.turn.steps.at(-1)?.step ?? 0;
11048
11516
  }
11517
+ /**
11518
+ * Failure shown for a turn ending: a recorded error, a sign-out cancellation,
11519
+ * or a turn that restart recovery closed because the QiLin process exited mid-turn.
11520
+ */
11521
+ function endFailure(reason) {
11522
+ switch (reason.kind) {
11523
+ case "error": return reason.error;
11524
+ case "aborted": return reason.reason.kind === "hook" && reason.reason.reason === "deepseek-account/signed-out" ? {
11525
+ message: "Stopped because you signed out of DeepSeek.",
11526
+ code: "ACCOUNT_SIGNED_OUT"
11527
+ } : void 0;
11528
+ case "interrupted": return {
11529
+ message: "The QiLin process exited before this turn finished.",
11530
+ code: "INTERRUPTED"
11531
+ };
11532
+ default: return;
11533
+ }
11534
+ }
11049
11535
  function failureFrom(match) {
11050
- if (match.event.type !== "turn/end" || match.event.data.reason.kind !== "error") return void 0;
11051
- const failure = match.event.data.reason.error;
11536
+ if (match.event.type !== "turn/end") return void 0;
11537
+ const failure = endFailure(match.event.data.reason);
11538
+ if (failure === void 0) return void 0;
11052
11539
  const display = displayFailure(failure);
11053
11540
  return {
11054
11541
  seq: match.event.seq,
@@ -11080,7 +11567,7 @@ window.__ModuleLoader__.load({
11080
11567
  id: String(event.data.turn),
11081
11568
  role: "start"
11082
11569
  };
11083
- if (event.type === "turn/end" && event.data.reason.kind === "error") return {
11570
+ if (event.type === "turn/end" && endFailure(event.data.reason) !== void 0) return {
11084
11571
  id: String(event.data.turn),
11085
11572
  role: "update"
11086
11573
  };
@@ -11275,7 +11762,7 @@ window.__ModuleLoader__.load({
11275
11762
  if (answer === null) return {
11276
11763
  turn: turn.turn,
11277
11764
  controlAnchorSeq,
11278
- processStartSeq: controlAnchorSeq,
11765
+ processStartSeq: turn.start?.seq ?? controlAnchorSeq,
11279
11766
  answerAnchorSeq: null,
11280
11767
  answerStep: null,
11281
11768
  inlineReasoning: false,
@@ -12000,6 +12487,55 @@ window.__ModuleLoader__.load({
12000
12487
  function reply(node) {
12001
12488
  return node.kind === "assistant-step" && hasAssistantReplyContent(node.data.blocks);
12002
12489
  }
12490
+ /** Fallback range for a Turn whose control Node is outside the loaded window. */
12491
+ const WHOLE_TURN = {
12492
+ start: Number.NEGATIVE_INFINITY,
12493
+ answerAnchor: null,
12494
+ answerStep: null
12495
+ };
12496
+ function processRange(nodes) {
12497
+ const control = nodes.find((node) => node.kind === "turn-process");
12498
+ if (control === void 0) return WHOLE_TURN;
12499
+ return {
12500
+ start: control.data.processStartSeq,
12501
+ answerAnchor: control.data.answerAnchorSeq,
12502
+ answerStep: control.data.answerStep
12503
+ };
12504
+ }
12505
+ /**
12506
+ * Whether one Node's evidence falls inside the Turn's process range. A live
12507
+ * Turn has no answer boundary yet, so every node from the range start belongs.
12508
+ */
12509
+ function inProcessRange(node, range, live) {
12510
+ return node.anchorSeq >= range.start && (live || range.answerAnchor === null || node.anchorSeq < range.answerAnchor);
12511
+ }
12512
+ /**
12513
+ * Whether one Node is the Turn's finalized answer step. A live Turn has no
12514
+ * answer boundary yet: its reply rows keep their own seat while streaming.
12515
+ */
12516
+ function isAnswerStep(node, range, live) {
12517
+ return !live && range.answerStep !== null && node.kind === "assistant-step" && node.data.step === range.answerStep;
12518
+ }
12519
+ /** Whether a new Turn-process range must reshape the Turn's group. */
12520
+ function processRangeChanged(previous, current) {
12521
+ if (previous.kind !== "turn-process" || current.kind !== "turn-process") return false;
12522
+ return previous.data.processStartSeq !== current.data.processStartSeq || previous.data.answerAnchorSeq !== current.data.answerAnchorSeq || previous.data.answerStep !== current.data.answerStep;
12523
+ }
12524
+ /**
12525
+ * Whether one render part of a Node belongs to the Turn's group. The finalized
12526
+ * answer and a live Turn's reply keep their own rows: the answer owns the
12527
+ * Turn's seat, and the collapsed body would otherwise swallow the prose the
12528
+ * reader is watching.
12529
+ * @param part - renderer-owned part of the Node, or undefined for the whole Node.
12530
+ * @param answer - whether the Node is the Turn's finalized answer step.
12531
+ * @param member - whether the Node's evidence falls inside the process range.
12532
+ * @param closed - whether the Turn is closed.
12533
+ * @returns whether the part joins the Turn's group.
12534
+ */
12535
+ function inGroup(part, answer, member, closed) {
12536
+ if (part === "response") return member && closed && !answer;
12537
+ return member;
12538
+ }
12003
12539
  function sameSummary(left, right) {
12004
12540
  return left.running === right.running && left.runningDetail === right.runningDetail && left.preparing === right.preparing && left.counts.length === right.counts.length && left.counts.every((value, index) => value.kind === right.counts[index]?.kind && value.count === right.counts[index].count);
12005
12541
  }
@@ -12008,7 +12544,7 @@ window.__ModuleLoader__.load({
12008
12544
  }
12009
12545
  function structureChanged(previous, current) {
12010
12546
  if (!isVisibleChatNode(current) && (previous === void 0 || !isVisibleChatNode(previous))) return false;
12011
- return previous === void 0 || previous.kind !== current.kind || turnOf(previous) !== turnOf(current) || isVisibleChatNode(previous) !== isVisibleChatNode(current) || reasoning(previous) !== reasoning(current) || reply(previous) !== reply(current);
12547
+ return previous === void 0 || previous.kind !== current.kind || turnOf(previous) !== turnOf(current) || isVisibleChatNode(previous) !== isVisibleChatNode(current) || reasoning(previous) !== reasoning(current) || reply(previous) !== reply(current) || processRangeChanged(previous, current);
12012
12548
  }
12013
12549
  function readNode(input, key) {
12014
12550
  const node = input.readNode(key);
@@ -12113,7 +12649,12 @@ window.__ModuleLoader__.load({
12113
12649
  const emit = (key, entry) => {
12114
12650
  roots.set(key, [...roots.get(key) ?? [], entry]);
12115
12651
  };
12116
- const flush = (closed) => {
12652
+ /**
12653
+ * Install the accumulated members as the Turn's group.
12654
+ * @param complete - whether the members form a finished range; the Turn's own
12655
+ * lifecycle closes the last group of a live Turn.
12656
+ */
12657
+ const commit = (complete) => {
12117
12658
  const first = pending[0];
12118
12659
  if (first === void 0) return;
12119
12660
  const key = this.extendedGroup(pending, added)?.key ?? brandString(JSON.stringify([
@@ -12124,60 +12665,67 @@ window.__ModuleLoader__.load({
12124
12665
  const previous = this.groups.get(key);
12125
12666
  const before = previous?.snapshot;
12126
12667
  const group = previous !== void 0 && sameMembers(previous.members, pending) ? previous : new ProcessGroup(key, this.turn, pending);
12127
- group.refresh(input, closed || input.timeline.turns.get(this.turn)?.status === "closed");
12668
+ group.refresh(input, complete || closed);
12128
12669
  groups.set(group.key, group);
12129
- emit(first.key, {
12670
+ roots.set(first.key, [{
12130
12671
  kind: "group",
12131
12672
  key: group.key
12132
- });
12673
+ }, ...roots.get(first.key) ?? []]);
12133
12674
  for (const member of pending) membership.set(member.key, group.key);
12134
12675
  if (group.snapshot !== before) upserts.push(group.snapshot);
12135
12676
  pending = [];
12136
12677
  };
12137
- let previous;
12138
- let followed = false;
12139
12678
  const keys = input.readTurn(this.turn);
12679
+ const nodes = keys.map((key) => readNode(input, key));
12140
12680
  const replies = questionReplyIds(input, keys);
12141
- for (const key of keys) {
12142
- const position = readPosition(input, key);
12143
- if (previous !== void 0 && position.previous !== previous) flush(true);
12144
- previous = key;
12145
- followed = position.next !== void 0;
12146
- const node = readNode(input, key);
12681
+ const closed = input.timeline.turns.get(this.turn)?.status === "closed";
12682
+ const live = !closed;
12683
+ const range = processRange(nodes);
12684
+ const split = () => {
12685
+ if (!closed) commit(true);
12686
+ };
12687
+ for (const [index, key] of keys.entries()) {
12688
+ const node = nodes[index];
12147
12689
  if (node.kind === "turn-trigger" && replies.has(node.id)) {
12148
- flush(true);
12690
+ split();
12149
12691
  continue;
12150
12692
  }
12151
- if (INDEPENDENT.has(node.kind) || !isVisibleChatNode(node)) {
12152
- flush(true);
12693
+ if (INDEPENDENT.has(node.kind)) {
12694
+ split();
12153
12695
  emit(key, {
12154
12696
  kind: "node",
12155
12697
  key
12156
12698
  });
12157
- } else if (node.kind === "turn-process") emit(key, {
12158
- kind: "node",
12159
- key
12160
- });
12161
- else if (node.kind === "assistant-step") {
12162
- if (reasoning(node)) pending.push({
12699
+ continue;
12700
+ }
12701
+ if (node.kind === "turn-process") {
12702
+ emit(key, {
12163
12703
  kind: "node",
12164
- key,
12165
- groupPart: "reasoning"
12704
+ key
12166
12705
  });
12167
- if (reply(node)) {
12168
- flush(true);
12169
- emit(key, {
12170
- kind: "node",
12171
- key,
12172
- groupPart: "response"
12173
- });
12174
- }
12175
- } else pending.push({
12706
+ continue;
12707
+ }
12708
+ const answer = isAnswerStep(node, range, live);
12709
+ const member = answer || inProcessRange(node, range, live);
12710
+ const parts = node.kind === "assistant-step" ? [...reasoning(node) ? [{
12711
+ kind: "node",
12712
+ key,
12713
+ groupPart: "reasoning"
12714
+ }] : [], ...reply(node) ? [{
12715
+ kind: "node",
12716
+ key,
12717
+ groupPart: "response"
12718
+ }] : []] : [{
12176
12719
  kind: "node",
12177
12720
  key
12178
- });
12721
+ }];
12722
+ for (const reference of parts) if (inGroup(reference.groupPart, answer, member, closed)) pending.push(reference);
12723
+ else {
12724
+ if (reference.groupPart === "response") split();
12725
+ emit(key, reference);
12726
+ }
12179
12727
  }
12180
- flush(followed);
12728
+ commit(closed);
12181
12729
  const removes = [...this.groups.keys()].filter((key) => !groups.has(key));
12182
12730
  this.groups = groups;
12183
12731
  this.membership = membership;
@@ -12263,7 +12811,10 @@ window.__ModuleLoader__.load({
12263
12811
  }
12264
12812
  if (turn !== void 0) touch(turn).add(after.key);
12265
12813
  }
12266
- for (const turn of input.changedTurns) touch(turn);
12814
+ for (const turn of input.changedTurns) {
12815
+ touch(turn);
12816
+ if (input.timeline.turns.get(turn)?.status === "closed" && (this.turns.get(turn)?.snapshots().length ?? 0) > 1) regroup.add(turn);
12817
+ }
12267
12818
  const upserts = [];
12268
12819
  const removes = [];
12269
12820
  for (const turn of regroup) {
@@ -13373,6 +13924,12 @@ window.__ModuleLoader__.load({
13373
13924
  const CHAT_SETTINGS_NAMESPACE = "ui-chat";
13374
13925
  /** Field carrying the work-details presentation mode. */
13375
13926
  const TRANSCRIPT_VIEW_FIELD = "transcriptView";
13927
+ /** Field carrying the independent work-details collapse timing. */
13928
+ const COLLAPSE_TIMING_FIELD = "collapseTiming";
13929
+ /** Available points when a completed Turn returns to its historical presentation. */
13930
+ const COLLAPSE_TIMINGS = ["completion", "next-input"];
13931
+ /** Preserve completion-time folding when no timing preference is saved. */
13932
+ const DEFAULT_COLLAPSE_TIMING = "completion";
13376
13933
  /** Work-details presentation modes a user can choose. */
13377
13934
  const TRANSCRIPT_VIEW_MODES = [
13378
13935
  "compact",
@@ -13399,9 +13956,12 @@ window.__ModuleLoader__.load({
13399
13956
  const PERFORMANCE_USAGE_MODES = ["compact", "detailed"];
13400
13957
  /** Preserve detailed accounting for users without an explicit preference. */
13401
13958
  const DEFAULT_PERFORMANCE_USAGE = "detailed";
13959
+ /** Preserve the built-in browser for users without an explicit preference. */
13960
+ const DEFAULT_LINK_OPENING = "sidebar";
13402
13961
  /** Durable Chat schema; also the wire envelope the browser scope validates against. */
13403
13962
  const ChatSettingsFields = {
13404
- linkOpening: Schema.union(["sidebar", "new-tab"]).default("sidebar"),
13963
+ [COLLAPSE_TIMING_FIELD]: Schema.union([...COLLAPSE_TIMINGS]).default(DEFAULT_COLLAPSE_TIMING),
13964
+ linkOpening: Schema.union(["sidebar", "new-tab"]).default(DEFAULT_LINK_OPENING),
13405
13965
  performanceUsage: Schema.union([...PERFORMANCE_USAGE_MODES]).default(DEFAULT_PERFORMANCE_USAGE),
13406
13966
  [TRANSCRIPT_VIEW_FIELD]: Schema.union([...TRANSCRIPT_VIEW_SETTING_VALUES]).default(DEFAULT_TRANSCRIPT_VIEW_MODE).loose()
13407
13967
  };
@@ -13419,7 +13979,7 @@ window.__ModuleLoader__.load({
13419
13979
  "next-input": "settings.collapse.nextInput"
13420
13980
  };
13421
13981
  /**
13422
- * Render work-details and browser-local collapse-timing selectors.
13982
+ * Render the independent, Host-backed work-details and collapse-timing selectors.
13423
13983
  * @param props - composed Settings slot props.
13424
13984
  * @returns the preference row.
13425
13985
  */
@@ -13454,7 +14014,7 @@ window.__ModuleLoader__.load({
13454
14014
  }
13455
14015
  //#endregion
13456
14016
  //#region lib/types/client/transcript-view.js
13457
- /** Host-backed work details and independent, in-memory collapse timing. */
14017
+ /** Independent work-details preferences backed by the same Host configuration form. */
13458
14018
  /** Live work-details preference consumed by Chat and its Settings row. */
13459
14019
  var TranscriptViewPolicy = class {
13460
14020
  host;
@@ -13462,8 +14022,8 @@ window.__ModuleLoader__.load({
13462
14022
  unsubscribe;
13463
14023
  /** Reactive current mode, including the client default before Host settings arrive. */
13464
14024
  mode;
13465
- /** Independent Client-lifetime choice; each new instance starts with completion-time folding. */
13466
- collapseTiming = (0, _qilin_agent_client_store.createSnapshotStore)("completion");
14025
+ /** Reactive collapse timing, defaulting to completion until Host settings arrive. */
14026
+ collapseTiming = (0, _qilin_agent_client_store.createSnapshotStore)(DEFAULT_COLLAPSE_TIMING);
13467
14027
  /**
13468
14028
  * @param host - durable Chat settings scope.
13469
14029
  * @param defaultMode - presentation used without an explicit saved mode.
@@ -13491,11 +14051,13 @@ window.__ModuleLoader__.load({
13491
14051
  this.host.set(TRANSCRIPT_VIEW_FIELD, mode);
13492
14052
  }
13493
14053
  /**
13494
- * Keep collapse timing in memory without writing Host or browser storage.
14054
+ * Publish and persist collapse timing without changing the work-details mode.
13495
14055
  * @param timing - immediate completion folding or deferred folding at the next input.
13496
14056
  */
13497
14057
  setCollapseTiming(timing) {
14058
+ if (this.collapseTiming.getSnapshot() === timing) return;
13498
14059
  this.collapseTiming.set(timing);
14060
+ this.host.set(COLLAPSE_TIMING_FIELD, timing);
13499
14061
  }
13500
14062
  /** Adopt the latest accepted Host section without writing it back. */
13501
14063
  adopt() {
@@ -13504,6 +14066,8 @@ window.__ModuleLoader__.load({
13504
14066
  const saved = section.transcriptView;
13505
14067
  const mode = saved === "normal" || saved === "expanded" ? "detailed" : saved ?? this.defaultMode;
13506
14068
  if (this.mode.getSnapshot() !== mode) this.mode.set(mode);
14069
+ const timing = section.collapseTiming ?? "completion";
14070
+ if (this.collapseTiming.getSnapshot() !== timing) this.collapseTiming.set(timing);
13507
14071
  }
13508
14072
  };
13509
14073
  //#endregion
@@ -13598,16 +14162,16 @@ window.__ModuleLoader__.load({
13598
14162
  /**
13599
14163
  * Resolve a stable policy for one detail mode and collapse timing.
13600
14164
  * @param mode - persisted work-details mode.
13601
- * @param collapseTiming - in-memory timing; defaults to immediate completion folding.
14165
+ * @param collapseTiming - persisted timing; defaults to immediate completion folding.
13602
14166
  * @returns the same policy object for the same pair of choices.
13603
14167
  */
13604
14168
  function presentationPolicyFor(mode, collapseTiming = "completion") {
13605
14169
  return TIMING_POLICIES[collapseTiming][mode];
13606
14170
  }
13607
14171
  /**
13608
- * Combine Host-backed detail and in-memory timing without owning subscriptions.
14172
+ * Combine Host-backed detail and persisted timing without owning subscriptions.
13609
14173
  * @param mode - live work-details mode.
13610
- * @param collapseTiming - live, in-memory collapse timing.
14174
+ * @param collapseTiming - live Host-backed collapse timing.
13611
14175
  * @returns observable policy whose subscriptions follow both preferences.
13612
14176
  */
13613
14177
  function derivePresentationPolicy(mode, collapseTiming) {