@khorsheed/dsh-message-timeline 0.1.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/CHANGELOG.md +11 -0
- package/README.en.md +79 -0
- package/README.i18n.yaml +6 -0
- package/README.md +79 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +521 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +11 -0
- package/lib/invariant.js +28 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/client/TimelineRail.d.ts +10 -0
- package/lib/types/client/TimelineRail.d.ts.map +1 -0
- package/lib/types/client/TimelineRail.js +152 -0
- package/lib/types/client/config.d.ts +39 -0
- package/lib/types/client/config.d.ts.map +1 -0
- package/lib/types/client/config.js +30 -0
- package/lib/types/client/index.d.ts +29 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +49 -0
- package/lib/types/client/locales.d.ts +20 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/locales.js +11 -0
- package/lib/types/client/preview.d.ts +13 -0
- package/lib/types/client/preview.d.ts.map +1 -0
- package/lib/types/client/preview.js +13 -0
- package/lib/types/client/rail-tracker.d.ts +84 -0
- package/lib/types/client/rail-tracker.d.ts.map +1 -0
- package/lib/types/client/rail-tracker.js +240 -0
- package/lib/types/client/slots.d.ts +62 -0
- package/lib/types/client/slots.d.ts.map +1 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +8 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +26 -0
- package/package.json +79 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TimelineRailProps } from './slots.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The header-utilities entry: the portal timeline panel.
|
|
4
|
+
* @param props - composed props (see {@link TimelineRailProps}).
|
|
5
|
+
* @returns nothing visible in the seat; the floating panel while the chat view shows.
|
|
6
|
+
*/
|
|
7
|
+
export declare function TimelineRail({ useSession, sessionId, includeSteering, panelWidth, initialPages, loadOlder, jumpTo, useRail, t, }: TimelineRailProps): import("react").ReactPortal | null;
|
|
8
|
+
/** Memoized export for the slot machinery (stable component identity). */
|
|
9
|
+
export declare const TimelineRailEntry: import("react").MemoExoticComponent<typeof TimelineRail>;
|
|
10
|
+
//# sourceMappingURL=TimelineRail.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Message timeline panel, browser half. One entry in the official
|
|
4
|
+
* conversation.session.header.utilities seat anchors the plugin into the
|
|
5
|
+
* session scope and renders, through a body portal, the flat floating
|
|
6
|
+
* timeline over the left edge of the chat scrollport: one row per loaded
|
|
7
|
+
* user message — a tick plus an ellipsized one-line preview, no frame and no
|
|
8
|
+
* visible scrollbar. At rest only the dimmed ticks show, the reading
|
|
9
|
+
* position's tick in blue (the latest message until the tracker answers);
|
|
10
|
+
* hovering or keyboard-focusing the panel reveals the row texts with the
|
|
11
|
+
* blue row on top, and clicking a row jumps the transcript to that message.
|
|
12
|
+
* The panel is always on while the chat view shows; the `enabled` config is
|
|
13
|
+
* the off switch.
|
|
14
|
+
*/
|
|
15
|
+
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
|
+
import { createPortal } from 'react-dom';
|
|
17
|
+
import { PANEL_WIDTH_MIN } from './config.js';
|
|
18
|
+
import { previewText } from './preview.js';
|
|
19
|
+
import css from './TimelineRail.module.css';
|
|
20
|
+
/** Extract the preview blocks of one user/steering node (kind-checked by the caller). */
|
|
21
|
+
function nodeContent(node) {
|
|
22
|
+
return node.data.content ?? [];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Horizontal panel padding, matching `.panel`'s `padding: 4px 8px`: the text
|
|
26
|
+
* band sits this far inside the box, so the width budget must give it back
|
|
27
|
+
* for {@link PANEL_GAP} to be the visible gap to the message flow.
|
|
28
|
+
*/
|
|
29
|
+
const PANEL_PADDING_X = 8;
|
|
30
|
+
/** Visible breathing gap between the timeline text and the message flow (px). */
|
|
31
|
+
const PANEL_GAP = 16;
|
|
32
|
+
/** Degraded width cap (fraction of the scrollport) while the flow probe is unanswered. */
|
|
33
|
+
const DEGRADED_WIDTH_RATIO = 0.4;
|
|
34
|
+
/**
|
|
35
|
+
* The header-utilities entry: the portal timeline panel.
|
|
36
|
+
* @param props - composed props (see {@link TimelineRailProps}).
|
|
37
|
+
* @returns nothing visible in the seat; the floating panel while the chat view shows.
|
|
38
|
+
*/
|
|
39
|
+
export function TimelineRail({ useSession, sessionId, includeSteering, panelWidth, initialPages, loadOlder, jumpTo, useRail, t, }) {
|
|
40
|
+
const rail = useRail(s => s);
|
|
41
|
+
const order = useSession(s => s.chat.order);
|
|
42
|
+
const nodes = useSession(s => s.chat.nodes);
|
|
43
|
+
const hasMore = useSession(s => s.hasMore);
|
|
44
|
+
const loadingOlder = useSession(s => s.loadingOlder);
|
|
45
|
+
const items = useMemo(() => {
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const key of order) {
|
|
48
|
+
const node = nodes.get(key);
|
|
49
|
+
if (node === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
const kind = node.kind;
|
|
52
|
+
if (kind !== 'user' && !(includeSteering && kind === 'steering'))
|
|
53
|
+
continue;
|
|
54
|
+
result.push({ key, node });
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}, [order, nodes, includeSteering]);
|
|
58
|
+
// The lit row: the hover/arrow preselection while it moves, otherwise the
|
|
59
|
+
// live reading position the tracker publishes, defaulting to the latest
|
|
60
|
+
// message before the tracker answers. ArrowUp/Down or hovering moves a
|
|
61
|
+
// bold preselection; Enter or a click confirms it and jumps.
|
|
62
|
+
const [focusKey, setFocusKey] = useState(null);
|
|
63
|
+
const current = focusKey ?? rail.activeKey ?? items.at(-1)?.key ?? null;
|
|
64
|
+
const panelRef = useRef(null);
|
|
65
|
+
// The panel is a chat-view affordance: hide it while the session shows
|
|
66
|
+
// another tab (trajectory etc.), detected through ChatView's data-chat-flow
|
|
67
|
+
// marker. `active` splits from `visible` so history paging can bootstrap: a
|
|
68
|
+
// session whose loaded event window holds no user message yet (a huge
|
|
69
|
+
// assistant turn pushed it past the first page) renders no rows but must
|
|
70
|
+
// still pull pages until one materializes.
|
|
71
|
+
const active = rail.chatView && rail.ready && rail.sessionId === sessionId;
|
|
72
|
+
const visible = active && items.length > 0;
|
|
73
|
+
// The panel must never cover the message flow: its width is the configured
|
|
74
|
+
// preferred width capped by the scrollport's left gutter — the message
|
|
75
|
+
// flow's left edge minus the panel's left edge, less the panel's right
|
|
76
|
+
// padding and the visible breathing gap. A gutter too small for the minimum
|
|
77
|
+
// usable width hides the panel entirely (the timeline is an overlay
|
|
78
|
+
// affordance; squeezed into nothing it only intercepts the transcript).
|
|
79
|
+
// When the flow probe is unanswered (official structure change), the width
|
|
80
|
+
// degrades to a fraction of the scrollport instead — never throws, never
|
|
81
|
+
// covers more than the fallback.
|
|
82
|
+
const gutter = rail.flowLeft === null ? null : rail.flowLeft - rail.left - PANEL_PADDING_X - PANEL_GAP;
|
|
83
|
+
const width = gutter === null
|
|
84
|
+
? Math.min(panelWidth, Math.max(PANEL_WIDTH_MIN, rail.scrollportWidth * DEGRADED_WIDTH_RATIO))
|
|
85
|
+
: Math.min(panelWidth, Math.max(0, gutter));
|
|
86
|
+
const tooNarrow = gutter !== null && width < PANEL_WIDTH_MIN;
|
|
87
|
+
// Keep the lit row in view: the panel follows the reading position (a new
|
|
88
|
+
// message scrolls its row in), and mouse browsing is never yanked because
|
|
89
|
+
// the hovered row is the current one and always visible under the pointer.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
panelRef.current?.querySelector(`[data-item-key=${JSON.stringify(current)}]`)
|
|
92
|
+
?.scrollIntoView({ block: 'nearest' });
|
|
93
|
+
}, [current]);
|
|
94
|
+
// Prefetch history while the panel is on a chat view: keep pulling pages
|
|
95
|
+
// until the first user message materializes (bootstrap), then until
|
|
96
|
+
// initialPages pages arrived, so the list starts near-complete. Older
|
|
97
|
+
// pages load on demand when the panel scrolls to its top.
|
|
98
|
+
const prefetchedPagesRef = useRef(0);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!active) {
|
|
101
|
+
prefetchedPagesRef.current = 0;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!hasMore || loadingOlder)
|
|
105
|
+
return;
|
|
106
|
+
if (items.length > 0 && prefetchedPagesRef.current >= initialPages)
|
|
107
|
+
return;
|
|
108
|
+
prefetchedPagesRef.current += 1;
|
|
109
|
+
void loadOlder();
|
|
110
|
+
}, [active, hasMore, loadingOlder, items.length, initialPages, loadOlder]);
|
|
111
|
+
// Older history loads by scrolling the panel to its top; the official chat
|
|
112
|
+
// view owns the load-older button, so the panel stays chromeless.
|
|
113
|
+
const onPanelScroll = (event) => {
|
|
114
|
+
if (!hasMore || loadingOlder)
|
|
115
|
+
return;
|
|
116
|
+
if (event.currentTarget.scrollTop <= 8)
|
|
117
|
+
void loadOlder();
|
|
118
|
+
};
|
|
119
|
+
const confirm = (key) => {
|
|
120
|
+
setFocusKey(null);
|
|
121
|
+
jumpTo(key);
|
|
122
|
+
};
|
|
123
|
+
if (!visible || tooNarrow)
|
|
124
|
+
return null;
|
|
125
|
+
return createPortal(_jsx("div", { ref: panelRef, className: css.panel, role: "navigation", "aria-label": t('rail.panel'), tabIndex: 0, "data-timeline-panel": "", style: { left: rail.left, top: rail.top, height: rail.height, width }, onScroll: onPanelScroll, onKeyDown: (event) => {
|
|
126
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
const anchor = items.findIndex(item => item.key === current);
|
|
129
|
+
const next = event.key === 'ArrowDown'
|
|
130
|
+
? items[Math.min(anchor + 1, items.length - 1)]
|
|
131
|
+
: items[Math.max(anchor - 1, 0)];
|
|
132
|
+
/* v8 ignore next -- a non-empty item list keeps the clamped index inside bounds */
|
|
133
|
+
if (next !== undefined)
|
|
134
|
+
setFocusKey(next.key);
|
|
135
|
+
}
|
|
136
|
+
else if (event.key === 'Enter' || event.key === ' ') {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
/* v8 ignore next -- the panel only renders with at least one item, so a current key always exists */
|
|
139
|
+
if (current !== null)
|
|
140
|
+
confirm(current);
|
|
141
|
+
}
|
|
142
|
+
}, onBlur: () => { setFocusKey(null); }, children: items.map((item) => {
|
|
143
|
+
const isCurrent = item.key === current;
|
|
144
|
+
const isFocused = item.key === focusKey;
|
|
145
|
+
const className = isFocused
|
|
146
|
+
? `${css.item} ${css.itemFocused}`
|
|
147
|
+
: isCurrent ? `${css.item} ${css.itemCurrent}` : css.item;
|
|
148
|
+
return (_jsxs("button", { type: "button", "data-item-key": item.key, "aria-current": isCurrent || undefined, className: className, onClick: () => { confirm(item.key); }, onMouseEnter: () => { setFocusKey(item.key); }, onMouseLeave: () => { setFocusKey(key => key === item.key ? null : key); }, children: [_jsx("span", { className: isCurrent ? `${css.tick} ${css.tickCurrent}` : css.tick, "aria-hidden": "true" }), _jsx("span", { className: css.itemText, children: previewText(nodeContent(item.node)) ?? t('rail.empty') })] }, item.key));
|
|
149
|
+
}) }), document.body);
|
|
150
|
+
}
|
|
151
|
+
/** Memoized export for the slot machinery (stable component identity). */
|
|
152
|
+
export const TimelineRailEntry = memo(TimelineRail);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-half configuration of the message-timeline plugin. The runner hands
|
|
3
|
+
* the plugin its validated entry config through the apply second parameter;
|
|
4
|
+
* this module supplies the defaults and clamps, so a composition that does not
|
|
5
|
+
* pass config still renders with the documented values and out-of-range input
|
|
6
|
+
* lands inside the legal bounds.
|
|
7
|
+
*/
|
|
8
|
+
/** Deployment-tunable rail behavior. */
|
|
9
|
+
export interface TimelineConfig {
|
|
10
|
+
/** Master switch: false hides the toggle and the rail entirely. */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
/** Count steering messages (user text admitted mid-turn) as rows. */
|
|
13
|
+
includeSteering: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Preferred timeline panel width in px (clamped 120–640; long text
|
|
16
|
+
* ellipsizes). The panel's right edge never crosses the message flow: the
|
|
17
|
+
* width is capped by the scrollport's left gutter, so a narrow column
|
|
18
|
+
* shrinks the panel automatically, and a gutter too small for
|
|
19
|
+
* {@link PANEL_WIDTH_MIN} hides the panel entirely.
|
|
20
|
+
*/
|
|
21
|
+
panelWidth: number;
|
|
22
|
+
/**
|
|
23
|
+
* History pages to prefetch when the rail opens (50 events each); older
|
|
24
|
+
* pages load on demand when the rail or the panel is scrolled to its top.
|
|
25
|
+
*/
|
|
26
|
+
initialPages: number;
|
|
27
|
+
}
|
|
28
|
+
/** Bounds of the deployment-tunable numbers (clamped in resolveConfig). */
|
|
29
|
+
export declare const PANEL_WIDTH_MIN = 120;
|
|
30
|
+
export declare const PANEL_WIDTH_MAX = 640;
|
|
31
|
+
/**
|
|
32
|
+
* Normalize the entry config into the full {@link TimelineConfig}: every
|
|
33
|
+
* omitted field takes its documented default and every numeric field is
|
|
34
|
+
* clamped into its legal range.
|
|
35
|
+
* @param config - the unvalidated entry config, when the runner passes one.
|
|
36
|
+
* @returns the effective rail behavior.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveConfig(config: Partial<TimelineConfig> | undefined): TimelineConfig;
|
|
39
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/client/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,OAAO,EAAE,OAAO,CAAA;IAChB,qEAAqE;IACrE,eAAe,EAAE,OAAO,CAAA;IACxB;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,2EAA2E;AAC3E,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,eAAe,MAAM,CAAA;AAQlC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,SAAS,GAAG,cAAc,CAOzF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-half configuration of the message-timeline plugin. The runner hands
|
|
3
|
+
* the plugin its validated entry config through the apply second parameter;
|
|
4
|
+
* this module supplies the defaults and clamps, so a composition that does not
|
|
5
|
+
* pass config still renders with the documented values and out-of-range input
|
|
6
|
+
* lands inside the legal bounds.
|
|
7
|
+
*/
|
|
8
|
+
/** Bounds of the deployment-tunable numbers (clamped in resolveConfig). */
|
|
9
|
+
export const PANEL_WIDTH_MIN = 120;
|
|
10
|
+
export const PANEL_WIDTH_MAX = 640;
|
|
11
|
+
const INITIAL_PAGES_MIN = 1;
|
|
12
|
+
const INITIAL_PAGES_MAX = 20;
|
|
13
|
+
function clamp(value, min, max) {
|
|
14
|
+
return Math.min(max, Math.max(min, value));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Normalize the entry config into the full {@link TimelineConfig}: every
|
|
18
|
+
* omitted field takes its documented default and every numeric field is
|
|
19
|
+
* clamped into its legal range.
|
|
20
|
+
* @param config - the unvalidated entry config, when the runner passes one.
|
|
21
|
+
* @returns the effective rail behavior.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveConfig(config) {
|
|
24
|
+
return {
|
|
25
|
+
enabled: config?.enabled ?? true,
|
|
26
|
+
includeSteering: config?.includeSteering ?? true,
|
|
27
|
+
panelWidth: clamp(config?.panelWidth ?? 360, PANEL_WIDTH_MIN, PANEL_WIDTH_MAX),
|
|
28
|
+
initialPages: clamp(config?.initialPages ?? 5, INITIAL_PAGES_MIN, INITIAL_PAGES_MAX),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message timeline plugin, browser half. Registers one entry into the
|
|
3
|
+
* official `conversation.session.header.utilities` seat (a right-aligned
|
|
4
|
+
* optional-utility slot) that anchors the plugin into the session scope; the
|
|
5
|
+
* component itself renders only through a body portal: the flat floating
|
|
6
|
+
* timeline panel over the chat scrollport's left edge — one row per loaded
|
|
7
|
+
* user message, no frame, no visible scrollbar. The
|
|
8
|
+
* panel reads the session chat snapshot through the framework `useSession`
|
|
9
|
+
* hook, jumps through the official row anchor attributes, and degrades to a
|
|
10
|
+
* hidden panel when those attributes change — no official code is modified.
|
|
11
|
+
* @module @khorsheed/dsh-message-timeline/client
|
|
12
|
+
*/
|
|
13
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
14
|
+
import { type TimelineConfig } from './config.ts';
|
|
15
|
+
export type { TimelineConfig } from './config.ts';
|
|
16
|
+
export { resolveConfig } from './config.ts';
|
|
17
|
+
export type { TimelineKey } from './locales.ts';
|
|
18
|
+
export { previewText } from './preview.ts';
|
|
19
|
+
export type { TimelineItem, TimelineRailInjected, TimelineRailProps, TimelineRailState } from './slots.ts';
|
|
20
|
+
/** Required services: the slot ledger, the session store, and the copy. */
|
|
21
|
+
export declare const inject: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Client plugin body: register the header-utilities entry and install the DOM
|
|
24
|
+
* tracker that publishes rail geometry and answers jumps.
|
|
25
|
+
* @param ctx - client root context.
|
|
26
|
+
* @param config - entry config; defaults apply when the runner passes none.
|
|
27
|
+
*/
|
|
28
|
+
export declare function apply(ctx: ClientContext, config?: Partial<TimelineConfig>): void;
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { resolveConfig } from './config.js';
|
|
2
|
+
import { en, zh } from './locales.js';
|
|
3
|
+
import { installRailTracker } from './rail-tracker.js';
|
|
4
|
+
import { TimelineRailEntry } from './TimelineRail.js';
|
|
5
|
+
export { resolveConfig } from './config.js';
|
|
6
|
+
export { previewText } from './preview.js';
|
|
7
|
+
/** Dictionary namespace owned by this plugin. */
|
|
8
|
+
const NS = 'message-timeline';
|
|
9
|
+
/** Required services: the slot ledger, the session store, and the copy. */
|
|
10
|
+
export const inject = ['slots', 'sessions', 'locale'];
|
|
11
|
+
/**
|
|
12
|
+
* Client plugin body: register the header-utilities entry and install the DOM
|
|
13
|
+
* tracker that publishes rail geometry and answers jumps.
|
|
14
|
+
* @param ctx - client root context.
|
|
15
|
+
* @param config - entry config; defaults apply when the runner passes none.
|
|
16
|
+
*/
|
|
17
|
+
export function apply(ctx, config) {
|
|
18
|
+
const options = resolveConfig(config);
|
|
19
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'message-timeline: dictionaries');
|
|
20
|
+
if (!options.enabled)
|
|
21
|
+
return;
|
|
22
|
+
const tracker = installRailTracker(ctx, options.includeSteering);
|
|
23
|
+
ctx.effect(() => () => { tracker.dispose(); }, 'message-timeline: rail tracker');
|
|
24
|
+
// The slot is declared by ui-conversation, whose apply order relative to
|
|
25
|
+
// this plugin is unconstrained: register through slots.inject so the entry
|
|
26
|
+
// waits for the declaration instead of crashing the loader at boot.
|
|
27
|
+
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
28
|
+
name: 'conversation.session.header.utilities',
|
|
29
|
+
id: 'message-timeline',
|
|
30
|
+
order: 100,
|
|
31
|
+
locale: NS,
|
|
32
|
+
inject: (sessionId) => {
|
|
33
|
+
const actx = ctx.sessions.scope(sessionId);
|
|
34
|
+
if (actx === undefined)
|
|
35
|
+
throw new Error('message-timeline: session resolved no scope');
|
|
36
|
+
const conversation = actx.get('conversation');
|
|
37
|
+
if (conversation === undefined)
|
|
38
|
+
throw new Error('message-timeline: conversation service unavailable');
|
|
39
|
+
return {
|
|
40
|
+
includeSteering: options.includeSteering,
|
|
41
|
+
panelWidth: options.panelWidth,
|
|
42
|
+
initialPages: options.initialPages,
|
|
43
|
+
loadOlder: () => conversation.loadOlder(),
|
|
44
|
+
jumpTo: (key) => { tracker.jumpTo(key); },
|
|
45
|
+
hooks: { rail: tracker.state },
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
}, TimelineRailEntry));
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `message-timeline` namespace dictionaries. */
|
|
2
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
3
|
+
export declare const zh: {
|
|
4
|
+
'rail.panel': string;
|
|
5
|
+
'rail.empty': string;
|
|
6
|
+
};
|
|
7
|
+
/** The message-timeline namespace key union. */
|
|
8
|
+
export type TimelineKey = keyof typeof zh;
|
|
9
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
10
|
+
interface LocaleNamespaceMap {
|
|
11
|
+
/** The message timeline panel's copy. */
|
|
12
|
+
'message-timeline': TimelineKey;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
16
|
+
export declare const en: {
|
|
17
|
+
'rail.panel': string;
|
|
18
|
+
'rail.empty': string;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,iDAAiD;AAEjD,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;CAGmB,CAAA;AAElC,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAA;AAEzC,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,yCAAyC;QACzC,kBAAkB,EAAE,WAAW,CAAA;KAChC;CACF;AAED,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;CAGwB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** `message-timeline` namespace dictionaries. */
|
|
2
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
3
|
+
export const zh = {
|
|
4
|
+
'rail.panel': '消息导览',
|
|
5
|
+
'rail.empty': '暂无用户消息',
|
|
6
|
+
};
|
|
7
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
8
|
+
export const en = {
|
|
9
|
+
'rail.panel': 'Message timeline',
|
|
10
|
+
'rail.empty': 'No user messages',
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text extraction for the timeline panel rows: user message content is a
|
|
3
|
+
* block list (text, image, …); the preview concatenates the text blocks so
|
|
4
|
+
* long messages ellipsize through CSS instead of dropping their first lines.
|
|
5
|
+
*/
|
|
6
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
7
|
+
/**
|
|
8
|
+
* Join the text blocks of one message into a single preview string.
|
|
9
|
+
* @param content - the message's content block list.
|
|
10
|
+
* @returns the concatenated text, or null when the message carries none.
|
|
11
|
+
*/
|
|
12
|
+
export declare function previewText(content: readonly ContentBlock[]): string | null;
|
|
13
|
+
//# sourceMappingURL=preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../src/client/preview.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,GAAG,IAAI,CAM3E"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join the text blocks of one message into a single preview string.
|
|
3
|
+
* @param content - the message's content block list.
|
|
4
|
+
* @returns the concatenated text, or null when the message carries none.
|
|
5
|
+
*/
|
|
6
|
+
export function previewText(content) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
for (const block of content) {
|
|
9
|
+
if (block.type === 'text' && block.text !== '')
|
|
10
|
+
parts.push(block.text);
|
|
11
|
+
}
|
|
12
|
+
return parts.length === 0 ? null : parts.join('\n');
|
|
13
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rail DOM tracker, apply world. The floating timeline panel needs the
|
|
3
|
+
* official chat scrollport's live geometry and the currently visible
|
|
4
|
+
* user-message row, so
|
|
5
|
+
* this module owns the only DOM the plugin touches — read-only probes plus
|
|
6
|
+
* the scroll write the jump performs — and publishes the result through the
|
|
7
|
+
* reserved hooks compartment (components never see the DOM or the sources).
|
|
8
|
+
*
|
|
9
|
+
* The probed attributes ([data-conversation-scroll], [data-chat-anchor-key],
|
|
10
|
+
* [data-chat-flow-kind]) are official render output, not a declared API: when
|
|
11
|
+
* they change, the tracker degrades — a missing scrollport hides the rail
|
|
12
|
+
* (one console.warn), missing rows only clear the active marker and make
|
|
13
|
+
* jumps no-ops. Nothing throws and no official code is modified.
|
|
14
|
+
*/
|
|
15
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
16
|
+
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
|
|
17
|
+
import type { TimelineRailState } from './slots.ts';
|
|
18
|
+
/**
|
|
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.
|
|
24
|
+
* The scrollport's own width rides along for the width-cap fallback when the
|
|
25
|
+
* message-flow probe is unanswered.
|
|
26
|
+
* @param scrollport - the official conversation scrollport element.
|
|
27
|
+
* @returns the panel box plus the scrollport width, or null while the
|
|
28
|
+
* scrollport has no laid-out size.
|
|
29
|
+
*/
|
|
30
|
+
export declare function measureGeometry(scrollport: HTMLElement): {
|
|
31
|
+
left: number;
|
|
32
|
+
top: number;
|
|
33
|
+
height: number;
|
|
34
|
+
width: number;
|
|
35
|
+
} | null;
|
|
36
|
+
/**
|
|
37
|
+
* 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.
|
|
42
|
+
* @param scrollport - the official conversation scrollport element.
|
|
43
|
+
* @returns the flow's left edge, or null while no flow row is rendered.
|
|
44
|
+
*/
|
|
45
|
+
export declare function flowLeftX(scrollport: HTMLElement): number | null;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the key of the user-message row the reading position belongs to:
|
|
48
|
+
* the first matching row whose bottom is still inside the viewport, or —
|
|
49
|
+
* while the reader sits inside a long assistant answer with no user row
|
|
50
|
+
* visible — the nearest user row above the viewport, so the lit tick stays
|
|
51
|
+
* anchored to the question being answered instead of jumping to the
|
|
52
|
+
* session's latest message.
|
|
53
|
+
* @param scrollport - the official conversation scrollport element.
|
|
54
|
+
* @param includeSteering - whether steering rows count as user messages.
|
|
55
|
+
* @returns the row's anchor key, or null when no user row is rendered.
|
|
56
|
+
*/
|
|
57
|
+
export declare function activeRowKey(scrollport: HTMLElement, includeSteering: boolean): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Scroll one user-message row to the top of the transcript scrollport.
|
|
60
|
+
* @param scrollport - the official conversation scrollport element.
|
|
61
|
+
* @param key - the target node's anchor key.
|
|
62
|
+
* @returns whether the row was found and scrolled.
|
|
63
|
+
*/
|
|
64
|
+
export declare function jumpRow(scrollport: HTMLElement, key: string): boolean;
|
|
65
|
+
/** The tracker's outward face: the observable state, the jump verb, and the disposer. */
|
|
66
|
+
export interface RailTracker {
|
|
67
|
+
/** Live rail geometry and active marker for the current session. */
|
|
68
|
+
readonly state: HostObservable<TimelineRailState>;
|
|
69
|
+
/** Scroll the transcript to the message addressed by `key` (no-op while unbound). */
|
|
70
|
+
jumpTo(key: string): void;
|
|
71
|
+
/** Unbind every listener and observer. */
|
|
72
|
+
dispose(): void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Install the rail tracker: bind the current session's scrollport, publish
|
|
76
|
+
* geometry/active updates on scroll and resize, and answer jump requests.
|
|
77
|
+
* Follows the current session through the sessions list and provide channels
|
|
78
|
+
* (the interface renders one conversation at a time, so one tracker suffices).
|
|
79
|
+
* @param ctx - client root context (sessions service).
|
|
80
|
+
* @param includeSteering - whether steering rows count as user dots.
|
|
81
|
+
* @returns the tracker face.
|
|
82
|
+
*/
|
|
83
|
+
export declare function installRailTracker(ctx: ClientContext, includeSteering: boolean): RailTracker;
|
|
84
|
+
//# sourceMappingURL=rail-tracker.d.ts.map
|
|
@@ -0,0 +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"}
|