bunnyquery 1.7.0 → 1.8.0

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/bunnyquery.css CHANGED
@@ -1072,6 +1072,18 @@
1072
1072
  font-style: italic;
1073
1073
  clear: both;
1074
1074
  }
1075
+ /* Timestamp under a settled bubble (created for a user turn, response time for an
1076
+ assistant turn). Muted and small so it never competes with the message. */
1077
+ .bq-msg-time {
1078
+ display: block;
1079
+ margin-top: 0.25rem;
1080
+ font-size: 0.62rem;
1081
+ line-height: 1;
1082
+ color: var(--bq-muted);
1083
+ opacity: 0.7;
1084
+ clear: both;
1085
+ }
1086
+ .bq-message.is-user .bq-msg-time { text-align: right; }
1075
1087
  .bq-cancel-queue-btn {
1076
1088
  float: right;
1077
1089
  margin: -0.25rem -0.25rem 0.2rem 0.4rem;
package/bunnyquery.js CHANGED
@@ -280,6 +280,9 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
280
280
  You are a dedicated assistant for the project ID: "${formattedServiceId}".
281
281
  Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage.
282
282
  Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
283
+ Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records that usually share a table. So a question about the data almost always spans many records across several files. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY relevant table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset.
284
+ Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
285
+ Embedded values: a search term is often stored inside a larger string. A merchant "GODADDY" appears as "DNH*GODADDY#4070277042", and a card as "4140****2941". Server-side index and tag filters match only exact values or leading prefixes, not substrings, so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
283
286
  File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
284
287
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
285
288
  - Most attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
@@ -614,6 +617,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
614
617
  if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
615
618
  return href.trim().replace(/\s/g, "%20");
616
619
  }
620
+ function repairUrlEntities(href) {
621
+ if (!href || href.indexOf("&") === -1) return href;
622
+ var out = href, prev = "";
623
+ while (out !== prev) {
624
+ prev = out;
625
+ out = out.replace(/&/gi, "&").replace(/&#0*38;/g, "&").replace(/&#x0*26;/gi, "&");
626
+ }
627
+ return out;
628
+ }
617
629
  function normalizeTrailingInlineToken(value) {
618
630
  if (!value) return value;
619
631
  var out = value.replace(/[.,;:!?]+$/, "");
@@ -661,8 +673,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
661
673
  var tail = full.slice(("src::" + rawPath).length);
662
674
  var srcIsUrl = isHttpUrlLike(rawPath);
663
675
  if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
676
+ var srcUrl = repairUrlEntities(rawPath);
664
677
  return {
665
- part: { type: "link", label: truncateLabelForDisplay(rawPath), fullLabel: rawPath, href: rawPath, expired: false },
678
+ part: { type: "link", label: truncateLabelForDisplay(srcUrl), fullLabel: srcUrl, href: srcUrl, expired: false },
666
679
  tail
667
680
  };
668
681
  }
@@ -696,6 +709,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
696
709
  }
697
710
  var originalHref = g3 || g6 || "";
698
711
  if (!originalHref) return null;
712
+ originalHref = repairUrlEntities(originalHref);
699
713
  var urlTail;
700
714
  if (!g3 && g6) {
701
715
  var trimmedUrl = normalizeTrailingInlineToken(originalHref);
@@ -795,6 +809,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
795
809
  };
796
810
  }
797
811
 
812
+ // src/engine/time.ts
813
+ function wallClockNow() {
814
+ return Date.now();
815
+ }
816
+ function formatChatTimestamp(ms) {
817
+ if (typeof ms !== "number" || !isFinite(ms) || ms <= 0) return "";
818
+ try {
819
+ return new Date(ms).toLocaleString(void 0, {
820
+ year: "numeric",
821
+ month: "short",
822
+ day: "numeric",
823
+ hour: "numeric",
824
+ minute: "2-digit",
825
+ second: "2-digit"
826
+ });
827
+ } catch (e) {
828
+ return "";
829
+ }
830
+ }
831
+
798
832
  // src/engine/requests.ts
799
833
  var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
800
834
  var ANTHROPIC_VERSION = "2023-06-01";
@@ -1329,6 +1363,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1329
1363
  var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
1330
1364
  var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
1331
1365
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1366
+ var createdTs = Number(item && item.created);
1367
+ var updatedTs = Number(item && item.updated);
1368
+ var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
1369
+ var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
1332
1370
  if (userText) {
1333
1371
  var displayContent;
1334
1372
  var indexFile = void 0;
@@ -1368,6 +1406,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1368
1406
  if (indexFile) userMsg._indexFile = indexFile;
1369
1407
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1370
1408
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1409
+ if (userTs !== void 0) userMsg._ts = userTs;
1371
1410
  mapped.push(userMsg);
1372
1411
  }
1373
1412
  if (isCancelledItem) ; else if (isInProcess) {
@@ -1382,11 +1421,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1382
1421
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
1383
1422
  if (item._isBgTask) em.isBackgroundTask = true;
1384
1423
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1424
+ if (replyTs !== void 0) em._ts = replyTs;
1385
1425
  mapped.push(em);
1386
1426
  } else if (assistantText) {
1387
1427
  var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1388
1428
  if (item._isBgTask) okm.isBackgroundTask = true;
1389
1429
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1430
+ if (replyTs !== void 0) okm._ts = replyTs;
1390
1431
  mapped.push(okm);
1391
1432
  }
1392
1433
  });
@@ -1799,7 +1840,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1799
1840
  var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1800
1841
  this.aiChatHistoryCache[key] = {
1801
1842
  messages: offExisting.messages.concat([
1802
- { role: "user", content: composed, _ownerKey: key },
1843
+ { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() },
1803
1844
  { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1804
1845
  ]),
1805
1846
  endOfList: offExisting.endOfList,
@@ -1831,7 +1872,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1831
1872
  serviceId: id.serviceId,
1832
1873
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1833
1874
  });
1834
- var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
1875
+ var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
1835
1876
  if (key) queuedBubble._ownerKey = key;
1836
1877
  if (useBgQueue) queuedBubble._useBgQueue = true;
1837
1878
  this.state.messages.push(queuedBubble);
@@ -1866,7 +1907,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1866
1907
  });
1867
1908
  return;
1868
1909
  }
1869
- this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
1910
+ this.state.messages.push({ role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} });
1870
1911
  this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1871
1912
  this.host.notify();
1872
1913
  this.updateHistoryCache();
@@ -1923,6 +1964,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1923
1964
  var existing = this.state.messages[nextIdx];
1924
1965
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1925
1966
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
1967
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1926
1968
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1927
1969
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1928
1970
  this.state.messages[nextIdx] = promoted;
@@ -1944,6 +1986,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1944
1986
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1945
1987
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1946
1988
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
1989
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1947
1990
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1948
1991
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1949
1992
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
@@ -1993,6 +2036,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1993
2036
  var repl = { role: "user", content: exist.content };
1994
2037
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
1995
2038
  if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
2039
+ if (exist._ts !== void 0) repl._ts = exist._ts;
1996
2040
  this.state.messages[userIdx] = repl;
1997
2041
  }
1998
2042
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -2001,6 +2045,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2001
2045
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
2002
2046
  }
2003
2047
  insertAtTarget(msg, targetIdx) {
2048
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
2004
2049
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) this.state.messages[targetIdx] = msg;
2005
2050
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
2006
2051
  else this.state.messages.push(msg);
@@ -2337,6 +2382,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2337
2382
  }
2338
2383
  enqueueTypewrite(idx, fullText, localId) {
2339
2384
  var self = this;
2385
+ var target = this.state.messages[idx];
2386
+ if (target && target._ts === void 0) target._ts = wallClockNow();
2340
2387
  this.typewriterQueue = this.typewriterQueue.then(function() {
2341
2388
  return self.typewriteIntoIndex(idx, fullText, localId);
2342
2389
  });
@@ -2406,6 +2453,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2406
2453
  var u = this.state.messages[uIdx];
2407
2454
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
2408
2455
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2456
+ if (u._ts !== void 0) cleaned._ts = u._ts;
2409
2457
  if (u._indexFile) cleaned._indexFile = u._indexFile;
2410
2458
  this.state.messages[uIdx] = cleaned;
2411
2459
  }
@@ -2478,6 +2526,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2478
2526
  var ex = this.state.messages[userIdx];
2479
2527
  var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2480
2528
  if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2529
+ if (ex._ts !== void 0) settledUser._ts = ex._ts;
2481
2530
  if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2482
2531
  if (ex._useBgQueue) settledUser._useBgQueue = true;
2483
2532
  this.state.messages[userIdx] = settledUser;
@@ -3254,7 +3303,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3254
3303
  cancellableIds: [],
3255
3304
  cancelling: false,
3256
3305
  mayHaveOlder: false,
3257
- anchorIndex: i
3306
+ // The run's first loaded pass, and never re-stamped: see the file
3307
+ // docstring. `anchorId` is filled in once every member is known.
3308
+ anchorIndex: i,
3309
+ anchorId: ""
3258
3310
  };
3259
3311
  order.push(runId);
3260
3312
  }
@@ -3268,7 +3320,6 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3268
3320
  g.passCount++;
3269
3321
  }
3270
3322
  g.members.push({ msg, index: i });
3271
- g.anchorIndex = i;
3272
3323
  runOfIndex[i] = runId;
3273
3324
  if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3274
3325
  if (ref && ref.name) keyByName[ref.name] = g.key;
@@ -3330,6 +3381,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3330
3381
  }
3331
3382
  }
3332
3383
  grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3384
+ var anchor = grp.members[0];
3385
+ grp.anchorIndex = anchor.index;
3386
+ grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
3333
3387
  }
3334
3388
  var out = [];
3335
3389
  for (var j = 0; j < list.length; j++) {
@@ -3347,7 +3401,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3347
3401
  (function() {
3348
3402
  var MCP_PROD = "https://mcp.broadwayinc.computer";
3349
3403
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
3350
- var BQ_VERSION = "1.7.0" ;
3404
+ var BQ_VERSION = "1.8.0" ;
3351
3405
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
3352
3406
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
3353
3407
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -6170,12 +6224,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6170
6224
  });
6171
6225
  bubble.appendChild(cancelBtn);
6172
6226
  }
6173
- var md = h("div", { class: "bq-md", html: parseMsgPartsHtml(msg.content) });
6227
+ var md = h("div", { class: "bq-md", translate: "no", html: parseMsgPartsHtml(msg.content) });
6174
6228
  md.addEventListener("click", onBubbleLinkClick);
6175
6229
  bubble.appendChild(md);
6176
6230
  if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
6177
6231
  if (msg.isCancelled) bubble.appendChild(h("span", { class: "bq-cancel-error", text: "(cancelled)" }));
6178
6232
  if (msg._cancelError) bubble.appendChild(h("span", { class: "bq-cancel-error", text: msg._cancelError }));
6233
+ var ts = formatChatTimestamp(msg._ts);
6234
+ if (ts) bubble.appendChild(h("time", { class: "bq-msg-time", text: ts }));
6179
6235
  }
6180
6236
  return h("div", { class: cls.join(" "), dataset: { msgIndex: String(idx) } }, bubble);
6181
6237
  }
@@ -6296,8 +6352,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6296
6352
  return id ? "s" + id + ":" + msg.role : "i" + index;
6297
6353
  }
6298
6354
  function indexGroupAnchorId(group) {
6299
- var last = group.members[group.members.length - 1];
6300
- return last && last.msg && last.msg._serverItemId || "";
6355
+ return group.anchorId || "";
6301
6356
  }
6302
6357
  function captureScrollAnchor() {
6303
6358
  var box = CS.messagesBox;
package/dist/engine.cjs CHANGED
@@ -286,6 +286,9 @@ function buildChatSystemPrompt(params) {
286
286
  You are a dedicated assistant for the project ID: "${formattedServiceId}".
287
287
  Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage.
288
288
  Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
289
+ Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records that usually share a table. So a question about the data almost always spans many records across several files. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY relevant table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset.
290
+ Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
291
+ Embedded values: a search term is often stored inside a larger string. A merchant "GODADDY" appears as "DNH*GODADDY#4070277042", and a card as "4140****2941". Server-side index and tag filters match only exact values or leading prefixes, not substrings, so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
289
292
  File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
290
293
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
291
294
  - Most attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
@@ -620,6 +623,15 @@ function repairUrlWhitespace(href) {
620
623
  if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
621
624
  return href.trim().replace(/\s/g, "%20");
622
625
  }
626
+ function repairUrlEntities(href) {
627
+ if (!href || href.indexOf("&") === -1) return href;
628
+ var out = href, prev = "";
629
+ while (out !== prev) {
630
+ prev = out;
631
+ out = out.replace(/&amp;/gi, "&").replace(/&#0*38;/g, "&").replace(/&#x0*26;/gi, "&");
632
+ }
633
+ return out;
634
+ }
623
635
  function normalizeTrailingInlineToken(value) {
624
636
  if (!value) return value;
625
637
  var out = value.replace(/[.,;:!?]+$/, "");
@@ -667,8 +679,9 @@ function classifyInlineLink(full, groups, ctx) {
667
679
  var tail = full.slice(("src::" + rawPath).length);
668
680
  var srcIsUrl = isHttpUrlLike(rawPath);
669
681
  if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
682
+ var srcUrl = repairUrlEntities(rawPath);
670
683
  return {
671
- part: { type: "link", label: truncateLabelForDisplay(rawPath), fullLabel: rawPath, href: rawPath, expired: false },
684
+ part: { type: "link", label: truncateLabelForDisplay(srcUrl), fullLabel: srcUrl, href: srcUrl, expired: false },
672
685
  tail
673
686
  };
674
687
  }
@@ -702,6 +715,7 @@ function classifyInlineLink(full, groups, ctx) {
702
715
  }
703
716
  var originalHref = g3 || g6 || "";
704
717
  if (!originalHref) return null;
718
+ originalHref = repairUrlEntities(originalHref);
705
719
  var urlTail;
706
720
  if (!g3 && g6) {
707
721
  var trimmedUrl = normalizeTrailingInlineToken(originalHref);
@@ -802,6 +816,26 @@ function buildBoundedChatMessages(options) {
802
816
  };
803
817
  }
804
818
 
819
+ // src/engine/time.ts
820
+ function wallClockNow() {
821
+ return Date.now();
822
+ }
823
+ function formatChatTimestamp(ms) {
824
+ if (typeof ms !== "number" || !isFinite(ms) || ms <= 0) return "";
825
+ try {
826
+ return new Date(ms).toLocaleString(void 0, {
827
+ year: "numeric",
828
+ month: "short",
829
+ day: "numeric",
830
+ hour: "numeric",
831
+ minute: "2-digit",
832
+ second: "2-digit"
833
+ });
834
+ } catch (e) {
835
+ return "";
836
+ }
837
+ }
838
+
805
839
  // src/engine/requests.ts
806
840
  var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
807
841
  var ANTHROPIC_MODELS_API_URL = "https://api.anthropic.com/v1/models";
@@ -1363,6 +1397,10 @@ function mapHistoryListToMessages(list, platform, opts) {
1363
1397
  var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
1364
1398
  var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
1365
1399
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1400
+ var createdTs = Number(item && item.created);
1401
+ var updatedTs = Number(item && item.updated);
1402
+ var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
1403
+ var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
1366
1404
  if (userText) {
1367
1405
  var displayContent;
1368
1406
  var indexFile = void 0;
@@ -1402,6 +1440,7 @@ function mapHistoryListToMessages(list, platform, opts) {
1402
1440
  if (indexFile) userMsg._indexFile = indexFile;
1403
1441
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1404
1442
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1443
+ if (userTs !== void 0) userMsg._ts = userTs;
1405
1444
  mapped.push(userMsg);
1406
1445
  }
1407
1446
  if (isCancelledItem) ; else if (isInProcess) {
@@ -1416,11 +1455,13 @@ function mapHistoryListToMessages(list, platform, opts) {
1416
1455
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
1417
1456
  if (item._isBgTask) em.isBackgroundTask = true;
1418
1457
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1458
+ if (replyTs !== void 0) em._ts = replyTs;
1419
1459
  mapped.push(em);
1420
1460
  } else if (assistantText) {
1421
1461
  var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1422
1462
  if (item._isBgTask) okm.isBackgroundTask = true;
1423
1463
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1464
+ if (replyTs !== void 0) okm._ts = replyTs;
1424
1465
  mapped.push(okm);
1425
1466
  }
1426
1467
  });
@@ -1833,7 +1874,7 @@ var ChatSession = class {
1833
1874
  var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1834
1875
  this.aiChatHistoryCache[key] = {
1835
1876
  messages: offExisting.messages.concat([
1836
- { role: "user", content: composed, _ownerKey: key },
1877
+ { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() },
1837
1878
  { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1838
1879
  ]),
1839
1880
  endOfList: offExisting.endOfList,
@@ -1865,7 +1906,7 @@ var ChatSession = class {
1865
1906
  serviceId: id.serviceId,
1866
1907
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1867
1908
  });
1868
- var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
1909
+ var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
1869
1910
  if (key) queuedBubble._ownerKey = key;
1870
1911
  if (useBgQueue) queuedBubble._useBgQueue = true;
1871
1912
  this.state.messages.push(queuedBubble);
@@ -1900,7 +1941,7 @@ var ChatSession = class {
1900
1941
  });
1901
1942
  return;
1902
1943
  }
1903
- this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
1944
+ this.state.messages.push({ role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} });
1904
1945
  this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1905
1946
  this.host.notify();
1906
1947
  this.updateHistoryCache();
@@ -1957,6 +1998,7 @@ var ChatSession = class {
1957
1998
  var existing = this.state.messages[nextIdx];
1958
1999
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1959
2000
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
2001
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1960
2002
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1961
2003
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1962
2004
  this.state.messages[nextIdx] = promoted;
@@ -1978,6 +2020,7 @@ var ChatSession = class {
1978
2020
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1979
2021
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1980
2022
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
2023
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1981
2024
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1982
2025
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1983
2026
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
@@ -2027,6 +2070,7 @@ var ChatSession = class {
2027
2070
  var repl = { role: "user", content: exist.content };
2028
2071
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
2029
2072
  if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
2073
+ if (exist._ts !== void 0) repl._ts = exist._ts;
2030
2074
  this.state.messages[userIdx] = repl;
2031
2075
  }
2032
2076
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -2035,6 +2079,7 @@ var ChatSession = class {
2035
2079
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
2036
2080
  }
2037
2081
  insertAtTarget(msg, targetIdx) {
2082
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
2038
2083
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) this.state.messages[targetIdx] = msg;
2039
2084
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
2040
2085
  else this.state.messages.push(msg);
@@ -2371,6 +2416,8 @@ var ChatSession = class {
2371
2416
  }
2372
2417
  enqueueTypewrite(idx, fullText, localId) {
2373
2418
  var self = this;
2419
+ var target = this.state.messages[idx];
2420
+ if (target && target._ts === void 0) target._ts = wallClockNow();
2374
2421
  this.typewriterQueue = this.typewriterQueue.then(function() {
2375
2422
  return self.typewriteIntoIndex(idx, fullText, localId);
2376
2423
  });
@@ -2440,6 +2487,7 @@ var ChatSession = class {
2440
2487
  var u = this.state.messages[uIdx];
2441
2488
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
2442
2489
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2490
+ if (u._ts !== void 0) cleaned._ts = u._ts;
2443
2491
  if (u._indexFile) cleaned._indexFile = u._indexFile;
2444
2492
  this.state.messages[uIdx] = cleaned;
2445
2493
  }
@@ -2512,6 +2560,7 @@ var ChatSession = class {
2512
2560
  var ex = this.state.messages[userIdx];
2513
2561
  var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2514
2562
  if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2563
+ if (ex._ts !== void 0) settledUser._ts = ex._ts;
2515
2564
  if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2516
2565
  if (ex._useBgQueue) settledUser._useBgQueue = true;
2517
2566
  this.state.messages[userIdx] = settledUser;
@@ -3288,7 +3337,10 @@ function buildChatDisplayList(messages, opts) {
3288
3337
  cancellableIds: [],
3289
3338
  cancelling: false,
3290
3339
  mayHaveOlder: false,
3291
- anchorIndex: i
3340
+ // The run's first loaded pass, and never re-stamped: see the file
3341
+ // docstring. `anchorId` is filled in once every member is known.
3342
+ anchorIndex: i,
3343
+ anchorId: ""
3292
3344
  };
3293
3345
  order.push(runId);
3294
3346
  }
@@ -3302,7 +3354,6 @@ function buildChatDisplayList(messages, opts) {
3302
3354
  g.passCount++;
3303
3355
  }
3304
3356
  g.members.push({ msg, index: i });
3305
- g.anchorIndex = i;
3306
3357
  runOfIndex[i] = runId;
3307
3358
  if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3308
3359
  if (ref && ref.name) keyByName[ref.name] = g.key;
@@ -3364,6 +3415,9 @@ function buildChatDisplayList(messages, opts) {
3364
3415
  }
3365
3416
  }
3366
3417
  grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3418
+ var anchor = grp.members[0];
3419
+ grp.anchorIndex = anchor.index;
3420
+ grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
3367
3421
  }
3368
3422
  var out = [];
3369
3423
  for (var j = 0; j < list.length; j++) {
@@ -3430,6 +3484,7 @@ exports.extractRemotePathFromAttachmentHref = extractRemotePathFromAttachmentHre
3430
3484
  exports.fillHistoryViewport = fillHistoryViewport;
3431
3485
  exports.filterListByClearHorizon = filterListByClearHorizon;
3432
3486
  exports.findAttachmentParser = findAttachmentParser;
3487
+ exports.formatChatTimestamp = formatChatTimestamp;
3433
3488
  exports.getAttachmentParsers = getAttachmentParsers;
3434
3489
  exports.getChatHistory = getChatHistory;
3435
3490
  exports.getContextWindow = getContextWindow;
@@ -3456,6 +3511,7 @@ exports.parseAttachmentContent = parseAttachmentContent;
3456
3511
  exports.parseIndexingLabel = parseIndexingLabel;
3457
3512
  exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
3458
3513
  exports.registerAttachmentParser = registerAttachmentParser;
3514
+ exports.repairUrlEntities = repairUrlEntities;
3459
3515
  exports.repairUrlWhitespace = repairUrlWhitespace;
3460
3516
  exports.safeDecodeURIComponent = safeDecodeURIComponent;
3461
3517
  exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
@@ -3463,5 +3519,6 @@ exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;
3463
3519
  exports.transformContentWithImages = transformContentWithImages;
3464
3520
  exports.transformContentWithOpenAIImages = transformContentWithOpenAIImages;
3465
3521
  exports.truncateLabelForDisplay = truncateLabelForDisplay;
3522
+ exports.wallClockNow = wallClockNow;
3466
3523
  //# sourceMappingURL=engine.cjs.map
3467
3524
  //# sourceMappingURL=engine.cjs.map