@shendeguize/dsh-agent-sidecar 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -11
- package/lib/client.js +4708 -2932
- package/lib/client.js.map +1 -1
- package/package.json +3 -1
- package/src/client/analysis/AnalysisPanel.tsx +19 -27
- package/src/client/analysis/analysis.module.css +36 -97
- package/src/client/board/Board.tsx +255 -48
- package/src/client/board/board.module.css +113 -92
- package/src/client/board/logic.ts +99 -21
- package/src/client/board/project-view-logic.ts +27 -34
- package/src/client/board/project-view.module.css +46 -83
- package/src/client/board/project-view.tsx +50 -8
- package/src/client/board/strings.ts +70 -85
- package/src/client/commands.ts +30 -15
- package/src/client/controller.ts +27 -7
- package/src/client/detail/SessionDetail.tsx +234 -37
- package/src/client/detail/detail.module.css +100 -153
- package/src/client/detail/logic.ts +220 -29
- package/src/client/detail/strings.ts +66 -77
- package/src/client/detail-glue.ts +33 -0
- package/src/client/detail-view.module.css +43 -35
- package/src/client/detail-view.tsx +138 -118
- package/src/client/dsh-tools/LineageTree.tsx +22 -13
- package/src/client/dsh-tools/SearchPanel.tsx +18 -11
- package/src/client/dsh-tools/dsh-tools.module.css +53 -140
- package/src/client/dsh-tools/strings.ts +42 -77
- package/src/client/index.ts +285 -124
- package/src/client/inject/InjectPanel.tsx +81 -61
- package/src/client/inject/inject.module.css +97 -197
- package/src/client/inject/logic.ts +38 -0
- package/src/client/lifecycle/handoff.ts +83 -0
- package/src/client/locales/en.ts +91 -4
- package/src/client/locales/host.ts +131 -0
- package/src/client/locales/index.ts +196 -8
- package/src/client/locales/react.ts +10 -0
- package/src/client/locales/view.ts +38 -0
- package/src/client/locales/zh.ts +200 -125
- package/src/client/mount.tsx +75 -41
- package/src/client/navigation/CenterOverlay.tsx +40 -0
- package/src/client/navigation/center-overlay.module.css +51 -0
- package/src/client/navigation/center.ts +45 -0
- package/src/client/navigation/modal-isolation.ts +152 -0
- package/src/client/navigation/modal-surface-anchor.ts +72 -0
- package/src/client/navigation/sidebar-entry.module.css +73 -0
- package/src/client/navigation/sidebar-entry.ts +309 -0
- package/src/client/primitives/StaticPill.tsx +21 -0
- package/src/client/settings-card.module.css +35 -119
- package/src/client/settings-card.tsx +31 -153
- package/src/client/settings-fields.tsx +131 -0
- package/src/client/sidebar/SidebarTab.tsx +156 -0
- package/src/client/sidebar/model.ts +65 -0
- package/src/client/sidebar/sidebar-tab.module.css +118 -0
- package/src/client/sidebar-tab.tsx +46 -320
- package/src/client/theme/agsc.module.css +31 -0
- package/src/client/theme/parts.ts +56 -0
- package/src/client/ui-integration.ts +86 -0
- package/src/client/widget.tsx +42 -16
- package/src/client/inject/overlay.module.css +0 -22
|
@@ -109,6 +109,30 @@ export function formatRelativeTime(thenMs: number, nowMs: number): string {
|
|
|
109
109
|
return formatTemplate(DETAIL_STRINGS.time.daysAgo, { n: Math.floor(delta / DAY_MS) })
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function pad2(n: number): string {
|
|
113
|
+
return n < 10 ? `0${n}` : String(n)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Absolute short timestamp for the time column (UX-13): the same local
|
|
118
|
+
* calendar day renders「HH:mm」, anything older (or a different day)
|
|
119
|
+
* renders「MM-DD HH:mm」— so a column of same-age rows stays tellable
|
|
120
|
+
* apart, unlike the coarse relative buckets. The format rule is
|
|
121
|
+
* copy-implemented to match the board column (no cross-surface import);
|
|
122
|
+
* the full ISO timestamp stays in the hover title.
|
|
123
|
+
*/
|
|
124
|
+
export function formatEventTime(thenMs: number, nowMs: number): string {
|
|
125
|
+
if (!Number.isFinite(thenMs) || !Number.isFinite(nowMs)) return ''
|
|
126
|
+
const then = new Date(thenMs)
|
|
127
|
+
const now = new Date(nowMs)
|
|
128
|
+
const hhmm = `${pad2(then.getHours())}:${pad2(then.getMinutes())}`
|
|
129
|
+
const sameDay =
|
|
130
|
+
then.getFullYear() === now.getFullYear() &&
|
|
131
|
+
then.getMonth() === now.getMonth() &&
|
|
132
|
+
then.getDate() === now.getDate()
|
|
133
|
+
return sameDay ? hhmm : `${pad2(then.getMonth() + 1)}-${pad2(then.getDate())} ${hhmm}`
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
// ---------------------------------------------------------------------------
|
|
113
137
|
// Event-kind classification (icon + label).
|
|
114
138
|
// ---------------------------------------------------------------------------
|
|
@@ -137,26 +161,43 @@ const KIND_GLYPHS: Record<TimelineKindToken, string> = {
|
|
|
137
161
|
other: '•',
|
|
138
162
|
}
|
|
139
163
|
|
|
164
|
+
/** One path segment → family token, or null when it names no family. */
|
|
165
|
+
function segmentToken(segment: string): TimelineKindToken | null {
|
|
166
|
+
if (segment === 'user') return 'user'
|
|
167
|
+
if (segment === 'assistant') return 'assistant'
|
|
168
|
+
if (segment === 'thinking' || segment === 'reasoning') return 'thinking'
|
|
169
|
+
if (segment === 'tool_call' || segment === 'tool-call' || segment === 'toolcall') {
|
|
170
|
+
return 'toolCall'
|
|
171
|
+
}
|
|
172
|
+
if (segment === 'tool_result' || segment === 'tool-result' || segment === 'toolresult') {
|
|
173
|
+
return 'toolResult'
|
|
174
|
+
}
|
|
175
|
+
if (segment.startsWith('turn_') || segment === 'turn') return 'turn'
|
|
176
|
+
if (segment.startsWith('step_') || segment === 'step') return 'step'
|
|
177
|
+
if (segment === 'error') return 'error'
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
140
181
|
/**
|
|
141
|
-
* Map a raw event kind onto the glyph/label vocabulary. Covers
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* slash-path types
|
|
145
|
-
*
|
|
182
|
+
* Map a raw event kind onto the glyph/label vocabulary. Covers the sidecar
|
|
183
|
+
* normalized kinds — user/assistant/thinking/tool_call/tool_result plus
|
|
184
|
+
* the turn_ and step_ prefixes (sidecar/model.py) — and dsh native
|
|
185
|
+
* slash-path types in BOTH orders: `message/user` style (family last) and
|
|
186
|
+
* the observed `user/message` / `assistant/chunk` / `turn/start` style
|
|
187
|
+
* (family first; live-data fact, UX-03 — without it the conversation
|
|
188
|
+
* filter would misfile real dsh user messages as protocol noise). The
|
|
189
|
+
* last segment stays authoritative; the first is only a fallback.
|
|
190
|
+
* Everything else is honestly 'other'.
|
|
146
191
|
*/
|
|
147
192
|
export function classifyKind(kind: string): TimelineKindToken {
|
|
148
193
|
const k = kind.trim().toLowerCase()
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
if (last
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return 'toolResult'
|
|
194
|
+
const segments = k.split('/')
|
|
195
|
+
const last = segmentToken(segments[segments.length - 1] ?? k)
|
|
196
|
+
if (last !== null) return last
|
|
197
|
+
if (segments.length > 1) {
|
|
198
|
+
const first = segmentToken(segments[0] ?? '')
|
|
199
|
+
if (first !== null) return first
|
|
156
200
|
}
|
|
157
|
-
if (last.startsWith('turn_') || last === 'turn') return 'turn'
|
|
158
|
-
if (last.startsWith('step_') || last === 'step') return 'step'
|
|
159
|
-
if (last === 'error') return 'error'
|
|
160
201
|
return 'other'
|
|
161
202
|
}
|
|
162
203
|
|
|
@@ -500,18 +541,26 @@ export function applyListenPage(vm: TimelineVM, page: TimelinePageWire): Timelin
|
|
|
500
541
|
// Gap detection + row derivation.
|
|
501
542
|
// ---------------------------------------------------------------------------
|
|
502
543
|
|
|
503
|
-
/** One rendered
|
|
544
|
+
/** One rendered event row (extracted so aggregation can carry members). */
|
|
545
|
+
export interface TimelineEventRowVM {
|
|
546
|
+
type: 'event'
|
|
547
|
+
key: string
|
|
548
|
+
entry: TimelineEntryVM
|
|
549
|
+
/** Absolute short time label (see {@link formatEventTime}). */
|
|
550
|
+
timeLabel: string
|
|
551
|
+
/** Hover title: ISO timestamp + raw kind + origin (+ seq) + relative age. */
|
|
552
|
+
hoverTitle: string
|
|
553
|
+
/** True when this entry arrived in the latest listen merge. */
|
|
554
|
+
isNew: boolean
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* One rendered timeline row: a real event, an honesty gap marker, or an
|
|
559
|
+
* aggregated run of adjacent empty streaming chunks (UX-03 — the members
|
|
560
|
+
* are carried verbatim so the view can expand the run without data loss).
|
|
561
|
+
*/
|
|
504
562
|
export type TimelineRowVM =
|
|
505
|
-
|
|
|
506
|
-
type: 'event'
|
|
507
|
-
key: string
|
|
508
|
-
entry: TimelineEntryVM
|
|
509
|
-
relativeTime: string
|
|
510
|
-
/** Hover title: ISO timestamp + raw kind + origin (+ seq). */
|
|
511
|
-
hoverTitle: string
|
|
512
|
-
/** True when this entry arrived in the latest listen merge. */
|
|
513
|
-
isNew: boolean
|
|
514
|
-
}
|
|
563
|
+
| TimelineEventRowVM
|
|
515
564
|
| {
|
|
516
565
|
type: 'gap'
|
|
517
566
|
key: string
|
|
@@ -519,6 +568,22 @@ export type TimelineRowVM =
|
|
|
519
568
|
missingCount: number
|
|
520
569
|
label: string
|
|
521
570
|
}
|
|
571
|
+
| {
|
|
572
|
+
type: 'chunks'
|
|
573
|
+
key: string
|
|
574
|
+
/** Raw wire kind shared by every member of the run. */
|
|
575
|
+
kindRaw: string
|
|
576
|
+
count: number
|
|
577
|
+
/** 「N 个流式分块」 */
|
|
578
|
+
label: string
|
|
579
|
+
/** Time label of the newest member. */
|
|
580
|
+
timeLabel: string
|
|
581
|
+
hoverTitle: string
|
|
582
|
+
/** True when any member arrived in the latest listen merge. */
|
|
583
|
+
isNew: boolean
|
|
584
|
+
/** The collapsed rows, verbatim, for lossless expansion. */
|
|
585
|
+
members: TimelineEventRowVM[]
|
|
586
|
+
}
|
|
522
587
|
|
|
523
588
|
function isoOrEmpty(ts: number): string {
|
|
524
589
|
if (!Number.isFinite(ts)) return ''
|
|
@@ -529,11 +594,12 @@ function isoOrEmpty(ts: number): string {
|
|
|
529
594
|
}
|
|
530
595
|
}
|
|
531
596
|
|
|
532
|
-
function eventHoverTitle(entry: TimelineEntryVM): string {
|
|
597
|
+
function eventHoverTitle(entry: TimelineEntryVM, nowMs: number): string {
|
|
533
598
|
const parts = [isoOrEmpty(entry.ts), entry.kindRaw, entry.origin]
|
|
534
599
|
if (entry.seq !== null) {
|
|
535
600
|
parts.push(formatTemplate(DETAIL_STRINGS.timeline.seq, { n: entry.seq }))
|
|
536
601
|
}
|
|
602
|
+
parts.push(formatRelativeTime(entry.ts, nowMs))
|
|
537
603
|
return parts.filter((p) => p !== '').join(' · ')
|
|
538
604
|
}
|
|
539
605
|
|
|
@@ -572,14 +638,139 @@ export function buildTimelineRows(vm: TimelineVM, nowMs: number): TimelineRowVM[
|
|
|
572
638
|
type: 'event',
|
|
573
639
|
key: entry.key,
|
|
574
640
|
entry,
|
|
575
|
-
|
|
576
|
-
hoverTitle: eventHoverTitle(entry),
|
|
641
|
+
timeLabel: formatEventTime(entry.ts, nowMs),
|
|
642
|
+
hoverTitle: eventHoverTitle(entry, nowMs),
|
|
577
643
|
isNew: newKeys.has(entry.key),
|
|
578
644
|
})
|
|
579
645
|
}
|
|
580
646
|
return rows
|
|
581
647
|
}
|
|
582
648
|
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Kind filter + chunk-run aggregation (UX-03: signal over protocol noise).
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
/** Kind-filter modes; pure UI state owned by the component. */
|
|
654
|
+
export type TimelineFilterMode = 'conversation' | 'all'
|
|
655
|
+
|
|
656
|
+
/** The kinds that count as conversation (review UX-03 vocabulary). */
|
|
657
|
+
export const CONVERSATION_KINDS: ReadonlySet<TimelineKindToken> = new Set([
|
|
658
|
+
'user',
|
|
659
|
+
'assistant',
|
|
660
|
+
'error',
|
|
661
|
+
])
|
|
662
|
+
|
|
663
|
+
export interface FilteredRows {
|
|
664
|
+
rows: TimelineRowVM[]
|
|
665
|
+
/** Exactly how many event rows the filter removed (honest count). */
|
|
666
|
+
hiddenCount: number
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Kind filter over derived rows: 'conversation' keeps user/assistant/error
|
|
671
|
+
* events only; 'all' passes everything through. Gap markers ALWAYS stay —
|
|
672
|
+
* honesty rows are not noise and hiding them could fake a clean timeline.
|
|
673
|
+
* Runs BEFORE {@link aggregateChunkRows} (gaps are detected on the full
|
|
674
|
+
* entry list upstream, so filtering can never fabricate a gap).
|
|
675
|
+
*/
|
|
676
|
+
export function filterTimelineRows(
|
|
677
|
+
rows: readonly TimelineRowVM[],
|
|
678
|
+
mode: TimelineFilterMode,
|
|
679
|
+
): FilteredRows {
|
|
680
|
+
if (mode === 'all') return { rows: [...rows], hiddenCount: 0 }
|
|
681
|
+
const out: TimelineRowVM[] = []
|
|
682
|
+
let hiddenCount = 0
|
|
683
|
+
for (const row of rows) {
|
|
684
|
+
if (row.type === 'event' && !CONVERSATION_KINDS.has(row.entry.kind)) {
|
|
685
|
+
hiddenCount += 1
|
|
686
|
+
continue
|
|
687
|
+
}
|
|
688
|
+
out.push(row)
|
|
689
|
+
}
|
|
690
|
+
return { rows: out, hiddenCount }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* True for a protocol streaming-chunk entry: an empty one-line summary and
|
|
695
|
+
* a chunk-flavored kind (`assistant/chunk` style, matched on the last
|
|
696
|
+
* slash segment). These are the rows that drowned real conversation in
|
|
697
|
+
* the walkthrough (18 of 38 rows, review UX-03).
|
|
698
|
+
*/
|
|
699
|
+
export function isStreamChunkEntry(entry: TimelineEntryVM): boolean {
|
|
700
|
+
if (entry.summary !== '') return false
|
|
701
|
+
const k = entry.kindRaw.trim().toLowerCase()
|
|
702
|
+
const last = k.includes('/') ? (k.split('/').pop() ?? k) : k
|
|
703
|
+
return last === 'chunk' || last.endsWith('_chunk') || last.endsWith('-chunk')
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Collapse each maximal run of ≥2 adjacent same-kind streaming-chunk rows
|
|
708
|
+
* into one 'chunks' row carrying the members verbatim (lossless — the view
|
|
709
|
+
* offers 展开). Gap markers and any non-chunk row break a run, so the
|
|
710
|
+
* aggregation can never paper over a seq discontinuity. Single chunks stay
|
|
711
|
+
* as plain rows (a 1-run header would add noise, not remove it).
|
|
712
|
+
*/
|
|
713
|
+
export function aggregateChunkRows(rows: readonly TimelineRowVM[]): TimelineRowVM[] {
|
|
714
|
+
const out: TimelineRowVM[] = []
|
|
715
|
+
let run: TimelineEventRowVM[] = []
|
|
716
|
+
|
|
717
|
+
const flush = (): void => {
|
|
718
|
+
if (run.length >= 2) {
|
|
719
|
+
const first = run[0]!
|
|
720
|
+
const last = run[run.length - 1]!
|
|
721
|
+
out.push({
|
|
722
|
+
type: 'chunks',
|
|
723
|
+
key: `chunks:${first.key}`,
|
|
724
|
+
kindRaw: first.entry.kindRaw,
|
|
725
|
+
count: run.length,
|
|
726
|
+
label: formatTemplate(DETAIL_STRINGS.timeline.chunkRun, { n: run.length }),
|
|
727
|
+
timeLabel: last.timeLabel,
|
|
728
|
+
hoverTitle: `${first.entry.kindRaw} ×${run.length}`,
|
|
729
|
+
isNew: run.some((r) => r.isNew),
|
|
730
|
+
members: run,
|
|
731
|
+
})
|
|
732
|
+
} else {
|
|
733
|
+
out.push(...run)
|
|
734
|
+
}
|
|
735
|
+
run = []
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
for (const row of rows) {
|
|
739
|
+
const chunk = row.type === 'event' && isStreamChunkEntry(row.entry)
|
|
740
|
+
if (chunk) {
|
|
741
|
+
const sameKind = run.length === 0 || run[run.length - 1]!.entry.kindRaw === row.entry.kindRaw
|
|
742
|
+
if (!sameKind) flush()
|
|
743
|
+
run.push(row as TimelineEventRowVM)
|
|
744
|
+
continue
|
|
745
|
+
}
|
|
746
|
+
flush()
|
|
747
|
+
out.push(row)
|
|
748
|
+
}
|
|
749
|
+
flush()
|
|
750
|
+
return out
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ---------------------------------------------------------------------------
|
|
754
|
+
// Viewport landing rule (UX-04: open on the newest events).
|
|
755
|
+
// ---------------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Whether the view should pin its scroll position to the newest rows:
|
|
759
|
+
* on the first non-empty render (initial landing — understanding the
|
|
760
|
+
* current context needs the latest events, review UX-04), and on every
|
|
761
|
+
* append while listen mode is on. Loading older history must never yank
|
|
762
|
+
* the viewport (`positioned` stays true after the first landing).
|
|
763
|
+
*/
|
|
764
|
+
export function shouldStickToLatest(input: {
|
|
765
|
+
entryCount: number
|
|
766
|
+
/** True once the initial landing already happened. */
|
|
767
|
+
positioned: boolean
|
|
768
|
+
listening: boolean
|
|
769
|
+
}): boolean {
|
|
770
|
+
if (input.entryCount === 0) return false
|
|
771
|
+
return !input.positioned || input.listening
|
|
772
|
+
}
|
|
773
|
+
|
|
583
774
|
// ---------------------------------------------------------------------------
|
|
584
775
|
// Segmented rendering bound (perf stopgap; no full virtualization, see task
|
|
585
776
|
// report — page sizes already bound growth, this bounds pathological cases).
|
|
@@ -1,98 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
* Centralized user-facing strings for the session-detail view (design §5.1
|
|
3
|
-
* view 2, §5.3 honesty wording). Deliberately a module-local table — NOT
|
|
4
|
-
* part of the main locale registry (locales/zh.ts / locales/en.ts): the
|
|
5
|
-
* locale tables are owned by parallel tasks and the S7 integration wave
|
|
6
|
-
* folds this table in centrally. Same posture as board/strings.ts.
|
|
7
|
-
*
|
|
8
|
-
* Chinese is the primary UI language. Templates use `{name}` placeholders
|
|
9
|
-
* resolved by `formatTemplate` in `logic.ts`.
|
|
10
|
-
*
|
|
11
|
-
* Pure data module: no imports, no logic.
|
|
12
|
-
*
|
|
13
|
-
* @module
|
|
14
|
-
*/
|
|
1
|
+
import { createLocaleView } from '../locales/view.ts'
|
|
15
2
|
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
/** Dynamic session-detail vocabulary; templates are interpolated by `formatTemplate`. */
|
|
4
|
+
export const DETAIL_STRINGS = createLocaleView({
|
|
18
5
|
header: {
|
|
19
|
-
close: '
|
|
20
|
-
listenOn: '
|
|
21
|
-
listenOff: '
|
|
22
|
-
listenHint: '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
6
|
+
close: 'detail.header.close',
|
|
7
|
+
listenOn: 'detail.header.listenOn',
|
|
8
|
+
listenOff: 'detail.header.listenOff',
|
|
9
|
+
listenHint: 'detail.header.listenHint',
|
|
10
|
+
refresh: 'detail.header.refresh',
|
|
11
|
+
refreshing: 'detail.header.refreshing',
|
|
12
|
+
refreshHint: 'detail.header.refreshHint',
|
|
13
|
+
copyIdTitle: 'detail.header.copyIdTitle',
|
|
14
|
+
copied: 'detail.header.copied',
|
|
15
|
+
untitled: 'detail.header.untitled',
|
|
16
|
+
unknownProject: 'detail.header.unknownProject',
|
|
17
|
+
observedDisclaimer: 'detail.header.observedDisclaimer',
|
|
27
18
|
},
|
|
28
|
-
/** Session status badge labels (observed values, see disclaimer). */
|
|
29
19
|
status: {
|
|
30
|
-
working: '
|
|
31
|
-
waiting: '
|
|
32
|
-
idle: '
|
|
33
|
-
dead: '
|
|
34
|
-
unknown: '
|
|
20
|
+
working: 'detail.status.working',
|
|
21
|
+
waiting: 'detail.status.waiting',
|
|
22
|
+
idle: 'detail.status.idle',
|
|
23
|
+
dead: 'detail.status.dead',
|
|
24
|
+
unknown: 'detail.status.unknown',
|
|
35
25
|
},
|
|
36
|
-
/** Timeline source badges (provenance of the merged page, honest labels). */
|
|
37
26
|
sources: {
|
|
38
|
-
title: '
|
|
39
|
-
dshLive: '
|
|
40
|
-
dshCold: '
|
|
41
|
-
sidecarReplay: '
|
|
42
|
-
sidecarBuffer: '
|
|
43
|
-
none: '
|
|
27
|
+
title: 'detail.sources.title',
|
|
28
|
+
dshLive: 'detail.sources.dshLive',
|
|
29
|
+
dshCold: 'detail.sources.dshCold',
|
|
30
|
+
sidecarReplay: 'detail.sources.sidecarReplay',
|
|
31
|
+
sidecarBuffer: 'detail.sources.sidecarBuffer',
|
|
32
|
+
none: 'detail.sources.none',
|
|
44
33
|
},
|
|
45
|
-
/** Normalized event-kind labels; unknown kinds keep their raw text. */
|
|
46
34
|
kind: {
|
|
47
|
-
user: '
|
|
48
|
-
assistant: '
|
|
49
|
-
thinking: '
|
|
50
|
-
toolCall: '
|
|
51
|
-
toolResult: '
|
|
52
|
-
turn: '
|
|
53
|
-
step: '
|
|
54
|
-
error: '
|
|
55
|
-
other: '
|
|
35
|
+
user: 'detail.kind.user',
|
|
36
|
+
assistant: 'detail.kind.assistant',
|
|
37
|
+
thinking: 'detail.kind.thinking',
|
|
38
|
+
toolCall: 'detail.kind.toolCall',
|
|
39
|
+
toolResult: 'detail.kind.toolResult',
|
|
40
|
+
turn: 'detail.kind.turn',
|
|
41
|
+
step: 'detail.kind.step',
|
|
42
|
+
error: 'detail.kind.error',
|
|
43
|
+
other: 'detail.kind.other',
|
|
56
44
|
},
|
|
57
|
-
/** Seq-discontinuity marker row (design §4.b.3 honest presentation). */
|
|
58
45
|
gap: {
|
|
59
|
-
label: '
|
|
46
|
+
label: 'detail.gap.label',
|
|
47
|
+
},
|
|
48
|
+
filter: {
|
|
49
|
+
conversation: 'detail.filter.conversation',
|
|
50
|
+
all: 'detail.filter.all',
|
|
51
|
+
hiddenNotice: 'detail.filter.hiddenNotice',
|
|
60
52
|
},
|
|
61
|
-
/** Timeline list chrome. */
|
|
62
53
|
timeline: {
|
|
63
|
-
loadMore: '
|
|
64
|
-
loadingMore: '
|
|
65
|
-
noMore: '
|
|
66
|
-
expand: '
|
|
67
|
-
collapse: '
|
|
68
|
-
newBadge: '
|
|
69
|
-
seq: 'seq
|
|
70
|
-
hiddenNotice: '
|
|
71
|
-
showAll: '
|
|
54
|
+
loadMore: 'detail.timeline.loadMore',
|
|
55
|
+
loadingMore: 'detail.timeline.loadingMore',
|
|
56
|
+
noMore: 'detail.timeline.noMore',
|
|
57
|
+
expand: 'detail.timeline.expand',
|
|
58
|
+
collapse: 'detail.timeline.collapse',
|
|
59
|
+
newBadge: 'detail.timeline.newBadge',
|
|
60
|
+
seq: 'detail.timeline.seq',
|
|
61
|
+
hiddenNotice: 'detail.timeline.hiddenNotice',
|
|
62
|
+
showAll: 'detail.timeline.showAll',
|
|
63
|
+
chunkRun: 'detail.timeline.chunkRun',
|
|
72
64
|
},
|
|
73
|
-
/** Loading / empty / error body states. */
|
|
74
65
|
states: {
|
|
75
|
-
loadingTitle: '
|
|
76
|
-
emptyTitle: '
|
|
77
|
-
emptyHint: '
|
|
78
|
-
errorTitle: '
|
|
79
|
-
|
|
80
|
-
errorFallback: '错误码:{reason}',
|
|
66
|
+
loadingTitle: 'detail.states.loadingTitle',
|
|
67
|
+
emptyTitle: 'detail.states.emptyTitle',
|
|
68
|
+
emptyHint: 'detail.states.emptyHint',
|
|
69
|
+
errorTitle: 'detail.states.errorTitle',
|
|
70
|
+
errorFallback: 'detail.states.errorFallback',
|
|
81
71
|
errors: {
|
|
82
|
-
session_not_found: '
|
|
83
|
-
invalid_cursor: '
|
|
84
|
-
fusion_not_wired: '
|
|
85
|
-
network_error: '
|
|
86
|
-
request_timeout: '
|
|
72
|
+
session_not_found: 'detail.states.errors.session_not_found',
|
|
73
|
+
invalid_cursor: 'detail.states.errors.invalid_cursor',
|
|
74
|
+
fusion_not_wired: 'detail.states.errors.fusion_not_wired',
|
|
75
|
+
network_error: 'detail.states.errors.network_error',
|
|
76
|
+
request_timeout: 'detail.states.errors.request_timeout',
|
|
87
77
|
},
|
|
88
78
|
},
|
|
89
|
-
/** Relative time templates (coarse buckets, matches the board wording). */
|
|
90
79
|
time: {
|
|
91
|
-
justNow: '
|
|
92
|
-
minutesAgo: '
|
|
93
|
-
hoursAgo: '
|
|
94
|
-
daysAgo: '
|
|
80
|
+
justNow: 'detail.time.justNow',
|
|
81
|
+
minutesAgo: 'detail.time.minutesAgo',
|
|
82
|
+
hoursAgo: 'detail.time.hoursAgo',
|
|
83
|
+
daysAgo: 'detail.time.daysAgo',
|
|
95
84
|
},
|
|
96
|
-
} as const
|
|
85
|
+
} as const)
|
|
97
86
|
|
|
98
87
|
export type DetailStrings = typeof DETAIL_STRINGS
|
|
@@ -83,6 +83,8 @@ export interface DetailGlueState {
|
|
|
83
83
|
error: string | null
|
|
84
84
|
hasMore: boolean
|
|
85
85
|
listening: boolean
|
|
86
|
+
/** True while a manual newest-window refresh is in flight (UX-07). */
|
|
87
|
+
refreshing: boolean
|
|
86
88
|
/** True once the initial load succeeded (timeline usable). */
|
|
87
89
|
ready: boolean
|
|
88
90
|
lineage: LineageSliceState
|
|
@@ -137,6 +139,7 @@ export class DetailStore {
|
|
|
137
139
|
private paging = false
|
|
138
140
|
private listenInFlight = false
|
|
139
141
|
private listenQueued = false
|
|
142
|
+
private refreshInFlight = false
|
|
140
143
|
private lineageStarted = false
|
|
141
144
|
|
|
142
145
|
constructor(sessionId: string, options: DetailStoreOptions = {}) {
|
|
@@ -152,6 +155,7 @@ export class DetailStore {
|
|
|
152
155
|
error: null,
|
|
153
156
|
hasMore: false,
|
|
154
157
|
listening: false,
|
|
158
|
+
refreshing: false,
|
|
155
159
|
ready: false,
|
|
156
160
|
lineage: INITIAL_LINEAGE,
|
|
157
161
|
}
|
|
@@ -241,6 +245,35 @@ export class DetailStore {
|
|
|
241
245
|
if (listening) this.scheduleListenRefetch()
|
|
242
246
|
}
|
|
243
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Manual newest-window refetch with visible feedback (UX-07), also fired
|
|
250
|
+
* once after a delivered injection (UX-05 observation loop). Unlike the
|
|
251
|
+
* silent best-effort listen refetch, it reports in-flight state and
|
|
252
|
+
* surfaces a failure reason (rendered as the inline banner). Appended
|
|
253
|
+
* entries get the listen-merge highlight. Coalesced: at most one manual
|
|
254
|
+
* refresh in flight, extra calls are dropped.
|
|
255
|
+
*/
|
|
256
|
+
async refreshNewest(): Promise<void> {
|
|
257
|
+
if (this.disposed || !this.state.ready || this.refreshInFlight) return
|
|
258
|
+
this.refreshInFlight = true
|
|
259
|
+
this.setState({ refreshing: true, error: null })
|
|
260
|
+
try {
|
|
261
|
+
const page = await this.fetchPageFn(this.state.sessionId, {
|
|
262
|
+
...(this.listenLimit !== undefined ? { limit: this.listenLimit } : {}),
|
|
263
|
+
})
|
|
264
|
+
if (this.disposed) return
|
|
265
|
+
this.setState({
|
|
266
|
+
timeline: applyListenPage(this.state.timeline, page),
|
|
267
|
+
refreshing: false,
|
|
268
|
+
})
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (this.disposed) return
|
|
271
|
+
this.setState({ refreshing: false, error: reasonOf(err) })
|
|
272
|
+
} finally {
|
|
273
|
+
this.refreshInFlight = false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
244
277
|
/**
|
|
245
278
|
* SSE `state` frame hook (one call per controller notification). Refreshes
|
|
246
279
|
* the header from the live board card when given, and in listen mode
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Detail-view container + board-tab view-switcher chrome (T5.10b, design
|
|
3
|
-
* §5.1): every color rides a --dsw-alias-* design token with a neutral
|
|
4
|
-
* fallback. Same posture as board.module.css / dsh-tools.module.css.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/* ------------------------------------------------ board-tab view switcher */
|
|
8
|
-
|
|
9
1
|
.switcherBar {
|
|
10
2
|
display: flex;
|
|
11
3
|
align-items: center;
|
|
@@ -19,28 +11,29 @@
|
|
|
19
11
|
border-radius: 999px;
|
|
20
12
|
border: 1px solid transparent;
|
|
21
13
|
background: transparent;
|
|
22
|
-
color: var(--dsw-alias-label-secondary
|
|
14
|
+
color: var(--dsw-alias-label-secondary);
|
|
23
15
|
cursor: pointer;
|
|
24
16
|
}
|
|
25
17
|
|
|
26
18
|
.switcherButton:hover {
|
|
27
|
-
background: var(--dsw-alias-bg-layer-1
|
|
19
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
28
20
|
}
|
|
29
21
|
|
|
30
22
|
.switcherButton[data-active='true'] {
|
|
31
23
|
font-weight: 600;
|
|
32
|
-
color: var(--dsw-alias-label-primary
|
|
33
|
-
background: var(--dsw-alias-bg-layer-2
|
|
34
|
-
border-color: var(--dsw-alias-border-l2
|
|
24
|
+
color: var(--dsw-alias-label-primary);
|
|
25
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
26
|
+
border-color: var(--dsw-alias-border-l2);
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
/* --------------------------------------------------- detail-view chrome */
|
|
38
|
-
|
|
39
29
|
.detailRoot {
|
|
40
30
|
display: flex;
|
|
41
31
|
flex-direction: column;
|
|
42
32
|
gap: 12px;
|
|
43
33
|
min-width: 0;
|
|
34
|
+
height: 100%;
|
|
35
|
+
overflow-y: auto;
|
|
36
|
+
box-sizing: border-box;
|
|
44
37
|
padding: 12px;
|
|
45
38
|
}
|
|
46
39
|
|
|
@@ -48,32 +41,47 @@
|
|
|
48
41
|
display: flex;
|
|
49
42
|
align-items: center;
|
|
50
43
|
gap: 8px;
|
|
44
|
+
flex-wrap: wrap;
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
background: var(--dsw-alias-bg-layer-1, rgba(0, 0, 0, 0.02));
|
|
59
|
-
color: var(--dsw-alias-label-primary, #1f2328);
|
|
60
|
-
cursor: pointer;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.actionButton:hover {
|
|
64
|
-
background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.actionButton:disabled {
|
|
68
|
-
opacity: 0.5;
|
|
69
|
-
cursor: not-allowed;
|
|
47
|
+
.analysisDisabledReason {
|
|
48
|
+
max-width: 360px;
|
|
49
|
+
font-size: 11px;
|
|
50
|
+
line-height: 16px;
|
|
51
|
+
color: var(--agsc-fg-secondary);
|
|
70
52
|
}
|
|
71
53
|
|
|
72
|
-
/* dsh deep-query tools section under the timeline. */
|
|
73
54
|
.toolsSection {
|
|
74
55
|
display: flex;
|
|
75
56
|
flex-direction: column;
|
|
76
57
|
gap: 16px;
|
|
77
|
-
border
|
|
78
|
-
|
|
58
|
+
border: 1px solid var(--agsc-border);
|
|
59
|
+
border-radius: var(--agsc-radius-control);
|
|
60
|
+
padding: 8px 12px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.toolsToggle {
|
|
64
|
+
align-self: flex-start;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.toolsToggleGlyph {
|
|
68
|
+
flex: none;
|
|
69
|
+
font-size: 10px;
|
|
70
|
+
color: var(--agsc-fg-dimmed);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.injectDialog {
|
|
74
|
+
gap: 0;
|
|
75
|
+
width: min(560px, 100%);
|
|
76
|
+
max-height: min(85vh, 720px);
|
|
77
|
+
padding: 0;
|
|
78
|
+
overflow: auto;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.injectDialog > [data-dsh-part='inject-panel'] {
|
|
82
|
+
width: 100%;
|
|
83
|
+
max-width: none;
|
|
84
|
+
border: 0;
|
|
85
|
+
border-radius: inherit;
|
|
86
|
+
background: transparent;
|
|
79
87
|
}
|