@khorsheed/dsh-context-lens 0.0.0-stage → 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.
Files changed (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.en.md +53 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +52 -2
  5. package/cordis.patch.yml +13 -0
  6. package/lib/client.js +26953 -0
  7. package/lib/index.js +3943 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +1569 -0
  10. package/lib/typert.remote-client.d.ts +40 -0
  11. package/lib/typert.remote-client.js +1202 -0
  12. package/lib/types/changes.d.ts +72 -0
  13. package/lib/types/changes.js +328 -0
  14. package/lib/types/client/AgentTabs.d.ts +34 -0
  15. package/lib/types/client/AgentTabs.js +89 -0
  16. package/lib/types/client/ChangeList.d.ts +28 -0
  17. package/lib/types/client/ChangeList.js +109 -0
  18. package/lib/types/client/ConfigFacts.d.ts +21 -0
  19. package/lib/types/client/ConfigFacts.js +30 -0
  20. package/lib/types/client/HeaderUtility.d.ts +12 -0
  21. package/lib/types/client/HeaderUtility.js +8 -0
  22. package/lib/types/client/InspectorPanel.d.ts +5 -0
  23. package/lib/types/client/InspectorPanel.js +486 -0
  24. package/lib/types/client/RawView.d.ts +18 -0
  25. package/lib/types/client/RawView.js +8 -0
  26. package/lib/types/client/RequestFacts.d.ts +19 -0
  27. package/lib/types/client/RequestFacts.js +39 -0
  28. package/lib/types/client/RoomDecisionsView.d.ts +24 -0
  29. package/lib/types/client/RoomDecisionsView.js +52 -0
  30. package/lib/types/client/SchemaFields.d.ts +13 -0
  31. package/lib/types/client/SchemaFields.js +96 -0
  32. package/lib/types/client/SegCtl.d.ts +32 -0
  33. package/lib/types/client/SegCtl.js +12 -0
  34. package/lib/types/client/SegmentBlocks.d.ts +28 -0
  35. package/lib/types/client/SegmentBlocks.js +225 -0
  36. package/lib/types/client/TimingView.d.ts +17 -0
  37. package/lib/types/client/TimingView.js +98 -0
  38. package/lib/types/client/TokenView.d.ts +21 -0
  39. package/lib/types/client/TokenView.js +22 -0
  40. package/lib/types/client/ToolCatalog.d.ts +9 -0
  41. package/lib/types/client/ToolCatalog.js +51 -0
  42. package/lib/types/client/Trend.d.ts +25 -0
  43. package/lib/types/client/Trend.js +100 -0
  44. package/lib/types/client/TurnAction.d.ts +14 -0
  45. package/lib/types/client/TurnAction.js +31 -0
  46. package/lib/types/client/TurnMenu.d.ts +36 -0
  47. package/lib/types/client/TurnMenu.js +50 -0
  48. package/lib/types/client/aggregate.d.ts +141 -0
  49. package/lib/types/client/aggregate.js +213 -0
  50. package/lib/types/client/contract.d.ts +58 -0
  51. package/lib/types/client/contract.js +1 -0
  52. package/lib/types/client/definition.d.ts +47 -0
  53. package/lib/types/client/definition.js +25 -0
  54. package/lib/types/client/icons-local.d.ts +19 -0
  55. package/lib/types/client/icons-local.js +3 -0
  56. package/lib/types/client/icons.d.ts +18 -0
  57. package/lib/types/client/icons.js +2 -0
  58. package/lib/types/client/index.d.ts +42 -0
  59. package/lib/types/client/index.js +139 -0
  60. package/lib/types/client/locales.d.ts +365 -0
  61. package/lib/types/client/locales.js +705 -0
  62. package/lib/types/client/preset-visibility.d.ts +99 -0
  63. package/lib/types/client/preset-visibility.js +108 -0
  64. package/lib/types/client/scroll-sync.d.ts +54 -0
  65. package/lib/types/client/scroll-sync.js +118 -0
  66. package/lib/types/client/timing.d.ts +29 -0
  67. package/lib/types/client/timing.js +117 -0
  68. package/lib/types/client/wire.d.ts +13 -0
  69. package/lib/types/client/wire.js +11 -0
  70. package/lib/types/index.d.ts +32 -0
  71. package/lib/types/index.js +25 -0
  72. package/lib/types/log-lane.d.ts +198 -0
  73. package/lib/types/log-lane.js +841 -0
  74. package/lib/types/messages.d.ts +96 -0
  75. package/lib/types/messages.js +254 -0
  76. package/lib/types/prefix-diff.d.ts +4 -0
  77. package/lib/types/prefix-diff.js +20 -0
  78. package/lib/types/remote.d.ts +45 -0
  79. package/lib/types/remote.js +99 -0
  80. package/lib/types/room.d.ts +66 -0
  81. package/lib/types/room.js +101 -0
  82. package/lib/types/segmenter.d.ts +100 -0
  83. package/lib/types/segmenter.js +184 -0
  84. package/lib/types/service.d.ts +145 -0
  85. package/lib/types/service.js +493 -0
  86. package/lib/types/store.d.ts +424 -0
  87. package/lib/types/store.js +1920 -0
  88. package/lib/types/tool-summary.d.ts +6 -0
  89. package/lib/types/tool-summary.js +7 -0
  90. package/lib/types/types.d.ts +703 -0
  91. package/lib/types/types.js +18 -0
  92. package/locale/en.json +6 -0
  93. package/locale/zh.json +6 -0
  94. package/package.json +133 -3
@@ -0,0 +1,21 @@
1
+ /** Request navigation and the selected capture's structured / JSON detail. */
2
+ import { type ReactNode } from 'react';
3
+ import type { AgentScopeOverview, CapturedRequest, RequestLink } from '../types.ts';
4
+ import { type CompositionFilter } from './aggregate.ts';
5
+ import type { InspectorTabProps } from './contract.ts';
6
+ type T = InspectorTabProps['t'];
7
+ export interface TokenViewProps {
8
+ readonly scope: AgentScopeOverview;
9
+ readonly onOpenRequest?: ((link: RequestLink) => void) | undefined;
10
+ readonly onReturn?: (() => void) | undefined;
11
+ readonly selectedSeq: number | null;
12
+ readonly request: CapturedRequest | null;
13
+ readonly filterCat: CompositionFilter;
14
+ readonly previousResponseNumber: number | null;
15
+ readonly onSelect: (seq: number) => void;
16
+ readonly onToggleFilter: (category: Exclude<CompositionFilter, null>) => void;
17
+ readonly t: T;
18
+ }
19
+ export declare function TokenView({ scope, selectedSeq, request, filterCat, previousResponseNumber, onSelect, onOpenRequest, onReturn, onToggleFilter, t }: TokenViewProps): ReactNode;
20
+ export {};
21
+ //# sourceMappingURL=TokenView.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /** Request navigation and the selected capture's structured / JSON detail. */
3
+ import { useState } from 'react';
4
+ import { requestNumberText, aggregateCategories, cachePct, duplicateStats, fmt, fmtSpan, fmtK, inputTotal, summaryText } from "./aggregate.js";
5
+ import { CacheFacts, RequestFacts } from "./RequestFacts.js";
6
+ import { RawView } from "./RawView.js";
7
+ import { Exchange, SegmentBlocks, categoryLabelKey } from "./SegmentBlocks.js";
8
+ import css from './TokenView.module.css';
9
+ function RequestCard({ request: r, selected, onSelect, t }) {
10
+ const pct = r.usage === undefined ? null : cachePct(r.usage);
11
+ return _jsxs("button", { type: "button", className: selected ? css.reqItemSel : css.reqItem, title: r.requestNumber === undefined && r.responseSummary.kind !== 'compaction' ? t('req.numberUnknown') : undefined, "aria-label": `${r.responseSummary.kind === 'compaction' ? '' : `#${requestNumberText(r)} · `}${summaryText(r.responseSummary, t('resp.directAnswer'), t('compaction.title'), t('resp.thinking'))}`, "aria-pressed": selected, onClick: () => onSelect(r.seq), children: [_jsxs("span", { className: css.reqName, children: [r.responseSummary.kind !== 'compaction' && _jsxs("span", { children: ["#", requestNumberText(r), " \u00B7 "] }), summaryText(r.responseSummary, t('resp.directAnswer'), t('compaction.title'), t('resp.thinking'))] }), _jsx("span", { className: css.reqHit, "data-low": pct === 0, children: pct === null ? '—' : `${pct.toFixed(1)}%` }), _jsx("span", { className: css.reqIo, children: r.usage ? `${fmtK(inputTotal(r.usage))} → ${fmtK(r.usage.outputTokens)}` : r.status === 'streaming' ? t('req.streaming') : r.status === 'failed' ? t('req.failed') : '—' }), _jsx("span", { className: css.reqTime, children: r.totalMs === undefined ? '—' : fmtSpan(r.totalMs) }), (r.toolErrors ?? 0) > 0 && _jsxs("span", { className: css.reqFlag, children: [t('tool.failures'), " ", r.toolErrors] }), r.status === 'failed' && _jsx("span", { className: css.reqFlag, children: t('req.failed') })] });
12
+ }
13
+ export function TokenView({ scope, selectedSeq, request, filterCat, previousResponseNumber, onSelect, onOpenRequest, onReturn, onToggleFilter, t }) {
14
+ const [raw, setRaw] = useState(false);
15
+ const selectedItem = scope.requests.find(r => r.seq === selectedSeq) ?? scope.requests[0];
16
+ const cats = request === null ? [] : aggregateCategories(request).filter(c => c.chars > 0);
17
+ const totalChars = cats.reduce((n, c) => n + c.chars, 0);
18
+ const duplicates = duplicateStats(request ? [...request.segments.system, ...request.segments.history, ...request.segments.added] : []);
19
+ return _jsxs("div", { className: css.md, children: [_jsx("nav", { className: css.reqList, "aria-label": t('view.token'), children: scope.requests.map(r => _jsx(RequestCard, { request: r, selected: selectedItem?.seq === r.seq, onSelect: onSelect, t: t }, r.seq)) }), _jsx("div", { className: css.reqDetail, children: request === null || selectedItem === undefined ? _jsx("div", { className: css.stateNote, role: "status", children: t('state.loading') }) : _jsxs(_Fragment, { children: [onReturn && _jsxs("button", { type: "button", className: css.jsonToggle, onClick: onReturn, children: ["\u2190 ", t('compaction.return')] }), _jsx(RequestFacts, { request: request, t: t, raw: raw, onToggleRaw: () => setRaw(v => !v) }), raw ? _jsx(RawView, { request: request, t: t }) : _jsxs(_Fragment, { children: [request.compaction && _jsxs("div", { className: css.prefix, children: [_jsxs("b", { children: [t('compaction.title'), request.compaction.hostTurn === undefined ? '' : ` · ${t('lens.sourceTurn', { turn: String(request.compaction.hostTurn) })}`] }), request.compaction.replacedCount !== undefined && _jsx("span", { children: t(request.compaction.applied ? 'compaction.replaced' : 'compaction.selected', { count: String(request.compaction.replacedCount) }) }), request.compaction.inputPartial && _jsxs("span", { title: t('compaction.partial'), children: [t('compaction.partialShort'), " \u24D8"] }), request.compaction.before && onOpenRequest && _jsxs("button", { type: "button", className: css.jsonToggle, onClick: () => onOpenRequest(request.compaction.before), children: [t('compaction.before'), " \u2197"] })] }), _jsx(CacheFacts, { request: request, onOpenRequest: onOpenRequest, onShowCategory: category => { if (filterCat !== category)
20
+ onToggleFilter(category); }, t: t }), _jsx(Exchange, { request: request, previousResponseNumber: previousResponseNumber, onOpenRequest: onOpenRequest, t: t }, `${request.scopeId}-${request.seq}`), _jsxs("section", { "aria-label": t('lens.composition'), children: [_jsxs("div", { className: css.sectionTitle, children: [_jsx("h3", { children: t(request.compaction?.inputPartial ? 'compaction.region' : 'lens.composition') }), _jsx("span", { children: t('chain.chars', { count: fmt(totalChars) }) }), _jsx("abbr", { className: css.info, title: t('lens.compositionHint'), children: "i" })] }), _jsxs("div", { className: css.stack, children: [_jsx("div", { className: css.catbar, "aria-hidden": "true", children: cats.map(c => _jsx("i", { "data-cat": c.category, "data-off": filterCat !== null && filterCat !== '__dup' && filterCat !== c.category, style: { flex: `${c.chars} 1 0` } }, c.category)) }), _jsxs("div", { className: css.legend, children: [cats.map(c => _jsxs("button", { type: "button", className: css.legendItem, "data-off": filterCat !== null && filterCat !== c.category, "aria-pressed": filterCat === c.category, onClick: () => onToggleFilter(c.category), children: [_jsx("i", { className: css.dot, "data-cat": c.category }), t(categoryLabelKey(c.category)), _jsxs("span", { children: [(c.chars / totalChars * 100).toFixed(1), "%"] })] }, c.category)), duplicates.extraChars > 0 && _jsxs("button", { type: "button", className: css.legendItem, "data-off": filterCat !== null && filterCat !== '__dup', "aria-pressed": filterCat === '__dup', onClick: () => onToggleFilter('__dup'), children: [_jsx("span", { className: css.badge, children: "\u00D7N" }), t('lens.duplicates'), _jsx("span", { children: fmtK(duplicates.extraChars) })] })] }), _jsx(SegmentBlocks, { request: request, filterCat: filterCat, onClearFilter: () => { if (filterCat !== null)
21
+ onToggleFilter(filterCat); }, previousResponseNumber: previousResponseNumber, onOpenRequest: onOpenRequest, t: t }, `${request.scopeId}-${request.seq}`)] })] })] })] }) })] });
22
+ }
@@ -0,0 +1,9 @@
1
+ import type { ToolDefItem } from '../types.ts';
2
+ import type { InspectorTabProps } from './contract.ts';
3
+ type T = InspectorTabProps['t'];
4
+ export declare function ToolCatalog({ tools, t }: {
5
+ readonly tools: ToolDefItem[];
6
+ readonly t: T;
7
+ }): import("react").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=ToolCatalog.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** A scan-friendly inventory with lossless inline schema inspection. */
3
+ import { useLayoutEffect, useRef, useState } from 'react';
4
+ import { fmt } from "./aggregate.js";
5
+ import { IconChevronDownOutlineMedium } from "./icons.js";
6
+ import css from './SegmentBlocks.module.css';
7
+ import { SchemaFields, schemaRecord } from "./SchemaFields.js";
8
+ function schema(raw) {
9
+ try {
10
+ const value = JSON.parse(raw);
11
+ if (!schemaRecord(value) && typeof value !== 'boolean')
12
+ return null;
13
+ return { value, count: schemaRecord(value) && schemaRecord(value.properties) ? Object.keys(value.properties).length : null };
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function RawSchema({ text, t }) {
20
+ const [copyState, setCopyState] = useState('idle');
21
+ const copy = async () => {
22
+ setCopyState('copying');
23
+ try {
24
+ await navigator.clipboard.writeText(text);
25
+ setCopyState('copied');
26
+ }
27
+ catch {
28
+ setCopyState('failed');
29
+ }
30
+ };
31
+ return _jsx("div", { className: css.rawSection, children: _jsxs("section", { className: css.rawCode, "aria-label": t('view.raw'), children: [_jsxs("div", { className: css.rawHead, children: [_jsx("span", { className: css.rawLanguage, children: "JSON" }), _jsx("span", { className: css.copyStatus, role: "status", "data-error": copyState === 'failed', children: copyState === 'copied' ? t('lens.copied') : copyState === 'failed' ? t('lens.copyFailed') : '' }), _jsx("button", { type: "button", className: css.copyButton, disabled: copyState === 'copying', onClick: () => void copy(), children: t('lens.copyJson') })] }), _jsx("pre", { className: css.pre, children: _jsx("code", { children: text }) })] }) });
32
+ }
33
+ function ToolRow({ tool, t }) {
34
+ const [open, setOpen] = useState(false);
35
+ const [raw, setRaw] = useState(false);
36
+ const [restore, setRestore] = useState(false);
37
+ const row = useRef(null);
38
+ const parsed = schema(tool.parameters);
39
+ useLayoutEffect(() => {
40
+ if (!restore)
41
+ return;
42
+ row.current?.scrollIntoView?.({ block: 'nearest', behavior: 'instant' });
43
+ row.current?.querySelector('summary')?.focus({ preventScroll: true });
44
+ setRestore(false);
45
+ }, [restore]);
46
+ return _jsxs("details", { ref: row, className: css.toolCell, open: open, children: [_jsxs("summary", { className: css.toolRow, title: tool.description, onClick: e => { e.preventDefault(); setOpen(!open); if (open)
47
+ setRestore(true); }, children: [_jsxs("span", { className: css.toolName, children: [tool.name, tool.added && _jsx("span", { className: css.badge, children: t('lens.layerAdded') })] }), _jsx("span", { className: css.preview, children: tool.description }), _jsx("span", { className: css.parameterCount, children: parsed && parsed.count !== null ? t('lens.parameterCount', { count: String(parsed.count) }) : '—' }), _jsx("span", { className: css.number, title: t('chain.chars', { count: fmt(tool.chars) }), children: fmt(tool.chars) }), _jsx(IconChevronDownOutlineMedium, { size: 16, className: css.chevron })] }), open && _jsxs("div", { className: css.toolBody, children: [_jsxs("div", { className: css.toolToolbar, children: [_jsx("span", { children: t('lens.toolDefinition') }), _jsx("button", { type: "button", className: css.more, "aria-pressed": raw, onClick: () => setRaw(!raw), children: t(raw ? 'view.structured' : 'view.raw') })] }), _jsx("div", { className: css.prose, children: tool.description }), raw ? _jsx(RawSchema, { text: tool.parameters, t: t }, tool.parameters) : parsed ? _jsx(SchemaFields, { value: parsed.value, t: t }) : _jsx("div", { className: css.schemaCaption, children: t('schema.unreadable') })] })] });
48
+ }
49
+ export function ToolCatalog({ tools, t }) {
50
+ return _jsx("div", { className: css.toolCatalog, children: tools.map(tool => _jsx(ToolRow, { tool: tool, t: t }, tool.name)) });
51
+ }
@@ -0,0 +1,25 @@
1
+ import type { SessionTrend } from '../types.ts';
2
+ import type { InspectorTabProps } from './contract.ts';
3
+ import { type TurnGap } from './aggregate.ts';
4
+ /**
5
+ * The session map: the main conversation's requests across turns, filling
6
+ * the panel width on one baseline. Each turn is a cell (wider when it holds
7
+ * more requests); the current turn is a tinted band with its number in
8
+ * bold. A run of turns not loaded shows as a narrow cell with its range;
9
+ * clicking it loads 30 turns in that direction. A dashed line marks where a
10
+ * compaction happened and opens it; the
11
+ * turn's other participants show as tabs below the map, not on it.
12
+ */
13
+ export declare function Trend({ data, turnSeq, selectedSeq, scopeId, scopeLabel, onSelect, gaps, onLoadGap, t, }: {
14
+ readonly data: SessionTrend;
15
+ readonly turnSeq: number | null;
16
+ readonly selectedSeq: number | null;
17
+ readonly scopeId: string;
18
+ readonly scopeLabel: string;
19
+ readonly onSelect: (turnSeq: number, seq: number, scopeId: string) => void;
20
+ /** Runs of turns the log holds but the panel has not loaded. */
21
+ readonly gaps?: readonly TurnGap[];
22
+ readonly onLoadGap?: ((gap: TurnGap) => void) | undefined;
23
+ readonly t: InspectorTabProps['t'];
24
+ }): import("react").JSX.Element;
25
+ //# sourceMappingURL=Trend.d.ts.map
@@ -0,0 +1,100 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { requestNumberText, cachePct, gapShown, inputTotal, turnNumber } from "./aggregate.js";
4
+ import css from './InspectorPanel.module.css';
5
+ const isCompaction = (scopeId) => scopeId.startsWith('compaction:');
6
+ /** Smallest cell a turn may shrink to (one label); below it the map scrolls. */
7
+ const MIN_CELL = 22;
8
+ /** A turn's cell grows with its request count past this many. */
9
+ const CELL_SHARE = 3;
10
+ /** Bar width that fits every request across the map (2–6px). */
11
+ function fitWidth(box, requests, turns) {
12
+ if (requests === 0 || box === 0)
13
+ return 6;
14
+ return Math.max(2, Math.min(6, Math.floor((box - turns * 8) / requests) - 2));
15
+ }
16
+ /**
17
+ * The session map: the main conversation's requests across turns, filling
18
+ * the panel width on one baseline. Each turn is a cell (wider when it holds
19
+ * more requests); the current turn is a tinted band with its number in
20
+ * bold. A run of turns not loaded shows as a narrow cell with its range;
21
+ * clicking it loads 30 turns in that direction. A dashed line marks where a
22
+ * compaction happened and opens it; the
23
+ * turn's other participants show as tabs below the map, not on it.
24
+ */
25
+ export function Trend({ data, turnSeq, selectedSeq, scopeId, scopeLabel, onSelect, gaps = [], onLoadGap, t, }) {
26
+ const ref = useRef(null);
27
+ const [width, setWidth] = useState(0);
28
+ useEffect(() => {
29
+ const el = ref.current;
30
+ if (!el)
31
+ return;
32
+ setWidth(el.clientWidth);
33
+ if (typeof ResizeObserver === 'undefined')
34
+ return;
35
+ const observer = new ResizeObserver(entries => setWidth(entries[0]?.contentRect.width ?? el.clientWidth));
36
+ observer.observe(el);
37
+ return () => observer.disconnect();
38
+ }, []);
39
+ useEffect(() => {
40
+ const el = ref.current, current = el?.querySelector('[data-current="true"]');
41
+ if (el && current && el.scrollWidth > el.clientWidth)
42
+ el.scrollLeft = current.offsetLeft - el.clientWidth / 2 + current.offsetWidth / 2;
43
+ }, [turnSeq, scopeId, data, width]);
44
+ const main = scopeId === 'main';
45
+ // On the session map a room round's member runs stand where the main turn would.
46
+ const rounds = new Set(main ? data.turns.filter(turn => turn.room !== undefined).map(turn => turn.turnSeq) : []);
47
+ const requests = data.requests.filter(r => r.scopeId === scopeId || rounds.has(r.turnSeq));
48
+ const compactions = main ? data.requests.filter(r => isCompaction(r.scopeId)) : [];
49
+ const turns = data.turns.filter(turn => requests.some(r => r.turnSeq === turn.turnSeq));
50
+ const max = Math.max(1, ...requests.map(r => (r.usage ? inputTotal(r.usage) : 0)));
51
+ const barWidth = fitWidth(width, requests.length, turns.length);
52
+ /** Compactions of this turn that happened right before `next` (or at the turn's end). */
53
+ const dividersBefore = (turn, next, previous) => compactions.filter(c => c.turnSeq === turn
54
+ && (previous === undefined || c.startedAt >= previous.startedAt)
55
+ && (next === undefined || c.startedAt < next.startedAt));
56
+ const firstLoaded = Math.min(...turns.map(turn => turn.hostTurn ?? Number.POSITIVE_INFINITY));
57
+ const gapCell = (gap) => {
58
+ const shown = gapShown(gap);
59
+ const label = t(gap.to < firstLoaded ? 'trend.loadOlder' : 'trend.loadNewer', { from: String(shown.from), to: String(shown.to) });
60
+ return _jsxs("button", { type: "button", className: css.trendGap, title: label, "aria-label": label, onClick: () => onLoadGap?.(gap), children: [_jsx("span", { className: css.trendGapBars, "aria-hidden": "true", children: "\u22EF" }), _jsx("span", { className: css.trendTurn, children: shown.from === shown.to ? shown.from : `${shown.from}–${shown.to}` })] }, `gap-${gap.from}`);
61
+ };
62
+ /**
63
+ * Coordinator handoffs, drawn before the first turn after them. Several in
64
+ * one gap share ONE marker (side-by-side markers stack their labels):
65
+ * it names who holds the room by that turn; the tooltip keeps the chain.
66
+ */
67
+ const handoffs = main ? [...data.handoffs ?? []].sort((a, b) => a.at - b.at) : [];
68
+ const handoffsBefore = (turn) => {
69
+ const batch = [];
70
+ while (handoffs.length > 0 && handoffs[0].at <= turn.startedAt)
71
+ batch.push(handoffs.shift());
72
+ const last = batch.at(-1);
73
+ if (last === undefined)
74
+ return [];
75
+ const label = batch.length === 1
76
+ ? t('trend.handoff', { name: last.name })
77
+ : t('trend.handoffMany', { name: last.name, count: String(batch.length - 1) });
78
+ const title = batch.map(h => t('trend.handoff', { name: h.name })).join(' → ');
79
+ return [_jsx("span", { className: css.trendHandoff, title: title, "aria-label": title, children: _jsx("span", { children: label }) }, `handoff-${last.at}`)];
80
+ };
81
+ /** Gap cells that sit before a turn (all gaps ending before its number). */
82
+ const placed = new Set();
83
+ const gapsBefore = (n) => n === undefined ? [] : gaps.filter(gap => !placed.has(gap.from) && gapShown(gap).to < n).map(gap => { placed.add(gap.from); return gapCell(gap); });
84
+ const divider = (turn, c) => _jsx("button", { type: "button", className: css.trendCompaction, title: t('trend.compaction'), "aria-label": t('trend.compaction'), onClick: () => onSelect(turn, c.seq, c.scopeId) }, c.seq);
85
+ return (_jsxs("section", { "aria-label": t('lens.trendScope', { scope: scopeLabel }), children: [_jsxs("div", { ref: ref, className: css.trend, style: { ['--trend-bar']: `${barWidth}px` }, children: [turns.map((turn) => {
86
+ const rows = requests.filter(r => r.turnSeq === turn.turnSeq);
87
+ const minWidth = Math.max(MIN_CELL, rows.length * (barWidth + 3) + 8);
88
+ return [
89
+ ...gapsBefore(turnNumber(turn)),
90
+ ...handoffsBefore(turn),
91
+ _jsxs("div", { className: css.trendGroup, "data-current": turnSeq === turn.turnSeq, style: { flex: `${Math.max(1, rows.length / CELL_SHARE)} 1 ${minWidth}px`, minWidth }, children: [_jsxs("div", { className: css.trendBars, children: [rows.map((r, i) => {
92
+ const total = r.usage ? inputTotal(r.usage) : 0, hit = r.usage ? cachePct(r.usage) : null;
93
+ return [
94
+ ...dividersBefore(turn.turnSeq, r, rows[i - 1]).map(c => divider(turn.turnSeq, c)),
95
+ _jsx("button", { type: "button", className: css.trendTarget, "data-selected": r.seq === selectedSeq, "aria-pressed": r.seq === selectedSeq, "aria-label": `${t('turn.label', { seq: String(turnNumber(turn) ?? turn.turnSeq) })} #${requestNumberText(r)} · ${hit ?? '—'}%`, title: `#${requestNumberText(r)} · ${total} tok · ${hit ?? '—'}%`, onClick: () => onSelect(turn.turnSeq, r.seq, r.scopeId), children: _jsx("span", { className: css.trendBar, "data-unknown": hit === null, "aria-hidden": "true", style: { height: `${Math.max(3, (total / max) * 40)}px` }, children: _jsx("i", { style: { height: `${hit ?? 0}%` } }) }) }, r.seq),
96
+ ];
97
+ }), dividersBefore(turn.turnSeq, undefined, rows.at(-1)).map(c => divider(turn.turnSeq, c))] }), _jsx("span", { className: css.trendTurn, children: turnNumber(turn) ?? '·' })] }, turn.turnSeq),
98
+ ];
99
+ }), gaps.filter(gap => !placed.has(gap.from)).map(gapCell)] }), requests.length === 0 && _jsx("span", { className: css.trendLabel, children: t('lens.trendEmpty') })] }));
100
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 「请求明细」: the per-turn icon in the finalized assistant message's
3
+ * IconActions row (`conversation.chat.assistant-actions`, the
4
+ * ui-message-feedback seat). One click resolves the message's turn
5
+ * (`TurnListItem.closingMessageId`, latest-turn fallback — the resolution
6
+ * lives in the injected `open`) and opens the inspector tab on it.
7
+ *
8
+ * @module @khorsheed/dsh-context-lens/client
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ import type { InspectorActionProps } from './contract.ts';
12
+ /** One message's context-lens control. */
13
+ export declare function TurnInspectAction({ messageId, sessionId, open, t }: InspectorActionProps): ReactNode;
14
+ //# sourceMappingURL=TurnAction.d.ts.map
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * 「请求明细」: the per-turn icon in the finalized assistant message's
4
+ * IconActions row (`conversation.chat.assistant-actions`, the
5
+ * ui-message-feedback seat). One click resolves the message's turn
6
+ * (`TurnListItem.closingMessageId`, latest-turn fallback — the resolution
7
+ * lives in the injected `open`) and opens the inspector tab on it.
8
+ *
9
+ * @module @khorsheed/dsh-context-lens/client
10
+ */
11
+ import { useEffect, useRef, useState } from 'react';
12
+ import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
13
+ import { IconRequestInspectorOutlineMedium } from "./icons-local.js";
14
+ import css from './EntryIcon.module.css';
15
+ /** One message's context-lens control. */
16
+ export function TurnInspectAction({ messageId, sessionId, open, t }) {
17
+ const [busy, setBusy] = useState(false);
18
+ const alive = useRef(true);
19
+ useEffect(() => () => { alive.current = false; }, []);
20
+ return (_jsx(Tooltip, { label: t('action.inspect'), side: "bottom", children: _jsx("button", { type: "button", className: css.action, "aria-label": t('action.inspect'), disabled: busy, onClick: () => {
21
+ if (busy)
22
+ return;
23
+ setBusy(true);
24
+ // open never throws: resolution failure falls back to the latest
25
+ // turn, a missing tab type logs and no-ops.
26
+ void open(sessionId, String(messageId)).finally(() => {
27
+ if (alive.current)
28
+ setBusy(false);
29
+ });
30
+ }, children: _jsx(IconRequestInspectorOutlineMedium, {}) }) }));
31
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The turn switcher: a trigger button (`turn N · query 预览`) opening the
3
+ * host's own `Menu` (ui-primitives — keyboard walk, Escape / outside-click,
4
+ * and focus return are the host's), one row per turn: turn number + query
5
+ * preview on the first line, request count and token totals on the second.
6
+ * Purpose pseudo-turns (no `hostTurn`) show their purpose label without a
7
+ * turn number, same as the query line.
8
+ *
9
+ * @module @khorsheed/dsh-context-lens/client
10
+ */
11
+ import { type ReactNode } from 'react';
12
+ import type { TurnListItem } from '../types.ts';
13
+ import { type TurnGap } from './aggregate.ts';
14
+ import type { InspectorTabProps } from './contract.ts';
15
+ /** The panel's translate face. */
16
+ type T = InspectorTabProps['t'];
17
+ /** One turn's one-line title: `turn N · query` for real turns, the purpose label otherwise. */
18
+ export declare function turnTitle(turn: TurnListItem, t: T): string;
19
+ export interface TurnMenuProps {
20
+ /** The session's turns, newest last. */
21
+ readonly turns: readonly TurnListItem[];
22
+ /** The selected turn's seq (null while the list loads). */
23
+ readonly turnSeq: number | null;
24
+ /** Row activation: select that turn. */
25
+ readonly onSelect: (turnSeq: number) => void;
26
+ /** Runs of turns not loaded; each becomes a row that loads it. */
27
+ readonly gaps?: readonly TurnGap[];
28
+ /** The log's last host turn, for the `N / total` label. */
29
+ readonly lastTurn?: number | undefined;
30
+ readonly onLoad?: (gap: TurnGap) => void;
31
+ readonly t: T;
32
+ }
33
+ /** The turn switcher menu. */
34
+ export declare function TurnMenu({ turns, turnSeq, onSelect, gaps, lastTurn, onLoad, t }: TurnMenuProps): ReactNode;
35
+ export {};
36
+ //# sourceMappingURL=TurnMenu.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The turn switcher: a trigger button (`turn N · query 预览`) opening the
4
+ * host's own `Menu` (ui-primitives — keyboard walk, Escape / outside-click,
5
+ * and focus return are the host's), one row per turn: turn number + query
6
+ * preview on the first line, request count and token totals on the second.
7
+ * Purpose pseudo-turns (no `hostTurn`) show their purpose label without a
8
+ * turn number, same as the query line.
9
+ *
10
+ * @module @khorsheed/dsh-context-lens/client
11
+ */
12
+ import { useState } from 'react';
13
+ import { Menu } from '@deepseek-ai/dsh-client-ui-primitives';
14
+ import { fmtK, gapShown, turnNumber } from "./aggregate.js";
15
+ import { IconChevronDownOutlineMedium } from "./icons.js";
16
+ import css from './TurnMenu.module.css';
17
+ /** One turn's one-line title: `turn N · query` for real turns, the purpose label otherwise. */
18
+ export function turnTitle(turn, t) {
19
+ const n = turnNumber(turn);
20
+ return turn.hostTurn === undefined && turn.room === undefined
21
+ ? turn.query === 'compaction' ? t('compaction.title') : turn.query
22
+ : `${t('turn.label', { seq: String(n ?? '·') })} · ${turn.room !== undefined ? `[${t('query.room')}] ` : ''}${turn.query !== '' ? turn.query : turn.trigger !== undefined ? `[${turn.trigger.source}] ${turn.trigger.text}` : ''}`;
23
+ }
24
+ /** One menu row: the title line, then request count + token totals. */
25
+ function TurnRow({ turn, t }) {
26
+ return (_jsxs("span", { className: css.row, children: [_jsx("span", { className: css.rowTitle, children: turnTitle(turn, t) }), _jsx("span", { className: css.rowMeta, children: t('turn.rowMeta', {
27
+ count: String(turn.requestCount),
28
+ input: fmtK(turn.totalInputTokens),
29
+ output: fmtK(turn.totalOutputTokens),
30
+ }) })] }));
31
+ }
32
+ /** The turn switcher menu. */
33
+ export function TurnMenu({ turns, turnSeq, onSelect, gaps = [], lastTurn, onLoad, t }) {
34
+ const [open, setOpen] = useState(false);
35
+ const current = turns.find(candidate => candidate.turnSeq === turnSeq);
36
+ return (_jsx(Menu, { open: open, portal: true, compact: true, selectedId: turnSeq === null ? undefined : String(turnSeq),
37
+ // Newest first: the overwhelmingly common destination is a recent turn.
38
+ items: [
39
+ ...[...turns].reverse().map(candidate => ({ key: turnNumber(candidate) ?? Number.NEGATIVE_INFINITY, id: String(candidate.turnSeq), label: _jsx(TurnRow, { turn: candidate, t: t }) })),
40
+ ...gaps.map(gap => ({ key: gapShown(gap).to, id: `gap:${gap.from}:${gap.to}`, label: _jsx("span", { className: css.gapRow, children: t('turn.loadRange', { from: String(gapShown(gap).from), to: String(gapShown(gap).to) }) }) })),
41
+ ].sort((x, y) => y.key - x.key).map(({ id, label }) => ({ id, label })), onSelect: id => {
42
+ setOpen(false);
43
+ if (id.startsWith('gap:')) {
44
+ const [, from, to] = id.split(':');
45
+ onLoad?.({ from: Number(from), to: Number(to) });
46
+ return;
47
+ }
48
+ onSelect(Number(id));
49
+ }, onClose: () => { setOpen(false); }, anchor: (_jsxs("button", { type: "button", className: css.trigger, "aria-haspopup": "menu", "aria-expanded": open, "aria-label": t('turn.switch'), onClick: () => { setOpen(value => !value); }, children: [_jsx("span", { className: css.triggerLabel, children: current === undefined ? t('turn.switch') : `${turnNumber(current) ?? (current.query === 'compaction' ? t('compaction.title') : current.query)} / ${lastTurn ?? turns.at(-1)?.hostTurn ?? turns.length}` }), _jsx(IconChevronDownOutlineMedium, {})] })) }));
50
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The client panel's pure aggregation and formatting helpers — everything the
3
+ * views compute from the Remote payloads, in one testable module. No React,
4
+ * no I/O, no clock.
5
+ *
6
+ * @module @khorsheed/dsh-context-lens/client/aggregate
7
+ */
8
+ import type { CapturedRequest, RequestUsage, ResponseSummary, SegmentCategory, SegmentItem, TimingLane, TurnListItem } from '../types.ts';
9
+ /**
10
+ * Render one {@link ResponseSummary} for the card title / anchor header:
11
+ * the tool case groups repeated names as `name ×N` (single calls keep hints), the text case is the
12
+ * caller's localized assistant reply, the reasoning case is a pure thinking response, an empty
13
+ * response is the plain dash.
14
+ */
15
+ export declare function summaryText(summary: ResponseSummary, assistantReply: string, compaction?: string, thinking?: string): string;
16
+ /**
17
+ * Resolve an assistant message id to its turn: the turn whose
18
+ * `closingMessageId` matches, else the latest turn (the list arrives newest
19
+ * last), else undefined when the session has no captured turns.
20
+ */
21
+ export declare function resolveTurnSeq(turns: readonly TurnListItem[], messageId: string | undefined): number | undefined;
22
+ /** The latest turn's seq (the panel's default), undefined on an empty list. */
23
+ export declare function latestTurnSeq(turns: readonly TurnListItem[]): number | undefined;
24
+ /** Turns per backfill window (the store's default `maxTurnsPerSession`). */
25
+ export declare const WINDOW_TURNS = 30;
26
+ /** A run of host turns the log holds but the panel has not loaded; `shown` is its span in the session's turn numbering. */
27
+ export interface TurnGap {
28
+ from: number;
29
+ to: number;
30
+ shown?: {
31
+ from: number;
32
+ to: number;
33
+ };
34
+ }
35
+ /** The unloaded runs: the store's (numbered with room rounds), else derived from the log's host-turn range. */
36
+ export declare function turnGaps(list: {
37
+ range?: {
38
+ first: number;
39
+ last: number;
40
+ };
41
+ unloaded?: {
42
+ from: number;
43
+ to: number;
44
+ firstHost: number;
45
+ lastHost: number;
46
+ }[];
47
+ } | undefined, turns: readonly TurnListItem[]): TurnGap[];
48
+ /** A gap's displayed span. */
49
+ export declare const gapShown: (gap: TurnGap) => {
50
+ from: number;
51
+ to: number;
52
+ };
53
+ /** A turn's displayed number: its place in the session's numbering, else its host turn. */
54
+ export declare const turnNumber: (turn: TurnListItem) => number | undefined;
55
+ /** The runs of turns in the log's range that the loaded list lacks, oldest first. */
56
+ export declare function unloadedGaps(range: {
57
+ first: number;
58
+ last: number;
59
+ } | undefined, turns: readonly TurnListItem[]): TurnGap[];
60
+ /** Total input = uncached + cache-read + cache-write (the host's usage fields are disjoint). */
61
+ export declare function inputTotal(usage: RequestUsage): number;
62
+ /** Input NOT served from cache = uncached + cache-write (a cache write was processed in full too). */
63
+ export declare function inputMiss(usage: RequestUsage): number;
64
+ /** Cache-hit percentage of the total input, one decimal; null when nothing was read. */
65
+ export declare function cachePct(usage: RequestUsage): number | null;
66
+ /** One row of the category stacked bar / legend. */
67
+ export interface CategoryAggregate {
68
+ readonly category: SegmentCategory;
69
+ /** Rows (messages/sections) in the category; tool definitions count once per tool. */
70
+ readonly count: number;
71
+ readonly chars: number;
72
+ }
73
+ /**
74
+ * One request's composition by category: the system/history/added message
75
+ * items plus the tools segment as `tool-definition`, sorted by chars desc.
76
+ */
77
+ export declare function aggregateCategories(request: CapturedRequest): CategoryAggregate[];
78
+ /** The timing vocabulary, in timeline order (the design's six-way split). */
79
+ export declare const TIMING_KINDS: readonly ["wait", "reasoning", "tool-args", "text", "tool-exec", "other"];
80
+ /** One timing bucket kind. */
81
+ export type TimingKind = (typeof TIMING_KINDS)[number];
82
+ /** Per-tool timing aggregate, derived from the lanes (tool-exec = calls/duration, tool-args = argument generation). */
83
+ export interface ToolAggregate {
84
+ readonly tool: string;
85
+ readonly calls: number;
86
+ readonly argsMs: number;
87
+ readonly execMs: number;
88
+ }
89
+ /** One request's lane sums (per kind) and wall span. */
90
+ export interface RequestLaneSummary {
91
+ readonly requestSeq: number;
92
+ readonly scopeId: string;
93
+ readonly startedAt: number;
94
+ readonly endedAt: number;
95
+ readonly byKind: Readonly<Record<TimingKind, number>>;
96
+ }
97
+ /** The timing tab's lane-derived view of (a scope of) a turn. */
98
+ export interface LaneSummary {
99
+ /** Summed milliseconds per bucket, inter-lane gaps folded into `other`. */
100
+ readonly buckets: Readonly<Record<TimingKind, number>>;
101
+ /** Lane count per kind (the legend's · N 次). */
102
+ readonly counts: Readonly<Record<TimingKind, number>>;
103
+ /** First lane start; 0 when empty. */
104
+ readonly t0: number;
105
+ /** Last lane end; 0 when empty. */
106
+ readonly t1: number;
107
+ /** Per-tool rows, exec time desc. */
108
+ readonly tools: readonly ToolAggregate[];
109
+ /** Per-request rows, in time order. */
110
+ readonly requests: readonly RequestLaneSummary[];
111
+ }
112
+ /**
113
+ * Reduce the turn's lanes to the timing tab's data. Lanes are strictly
114
+ * ordered and carry their scope, so a scope filter is exact; gaps between
115
+ * lanes (inter-request overhead) fold into `other`, the reference design's
116
+ * 调用间隙并入其他开销. Whole-turn, the bucket sums equal the host's
117
+ * `TurnTiming.buckets` by construction (the host sums the same lanes).
118
+ */
119
+ export declare function summarizeLanes(lanes: readonly TimingLane[], scopeId?: string): LaneSummary;
120
+ /** Thousands-separated integer (the design's fmt). */
121
+ export declare function fmt(n: number): string;
122
+ /** Compact count: 12.3k over a thousand (the design's fmtK). */
123
+ export declare function fmtK(n: number): string;
124
+ /** Wall-clock HH:MM of a timestamp (the timeline scale). */
125
+ export declare function fmtClock(timestamp: number): string;
126
+ /** Row-level duration: milliseconds under a second, whole seconds above (the design's fmtDur). */
127
+ export declare function fmtDur(ms: number): string;
128
+ /** Span duration: one decimal in seconds, minutes above 60s (the design's ms()). */
129
+ export declare function fmtSpan(ms: number): string;
130
+ /** A composition filter never changes the request totals or exchange. */
131
+ export type CompositionFilter = SegmentCategory | '__dup' | null;
132
+ /** Exact full-text matches, excluding the first occurrence from excess size. */
133
+ export declare function duplicateStats(items: readonly SegmentItem[]): {
134
+ counts: Map<string, number>;
135
+ extraChars: number;
136
+ };
137
+ /** Public ordinal only. The internal seq must never be a display fallback. */
138
+ export declare function requestNumberText(request: {
139
+ requestNumber?: number | undefined;
140
+ } | null | undefined): string;
141
+ //# sourceMappingURL=aggregate.d.ts.map