@khorsheed/dsh-message-timeline 0.1.0 → 0.2.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.
@@ -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({ useSession, sessionId, includeSteering, panelWidth, initialPages, loadOlder, jumpTo, useRail, t, }: TimelineRailProps): import("react").ReactPortal | null;
7
+ export declare function TimelineRail({ sessionId, includeSteering, panelWidth, initialPages, loadOlder, 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
@@ -1 +1 @@
1
- {"version":3,"file":"TimelineRail.d.ts","sourceRoot":"","sources":["../../../src/client/TimelineRail.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAgB,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAoBjE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EAAE,SAAS,EACrB,eAAe,EAAE,UAAU,EAAE,YAAY,EACzC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAC9B,EAAE,iBAAiB,sCA4InB;AAED,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,0DAAqB,CAAA"}
1
+ {"version":3,"file":"TimelineRail.d.ts","sourceRoot":"","sources":["../../../src/client/TimelineRail.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAgB,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAsBjE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,eAAe,EAAE,UAAU,EAAE,YAAY,EACzC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAC7B,GAAG,QAAQ,EACZ,EAAE,iBAAiB,sCAmMnB;AAED,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,0DAAqB,CAAA"}
@@ -15,7 +15,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  import { memo, useEffect, useMemo, useRef, useState } from 'react';
16
16
  import { createPortal } from 'react-dom';
17
17
  import { PANEL_WIDTH_MIN } from './config.js';
18
+ import { chatHookOf } from './chat-hook.js';
18
19
  import { previewText } from './preview.js';
20
+ import { foldHiddenSpans, isSeqHidden } from './hidden-spans.js';
21
+ import { isTimelineRowKind, EDIT_KIND, RESTORED_KIND } from './timeline-kinds.js';
19
22
  import css from './TimelineRail.module.css';
20
23
  /** Extract the preview blocks of one user/steering node (kind-checked by the caller). */
21
24
  function nodeContent(node) {
@@ -36,23 +39,84 @@ const DEGRADED_WIDTH_RATIO = 0.4;
36
39
  * @param props - composed props (see {@link TimelineRailProps}).
37
40
  * @returns nothing visible in the seat; the floating panel while the chat view shows.
38
41
  */
39
- export function TimelineRail({ useSession, sessionId, includeSteering, panelWidth, initialPages, loadOlder, jumpTo, useRail, t, }) {
42
+ export function TimelineRail({ sessionId, includeSteering, panelWidth, initialPages, loadOlder, jumpTo, useRail, t, ...standard }) {
40
43
  const rail = useRail(s => s);
41
- const order = useSession(s => s.chat.order);
42
- const nodes = useSession(s => s.chat.nodes);
44
+ // Chat data lives in the `useChat` standard prop on 0.1.2 (the rc-line
45
+ // session-snapshot seat is gone); the helper degrades to an empty slice
46
+ // when the seat is absent.
47
+ const useChatSlice = chatHookOf(standard);
48
+ const useSession = standard.useSession;
49
+ const order = useChatSlice(c => c.order);
50
+ const nodes = useChatSlice(c => c.nodes);
43
51
  const hasMore = useSession(s => s.hasMore);
44
52
  const loadingOlder = useSession(s => s.loadingOlder);
45
53
  const items = useMemo(() => {
54
+ // The node store is an in-memory snapshot; take it once so the span fold
55
+ // and the message-tools append read the same snapshot.
56
+ let store = [];
57
+ let spans = [];
58
+ try {
59
+ store = nodes.values();
60
+ spans = foldHiddenSpans(store);
61
+ }
62
+ catch {
63
+ // Degrade, don't explode: a failed store read leaves no spans and no
64
+ // appended bubbles, so the baseline order rows still render (a withdraw
65
+ // span reappears, and bubbles re-appear, after the store recovers).
66
+ }
67
+ // A message-tools withdraw (or an in-place edit) leaves the withdrawn
68
+ // originals in the store with a sibling `message-tools-withdrawn` /
69
+ // `message-tools-edited` node carrying the span they cover as
70
+ // `{ hiddenStartSeq, seq }`. Those originals can still surface in the
71
+ // host's `order`, but the DOM hider hides their transcript rows — a rail
72
+ // row for one is a dead row (clicking cannot scroll to it). Drop any such
73
+ // row whose anchorSeq sits inside a span, from either loop below. An
74
+ // ordinary session carries no span node, so the fold is empty and every
75
+ // order row still renders: this filter never alters the baseline.
76
+ const hidden = (node) => isSeqHidden(spans, node.anchorSeq);
46
77
  const result = [];
78
+ const seen = new Set();
47
79
  for (const key of order) {
48
80
  const node = nodes.get(key);
49
81
  if (node === undefined)
50
82
  continue;
51
- const kind = node.kind;
52
- if (kind !== 'user' && !(includeSteering && kind === 'steering'))
83
+ if (!isTimelineRowKind(node.kind, includeSteering))
84
+ continue;
85
+ if (hidden(node))
53
86
  continue;
54
87
  result.push({ key, node });
88
+ seen.add(key);
89
+ }
90
+ // After a message-tools in-place edit the replacement materializes as a
91
+ // `message-tools-edited` bubble (a restore as `message-tools-restored`)
92
+ // that the host's visible `order` does not always surface, even though it
93
+ // renders in the flow — an edit of the first message would drain the rail
94
+ // to zero rows. Append any such visible bubble the order omitted, provided
95
+ // it is not itself a withdrawn original (inside a span). Only these two
96
+ // bubble kinds are appended — a plain user/steering node not in `order`
97
+ // must never be resurrected as an orphan row. Normal sessions carry no
98
+ // such nodes, so this never alters the baseline.
99
+ for (const node of store) {
100
+ if (node.kind !== EDIT_KIND && node.kind !== RESTORED_KIND)
101
+ continue;
102
+ if (node.visibility === 'hidden')
103
+ continue;
104
+ if (seen.has(node.key))
105
+ continue;
106
+ if (hidden(node))
107
+ continue;
108
+ result.push({ key: node.key, node });
109
+ seen.add(node.key);
55
110
  }
111
+ // The order loop is seq-sorted, but the appended bubbles walked the store
112
+ // (whose iteration order is not seq-ordered), so a bubble for an OLDER
113
+ // message would otherwise land at the very end, past the newest row.
114
+ // Sort the combined rows by anchorSeq so every row — original or appended
115
+ // — sits at its true transcript position. JS sort is stable, and an
116
+ // ordinary session has no appended bubbles, so this never reorders the
117
+ // baseline (which is already seq-sorted). Dropped rows are gone before the
118
+ // sort, so a withdrawn original never resurfaces.
119
+ result.sort((left, right) => left.node.anchorSeq - right.node.anchorSeq);
56
120
  return result;
57
121
  }, [order, nodes, includeSteering]);
58
122
  // The lit row: the hover/arrow preselection while it moves, otherwise the
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Chat snapshot hook seat. Host 0.1.2 split the chat snapshot out of the
3
+ * session snapshot into the `useChat` session standard prop (ui-chat); its
4
+ * snapshot carries the fields the panel reads (`order`, `nodes`) at the top
5
+ * level. The rc-line `useSession(...).chat` seat went away with the baseline
6
+ * flip, so there is no second arm: the helper reads `useChat` and degrades to
7
+ * an empty slice (panel hidden, never a throw) when the prop is absent.
8
+ */
9
+ import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
10
+ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
11
+ /**
12
+ * The chat slice the panel's selectors read — the narrow face of ui-chat's
13
+ * ChatSnapshot the rail depends on (a structural subset, so the official
14
+ * `UseChat` hook is directly assignable).
15
+ */
16
+ export interface ChatSlice {
17
+ /** Visible chat node keys in render order. */
18
+ readonly order: readonly string[];
19
+ /** Live per-key node store. */
20
+ readonly nodes: {
21
+ get(key: string): ChatConversationViewNode | undefined;
22
+ values(): readonly ChatConversationViewNode[];
23
+ };
24
+ }
25
+ /** Selector hook over the chat slice. */
26
+ export type ChatSliceHook = SnapshotSelectorHook<ChatSlice>;
27
+ /**
28
+ * Pick the live chat hook for this entry.
29
+ * @param props - the entry's composed props (useChat present on 0.1.2 hosts).
30
+ * @returns the `useChat` prop, or the empty-slice hook when the seat is absent.
31
+ */
32
+ export declare function chatHookOf(props: {
33
+ useChat?: SnapshotSelectorHook<ChatSlice>;
34
+ }): ChatSliceHook;
35
+ //# sourceMappingURL=chat-hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-hook.d.ts","sourceRoot":"","sources":["../../../src/client/chat-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAA;AACtF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAE5E;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,EAAE;QACd,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,wBAAwB,GAAG,SAAS,CAAA;QACtD,MAAM,IAAI,SAAS,wBAAwB,EAAE,CAAA;KAC9C,CAAA;CACF;AAED,yCAAyC;AACzC,MAAM,MAAM,aAAa,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAA;AAW3D;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAAE,OAAO,CAAC,EAAE,oBAAoB,CAAC,SAAS,CAAC,CAAA;CAAE,GAAG,aAAa,CAE9F"}
@@ -0,0 +1,15 @@
1
+ /** Empty slice behind the absent-prop degrade: no rows, so the panel hides. */
2
+ const EMPTY_SLICE = {
3
+ order: [],
4
+ nodes: { get: () => undefined, values: () => [] },
5
+ };
6
+ /** Constant hook answering every selector from the empty slice. */
7
+ const EMPTY_HOOK = (selector) => selector(EMPTY_SLICE);
8
+ /**
9
+ * Pick the live chat hook for this entry.
10
+ * @param props - the entry's composed props (useChat present on 0.1.2 hosts).
11
+ * @returns the `useChat` prop, or the empty-slice hook when the seat is absent.
12
+ */
13
+ export function chatHookOf(props) {
14
+ return props.useChat ?? EMPTY_HOOK;
15
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Hidden-span folding for the rail. A message-tools withdraw (or an in-place
3
+ * edit) shadows a span of the transcript but leaves the withdrawn original
4
+ * rows in the node store — and, because the projection offers no suppression
5
+ * seam, a withdrawn user message stays visible in the host `order` while the
6
+ * sibling DOM hider hides its row by CSS. A rail row for such a node is a dead
7
+ * row: the transcript row it would target is `display:none`, so a click cannot
8
+ * scroll to it.
9
+ *
10
+ * The span a withdraw or edit covers is carried right on the store by the
11
+ * sibling `message-tools-withdrawn` / `message-tools-edited` node as
12
+ * `{ hiddenStartSeq, seq }` (its `anchorSeq` is `seq`, the exclusive end, so
13
+ * the divider/edited bubble itself is never inside its own span). Folding
14
+ * those spans — exactly the pair list message-tools' `foldHiddenRanges`
15
+ * builds — lets the rail skip any row whose `anchorSeq` falls inside one.
16
+ *
17
+ * This module reads only generic store node data (no import from message-tools
18
+ * and no dependence on its type map). An ordinary session carries no such
19
+ * node, so the fold is empty and every order row still renders: the fix never
20
+ * alters the baseline path.
21
+ */
22
+ import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
23
+ /**
24
+ * Fold the hidden seq spans out of the store into the flat
25
+ * `[start, endExclusive, ...]` pair list ordered by start — the same shape
26
+ * message-tools' renderers and DOM hider consume (`{ hiddenStartSeq, seq }`,
27
+ * inclusive start, exclusive end). A malformed carrier (missing or inverted
28
+ * bounds) is skipped rather than poisoning the fold.
29
+ * @param nodes - every materialized chat node (any order).
30
+ * @returns the flattened hidden spans; empty when no withdraw/edit landed.
31
+ */
32
+ export declare function foldHiddenSpans(nodes: readonly ChatConversationViewNode[]): number[];
33
+ /**
34
+ * Whether a node seq falls inside one of the folded hidden spans.
35
+ * @param spans - the flattened spans from {@link foldHiddenSpans}.
36
+ * @param seq - the node seq to test.
37
+ * @returns true when the seq is hidden (a withdrawn original).
38
+ */
39
+ export declare function isSeqHidden(spans: readonly number[], seq: number): boolean;
40
+ //# sourceMappingURL=hidden-spans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hidden-spans.d.ts","sourceRoot":"","sources":["../../../src/client/hidden-spans.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAA;AAStF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,wBAAwB,EAAE,GAAG,MAAM,EAAE,CAWpF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAO1E"}
@@ -0,0 +1,40 @@
1
+ import { isHiddenSpanCarrierKind } from './timeline-kinds.js';
2
+ /**
3
+ * Fold the hidden seq spans out of the store into the flat
4
+ * `[start, endExclusive, ...]` pair list ordered by start — the same shape
5
+ * message-tools' renderers and DOM hider consume (`{ hiddenStartSeq, seq }`,
6
+ * inclusive start, exclusive end). A malformed carrier (missing or inverted
7
+ * bounds) is skipped rather than poisoning the fold.
8
+ * @param nodes - every materialized chat node (any order).
9
+ * @returns the flattened hidden spans; empty when no withdraw/edit landed.
10
+ */
11
+ export function foldHiddenSpans(nodes) {
12
+ const pairs = [];
13
+ for (const node of nodes) {
14
+ if (!isHiddenSpanCarrierKind(node.kind))
15
+ continue;
16
+ const data = node.data;
17
+ if (data.hiddenStartSeq === undefined || data.seq === undefined)
18
+ continue;
19
+ if (data.hiddenStartSeq >= data.seq)
20
+ continue;
21
+ pairs.push([data.hiddenStartSeq, data.seq]);
22
+ }
23
+ pairs.sort((left, right) => left[0] - right[0]);
24
+ return pairs.flat();
25
+ }
26
+ /**
27
+ * Whether a node seq falls inside one of the folded hidden spans.
28
+ * @param spans - the flattened spans from {@link foldHiddenSpans}.
29
+ * @param seq - the node seq to test.
30
+ * @returns true when the seq is hidden (a withdrawn original).
31
+ */
32
+ export function isSeqHidden(spans, seq) {
33
+ for (let index = 0; index + 1 < spans.length; index += 2) {
34
+ const start = spans[index];
35
+ const end = spans[index + 1];
36
+ if (start !== undefined && end !== undefined && seq >= start && seq < end)
37
+ return true;
38
+ }
39
+ return false;
40
+ }
@@ -10,7 +10,7 @@
10
10
  * hidden panel when those attributes change — no official code is modified.
11
11
  * @module @khorsheed/dsh-message-timeline/client
12
12
  */
13
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
13
+ import type { Context } from '@deepseek-ai/cordis';
14
14
  import { type TimelineConfig } from './config.ts';
15
15
  export type { TimelineConfig } from './config.ts';
16
16
  export { resolveConfig } from './config.ts';
@@ -25,5 +25,5 @@ export declare const inject: string[];
25
25
  * @param ctx - client root context.
26
26
  * @param config - entry config; defaults apply when the runner passes none.
27
27
  */
28
- export declare function apply(ctx: ClientContext, config?: Partial<TimelineConfig>): void;
28
+ export declare function apply(ctx: Context, config?: Partial<TimelineConfig>): void;
29
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAM3E,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAA;AAMhE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAK1G,2EAA2E;AAC3E,eAAO,MAAM,MAAM,UAAkC,CAAA;AAErD;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CA+BhF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAclD,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAA;AAMhE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAK1G,2EAA2E;AAC3E,eAAO,MAAM,MAAM,UAAkC,CAAA;AAErD;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CA+B1E"}
@@ -12,15 +12,20 @@
12
12
  * (one console.warn), missing rows only clear the active marker and make
13
13
  * jumps no-ops. Nothing throws and no official code is modified.
14
14
  */
15
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
15
+ import type { Context } from '@deepseek-ai/cordis';
16
16
  import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
17
17
  import type { TimelineRailState } from './slots.ts';
18
18
  /**
19
19
  * Measure the panel's viewport box from one scrollport: its rect inset by the
20
- * panel padding, minus the sticky composer seat at the bottom and the
21
- * conversation tab strip at the top. The tabs render just above the
22
- * scrollport, but centering reads against the whole window, so the strip
23
- * height leaves the box either way — otherwise the list sits visibly high.
20
+ * panel padding, minus the chat input card at the bottom and the conversation
21
+ * tab strip at the top. The tabs render just above the scrollport, but
22
+ * centering reads against the whole window, so the strip height leaves the
23
+ * box either way — otherwise the list sits visibly high.
24
+ * The bottom ends at the `[data-composer-card]` top — the chat box — NOT the
25
+ * whole `[data-composer-seat]` top: dock cards (goal/todo/queue) sit above
26
+ * the input inside the seat, and counting them would push the timeline up off
27
+ * the conversation. Falls back to the seat top, then the column bottom, when
28
+ * the markers are absent.
24
29
  * The scrollport's own width rides along for the width-cap fallback when the
25
30
  * message-flow probe is unanswered.
26
31
  * @param scrollport - the official conversation scrollport element.
@@ -35,12 +40,19 @@ export declare function measureGeometry(scrollport: HTMLElement): {
35
40
  } | null;
36
41
  /**
37
42
  * The viewport x of the message flow's left edge: the left of the first
38
- * rendered `[data-chat-flow-kind]` row, which sits flush inside the official
39
- * centered content column (max 748px, `margin: 0 auto`). Every flow row
40
- * shares that edge, so the first one found suffices. The panel's right edge
41
- * stays left of it — the panel may only occupy the scrollport's left gutter.
43
+ * laid-out `[data-chat-flow-kind]` row, which sits flush inside the official
44
+ * centered content column (max 748px, `margin: 0 auto`). Laid-out flow rows
45
+ * share that edge, so the first meaningful one suffices. The panel's right
46
+ * edge stays left of it — the panel may only occupy the scrollport's left
47
+ * gutter.
48
+ *
49
+ * Rows that are not laid out in the flow are skipped: a message-tools edit
50
+ * leaves the withdrawn originals in the DOM (hidden, zero-size, or off the
51
+ * column at x=0), and probing their left edge yields 0 — which would make the
52
+ * width gate compute a negative left gutter and hide the entire rail. Any
53
+ * real flow row sits inside the conversation column at a positive x.
42
54
  * @param scrollport - the official conversation scrollport element.
43
- * @returns the flow's left edge, or null while no flow row is rendered.
55
+ * @returns the flow's left edge, or null while no laid-out flow row is rendered.
44
56
  */
45
57
  export declare function flowLeftX(scrollport: HTMLElement): number | null;
46
58
  /**
@@ -80,5 +92,5 @@ export interface RailTracker {
80
92
  * @param includeSteering - whether steering rows count as user dots.
81
93
  * @returns the tracker face.
82
94
  */
83
- export declare function installRailTracker(ctx: ClientContext, includeSteering: boolean): RailTracker;
95
+ export declare function installRailTracker(ctx: Context, includeSteering: boolean): RailTracker;
84
96
  //# sourceMappingURL=rail-tracker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rail-tracker.d.ts","sourceRoot":"","sources":["../../../src/client/rail-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,wCAAwC,CAAA;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAenD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAoB5H;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAa7F;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrE;AAED,yFAAyF;AACzF,MAAM,WAAW,WAAW;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAA;IACjD,qFAAqF;IACrF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,0CAA0C;IAC1C,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,EAAE,eAAe,EAAE,OAAO,GAAG,WAAW,CAoI5F"}
1
+ {"version":3,"file":"rail-tracker.d.ts","sourceRoot":"","sources":["../../../src/client/rail-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AAGtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAgBnD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAyB5H;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAQhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAa7F;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrE;AAED,yFAAyF;AACzF,MAAM,WAAW,WAAW;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAA;IACjD,qFAAqF;IACrF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,0CAA0C;IAC1C,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,GAAG,WAAW,CA2KtF"}
@@ -1,3 +1,4 @@
1
+ import { isTimelineRowKind } from './timeline-kinds.js';
1
2
  /** Horizontal inset of the rail from the scrollport's left edge (px). */
2
3
  const RAIL_LEFT_INSET = 6;
3
4
  /** Vertical padding above and below the rail inside the scrollport (px). */
@@ -11,10 +12,15 @@ const IDLE = {
11
12
  };
12
13
  /**
13
14
  * Measure the panel's viewport box from one scrollport: its rect inset by the
14
- * panel padding, minus the sticky composer seat at the bottom and the
15
- * conversation tab strip at the top. The tabs render just above the
16
- * scrollport, but centering reads against the whole window, so the strip
17
- * height leaves the box either way — otherwise the list sits visibly high.
15
+ * panel padding, minus the chat input card at the bottom and the conversation
16
+ * tab strip at the top. The tabs render just above the scrollport, but
17
+ * centering reads against the whole window, so the strip height leaves the
18
+ * box either way — otherwise the list sits visibly high.
19
+ * The bottom ends at the `[data-composer-card]` top — the chat box — NOT the
20
+ * whole `[data-composer-seat]` top: dock cards (goal/todo/queue) sit above
21
+ * the input inside the seat, and counting them would push the timeline up off
22
+ * the conversation. Falls back to the seat top, then the column bottom, when
23
+ * the markers are absent.
18
24
  * The scrollport's own width rides along for the width-cap fallback when the
19
25
  * message-flow probe is unanswered.
20
26
  * @param scrollport - the official conversation scrollport element.
@@ -25,8 +31,6 @@ export function measureGeometry(scrollport) {
25
31
  const rect = scrollport.getBoundingClientRect();
26
32
  if (rect.width === 0 && rect.height === 0)
27
33
  return null;
28
- const composer = scrollport.querySelector('[data-composer-seat]');
29
- const composerHeight = composer?.getBoundingClientRect().height ?? 0;
30
34
  let topInset = RAIL_VERTICAL_PADDING;
31
35
  for (const tabs of scrollport.ownerDocument.querySelectorAll('[role="tablist"]')) {
32
36
  const tabsRect = tabs.getBoundingClientRect();
@@ -36,25 +40,46 @@ export function measureGeometry(scrollport) {
36
40
  break;
37
41
  }
38
42
  }
43
+ const card = scrollport.querySelector('[data-composer-card]');
44
+ const seat = scrollport.querySelector('[data-composer-seat]');
45
+ const bottom = card !== null
46
+ ? card.getBoundingClientRect().top - RAIL_VERTICAL_PADDING
47
+ : seat !== null
48
+ ? seat.getBoundingClientRect().top - RAIL_VERTICAL_PADDING
49
+ : rect.bottom - RAIL_VERTICAL_PADDING;
39
50
  return {
40
51
  left: rect.left + RAIL_LEFT_INSET,
41
52
  top: rect.top + topInset,
42
- height: Math.max(0, rect.height - composerHeight - topInset - RAIL_VERTICAL_PADDING),
53
+ height: Math.max(0, bottom - rect.top - topInset),
43
54
  width: rect.width,
44
55
  };
45
56
  }
46
57
  /**
47
58
  * The viewport x of the message flow's left edge: the left of the first
48
- * rendered `[data-chat-flow-kind]` row, which sits flush inside the official
49
- * centered content column (max 748px, `margin: 0 auto`). Every flow row
50
- * shares that edge, so the first one found suffices. The panel's right edge
51
- * stays left of it — the panel may only occupy the scrollport's left gutter.
59
+ * laid-out `[data-chat-flow-kind]` row, which sits flush inside the official
60
+ * centered content column (max 748px, `margin: 0 auto`). Laid-out flow rows
61
+ * share that edge, so the first meaningful one suffices. The panel's right
62
+ * edge stays left of it — the panel may only occupy the scrollport's left
63
+ * gutter.
64
+ *
65
+ * Rows that are not laid out in the flow are skipped: a message-tools edit
66
+ * leaves the withdrawn originals in the DOM (hidden, zero-size, or off the
67
+ * column at x=0), and probing their left edge yields 0 — which would make the
68
+ * width gate compute a negative left gutter and hide the entire rail. Any
69
+ * real flow row sits inside the conversation column at a positive x.
52
70
  * @param scrollport - the official conversation scrollport element.
53
- * @returns the flow's left edge, or null while no flow row is rendered.
71
+ * @returns the flow's left edge, or null while no laid-out flow row is rendered.
54
72
  */
55
73
  export function flowLeftX(scrollport) {
56
- const row = scrollport.querySelector('[data-chat-flow-kind]');
57
- return row === null ? null : row.getBoundingClientRect().left;
74
+ for (const row of scrollport.querySelectorAll('[data-chat-flow-kind]')) {
75
+ const rect = row.getBoundingClientRect();
76
+ if (rect.width === 0 && rect.height === 0)
77
+ continue;
78
+ if (rect.left <= 0)
79
+ continue;
80
+ return rect.left;
81
+ }
82
+ return null;
58
83
  }
59
84
  /**
60
85
  * Resolve the key of the user-message row the reading position belongs to:
@@ -72,7 +97,7 @@ export function activeRowKey(scrollport, includeSteering) {
72
97
  let lastAbove = null;
73
98
  for (const row of scrollport.querySelectorAll('[data-chat-flow-kind]')) {
74
99
  const kind = row.dataset.chatFlowKind;
75
- if (kind !== 'user' && !(includeSteering && kind === 'steering'))
100
+ if (kind === undefined || !isTimelineRowKind(kind, includeSteering))
76
101
  continue;
77
102
  if (row.getBoundingClientRect().bottom <= viewTop) {
78
103
  lastAbove = row.dataset.chatAnchorKey ?? lastAbove;
@@ -114,6 +139,12 @@ export function installRailTracker(ctx, includeSteering) {
114
139
  let resizeObserver;
115
140
  let rafPending = false;
116
141
  let warned = false;
142
+ // Bind-generation token: every teardown (a rebind or a dispose) bumps it, so
143
+ // a still-pending bind frame that closed over an older token becomes stale
144
+ // and bails. Without this, a bind scheduled before a dispose/rebind would
145
+ // run afterward — binding the global scrollport under a stale session or,
146
+ // worse, re-adding listeners and a ResizeObserver to a disposed tracker.
147
+ let bindToken = 0;
117
148
  const nextFrame = typeof requestAnimationFrame === 'function'
118
149
  ? requestAnimationFrame
119
150
  : (callback) => { setTimeout(callback, 16); };
@@ -130,10 +161,37 @@ export function installRailTracker(ctx, includeSteering) {
130
161
  for (const fn of [...listeners])
131
162
  fn();
132
163
  };
164
+ const attach = (el) => {
165
+ if (scrollport !== null)
166
+ scrollport.removeEventListener('scroll', onScroll);
167
+ resizeObserver?.disconnect();
168
+ scrollport = el;
169
+ scrollport.addEventListener('scroll', onScroll, { passive: true });
170
+ if (typeof ResizeObserver === 'function') {
171
+ resizeObserver = new ResizeObserver(scheduleUpdate);
172
+ resizeObserver.observe(el);
173
+ const composer = el.querySelector('[data-composer-seat]');
174
+ if (composer !== null)
175
+ resizeObserver.observe(composer);
176
+ }
177
+ };
133
178
  const update = () => {
134
- if (scrollport === null)
135
- return;
136
- const geometry = measureGeometry(scrollport);
179
+ // Self-heal the scrollport binding: while unbound (or bound to a node the
180
+ // host remounted away — the conversation view re-renders under a stable
181
+ // session), re-probe the DOM. On rc hosts currentProvideInfo announced
182
+ // the remount; on 0.1.2 the feed is gone, so the observer/resize/scroll
183
+ // cadence that already lands here owns re-resolution on both lines.
184
+ let el = scrollport;
185
+ if (el === null || !el.isConnected) {
186
+ if (activeSession === undefined)
187
+ return;
188
+ const found = document.querySelector('[data-conversation-scroll]');
189
+ if (found === null)
190
+ return;
191
+ attach(found);
192
+ el = found;
193
+ }
194
+ const geometry = measureGeometry(el);
137
195
  if (geometry === null)
138
196
  return;
139
197
  publish({
@@ -143,9 +201,9 @@ export function installRailTracker(ctx, includeSteering) {
143
201
  top: geometry.top,
144
202
  height: geometry.height,
145
203
  scrollportWidth: geometry.width,
146
- flowLeft: flowLeftX(scrollport),
147
- activeKey: activeRowKey(scrollport, includeSteering),
148
- chatView: scrollport.querySelector('[data-chat-flow]') !== null,
204
+ flowLeft: flowLeftX(el),
205
+ activeKey: activeRowKey(el, includeSteering),
206
+ chatView: el.querySelector('[data-chat-flow]') !== null,
149
207
  });
150
208
  };
151
209
  const scheduleUpdate = () => {
@@ -159,6 +217,7 @@ export function installRailTracker(ctx, includeSteering) {
159
217
  };
160
218
  const onScroll = () => { scheduleUpdate(); };
161
219
  const teardownBind = () => {
220
+ bindToken++;
162
221
  if (scrollport !== null)
163
222
  scrollport.removeEventListener('scroll', onScroll);
164
223
  scrollport = null;
@@ -175,7 +234,14 @@ export function installRailTracker(ctx, includeSteering) {
175
234
  publish(IDLE);
176
235
  return;
177
236
  }
237
+ const token = bindToken;
178
238
  nextFrame(() => {
239
+ // This frame was scheduled by an earlier bind whose generation has since
240
+ // been superseded by a rebind or a dispose: ignore it entirely rather
241
+ // than binding the scrollport under a stale session or binding into a
242
+ // disposed tracker.
243
+ if (token !== bindToken)
244
+ return;
179
245
  const found = document.querySelector('[data-conversation-scroll]');
180
246
  if (found === null) {
181
247
  if (!warned) {
@@ -185,15 +251,7 @@ export function installRailTracker(ctx, includeSteering) {
185
251
  publish(IDLE);
186
252
  return;
187
253
  }
188
- scrollport = found;
189
- scrollport.addEventListener('scroll', onScroll, { passive: true });
190
- if (typeof ResizeObserver === 'function') {
191
- resizeObserver = new ResizeObserver(scheduleUpdate);
192
- resizeObserver.observe(scrollport);
193
- const composer = scrollport.querySelector('[data-composer-seat]');
194
- if (composer !== null)
195
- resizeObserver.observe(composer);
196
- }
254
+ attach(found);
197
255
  update();
198
256
  });
199
257
  };
@@ -201,7 +259,15 @@ export function installRailTracker(ctx, includeSteering) {
201
259
  bind(ctx.sessions.list.getSnapshot().current);
202
260
  };
203
261
  const stopList = ctx.sessions.list.subscribe(bindCurrent);
204
- const stopProvide = ctx.sessions.currentProvideInfo.subscribe(bindCurrent);
262
+ // Host 0.1.2-alpha.1 removed ISessions.currentProvideInfo (commit
263
+ // be531688f3 — the provide-channel projection went with the runtime
264
+ // package). rc hosts still publish it, and it is the precise remount
265
+ // signal there, so subscribe when the feed exists and skip it when the
266
+ // service predates/omits it — update() below re-resolves a missing or
267
+ // detached scrollport from the MutationObserver/resize cadence, which
268
+ // covers the remount signal on both lines.
269
+ const provideFeed = ctx.sessions.currentProvideInfo;
270
+ const stopProvide = provideFeed?.subscribe(bindCurrent);
205
271
  bindCurrent();
206
272
  // Layout fallbacks beyond the scrollport's own ResizeObserver: a window
207
273
  // resize re-measures, and a body MutationObserver catches panel folds and
@@ -230,7 +296,7 @@ export function installRailTracker(ctx, includeSteering) {
230
296
  },
231
297
  dispose: () => {
232
298
  stopList();
233
- stopProvide();
299
+ stopProvide?.();
234
300
  if (typeof window !== 'undefined')
235
301
  window.removeEventListener('resize', onWindowResize);
236
302
  mutationObserver?.disconnect();
@@ -3,7 +3,7 @@
3
3
  * face (jump/loadOlder plus the rail geometry hook) and the composed props of
4
4
  * the `conversation.session.header.utilities` entry.
5
5
  */
6
- import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-runtime/client';
6
+ import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
7
7
  import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
8
8
  /**
9
9
  * Live rail geometry plus the currently visible user message, published by
@@ -18,7 +18,11 @@ export interface TimelineRailState {
18
18
  left: number;
19
19
  /** Viewport y of the rail's top edge (below the session header). */
20
20
  top: number;
21
- /** Rail height in px (scrollport minus the composer seat). */
21
+ /**
22
+ * Rail height in px: the scrollport from below the tab strip down to the
23
+ * chat input card top — dock cards (goal/todo/queue) above the input never
24
+ * shorten it, so the timeline stays flush with the chat box.
25
+ */
22
26
  height: number;
23
27
  /** Scrollport width in px (the degraded width cap when the flow probe fails). */
24
28
  scrollportWidth: number;
@@ -1 +1 @@
1
- {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../../src/client/slots.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAA;AACtF,OAAO,KAAK,EACV,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EACtD,MAAM,kCAAkC,CAAA;AAOzC;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,sEAAsE;IACtE,KAAK,EAAE,OAAO,CAAA;IACd,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAA;IACX,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAA;IACd,iFAAiF;IACjF,eAAe,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oEAAoE;IACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,yEAAyE;IACzE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,eAAe,EAAE,OAAO,CAAA;IACxB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B,wEAAwE;IACxE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,KAAK,EAAE;QACL,qEAAqE;QACrE,IAAI,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAA;KACxC,CAAA;CACF;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAA;CACxC;AAED,gDAAgD;AAChD,MAAM,MAAM,iBAAiB,GAC3B,YAAY,CAAC,uCAAuC,CAAC,GACnD,UAAU,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,kBAAkB,CAAC,CAAA"}
1
+ {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../../src/client/slots.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAA;AACtF,OAAO,KAAK,EACV,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EACtD,MAAM,kCAAkC,CAAA;AAUzC;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,sEAAsE;IACtE,KAAK,EAAE,OAAO,CAAA;IACd,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,iFAAiF;IACjF,eAAe,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oEAAoE;IACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,yEAAyE;IACzE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,eAAe,EAAE,OAAO,CAAA;IACxB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B,wEAAwE;IACxE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,KAAK,EAAE;QACL,qEAAqE;QACrE,IAAI,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAA;KACxC,CAAA;CACF;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAA;CACxC;AAED,gDAAgD;AAChD,MAAM,MAAM,iBAAiB,GAC3B,YAAY,CAAC,uCAAuC,CAAC,GACnD,UAAU,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,kBAAkB,CAAC,CAAA"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Timeline node-kind classification. The rail's projection (which store nodes
3
+ * become rows) and its DOM tracking (which flow rows read as the reading
4
+ * position) must agree on the set of "timeline row" kinds, and the hidden-
5
+ * span fold must agree on the set of "span carrier" kinds. Rather than repeat
6
+ * these inline predicates at every site — where a future kind could be added
7
+ * to one place and silently missed in another — they live here as the single
8
+ * source of truth. Both are pure and read only the `kind` string, so they hold
9
+ * no dependency on the message-tools package and work for any session.
10
+ */
11
+ /** A message-tools edited-bubble row (an in-place edit replacement). */
12
+ export declare const EDIT_KIND = "message-tools-edited";
13
+ /** A message-tools restored row (a withdrawn message replayed at the tail). */
14
+ export declare const RESTORED_KIND = "message-tools-restored";
15
+ /**
16
+ * Whether a node kind renders as a timeline row. The rail lists user messages
17
+ * (and optionally steering), plus the message-tools edited/restored bubbles
18
+ * that the host `order` does not always surface.
19
+ * @param kind - the node's kind string.
20
+ * @param includeSteering - whether steering rows count as user messages.
21
+ * @returns true when the kind belongs on the timeline.
22
+ */
23
+ export declare function isTimelineRowKind(kind: string, includeSteering: boolean): boolean;
24
+ /**
25
+ * Whether a node kind carries a hidden span (`{ hiddenStartSeq, seq }`) that
26
+ * the rail must fold to drop the covered originals. The withdrawal divider and
27
+ * the edited bubble both declare the span they shadow.
28
+ * @param kind - the node's kind string.
29
+ * @returns true when the kind declares a hidden span.
30
+ */
31
+ export declare function isHiddenSpanCarrierKind(kind: string): boolean;
32
+ //# sourceMappingURL=timeline-kinds.d.ts.map