@khorsheed/dsh-message-timeline 0.2.3 → 0.2.5
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/CHANGELOG.md +14 -0
- package/README.en.md +16 -12
- package/README.i18n.yaml +2 -2
- package/README.md +16 -12
- package/lib/client.js +305 -46
- package/lib/types/client/TimelineRail.d.ts +1 -1
- package/lib/types/client/TimelineRail.js +179 -33
- package/lib/types/client/config.d.ts +5 -2
- package/lib/types/client/index.d.ts +9 -5
- package/lib/types/client/index.js +5 -1
- package/lib/types/client/locales.d.ts +6 -0
- package/lib/types/client/locales.js +8 -2
- package/lib/types/client/outline.d.ts +12 -0
- package/lib/types/client/outline.js +40 -0
- package/lib/types/client/rail-tracker.d.ts +5 -2
- package/lib/types/client/rail-tracker.js +1 -4
- package/lib/types/client/rows.d.ts +77 -0
- package/lib/types/client/rows.js +86 -0
- package/lib/types/client/slots.d.ts +17 -4
- package/lib/types/client/timeline-kinds.d.ts +7 -2
- package/lib/types/client/timeline-kinds.js +8 -3
- package/package.json +16 -11
- package/lib/invariant.js +0 -28
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.js +0 -26
package/lib/client.js
CHANGED
|
@@ -33,23 +33,32 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
34
34
|
const zh = {
|
|
35
35
|
"rail.panel": "消息导览",
|
|
36
|
-
"rail.empty": "
|
|
36
|
+
"rail.empty": "无文本预览",
|
|
37
|
+
"rail.turn": "第 {turn} 轮",
|
|
38
|
+
"rail.image": "图片消息",
|
|
39
|
+
"rail.attachment": "附件消息"
|
|
37
40
|
};
|
|
38
41
|
/** English dictionary, checked complete against the zh key set. */
|
|
39
42
|
const en = {
|
|
40
43
|
"rail.panel": "Message timeline",
|
|
41
|
-
"rail.empty": "No
|
|
44
|
+
"rail.empty": "No text preview",
|
|
45
|
+
"rail.turn": "Turn {turn}",
|
|
46
|
+
"rail.image": "Image message",
|
|
47
|
+
"rail.attachment": "Attachment message"
|
|
42
48
|
};
|
|
43
49
|
/**
|
|
44
50
|
* Whether a node kind renders as a timeline row. The rail lists user messages
|
|
45
|
-
* (and optionally steering),
|
|
46
|
-
*
|
|
51
|
+
* (and optionally steering), the message-tools edited/restored bubbles that
|
|
52
|
+
* the host `order` does not always surface, and automated-turn trigger cards
|
|
53
|
+
* (whose turns have no human prompt but real transcript content). The same
|
|
54
|
+
* set drives the reading-position anchor, so landing on a trigger card keeps
|
|
55
|
+
* the selection on its turn instead of moving to the next user row.
|
|
47
56
|
* @param kind - the node's kind string.
|
|
48
57
|
* @param includeSteering - whether steering rows count as user messages.
|
|
49
58
|
* @returns true when the kind belongs on the timeline.
|
|
50
59
|
*/
|
|
51
60
|
function isTimelineRowKind(kind, includeSteering) {
|
|
52
|
-
return kind === "user" || includeSteering && kind === "steering" || kind === "message-tools-edited" || kind === "message-tools-restored";
|
|
61
|
+
return kind === "user" || includeSteering && kind === "steering" || kind === "message-tools-edited" || kind === "message-tools-restored" || kind === "turn-trigger";
|
|
53
62
|
}
|
|
54
63
|
/**
|
|
55
64
|
* Whether a node kind carries a hidden span (`{ hiddenStartSeq, seq }`) that
|
|
@@ -319,9 +328,7 @@ window.__ModuleLoader__.load({
|
|
|
319
328
|
};
|
|
320
329
|
}
|
|
321
330
|
},
|
|
322
|
-
jumpTo: (key) =>
|
|
323
|
-
if (scrollport !== null) jumpRow(scrollport, key);
|
|
324
|
-
},
|
|
331
|
+
jumpTo: (key) => scrollport !== null && jumpRow(scrollport, key),
|
|
325
332
|
dispose: () => {
|
|
326
333
|
stopList();
|
|
327
334
|
stopProvide?.();
|
|
@@ -352,6 +359,129 @@ window.__ModuleLoader__.load({
|
|
|
352
359
|
return props.useChat ?? EMPTY_HOOK;
|
|
353
360
|
}
|
|
354
361
|
//#endregion
|
|
362
|
+
//#region src/client/outline.ts
|
|
363
|
+
/**
|
|
364
|
+
* Narrow one wire projection value to outline entries. `turn` and `seq` are
|
|
365
|
+
* load-bearing — a row cannot exist or jump without them — so damage there
|
|
366
|
+
* drops the entry; the previews are decorative and degrade to ''. A non-array
|
|
367
|
+
* value (absent unit included) narrows to undefined.
|
|
368
|
+
* @param value - the raw `useProjection('turnOutline')` read.
|
|
369
|
+
* @returns the surviving entries in wire order, or undefined when absent.
|
|
370
|
+
*/
|
|
371
|
+
function outlineEntries(value) {
|
|
372
|
+
if (!Array.isArray(value)) return void 0;
|
|
373
|
+
const entries = [];
|
|
374
|
+
for (const raw of value) {
|
|
375
|
+
const entry = outlineEntry(raw);
|
|
376
|
+
if (entry !== void 0) entries.push(entry);
|
|
377
|
+
}
|
|
378
|
+
return entries;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Narrow one wire entry. `turn` and `seq` must be non-negative safe integers
|
|
382
|
+
* (seq additionally rejects -0); the previews default to '' when malformed.
|
|
383
|
+
* @param value - one raw wire entry.
|
|
384
|
+
* @returns the entry, or undefined when a load-bearing field is damaged.
|
|
385
|
+
*/
|
|
386
|
+
function outlineEntry(value) {
|
|
387
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
388
|
+
const entry = value;
|
|
389
|
+
if (typeof entry.turn !== "number" || !Number.isSafeInteger(entry.turn) || entry.turn < 0) return void 0;
|
|
390
|
+
if (typeof entry.seq !== "number" || !Number.isSafeInteger(entry.seq) || entry.seq < 0 || Object.is(entry.seq, -0)) return void 0;
|
|
391
|
+
return {
|
|
392
|
+
turn: entry.turn,
|
|
393
|
+
seq: entry.seq,
|
|
394
|
+
prompt: typeof entry.prompt === "string" ? entry.prompt : "",
|
|
395
|
+
response: typeof entry.response === "string" ? entry.response : ""
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/client/rows.ts
|
|
400
|
+
/**
|
|
401
|
+
* The first visible loaded row of a turn, of any node kind: the landing
|
|
402
|
+
* target for a turn with no user-kind row (an automated turn renders a
|
|
403
|
+
* `turn-trigger` card, not a user bubble), mirroring the official rail's
|
|
404
|
+
* `user ?? loaded[0]` anchor. Only nodes in the host's visible `order`
|
|
405
|
+
* qualify — a store-only or hidden node is not a jump target.
|
|
406
|
+
* @param order - visible chat node keys in render order.
|
|
407
|
+
* @param nodes - the live node store.
|
|
408
|
+
* @param seq - the turn's `turn/start` seq (inclusive).
|
|
409
|
+
* @param end - the next turn's start seq (exclusive), or Infinity for the last turn.
|
|
410
|
+
* @returns the anchor key, or null when the turn has no visible loaded row.
|
|
411
|
+
*/
|
|
412
|
+
function firstVisibleKeyInTurn(order, nodes, seq, end) {
|
|
413
|
+
for (const key of order) {
|
|
414
|
+
const node = nodes.get(key);
|
|
415
|
+
if (node === void 0) continue;
|
|
416
|
+
if (node.anchorSeq < seq) continue;
|
|
417
|
+
if (node.anchorSeq >= end) break;
|
|
418
|
+
if (node.visibility === "hidden") continue;
|
|
419
|
+
return key;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Wrap one loaded item as a rail row.
|
|
425
|
+
* @param item - the window row.
|
|
426
|
+
* @param turn - its owning outline turn, or undefined.
|
|
427
|
+
* @returns the loaded rail row.
|
|
428
|
+
*/
|
|
429
|
+
function toLoadedRow(item, turn) {
|
|
430
|
+
return {
|
|
431
|
+
kind: "loaded",
|
|
432
|
+
id: item.key,
|
|
433
|
+
key: item.key,
|
|
434
|
+
node: item.node,
|
|
435
|
+
anchorSeq: item.node.anchorSeq,
|
|
436
|
+
turn
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Merge loaded rows with the outline into the full row list. Both inputs are
|
|
441
|
+
* transcript-ordered (loaded rows anchorSeq-ascending, the outline strictly
|
|
442
|
+
* increasing by turn), so one pass interleaves them without a final sort: an
|
|
443
|
+
* outline entry whose seq range holds at least one loaded row is covered and
|
|
444
|
+
* emits nothing; every other entry emits its outline row at the range's
|
|
445
|
+
* position. A loaded row belongs to the last entry whose start it follows —
|
|
446
|
+
* when the window runs ahead of the projection feed, the newest rows read as
|
|
447
|
+
* the last named turn until the feed catches up (suppressing its outline row
|
|
448
|
+
* beats duplicating the turn).
|
|
449
|
+
* @param loaded - the window's user rows, anchorSeq-ascending.
|
|
450
|
+
* @param outline - narrowed outline entries, or undefined (capability absent).
|
|
451
|
+
* @returns the merged row list, transcript-ordered.
|
|
452
|
+
*/
|
|
453
|
+
function mergeRows(loaded, outline) {
|
|
454
|
+
if (outline === void 0) return loaded.map((item) => toLoadedRow(item, void 0));
|
|
455
|
+
const rows = [];
|
|
456
|
+
let cursor = 0;
|
|
457
|
+
for (const [index, entry] of outline.entries()) {
|
|
458
|
+
const end = outline.at(index + 1)?.seq ?? Number.POSITIVE_INFINITY;
|
|
459
|
+
let covered = false;
|
|
460
|
+
for (;;) {
|
|
461
|
+
const item = loaded[cursor];
|
|
462
|
+
if (item === void 0 || item.node.anchorSeq >= end) break;
|
|
463
|
+
rows.push(toLoadedRow(item, item.node.anchorSeq >= entry.seq ? entry.turn : void 0));
|
|
464
|
+
covered = covered || item.node.anchorSeq >= entry.seq;
|
|
465
|
+
cursor += 1;
|
|
466
|
+
}
|
|
467
|
+
if (!covered) rows.push({
|
|
468
|
+
kind: "outline",
|
|
469
|
+
id: `turn:${entry.turn}`,
|
|
470
|
+
turn: entry.turn,
|
|
471
|
+
seq: entry.seq,
|
|
472
|
+
anchorSeq: entry.seq,
|
|
473
|
+
prompt: entry.prompt
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
for (;;) {
|
|
477
|
+
const item = loaded[cursor];
|
|
478
|
+
if (item === void 0) break;
|
|
479
|
+
rows.push(toLoadedRow(item, void 0));
|
|
480
|
+
cursor += 1;
|
|
481
|
+
}
|
|
482
|
+
return rows;
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
|
355
485
|
//#region src/client/preview.ts
|
|
356
486
|
/**
|
|
357
487
|
* Join the text blocks of one message into a single preview string.
|
|
@@ -402,7 +532,7 @@ window.__ModuleLoader__.load({
|
|
|
402
532
|
}
|
|
403
533
|
//#endregion
|
|
404
534
|
//#region \0dsh-css:src/client/TimelineRail.module.css.mjs
|
|
405
|
-
const css = ".
|
|
535
|
+
const css = "._8u17xW_panel{scrollbar-width:none;z-index:100;pointer-events:none;flex-direction:column;padding:4px 8px;display:flex;position:fixed;overflow-y:auto}._8u17xW_panel:before,._8u17xW_panel:after{content:\"\";margin:auto}._8u17xW_panel::-webkit-scrollbar{display:none}._8u17xW_item{width:100%;max-width:20px;color:var(--dsw-alias-label-tertiary);text-align:left;cursor:pointer;-webkit-user-select:none;user-select:none;pointer-events:auto;opacity:.55;background:0 0;border:none;flex:none;align-items:center;gap:8px;padding:5px 0;font-size:13px;line-height:1.5;transition:opacity .12s;display:flex}._8u17xW_panel:hover ._8u17xW_item,._8u17xW_panel:focus-within ._8u17xW_item{opacity:1;max-width:100%}._8u17xW_tick{background:var(--dsw-alias-label-tertiary);border-radius:2px;flex:none;width:3px;height:14px}._8u17xW_itemCurrent{color:var(--dsw-static-deepseek-500);opacity:1;font-weight:600}._8u17xW_tickCurrent{background:var(--dsw-static-deepseek-500);height:18px}._8u17xW_itemText{white-space:nowrap;text-overflow:ellipsis;opacity:0;transition:opacity .12s;overflow:hidden}._8u17xW_panel:hover ._8u17xW_itemText,._8u17xW_panel:focus-within ._8u17xW_itemText{opacity:1;transition-delay:90ms}._8u17xW_itemFocused{color:var(--dsw-alias-label-primary);font-weight:600}._8u17xW_item:focus:not(:focus-visible){outline:none}._8u17xW_item:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:1px}._8u17xW_itemPending ._8u17xW_tick{background:var(--dsw-static-deepseek-400);animation:1s ease-in-out infinite _8u17xW_railPending}@keyframes _8u17xW_railPending{50%{opacity:.2}}@media (prefers-reduced-motion:reduce){._8u17xW_itemPending ._8u17xW_tick{animation:none}}";
|
|
406
536
|
const tagId = "@khorsheed/dsh-message-timeline/TimelineRail.module.css";
|
|
407
537
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
408
538
|
const tag = document.createElement("style");
|
|
@@ -412,13 +542,15 @@ window.__ModuleLoader__.load({
|
|
|
412
542
|
document.head.appendChild(tag);
|
|
413
543
|
}
|
|
414
544
|
var TimelineRail_module_css_default = {
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
"
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"
|
|
545
|
+
"itemText": "_8u17xW_itemText",
|
|
546
|
+
"railPending": "_8u17xW_railPending",
|
|
547
|
+
"panel": "_8u17xW_panel",
|
|
548
|
+
"tickCurrent": "_8u17xW_tickCurrent",
|
|
549
|
+
"item": "_8u17xW_item",
|
|
550
|
+
"itemCurrent": "_8u17xW_itemCurrent",
|
|
551
|
+
"itemPending": "_8u17xW_itemPending",
|
|
552
|
+
"tick": "_8u17xW_tick",
|
|
553
|
+
"itemFocused": "_8u17xW_itemFocused"
|
|
422
554
|
};
|
|
423
555
|
//#endregion
|
|
424
556
|
//#region src/client/TimelineRail.tsx
|
|
@@ -426,20 +558,52 @@ window.__ModuleLoader__.load({
|
|
|
426
558
|
* Message timeline panel, browser half. One entry in the official
|
|
427
559
|
* conversation.session.header.utilities seat anchors the plugin into the
|
|
428
560
|
* session scope and renders, through a body portal, the flat floating
|
|
429
|
-
* timeline over the left edge of the chat scrollport: one row per
|
|
430
|
-
*
|
|
561
|
+
* timeline over the left edge of the chat scrollport: one row per user
|
|
562
|
+
* message — a tick plus an ellipsized one-line preview, no frame and no
|
|
431
563
|
* visible scrollbar. At rest only the dimmed ticks show, the reading
|
|
432
564
|
* position's tick in blue (the latest message until the tracker answers);
|
|
433
565
|
* hovering or keyboard-focusing the panel reveals the row texts with the
|
|
434
566
|
* blue row on top, and clicking a row jumps the transcript to that message.
|
|
435
567
|
* The panel is always on while the chat view shows; the `enabled` config is
|
|
436
568
|
* the off switch.
|
|
569
|
+
*
|
|
570
|
+
* The row list is whole-session from the moment the session opens: the host
|
|
571
|
+
* `turnOutline` projection names every started turn, and the loaded chat
|
|
572
|
+
* window overlays richer previews (plus steering rows and message-tools
|
|
573
|
+
* bubbles) for the turns it covers. A turn outside the window renders as a
|
|
574
|
+
* dimmed outline row; confirming it pages history back through the turn's
|
|
575
|
+
* `turn/start` seq (the session jump loader) and lands on the freshly loaded
|
|
576
|
+
* row. When the host serves no turnOutline value the panel degrades to the
|
|
577
|
+
* loaded window alone, paged by prefetch and panel-top scrolls.
|
|
437
578
|
*/
|
|
438
579
|
/** Extract the preview blocks of one user/steering node (kind-checked by the caller). */
|
|
439
580
|
function nodeContent(node) {
|
|
440
581
|
return node.data.content ?? [];
|
|
441
582
|
}
|
|
442
583
|
/**
|
|
584
|
+
* One row's display text: the loaded row's joined text blocks, or the outline
|
|
585
|
+
* entry's bounded first-prompt preview. `null` means the message carries no
|
|
586
|
+
* text (an image/attachment-only input, or an automated turn with no human
|
|
587
|
+
* prompt) — the row renders the `rail.empty` label instead.
|
|
588
|
+
*/
|
|
589
|
+
function rowPreview(row) {
|
|
590
|
+
return row.kind === "loaded" ? previewText(nodeContent(row.node)) : row.prompt !== "" ? row.prompt : null;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* The row's display label: the text preview when there is one, else a
|
|
594
|
+
* kind-aware fallback — the turn number for an outline-only turn, and an
|
|
595
|
+
* image/attachment label for a loaded message with no text blocks.
|
|
596
|
+
*/
|
|
597
|
+
function rowLabel(row, t) {
|
|
598
|
+
const preview = rowPreview(row);
|
|
599
|
+
if (preview !== null) return preview;
|
|
600
|
+
if (row.kind === "outline") return t("rail.turn", { turn: row.turn });
|
|
601
|
+
const types = new Set(nodeContent(row.node).map((block) => block.type));
|
|
602
|
+
if (types.has("image")) return t("rail.image");
|
|
603
|
+
if (types.has("file")) return t("rail.attachment");
|
|
604
|
+
return t("rail.empty");
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
443
607
|
* Horizontal panel padding, matching `.panel`'s `padding: 4px 8px`: the text
|
|
444
608
|
* band sits this far inside the box, so the width budget must give it back
|
|
445
609
|
* for {@link PANEL_GAP} to be the visible gap to the message flow.
|
|
@@ -450,11 +614,17 @@ window.__ModuleLoader__.load({
|
|
|
450
614
|
/** Degraded width cap (fraction of the scrollport) while the flow probe is unanswered. */
|
|
451
615
|
const DEGRADED_WIDTH_RATIO = .4;
|
|
452
616
|
/**
|
|
617
|
+
* Frames an outline jump retries the DOM scroll after its row materializes in
|
|
618
|
+
* the chat store: the transcript renders the new page in the same commit or a
|
|
619
|
+
* near frame, and a half-second budget covers the skew without hanging.
|
|
620
|
+
*/
|
|
621
|
+
const JUMP_RETRY_FRAMES = 30;
|
|
622
|
+
/**
|
|
453
623
|
* The header-utilities entry: the portal timeline panel.
|
|
454
624
|
* @param props - composed props (see {@link TimelineRailProps}).
|
|
455
625
|
* @returns nothing visible in the seat; the floating panel while the chat view shows.
|
|
456
626
|
*/
|
|
457
|
-
function TimelineRail({ sessionId, includeSteering, panelWidth, initialPages, loadOlder, jumpTo, useRail, t, ...standard }) {
|
|
627
|
+
function TimelineRail({ sessionId, includeSteering, panelWidth, initialPages, loadOlder, loadThrough, jumpTo, useRail, t, ...standard }) {
|
|
458
628
|
const rail = useRail((s) => s);
|
|
459
629
|
const useChatSlice = chatHookOf(standard);
|
|
460
630
|
const useSession = standard.useSession;
|
|
@@ -462,6 +632,8 @@ window.__ModuleLoader__.load({
|
|
|
462
632
|
const nodes = useChatSlice((c) => c.nodes);
|
|
463
633
|
const hasMore = useSession((s) => s.hasMore);
|
|
464
634
|
const loadingOlder = useSession((s) => s.loadingOlder);
|
|
635
|
+
const outlineValue = standard.useProjection?.("turnOutline");
|
|
636
|
+
const outline = (0, react.useMemo)(() => outlineEntries(outlineValue), [outlineValue]);
|
|
465
637
|
const items = (0, react.useMemo)(() => {
|
|
466
638
|
let store = [];
|
|
467
639
|
let spans = [];
|
|
@@ -470,14 +642,14 @@ window.__ModuleLoader__.load({
|
|
|
470
642
|
spans = foldHiddenSpans(store);
|
|
471
643
|
} catch {}
|
|
472
644
|
const hidden = (node) => isSeqHidden(spans, node.anchorSeq);
|
|
473
|
-
const
|
|
645
|
+
const loaded = [];
|
|
474
646
|
const seen = /* @__PURE__ */ new Set();
|
|
475
647
|
for (const key of order) {
|
|
476
648
|
const node = nodes.get(key);
|
|
477
649
|
if (node === void 0) continue;
|
|
478
650
|
if (!isTimelineRowKind(node.kind, includeSteering)) continue;
|
|
479
651
|
if (hidden(node)) continue;
|
|
480
|
-
|
|
652
|
+
loaded.push({
|
|
481
653
|
key,
|
|
482
654
|
node
|
|
483
655
|
});
|
|
@@ -488,22 +660,89 @@ window.__ModuleLoader__.load({
|
|
|
488
660
|
if (node.visibility === "hidden") continue;
|
|
489
661
|
if (seen.has(node.key)) continue;
|
|
490
662
|
if (hidden(node)) continue;
|
|
491
|
-
|
|
663
|
+
loaded.push({
|
|
492
664
|
key: node.key,
|
|
493
665
|
node
|
|
494
666
|
});
|
|
495
667
|
seen.add(node.key);
|
|
496
668
|
}
|
|
497
|
-
|
|
498
|
-
return
|
|
669
|
+
loaded.sort((left, right) => left.node.anchorSeq - right.node.anchorSeq);
|
|
670
|
+
return mergeRows(loaded, outline);
|
|
499
671
|
}, [
|
|
500
672
|
order,
|
|
501
673
|
nodes,
|
|
502
|
-
includeSteering
|
|
674
|
+
includeSteering,
|
|
675
|
+
outline
|
|
503
676
|
]);
|
|
504
677
|
const [focusKey, setFocusKey] = (0, react.useState)(null);
|
|
505
|
-
const current = focusKey ?? rail.activeKey ?? items.at(-1)?.
|
|
678
|
+
const current = focusKey ?? rail.activeKey ?? items.at(-1)?.id ?? null;
|
|
506
679
|
const panelRef = (0, react.useRef)(null);
|
|
680
|
+
const [pendingJump, setPendingJump] = (0, react.useState)(null);
|
|
681
|
+
(0, react.useEffect)(() => {
|
|
682
|
+
setPendingJump(null);
|
|
683
|
+
}, [sessionId]);
|
|
684
|
+
const firstSeq = (0, react.useMemo)(() => {
|
|
685
|
+
const firstKey = order[0];
|
|
686
|
+
if (firstKey === void 0) return null;
|
|
687
|
+
return nodes.get(firstKey)?.anchorSeq ?? null;
|
|
688
|
+
}, [order, nodes]);
|
|
689
|
+
(0, react.useEffect)(() => {
|
|
690
|
+
if (pendingJump === null || pendingJump.sessionId !== sessionId) return;
|
|
691
|
+
const landOn = (key) => {
|
|
692
|
+
let cancelled = false;
|
|
693
|
+
let frames = 0;
|
|
694
|
+
const attempt = () => {
|
|
695
|
+
if (cancelled) return;
|
|
696
|
+
frames += 1;
|
|
697
|
+
if (jumpTo(key) || frames >= JUMP_RETRY_FRAMES) {
|
|
698
|
+
setPendingJump(null);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
(typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout)(attempt);
|
|
702
|
+
};
|
|
703
|
+
attempt();
|
|
704
|
+
return () => {
|
|
705
|
+
cancelled = true;
|
|
706
|
+
};
|
|
707
|
+
};
|
|
708
|
+
const landed = items.find((row) => row.kind === "loaded" && row.turn === pendingJump.turn);
|
|
709
|
+
if (landed !== void 0) return landOn(landed.key);
|
|
710
|
+
const end = outline?.find((entry) => entry.turn > pendingJump.turn)?.seq ?? Number.POSITIVE_INFINITY;
|
|
711
|
+
const anyKey = firstVisibleKeyInTurn(order, nodes, pendingJump.seq, end);
|
|
712
|
+
if (anyKey !== null) return landOn(anyKey);
|
|
713
|
+
if (pendingJump.awaiting || loadingOlder) return;
|
|
714
|
+
if ((firstSeq === null || firstSeq > pendingJump.seq) && hasMore && (!pendingJump.requested || pendingJump.fromHead !== firstSeq)) {
|
|
715
|
+
const jump = pendingJump;
|
|
716
|
+
setPendingJump({
|
|
717
|
+
...jump,
|
|
718
|
+
requested: true,
|
|
719
|
+
awaiting: true,
|
|
720
|
+
fromHead: firstSeq
|
|
721
|
+
});
|
|
722
|
+
loadThrough(jump.seq).then(() => {
|
|
723
|
+
setPendingJump((current) => current !== null && current.sessionId === jump.sessionId && current.turn === jump.turn ? {
|
|
724
|
+
...current,
|
|
725
|
+
awaiting: false
|
|
726
|
+
} : current);
|
|
727
|
+
});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const fallback = items.find((row) => row.kind === "loaded" && row.anchorSeq >= pendingJump.seq);
|
|
731
|
+
if (fallback !== void 0) jumpTo(fallback.key);
|
|
732
|
+
setPendingJump(null);
|
|
733
|
+
}, [
|
|
734
|
+
items,
|
|
735
|
+
pendingJump,
|
|
736
|
+
sessionId,
|
|
737
|
+
loadingOlder,
|
|
738
|
+
hasMore,
|
|
739
|
+
firstSeq,
|
|
740
|
+
jumpTo,
|
|
741
|
+
loadThrough,
|
|
742
|
+
order,
|
|
743
|
+
nodes,
|
|
744
|
+
outline
|
|
745
|
+
]);
|
|
507
746
|
const active = rail.chatView && rail.ready && rail.sessionId === sessionId;
|
|
508
747
|
const visible = active && items.length > 0;
|
|
509
748
|
const gutter = rail.flowLeft === null ? null : rail.flowLeft - rail.left - PANEL_PADDING_X - PANEL_GAP;
|
|
@@ -518,6 +757,7 @@ window.__ModuleLoader__.load({
|
|
|
518
757
|
prefetchedPagesRef.current = 0;
|
|
519
758
|
return;
|
|
520
759
|
}
|
|
760
|
+
if (outline !== void 0) return;
|
|
521
761
|
if (!hasMore || loadingOlder) return;
|
|
522
762
|
if (items.length > 0 && prefetchedPagesRef.current >= initialPages) return;
|
|
523
763
|
prefetchedPagesRef.current += 1;
|
|
@@ -528,15 +768,29 @@ window.__ModuleLoader__.load({
|
|
|
528
768
|
loadingOlder,
|
|
529
769
|
items.length,
|
|
530
770
|
initialPages,
|
|
531
|
-
loadOlder
|
|
771
|
+
loadOlder,
|
|
772
|
+
outline
|
|
532
773
|
]);
|
|
533
774
|
const onPanelScroll = (event) => {
|
|
775
|
+
if (outline !== void 0) return;
|
|
534
776
|
if (!hasMore || loadingOlder) return;
|
|
535
777
|
if (event.currentTarget.scrollTop <= 8) loadOlder();
|
|
536
778
|
};
|
|
537
|
-
const confirm = (
|
|
779
|
+
const confirm = (row) => {
|
|
538
780
|
setFocusKey(null);
|
|
539
|
-
|
|
781
|
+
if (row.kind === "loaded") {
|
|
782
|
+
setPendingJump(null);
|
|
783
|
+
jumpTo(row.key);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
setPendingJump({
|
|
787
|
+
sessionId,
|
|
788
|
+
turn: row.turn,
|
|
789
|
+
seq: row.seq,
|
|
790
|
+
requested: false,
|
|
791
|
+
awaiting: false,
|
|
792
|
+
fromHead: null
|
|
793
|
+
});
|
|
540
794
|
};
|
|
541
795
|
if (!visible || tooNarrow) return null;
|
|
542
796
|
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -556,44 +810,48 @@ window.__ModuleLoader__.load({
|
|
|
556
810
|
onKeyDown: (event) => {
|
|
557
811
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
558
812
|
event.preventDefault();
|
|
559
|
-
const anchor = items.findIndex((item) => item.
|
|
813
|
+
const anchor = items.findIndex((item) => item.id === current);
|
|
560
814
|
const next = event.key === "ArrowDown" ? items[Math.min(anchor + 1, items.length - 1)] : items[Math.max(anchor - 1, 0)];
|
|
561
815
|
/* v8 ignore next -- a non-empty item list keeps the clamped index inside bounds */
|
|
562
|
-
if (next !== void 0) setFocusKey(next.
|
|
816
|
+
if (next !== void 0) setFocusKey(next.id);
|
|
563
817
|
} else if (event.key === "Enter" || event.key === " ") {
|
|
564
818
|
event.preventDefault();
|
|
565
|
-
|
|
566
|
-
if (
|
|
819
|
+
const row = items.find((item) => item.id === current);
|
|
820
|
+
if (row !== void 0) confirm(row);
|
|
567
821
|
}
|
|
568
822
|
},
|
|
569
823
|
onBlur: () => {
|
|
570
824
|
setFocusKey(null);
|
|
571
825
|
},
|
|
572
|
-
children: items.map((
|
|
573
|
-
const isCurrent =
|
|
574
|
-
const
|
|
826
|
+
children: items.map((row) => {
|
|
827
|
+
const isCurrent = row.id === current;
|
|
828
|
+
const isFocused = row.id === focusKey;
|
|
829
|
+
const isPending = row.kind === "outline" && pendingJump !== null && pendingJump.turn === row.turn;
|
|
830
|
+
const className = isFocused ? `${TimelineRail_module_css_default.item} ${TimelineRail_module_css_default.itemFocused}` : isCurrent ? `${TimelineRail_module_css_default.item} ${TimelineRail_module_css_default.itemCurrent}` : TimelineRail_module_css_default.item;
|
|
831
|
+
const classNames = isPending ? `${className} ${TimelineRail_module_css_default.itemPending}` : className;
|
|
575
832
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
576
833
|
type: "button",
|
|
577
|
-
"data-item-key":
|
|
834
|
+
"data-item-key": row.id,
|
|
578
835
|
"aria-current": isCurrent || void 0,
|
|
579
|
-
|
|
836
|
+
"aria-busy": isPending || void 0,
|
|
837
|
+
className: classNames,
|
|
580
838
|
onClick: () => {
|
|
581
|
-
confirm(
|
|
839
|
+
confirm(row);
|
|
582
840
|
},
|
|
583
841
|
onMouseEnter: () => {
|
|
584
|
-
setFocusKey(
|
|
842
|
+
setFocusKey(row.id);
|
|
585
843
|
},
|
|
586
844
|
onMouseLeave: () => {
|
|
587
|
-
setFocusKey((key) => key ===
|
|
845
|
+
setFocusKey((key) => key === row.id ? null : key);
|
|
588
846
|
},
|
|
589
847
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
590
848
|
className: isCurrent ? `${TimelineRail_module_css_default.tick} ${TimelineRail_module_css_default.tickCurrent}` : TimelineRail_module_css_default.tick,
|
|
591
849
|
"aria-hidden": "true"
|
|
592
850
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
593
851
|
className: TimelineRail_module_css_default.itemText,
|
|
594
|
-
children:
|
|
852
|
+
children: rowLabel(row, t)
|
|
595
853
|
})]
|
|
596
|
-
},
|
|
854
|
+
}, row.id);
|
|
597
855
|
})
|
|
598
856
|
}), document.body);
|
|
599
857
|
}
|
|
@@ -636,14 +894,15 @@ window.__ModuleLoader__.load({
|
|
|
636
894
|
if (actx === void 0) throw new Error("message-timeline: session resolved no scope");
|
|
637
895
|
const conversation = actx.get("conversation");
|
|
638
896
|
if (conversation === void 0) throw new Error("message-timeline: conversation service unavailable");
|
|
897
|
+
const binding = ctx.sessions.binding(sessionId);
|
|
898
|
+
if (binding === void 0) throw new Error("message-timeline: session resolved no binding");
|
|
639
899
|
return {
|
|
640
900
|
includeSteering: options.includeSteering,
|
|
641
901
|
panelWidth: options.panelWidth,
|
|
642
902
|
initialPages: options.initialPages,
|
|
643
903
|
loadOlder: () => conversation.loadOlder(),
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
},
|
|
904
|
+
loadThrough: (seq) => binding.session.loadThrough(seq),
|
|
905
|
+
jumpTo: (key) => tracker.jumpTo(key),
|
|
647
906
|
hooks: { rail: tracker.state }
|
|
648
907
|
};
|
|
649
908
|
}
|
|
@@ -4,7 +4,7 @@ import type { TimelineRailProps } from './slots.ts';
|
|
|
4
4
|
* @param props - composed props (see {@link TimelineRailProps}).
|
|
5
5
|
* @returns nothing visible in the seat; the floating panel while the chat view shows.
|
|
6
6
|
*/
|
|
7
|
-
export declare function TimelineRail({ sessionId, includeSteering, panelWidth, initialPages, loadOlder, jumpTo, useRail, t, ...standard }: TimelineRailProps): import("react").ReactPortal | null;
|
|
7
|
+
export declare function TimelineRail({ sessionId, includeSteering, panelWidth, initialPages, loadOlder, loadThrough, jumpTo, useRail, t, ...standard }: TimelineRailProps): import("react").ReactPortal | null;
|
|
8
8
|
/** Memoized export for the slot machinery (stable component identity). */
|
|
9
9
|
export declare const TimelineRailEntry: import("react").MemoExoticComponent<typeof TimelineRail>;
|
|
10
10
|
//# sourceMappingURL=TimelineRail.d.ts.map
|