@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,486 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Trend } from "./Trend.js";
3
+ import { AgentTabs, ParentLine, ROOM_TAB } from "./AgentTabs.js";
4
+ import { RoomDecisionsView } from "./RoomDecisionsView.js";
5
+ /** Session/turn/scope navigation; request detail owns its JSON toggle. */
6
+ import { useEffect, useRef, useState } from 'react';
7
+ import { PanelRoot } from '@khorsheed/dsh-client-ui-panel-kit/src/client/index.ts';
8
+ import { WINDOW_TURNS, fmtSpan, gapShown, latestTurnSeq, resolveTurnSeq, turnGaps, turnNumber } from "./aggregate.js";
9
+ import { hasTurnAnchor, scrollConversationToBottom, scrollConversationToTurn } from "./scroll-sync.js";
10
+ import { SegCtl } from "./SegCtl.js";
11
+ import { TimingView } from "./TimingView.js";
12
+ import { TokenView } from "./TokenView.js";
13
+ import { TurnMenu } from "./TurnMenu.js";
14
+ import css from './InspectorPanel.module.css';
15
+ /** The failure state page — a failed run with 0 requests IS the diagnostic. */
16
+ function FailureCard({ scope, t }) {
17
+ const duration = scope.endedAt === undefined ? '—' : fmtSpan(Math.max(0, scope.endedAt - scope.startedAt));
18
+ return (_jsxs("div", { className: css.failCard, role: "alert", children: [_jsxs("div", { className: css.failRow, children: [_jsx("span", { className: css.failDot }), _jsx("b", { children: t('run.failed') }), scope.error !== undefined && _jsx("span", { className: css.failMono, children: scope.error }), _jsx("span", { className: css.failMeta, children: t('run.failedMeta', { duration, count: String(scope.requests.length) }) })] }), scope.requests.length === 0 && (_jsx("div", { className: css.failHint, children: t('run.failedHint', { id: `${scope.scopeId.slice(0, 8)}…` }) }))] }));
19
+ }
20
+ /** The tab body. */
21
+ /** The list's window facts the panel keeps beside its turns. */
22
+ const listMetaOf = ({ range, unloaded, total }) => ({ ...range === undefined ? {} : { range }, ...unloaded === undefined ? {} : { unloaded }, ...total === undefined ? {} : { total } });
23
+ export function InspectorPanel({ sessionId: ownSessionId, useTabInfo, t, listTurns, loadTurns, loadThrough, getTurn, getRequest, getTurnTiming, getSessionTrend, }) {
24
+ const { tab } = useTabInfo();
25
+ // A navigation may point the panel at another session (a room member's
26
+ // child session); the next navigation without one returns to the own session.
27
+ const [viewed, setViewed] = useState(null);
28
+ const sessionId = (viewed ?? ownSessionId);
29
+ /** The message whose turn the next turn list should select (from a navigation). */
30
+ const pendingMessage = useRef(undefined);
31
+ /** The room decision whose turn the next turn list should select (from a navigation). */
32
+ const pendingDecision = useRef(undefined);
33
+ /** The selected room decision on the 房间 tab. */
34
+ const [decisionId, setDecisionId] = useState(null);
35
+ const [trend, setTrend] = useState(null);
36
+ /** The latest conversation jump; an older one still waiting for history gives up. */
37
+ const jumpToken = useRef(0);
38
+ /** Bumped when the loaded window moves, so the session map refetches. */
39
+ const [trendKey, setTrendKey] = useState(0);
40
+ useEffect(() => {
41
+ let cancelled = false;
42
+ setTrend(null);
43
+ if (getSessionTrend)
44
+ void getSessionTrend(sessionId).then(r => { if (!cancelled && r.ok)
45
+ setTrend(r.value); }).catch(() => { });
46
+ return () => { cancelled = true; };
47
+ }, [sessionId, getSessionTrend, tab.navigation.revision, trendKey]);
48
+ const [turns, setTurns] = useState(null);
49
+ const [backfill, setBackfill] = useState(null);
50
+ const [parent, setParent] = useState(null);
51
+ const [listMeta, setListMeta] = useState({});
52
+ const range = listMeta.range;
53
+ const [turnSeq, setTurnSeq] = useState(null);
54
+ const [overview, setOverview] = useState(null);
55
+ const [timing, setTiming] = useState(null);
56
+ const [request, setRequest] = useState(null);
57
+ const [mainTab, setMainTab] = useState('token');
58
+ const [filterCat, setFilterCat] = useState(null);
59
+ const [requestTrail, setRequestTrail] = useState([]);
60
+ const [scopeId, setScopeId] = useState('main');
61
+ const [selectedSeq, setSelectedSeq] = useState(null);
62
+ const [error, setError] = useState(null);
63
+ const [turnMissing, setTurnMissing] = useState(false);
64
+ /** Bump to re-run the pull (the error state's retry). */
65
+ const [reloadKey, setReloadKey] = useState(0);
66
+ /** The selected turn's fetched request details (seq → capture). */
67
+ const detailCache = useRef(new Map());
68
+ /** Retry a failed load: back to the loading state, re-run the pull. */
69
+ const onRetry = () => {
70
+ setError(null);
71
+ setRequestTrail([]);
72
+ setTurns(null);
73
+ setOverview(null);
74
+ setReloadKey(key => key + 1);
75
+ };
76
+ // Navigation params apply once per revision (a repeat open re-navigates the
77
+ // one tab): the turn action's { turnSeq } selects that turn and scroll-syncs
78
+ // the conversation to it.
79
+ const revision = tab.navigation.revision;
80
+ const navParams = tab.navigation.params;
81
+ const navTurnSeq = navParams?.turnSeq;
82
+ const lastNavRevision = useRef(0);
83
+ useEffect(() => {
84
+ if (revision === 0 || revision === lastNavRevision.current)
85
+ return;
86
+ lastNavRevision.current = revision;
87
+ const target = navParams?.sessionId;
88
+ setViewed(target === undefined || target === ownSessionId ? null : target);
89
+ pendingMessage.current = navParams?.messageId;
90
+ pendingDecision.current = navParams?.roomDecision;
91
+ if (navParams?.roomDecision !== undefined) {
92
+ setTurnSeq(null);
93
+ setScopeId(ROOM_TAB);
94
+ setDecisionId(navParams.roomDecision);
95
+ }
96
+ if (navParams?.messageId !== undefined) {
97
+ setTurnSeq(null);
98
+ setScopeId('main');
99
+ setSelectedSeq(null);
100
+ setFilterCat(null);
101
+ setMainTab('token');
102
+ }
103
+ if (navTurnSeq === undefined)
104
+ return;
105
+ setTurnSeq(navTurnSeq);
106
+ setScopeId('main');
107
+ setSelectedSeq(null);
108
+ setFilterCat(null);
109
+ setMainTab('token');
110
+ // Real turns key on the host's turn number, so a positive seq scrolls even
111
+ // before the turn list resolves; pseudo-turns (negative) never scroll.
112
+ if (navTurnSeq > 0 && navParams?.sessionId === undefined)
113
+ scrollConversationToTurn(navTurnSeq);
114
+ }, [revision, navTurnSeq, navParams, ownSessionId]);
115
+ // The session's turn list, on mount (pull-based: refetch = reopen or retry).
116
+ useEffect(() => {
117
+ let cancelled = false;
118
+ setError(null);
119
+ void listTurns(sessionId).then(result => {
120
+ if (cancelled || tab.signal.aborted)
121
+ return;
122
+ if (!result.ok) {
123
+ setError(result.error.message);
124
+ setTurns([]);
125
+ return;
126
+ }
127
+ setTurns(result.value.turns);
128
+ setBackfill(result.value.backfill);
129
+ setParent(result.value.parent ?? null);
130
+ setListMeta(listMetaOf(result.value));
131
+ const decided = pendingDecision.current === undefined ? undefined
132
+ : result.value.turns.find(turn => turn.roomDecisionIds?.includes(pendingDecision.current) === true)?.turnSeq;
133
+ pendingDecision.current = undefined;
134
+ const asked = decided ?? (pendingMessage.current === undefined ? undefined : resolveTurnSeq(result.value.turns, pendingMessage.current));
135
+ pendingMessage.current = undefined;
136
+ setTurnSeq(previous => asked !== undefined
137
+ ? asked
138
+ : previous !== null && result.value.turns.some(turn => turn.turnSeq === previous)
139
+ ? previous
140
+ : latestTurnSeq(result.value.turns) ?? null);
141
+ }).catch((cause) => {
142
+ // A call that rejects (e.g. the gateway's arity gate, a disconnected
143
+ // carrier) is the same load failure as a refusal: the error state, not
144
+ // a spinner that never resolves.
145
+ if (!cancelled)
146
+ setError(cause instanceof Error ? cause.message : String(cause));
147
+ });
148
+ return () => { cancelled = true; };
149
+ }, [sessionId, listTurns, tab.signal, reloadKey, revision]);
150
+ // The selected turn's scope/request overview.
151
+ useEffect(() => {
152
+ if (turnSeq === null) {
153
+ setOverview(null);
154
+ return;
155
+ }
156
+ let cancelled = false;
157
+ const seq = turnSeq;
158
+ detailCache.current.clear();
159
+ setTurnMissing(false);
160
+ void getTurn(sessionId, seq).then(result => {
161
+ if (cancelled || tab.signal.aborted)
162
+ return;
163
+ if (!result.ok) {
164
+ setError(result.error.message);
165
+ return;
166
+ }
167
+ if (result.value === undefined) {
168
+ // The turn left the capture's ring buffer (eviction) between the list
169
+ // and this read — an honest empty state, not a spinner forever.
170
+ setTurnMissing(true);
171
+ setOverview(null);
172
+ return;
173
+ }
174
+ setOverview(result.value);
175
+ setTiming(null);
176
+ setRequest(null);
177
+ }).catch((cause) => {
178
+ if (!cancelled)
179
+ setError(cause instanceof Error ? cause.message : String(cause));
180
+ });
181
+ return () => { cancelled = true; };
182
+ }, [sessionId, turnSeq, getTurn, tab.signal]);
183
+ const scopes = overview?.scopes ?? [];
184
+ const scope = scopes.find(candidate => candidate.scopeId === scopeId)
185
+ ?? scopes.find(candidate => candidate.kind === 'main')
186
+ ?? scopes[0];
187
+ const effectiveScope = scope;
188
+ const scopeRequests = effectiveScope?.requests ?? [];
189
+ const selectedItem = scopeRequests.find(candidate => candidate.seq === selectedSeq) ?? scopeRequests[0];
190
+ const selectedRequestSeq = selectedItem?.seq ?? null;
191
+ const selectedIndex = selectedRequestSeq === null
192
+ ? -1
193
+ : scopeRequests.findIndex(candidate => candidate.seq === selectedRequestSeq);
194
+ // The 新增 provenance label's N: the previous request in the SAME scope.
195
+ // Scopes are continuous across turns, so the scope's first request of a turn
196
+ // looks back to the previous REAL turn's last request in that scope — a lazy
197
+ // fetch, only while the selection needs it.
198
+ const previousRealTurnSeq = (() => {
199
+ if (turns === null || turnSeq === null)
200
+ return undefined;
201
+ const index = turns.findIndex(candidate => candidate.turnSeq === turnSeq);
202
+ for (let i = index - 1; i >= 0; i -= 1) {
203
+ const candidate = turns[i];
204
+ if (candidate !== undefined && candidate.hostTurn !== undefined)
205
+ return candidate.turnSeq;
206
+ }
207
+ return undefined;
208
+ })();
209
+ const needsPreviousTurn = selectedIndex === 0 && previousRealTurnSeq !== undefined;
210
+ const [previousTurn, setPreviousTurn] = useState(null);
211
+ useEffect(() => {
212
+ if (!needsPreviousTurn || previousRealTurnSeq === undefined) {
213
+ if (previousTurn !== null)
214
+ setPreviousTurn(null);
215
+ return;
216
+ }
217
+ let cancelled = false;
218
+ const seq = previousRealTurnSeq;
219
+ void getTurn(sessionId, seq).then(result => {
220
+ if (cancelled || tab.signal.aborted)
221
+ return;
222
+ if (result.ok)
223
+ setPreviousTurn(result.value ?? null);
224
+ }).catch(() => { });
225
+ return () => { cancelled = true; };
226
+ // previousTurn is the effect's own sink — depending on it would refetch in a loop.
227
+ // eslint-disable-next-line react-hooks/exhaustive-deps
228
+ }, [sessionId, needsPreviousTurn, previousRealTurnSeq, getTurn, tab.signal]);
229
+ const previousResponseNumber = (() => {
230
+ if (selectedItem?.responseSummary.kind === 'compaction')
231
+ return null;
232
+ if (selectedIndex > 0)
233
+ return scopeRequests.slice(0, selectedIndex).filter(r => r.responseSummary.kind !== 'compaction').at(-1)?.requestNumber ?? null;
234
+ if (selectedIndex === 0 && effectiveScope !== undefined && previousTurn !== null) {
235
+ const previousScope = previousTurn.scopes.find(candidate => candidate.scopeId === effectiveScope.scopeId);
236
+ return previousScope?.requests.filter(r => r.responseSummary.kind !== 'compaction').at(-1)?.requestNumber ?? null;
237
+ }
238
+ return null;
239
+ })();
240
+ // The selected request's full capture (cached per turn).
241
+ useEffect(() => {
242
+ if (selectedRequestSeq === null) {
243
+ setRequest(null);
244
+ return;
245
+ }
246
+ const cached = detailCache.current.get(selectedRequestSeq);
247
+ if (cached !== undefined) {
248
+ setRequest(cached);
249
+ return;
250
+ }
251
+ setRequest(null);
252
+ let cancelled = false;
253
+ const seq = selectedRequestSeq;
254
+ void getRequest(sessionId, seq).then(result => {
255
+ if (cancelled || tab.signal.aborted)
256
+ return;
257
+ // The panel never sends a scope filter, so a refusal cannot arrive;
258
+ // the narrow is for the wire type only.
259
+ if (result.ok && result.value !== undefined && !('refused' in result.value)) {
260
+ detailCache.current.set(seq, result.value);
261
+ setRequest(result.value);
262
+ }
263
+ else if (!result.ok) {
264
+ setError(result.error.message);
265
+ }
266
+ }).catch((cause) => {
267
+ if (!cancelled)
268
+ setError(cause instanceof Error ? cause.message : String(cause));
269
+ });
270
+ return () => { cancelled = true; };
271
+ }, [sessionId, selectedRequestSeq, getRequest, tab.signal]);
272
+ // The timing tab's whole-turn aggregation.
273
+ useEffect(() => {
274
+ if (mainTab !== 'time' || turnSeq === null)
275
+ return;
276
+ let cancelled = false;
277
+ const seq = turnSeq;
278
+ void getTurnTiming(sessionId, seq).then(result => {
279
+ if (cancelled || tab.signal.aborted)
280
+ return;
281
+ if (result.ok)
282
+ setTiming(result.value ?? null);
283
+ else
284
+ setError(result.error.message);
285
+ }).catch((cause) => {
286
+ if (!cancelled)
287
+ setError(cause instanceof Error ? cause.message : String(cause));
288
+ });
289
+ return () => { cancelled = true; };
290
+ }, [sessionId, turnSeq, mainTab, getTurnTiming, tab.signal]);
291
+ const turn = turns?.find(candidate => candidate.turnSeq === turnSeq);
292
+ /** The shared per-turn selection reset (no scrolling — the caller picks the anchor). */
293
+ const selectTurnState = (next) => {
294
+ setTurnSeq(next);
295
+ setScopeId('main');
296
+ setSelectedSeq(null);
297
+ setFilterCat(null);
298
+ setOverview(null);
299
+ setRequest(null);
300
+ setTiming(null);
301
+ };
302
+ /** Scroll the conversation to one selected turn (real turns only; pseudo-turns never scroll). */
303
+ const syncScrollToTurn = (seq, list = turns) => {
304
+ const target = list?.find(candidate => candidate.turnSeq === seq);
305
+ // Real turns key on the host's turn number (M5), so a positive seq is the
306
+ // anchor even before the list resolves (the navigation-params path).
307
+ const hostTurn = target?.hostTurn ?? (seq > 0 ? seq : undefined);
308
+ if (hostTurn === undefined)
309
+ return;
310
+ const jump = ++jumpToken.current;
311
+ if (hasTurnAnchor(hostTurn) || !loadThrough || target?.startSeq === undefined) {
312
+ scrollConversationToTurn(hostTurn);
313
+ return;
314
+ }
315
+ // The chat has not rendered that turn yet: page its history back to the
316
+ // turn's start (the session's own loader), then land once the row mounts.
317
+ // A newer jump supersedes this one; no progress falls back to the nearest row.
318
+ void loadThrough(sessionId, target.startSeq).catch(() => undefined).then(() => {
319
+ let frames = 0;
320
+ const land = () => {
321
+ if (jump !== jumpToken.current)
322
+ return;
323
+ if (hasTurnAnchor(hostTurn) || frames >= 60) {
324
+ scrollConversationToTurn(hostTurn);
325
+ return;
326
+ }
327
+ frames += 1;
328
+ requestAnimationFrame(land);
329
+ };
330
+ land();
331
+ });
332
+ };
333
+ const onSelectTurn = (next) => {
334
+ if (next === turnSeq)
335
+ return;
336
+ selectTurnState(next);
337
+ syncScrollToTurn(next);
338
+ };
339
+ /** The latest REAL turn or room round — the 最新 button's target (pseudo-turns have no conversation anchor). */
340
+ const latestRealTurn = (turns ?? []).filter(candidate => candidate.hostTurn !== undefined || candidate.room !== undefined).at(-1);
341
+ const latestMainTurn = (turns ?? []).filter(candidate => candidate.hostTurn !== undefined).at(-1);
342
+ const gaps = turnGaps(listMeta, turns ?? []);
343
+ const latestUnloaded = range !== undefined && latestMainTurn?.hostTurn !== undefined && range.last > latestMainTurn.hostTurn;
344
+ /** Move the loaded window (latest when `end` is null), then select `select` or the newest loaded turn. */
345
+ const loadWindow = (end, select) => {
346
+ if (!loadTurns)
347
+ return;
348
+ void loadTurns(sessionId, end).then(result => {
349
+ if (!result.ok) {
350
+ setError(result.error.message);
351
+ return;
352
+ }
353
+ setTurns(result.value.turns);
354
+ setBackfill(result.value.backfill);
355
+ setListMeta(listMetaOf(result.value));
356
+ setTrendKey(key => key + 1);
357
+ const target = select !== undefined && result.value.turns.some(turn => turn.turnSeq === select) ? select : latestTurnSeq(result.value.turns);
358
+ if (target !== undefined) {
359
+ selectTurnState(target);
360
+ syncScrollToTurn(target, result.value.turns);
361
+ }
362
+ }).catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)));
363
+ };
364
+ /** A run of unloaded turns: older runs load the 30 ending at its end, newer runs the 30 starting at its start. */
365
+ const onLoadGap = (gap) => {
366
+ const firstLoaded = Math.min(...(turns ?? []).map(turn => turn.hostTurn ?? Number.POSITIVE_INFINITY));
367
+ if (gap.to < firstLoaded)
368
+ loadWindow(gap.to, gap.to);
369
+ else
370
+ loadWindow(Math.min(gap.from + WINDOW_TURNS - 1, range?.last ?? gap.to), gap.from);
371
+ };
372
+ const onLatest = () => {
373
+ if (latestUnloaded) {
374
+ loadWindow(null);
375
+ scrollConversationToBottom();
376
+ return;
377
+ }
378
+ if (latestRealTurn === undefined)
379
+ return;
380
+ if (turnSeq !== latestRealTurn.turnSeq)
381
+ selectTurnState(latestRealTurn.turnSeq);
382
+ scrollConversationToBottom();
383
+ };
384
+ /** The unloaded run right next to a turn, in the session's turn numbering. */
385
+ const adjacentGap = (turn, delta) => {
386
+ const n = turn === undefined ? undefined : turnNumber(turn);
387
+ return n === undefined ? undefined : gaps.find(g => delta < 0 ? gapShown(g).to === n - 1 : gapShown(g).from === n + 1);
388
+ };
389
+ /** Step one turn; at the loaded window's edge, load the adjacent window and keep going. */
390
+ const onStep = (delta) => {
391
+ const list = turns ?? [];
392
+ const i = list.findIndex(v => v.turnSeq === turnSeq);
393
+ const next = list[i + delta];
394
+ const gap = adjacentGap(list[i], delta);
395
+ if (gap !== undefined)
396
+ onLoadGap(gap);
397
+ else if (next)
398
+ onSelectTurn(next.turnSeq);
399
+ };
400
+ const stepIndex = (turns ?? []).findIndex(v => v.turnSeq === turnSeq);
401
+ const canStep = (delta) => {
402
+ if (adjacentGap((turns ?? [])[stepIndex], delta) !== undefined)
403
+ return true;
404
+ return stepIndex >= 0 && stepIndex + delta >= 0 && stepIndex + delta < (turns ?? []).length;
405
+ };
406
+ const onSelectScope = (next) => {
407
+ // On the room tab the effective scope falls back to the main agent, so
408
+ // compare with the tab actually shown, or the main tab could never be re-picked.
409
+ if (next === (scopeId === ROOM_TAB ? ROOM_TAB : effectiveScope?.scopeId))
410
+ return;
411
+ setScopeId(next);
412
+ setSelectedSeq(null);
413
+ setFilterCat(null);
414
+ };
415
+ const onMainTab = (next) => {
416
+ setMainTab(next);
417
+ };
418
+ /** A timing tab call row's click-through: the token tab, that scope, that request. */
419
+ const onOpenRequest = (seq, targetScopeId) => {
420
+ setMainTab('token');
421
+ setScopeId(targetScopeId);
422
+ setSelectedSeq(seq);
423
+ setFilterCat(null);
424
+ };
425
+ // A load failure (refusal, disconnect, a call rejected before any network
426
+ // traffic) is an explicit state with a retry — loading shows only while no
427
+ // error exists.
428
+ const errorBody = error === null ? undefined : (_jsxs("div", { className: css.stateNote, role: "alert", children: [t('state.error', { message: error }), _jsx("button", { type: "button", className: css.retry, onClick: onRetry, children: t('state.retry') })] }));
429
+ let body;
430
+ if (turns === null) {
431
+ body = errorBody ?? _jsx("div", { className: css.stateNote, role: "status", children: t('state.loading') });
432
+ }
433
+ else if (errorBody !== undefined && (turns.length === 0 || overview === null)) {
434
+ // A failed verb (listTurns or getTurn) outranks the empty states.
435
+ body = errorBody;
436
+ }
437
+ else if (turns.length === 0) {
438
+ body = backfill?.state === 'broken'
439
+ // The log exists but refused to decode: a note, not an error page.
440
+ ? _jsx("div", { className: css.stateNote, role: "status", children: t('state.logBroken', { reason: backfill.reason ?? '' }) })
441
+ : _jsx("div", { className: css.stateNote, role: "status", children: t('state.noTurns') });
442
+ }
443
+ else if (turnMissing) {
444
+ body = _jsx("div", { className: css.stateNote, role: "status", children: t('state.turnMissing') });
445
+ }
446
+ else if (overview === null) {
447
+ body = _jsx("div", { className: css.stateNote, role: "status", children: t('state.loading') });
448
+ }
449
+ else if (scopeId === ROOM_TAB && (overview.roomDecisions?.length ?? 0) > 0) {
450
+ body = (_jsx("div", { className: css.bodyFill, children: _jsx(RoomDecisionsView, { decisions: overview.roomDecisions, selectedId: decisionId, onSelect: setDecisionId, t: t }) }));
451
+ }
452
+ else if (effectiveScope === undefined) {
453
+ body = _jsx("div", { className: css.stateNote, role: "status", children: t('state.emptyScope') });
454
+ }
455
+ else if (effectiveScope.requests.length === 0 && effectiveScope.status !== 'failed') {
456
+ body = _jsx("div", { className: css.stateNote, role: "status", children: t('state.noRequests') });
457
+ }
458
+ else if (effectiveScope.status === 'failed' && effectiveScope.requests.length === 0) {
459
+ body = (_jsx("div", { className: css.scrollBody, children: _jsx(FailureCard, { scope: effectiveScope, t: t }) }));
460
+ }
461
+ else if (mainTab === 'token') {
462
+ body = (_jsxs("div", { className: css.bodyFill, children: [effectiveScope.status === 'failed' && _jsx(FailureCard, { scope: effectiveScope, t: t }), _jsx(TokenView, { onOpenRequest: link => {
463
+ if (turnSeq !== null && selectedRequestSeq !== null)
464
+ setRequestTrail(previous => [...previous, { turnSeq, scopeId: effectiveScope.scopeId, seq: selectedRequestSeq }]);
465
+ setTurnSeq(link.turnSeq);
466
+ onOpenRequest(link.seq, link.scopeId);
467
+ }, onReturn: requestTrail.length === 0 ? undefined : () => {
468
+ const target = requestTrail.at(-1);
469
+ setRequestTrail(previous => previous.slice(0, -1));
470
+ setTurnSeq(target.turnSeq);
471
+ onOpenRequest(target.seq, target.scopeId);
472
+ }, scope: effectiveScope, selectedSeq: selectedRequestSeq, request: request, filterCat: filterCat, previousResponseNumber: previousResponseNumber, onSelect: seq => { setSelectedSeq(seq); setFilterCat(null); }, onToggleFilter: category => { setFilterCat(previous => previous === category ? null : category); }, t: t })] }));
473
+ }
474
+ else {
475
+ body = (_jsxs("div", { className: css.bodyFill, children: [effectiveScope.status === 'failed' && _jsx(FailureCard, { scope: effectiveScope, t: t }), _jsx(TimingView, { timing: timing, scopes: scopes, filterScopeId: effectiveScope.scopeId, filterIsSubagent: effectiveScope.kind === 'subagent', config: effectiveScope.config, onSelectScope: onSelectScope, onOpenRequest: onOpenRequest, t: t })] }));
476
+ }
477
+ return (_jsxs(PanelRoot, { className: css.root, role: "region", "aria-label": t('tab.title'), children: [_jsxs("div", { className: css.head, children: [_jsxs("div", { className: css.r1, children: [_jsx("span", { className: css.title, children: t('tab.title') }), _jsxs("div", { className: css.stepper, children: [_jsx("button", { type: "button", "aria-label": t('lens.previous'), disabled: !canStep(-1), onClick: () => onStep(-1), children: "\u2039" }), _jsx(TurnMenu, { turns: turns ?? [], turnSeq: turnSeq, onSelect: onSelectTurn, gaps: loadTurns ? gaps : [], lastTurn: listMeta.total ?? range?.last, onLoad: onLoadGap, t: t }), _jsx("button", { type: "button", "aria-label": t('lens.next'), disabled: !canStep(1), onClick: () => onStep(1), children: "\u203A" })] }), latestRealTurn && (turnSeq !== latestRealTurn.turnSeq || (latestUnloaded && loadTurns)) && _jsx("button", { type: "button", className: css.latestText, "aria-label": t('turn.latest'), onClick: onLatest, children: t('lens.latest') }), _jsx("div", { className: css.headTabs, children: _jsx(SegCtl, { mini: true, label: t('tab.title'), options: [
478
+ { id: 'token', label: t('view.token') },
479
+ { id: 'time', label: t('view.time') },
480
+ ], value: mainTab, onChange: onMainTab }) })] }), parent && _jsx(ParentLine, { parent: parent, t: t }), trend && _jsx(Trend, { data: trend, turnSeq: turnSeq, selectedSeq: effectiveScope?.kind === 'main' ? selectedRequestSeq : null, scopeId: "main", scopeLabel: t('scope.mainName'), gaps: loadTurns ? gaps : [], onLoadGap: onLoadGap, t: t, onSelect: (next, seq, targetScope) => {
481
+ // On the timing view the map picks the turn and stays there; on the request view it opens the request.
482
+ onSelectTurn(next);
483
+ if (mainTab !== 'time')
484
+ onOpenRequest(seq, targetScope);
485
+ } }), turn !== undefined && (turn.query !== '' || turn.trigger !== undefined) && _jsxs("div", { className: css.query, children: [turnNumber(turn) !== undefined && _jsx("span", { className: css.queryTurn, children: t('query.turn', { turn: String(turnNumber(turn)) }) }), turn.room !== undefined && _jsxs("code", { className: css.querySource, children: [t('query.room'), turn.room.targets.length > 0 ? ` → ${turn.room.targets.join('、')}` : ''] }), turn.query === '' && turn.trigger !== undefined && _jsx("code", { className: css.querySource, children: turn.trigger.source }), _jsx("b", { title: turn.query !== '' ? turn.query : turn.trigger?.text, children: turn.hostTurn === undefined && turn.query === 'compaction' ? t('compaction.title') : turn.query !== '' ? turn.query : turn.trigger.text })] }), (scopes.length > 0 || (overview?.roomDecisions?.length ?? 0) > 0) && (_jsx(AgentTabs, { scopes: scopes, roomCount: overview?.roomDecisions?.length, value: scopeId === ROOM_TAB && (overview?.roomDecisions?.length ?? 0) > 0 ? ROOM_TAB : effectiveScope?.scopeId ?? 'main', onSelect: onSelectScope, onOpenRequest: link => { setTurnSeq(link.turnSeq); onOpenRequest(link.seq, link.scopeId); }, t: t }))] }), _jsx("div", { className: css.body, children: body })] }));
486
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The 原始 JSON view: the selected request's CapturedRequest serialized
3
+ * verbatim — the structured panel's escape hatch for exact field values.
4
+ *
5
+ * @module @khorsheed/dsh-context-lens/client
6
+ */
7
+ import type { ReactNode } from 'react';
8
+ import type { CapturedRequest } from '../types.ts';
9
+ import type { InspectorTabProps } from './contract.ts';
10
+ /** The panel's translate face. */
11
+ type T = InspectorTabProps['t'];
12
+ /** The raw view body. */
13
+ export declare function RawView({ request, t }: {
14
+ readonly request: CapturedRequest;
15
+ readonly t: T;
16
+ }): ReactNode;
17
+ export {};
18
+ //# sourceMappingURL=RawView.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { requestNumberText, fmt } from "./aggregate.js";
3
+ import css from './RawView.module.css';
4
+ /** The raw view body. */
5
+ export function RawView({ request, t }) {
6
+ const serialized = JSON.stringify(request, null, 1);
7
+ return (_jsxs("div", { className: css.scroll, children: [_jsx("div", { className: css.hint, children: t('raw.hint', { seq: requestNumberText(request), chars: fmt(serialized.length) }) }), _jsx("pre", { className: css.pre, children: serialized })] }));
8
+ }
@@ -0,0 +1,19 @@
1
+ import type { CapturedRequest, RequestLink, SegmentCategory } from '../types.ts';
2
+ import type { InspectorTabProps } from './contract.ts';
3
+ type T = InspectorTabProps['t'];
4
+ /** The request identity and measurements remain visible in JSON mode. */
5
+ export declare function RequestFacts({ request: r, t, raw, onToggleRaw }: {
6
+ readonly request: CapturedRequest;
7
+ readonly t: T;
8
+ readonly raw?: boolean;
9
+ readonly onToggleRaw?: () => void;
10
+ }): import("react").JSX.Element;
11
+ /** Provider usage and exact prefix comparison are distinct facts. */
12
+ export declare function CacheFacts({ request: r, onOpenRequest, onShowCategory, t }: {
13
+ readonly request: CapturedRequest;
14
+ readonly onOpenRequest?: ((link: RequestLink) => void) | undefined;
15
+ readonly onShowCategory?: ((category: SegmentCategory) => void) | undefined;
16
+ readonly t: T;
17
+ }): import("react").JSX.Element;
18
+ export {};
19
+ //# sourceMappingURL=RequestFacts.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { ChangeList } from "./ChangeList.js";
3
+ import { requestNumberText, aggregateCategories, cachePct, fmt, fmtK, fmtSpan, inputMiss, inputTotal, summaryText } from "./aggregate.js";
4
+ import css from './TokenView.module.css';
5
+ import { ConfigFacts } from "./ConfigFacts.js";
6
+ /** The request identity and measurements remain visible in JSON mode. */
7
+ export function RequestFacts({ request: r, t, raw = false, onToggleRaw }) {
8
+ const u = r.usage;
9
+ const chars = aggregateCategories(r).reduce((n, c) => n + c.chars, 0);
10
+ const duration = (ms) => ms === undefined ? '—' : fmtSpan(ms);
11
+ const metrics = [
12
+ { label: t('req.input'), value: u ? fmt(inputTotal(u)) : '—', unit: 'tok', sub: t(r.compaction?.inputPartial ? 'compaction.recoveredChars' : 'chain.chars', { count: fmt(chars) }) },
13
+ { label: t('lens.hit'), value: u ? (cachePct(u)?.toFixed(1) ?? '—') : '—', unit: '%', sub: u?.cacheReadTokens !== undefined ? `${fmt(u.cacheReadTokens)} tok` : '—' },
14
+ { label: t('lens.miss'), value: u ? fmt(inputMiss(u)) : '—', unit: 'tok', sub: r.config.maxTokens === undefined ? '' : t('lens.outputLimit', { count: fmtK(r.config.maxTokens) }) },
15
+ { label: t(r.compaction?.inputPartial ? 'compaction.interval' : 'lens.duration'), value: duration(r.timing.totalMs), unit: '', sub: `${t('req.output')} ${u ? fmt(u.outputTokens) : '—'} tok · ${t('lens.firstToken', { duration: duration(r.timing.waitMs) })}` },
16
+ ];
17
+ return _jsxs(_Fragment, { children: [_jsxs("div", { className: css.detailHead, children: [_jsxs("b", { title: r.requestNumber === undefined && r.purpose !== 'compaction' ? t('req.numberUnknown') : undefined, children: [r.purpose !== 'compaction' && _jsxs(_Fragment, { children: ["#", requestNumberText(r), " \u00B7 "] }), summaryText(r.responseSummary, t('resp.directAnswer'), t('compaction.title'), t('resp.thinking'))] }), onToggleRaw && _jsx("button", { type: "button", className: css.jsonToggle, "aria-label": t('view.raw'), title: t('view.raw'), "aria-pressed": raw, onClick: onToggleRaw, children: '{ }' })] }), _jsx(ConfigFacts, { config: r.config, t: t }), _jsx("div", { className: css.metrics, children: metrics.map(m => _jsxs("div", { children: [_jsx("span", { children: m.label }), _jsxs("b", { children: [m.value, _jsx("small", { children: m.unit })] }), _jsx("span", { title: m.sub, children: m.sub })] }, m.label)) })] });
18
+ }
19
+ /** Provider usage and exact prefix comparison are distinct facts. */
20
+ export function CacheFacts({ request: r, onOpenRequest, onShowCategory, t }) {
21
+ const u = r.usage, prefix = r.prefix;
22
+ return _jsxs("section", { children: [_jsx("h3", { className: css.sectionTitle, children: t('lens.cache') }), _jsxs("div", { className: css.prefix, children: [u && _jsxs(_Fragment, { children: [_jsx("div", { className: css.cacheStrip, children: _jsx("i", { style: { width: `${cachePct(u) ?? 0}%` } }) }), _jsxs("div", { className: css.cacheLegend, children: [_jsxs("span", { children: [_jsx("i", {}), t('lens.hit'), " ", fmt(u.cacheReadTokens ?? 0)] }), _jsxs("span", { children: [_jsx("i", {}), t('lens.miss'), " ", fmt(inputMiss(u))] })] })] }), prefix === undefined
23
+ ? _jsx("span", { title: t('lens.prefixBaselineHint'), children: t(r.compaction?.inputPartial ? 'compaction.noPrefix' : 'lens.prefixUnknown') })
24
+ : prefix.changes === undefined ? _jsx(LegacyPrefix, { prefix: prefix, t: t })
25
+ : _jsx(ChangeList, { request: r, prefix: prefix, onOpenRequest: onOpenRequest, onShowCategory: onShowCategory, t: t })] })] });
26
+ }
27
+ /** A capture from before the change list existed keeps its first-difference view. */
28
+ function LegacyPrefix({ prefix, t }) {
29
+ return _jsxs(_Fragment, { children: [prefix.changed ? _jsxs(_Fragment, { children: [_jsx("span", { children: t('lens.prefixChanged', { index: String(prefix.changed.index).padStart(3, '0'), seq: requestNumberText({ requestNumber: prefix.previousRequestNumber }) }) }), _jsx(PrefixDifference, { changed: prefix.changed, t: t })] }) : _jsx("span", { children: t('lens.prefixSame', { seq: requestNumberText({ requestNumber: prefix.previousRequestNumber }), gap: fmtSpan(prefix.gapMs) }) }), prefix.toolsChanged && _jsx("div", { children: t('lens.toolsChanged') })] });
30
+ }
31
+ function PrefixDifference({ changed, t }) {
32
+ const d = changed.text;
33
+ if (!d)
34
+ return _jsxs(_Fragment, { children: [_jsxs("pre", { className: css.before, children: ["\u2212 ", changed.before] }), _jsxs("pre", { className: css.after, children: ["+ ", changed.after] })] });
35
+ const leading = `${d.leadingOmitted ? '…' : ''}${d.leading}`;
36
+ const trailing = `${d.trailing}${d.trailingOmitted ? '…' : ''}`;
37
+ const truncated = d.removedChars > 160 || d.addedChars > 160;
38
+ return _jsxs(_Fragment, { children: [changed.beforeCategory !== changed.afterCategory && _jsx("span", { children: t('lens.diffCategory', { before: changed.beforeCategory ? t(`cat.${changed.beforeCategory}`) : '—', after: changed.afterCategory ? t(`cat.${changed.afterCategory}`) : '—' }) }), (d.removedChars > 0 || d.addedChars > 0) && _jsxs(_Fragment, { children: [_jsx("span", { className: css.diffMeta, children: t('lens.diffPosition', { offset: fmt(d.offset + 1), before: fmt(d.removedChars), after: fmt(d.addedChars) }) }), _jsxs("pre", { className: css.diffLine, children: [_jsx("span", { className: css.before, children: "\u2212 " }), leading, d.removedChars > 0 && _jsxs("del", { children: [d.removed, d.removedChars > 160 ? '…' : ''] }), trailing, !leading && !d.removed && !trailing && t('lens.diffEmpty')] }), _jsxs("pre", { className: css.diffLine, children: [_jsx("span", { className: css.after, children: "+ " }), leading, d.addedChars > 0 && _jsxs("ins", { children: [d.added, d.addedChars > 160 ? '…' : ''] }), trailing, !leading && !d.added && !trailing && t('lens.diffEmpty')] }), truncated && _jsx("span", { className: css.diffMeta, children: t('lens.diffTruncated') })] })] });
39
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The 房间 tab: the decision-model calls the room made during this turn (its
3
+ * own scenarios — automatic task filing, automatic report timing), listed
4
+ * like requests (#n · scenario, the probability the outcome was taken with,
5
+ * input → output tokens, latency) and opened like one: the model facts, the
6
+ * state the room sent, the fixed question, the candidates with their
7
+ * probabilities and the outcome — or, through `{ }`, the request and response
8
+ * as JSON.
9
+ *
10
+ * @module @khorsheed/dsh-context-lens/client
11
+ */
12
+ import { type ReactNode } from 'react';
13
+ import type { RoomDecisionRow } from '../types.ts';
14
+ import type { InspectorTabProps } from './contract.ts';
15
+ type T = InspectorTabProps['t'];
16
+ /** The 房间 tab body. */
17
+ export declare function RoomDecisionsView({ decisions, selectedId, onSelect, t }: {
18
+ readonly decisions: readonly RoomDecisionRow[];
19
+ readonly selectedId: string | null;
20
+ readonly onSelect: (id: string) => void;
21
+ readonly t: T;
22
+ }): ReactNode;
23
+ export {};
24
+ //# sourceMappingURL=RoomDecisionsView.d.ts.map