@shendeguize/dsh-agent-sidecar 0.1.1 → 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 +8659 -7589
- 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 +115 -61
- package/src/client/board/board.module.css +72 -151
- package/src/client/board/project-view-logic.ts +21 -34
- package/src/client/board/project-view.module.css +41 -96
- package/src/client/board/project-view.tsx +29 -13
- package/src/client/board/strings.ts +68 -107
- package/src/client/commands.ts +30 -15
- package/src/client/detail/SessionDetail.tsx +92 -58
- package/src/client/detail/detail.module.css +62 -218
- package/src/client/detail/strings.ts +64 -88
- package/src/client/detail-view.module.css +30 -55
- package/src/client/detail-view.tsx +80 -108
- 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 +50 -139
- package/src/client/dsh-tools/strings.ts +42 -77
- package/src/client/index.ts +285 -124
- package/src/client/inject/InjectPanel.tsx +31 -64
- package/src/client/inject/inject.module.css +97 -198
- package/src/client/lifecycle/handoff.ts +83 -0
- package/src/client/locales/en.ts +74 -2
- 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 +194 -134
- package/src/client/mount.tsx +72 -38
- 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 +9 -8
- package/src/client/inject/overlay.module.css +0 -22
package/src/client/commands.ts
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* `dsh-agent-teams/src/command.ts`).
|
|
19
19
|
* - The only `ui.kind` this dsh version supports is `popupSelect`:
|
|
20
20
|
* an async `options(session, signal)` provider plus `onSelect`. The
|
|
21
|
-
* `/sidecar` overview therefore presents as a popup card of rows
|
|
22
|
-
*
|
|
21
|
+
* `/sidecar` overview therefore presents as a popup card of rows; its
|
|
22
|
+
* board row opens Agent Center while informational rows remain inert.
|
|
23
23
|
*
|
|
24
24
|
* The `commandUi` service type is NOT part of the published plugin SDK
|
|
25
25
|
* (same situation as `settingsScope` in ./index.ts), so this module keeps
|
|
@@ -55,7 +55,12 @@ import type {
|
|
|
55
55
|
SessionStatusToken,
|
|
56
56
|
WidgetConnection,
|
|
57
57
|
} from './board/logic.ts'
|
|
58
|
+
import {
|
|
59
|
+
acquireWithHandoff,
|
|
60
|
+
isRegistrationCollision,
|
|
61
|
+
} from './lifecycle/handoff.ts'
|
|
58
62
|
import { tCommand } from './locales/command.ts'
|
|
63
|
+
import type { CenterNavigation } from './navigation/center.ts'
|
|
59
64
|
|
|
60
65
|
/** The slash command name (without the leading slash). */
|
|
61
66
|
export const SIDECAR_COMMAND_NAME = 'sidecar'
|
|
@@ -399,6 +404,8 @@ export interface SidecarCommandDeps {
|
|
|
399
404
|
now?: () => number
|
|
400
405
|
/** Session-row cap; defaults to {@link DEFAULT_OVERVIEW_TOP_N}. */
|
|
401
406
|
topN?: number
|
|
407
|
+
/** Opens Agent Center when the board option is selected. */
|
|
408
|
+
openCenter?: CenterNavigation
|
|
402
409
|
}
|
|
403
410
|
|
|
404
411
|
/**
|
|
@@ -435,8 +442,8 @@ export function createSidecarCommandContribution(
|
|
|
435
442
|
if (deps.topN !== undefined) overviewOpts.topN = deps.topN
|
|
436
443
|
return overviewToOptions(buildOverview(snapshot, overviewOpts))
|
|
437
444
|
},
|
|
438
|
-
onSelect: () => {
|
|
439
|
-
|
|
445
|
+
onSelect: (option) => {
|
|
446
|
+
if (option.id === 'board') deps.openCenter?.()
|
|
440
447
|
},
|
|
441
448
|
},
|
|
442
449
|
}
|
|
@@ -451,7 +458,10 @@ export function createSidecarCommandContribution(
|
|
|
451
458
|
* `ClientContext` satisfies it structurally (cordis `ctx.inject`).
|
|
452
459
|
*/
|
|
453
460
|
export interface CommandMountContext {
|
|
454
|
-
inject(
|
|
461
|
+
inject(
|
|
462
|
+
deps: readonly string[],
|
|
463
|
+
callback: (ctx: unknown) => (() => void) | void,
|
|
464
|
+
): unknown
|
|
455
465
|
}
|
|
456
466
|
|
|
457
467
|
/**
|
|
@@ -459,11 +469,9 @@ export interface CommandMountContext {
|
|
|
459
469
|
* the design's `ctx.commands` consumption row — a composition without the
|
|
460
470
|
* slash-menu runtime simply never gains the command).
|
|
461
471
|
*
|
|
462
|
-
*
|
|
463
|
-
* name
|
|
464
|
-
*
|
|
465
|
-
* client half down. Disposal is owned by the registry's effect on the
|
|
466
|
-
* injected fiber — unloading the plugin unregisters the command.
|
|
472
|
+
* HMR handoff: a duplicate contribution means the old fiber still owns the
|
|
473
|
+
* name. The new injected fiber retries briefly, then registers its own fresh
|
|
474
|
+
* contribution after the old disposer runs. Foreign squatters time out.
|
|
467
475
|
*/
|
|
468
476
|
export function registerSidecarCommand(
|
|
469
477
|
ctx: CommandMountContext,
|
|
@@ -472,11 +480,18 @@ export function registerSidecarCommand(
|
|
|
472
480
|
try {
|
|
473
481
|
ctx.inject(['commandUi'], (injected) => {
|
|
474
482
|
const { commandUi } = injected as { commandUi: CommandRegistryFace }
|
|
475
|
-
|
|
476
|
-
commandUi.register(createSidecarCommandContribution(deps))
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
483
|
+
return acquireWithHandoff(
|
|
484
|
+
() => commandUi.register(createSidecarCommandContribution(deps)),
|
|
485
|
+
{
|
|
486
|
+
isCollision: isRegistrationCollision,
|
|
487
|
+
onError: (error) => {
|
|
488
|
+
console.error('agent-sidecar: /sidecar command registration failed', error)
|
|
489
|
+
},
|
|
490
|
+
onTimeout: () => {
|
|
491
|
+
console.error('agent-sidecar: /sidecar command handoff timed out')
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
)
|
|
480
495
|
})
|
|
481
496
|
} catch (err) {
|
|
482
497
|
console.error('agent-sidecar: commandUi injection failed', err)
|
|
@@ -21,6 +21,13 @@
|
|
|
21
21
|
* else comes through props.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import {
|
|
25
|
+
Button,
|
|
26
|
+
Pill,
|
|
27
|
+
StateDot,
|
|
28
|
+
writeClipboard,
|
|
29
|
+
type StateDotState,
|
|
30
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
24
31
|
import { Fragment, useEffect, useRef, useState, type ReactElement } from 'react'
|
|
25
32
|
import {
|
|
26
33
|
aggregateChunkRows,
|
|
@@ -40,6 +47,8 @@ import {
|
|
|
40
47
|
type TimelineVM,
|
|
41
48
|
} from './logic.ts'
|
|
42
49
|
import { DETAIL_STRINGS } from './strings.ts'
|
|
50
|
+
import { StaticPill } from '../primitives/StaticPill.tsx'
|
|
51
|
+
import { surfaceProps } from '../theme/parts.ts'
|
|
43
52
|
import styles from './detail.module.css'
|
|
44
53
|
|
|
45
54
|
export interface SessionDetailHeaderVM {
|
|
@@ -100,19 +109,22 @@ function EventRow(props: {
|
|
|
100
109
|
{DETAIL_STRINGS.timeline.seq.replace('{n}', String(entry.seq))}
|
|
101
110
|
</span>
|
|
102
111
|
)}
|
|
103
|
-
{row.isNew && <
|
|
112
|
+
{row.isNew && <Pill className={styles['eventNew']}>{DETAIL_STRINGS.timeline.newBadge}</Pill>}
|
|
104
113
|
<span className={styles['eventSpacer']} />
|
|
105
114
|
<span className={styles['eventTime']}>{row.timeLabel}</span>
|
|
106
115
|
</div>
|
|
107
116
|
{entry.summary !== '' && <div className={styles['eventSummary']}>{entry.summary}</div>}
|
|
108
117
|
{entry.expandable && (
|
|
109
|
-
<
|
|
118
|
+
<Button
|
|
110
119
|
type="button"
|
|
120
|
+
size="sm"
|
|
121
|
+
variant="ghost"
|
|
111
122
|
className={styles['expandButton']}
|
|
123
|
+
aria-expanded={expanded}
|
|
112
124
|
onClick={() => onToggleExpand(entry.key)}
|
|
113
125
|
>
|
|
114
126
|
{expanded ? DETAIL_STRINGS.timeline.collapse : DETAIL_STRINGS.timeline.expand}
|
|
115
|
-
</
|
|
127
|
+
</Button>
|
|
116
128
|
)}
|
|
117
129
|
{entry.expandable && expanded && entry.body !== null && (
|
|
118
130
|
<pre className={styles['eventBody']}>{entry.body}</pre>
|
|
@@ -139,13 +151,16 @@ function ChunkRunRow(props: {
|
|
|
139
151
|
data-testid="agent-sidecar-detail-chunks"
|
|
140
152
|
>
|
|
141
153
|
<span className={styles['chunkRunLabel']}>{row.label}</span>
|
|
142
|
-
<
|
|
154
|
+
<Button
|
|
143
155
|
type="button"
|
|
156
|
+
size="sm"
|
|
157
|
+
variant="ghost"
|
|
144
158
|
className={styles['expandButton']}
|
|
159
|
+
aria-expanded={props.expanded}
|
|
145
160
|
onClick={() => props.onToggleRun(row.key)}
|
|
146
161
|
>
|
|
147
162
|
{props.expanded ? DETAIL_STRINGS.timeline.collapse : DETAIL_STRINGS.timeline.expand}
|
|
148
|
-
</
|
|
163
|
+
</Button>
|
|
149
164
|
<span className={styles['eventSpacer']} />
|
|
150
165
|
<span className={styles['eventTime']}>{row.timeLabel}</span>
|
|
151
166
|
</li>
|
|
@@ -173,6 +188,7 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
173
188
|
const listRef = useRef<HTMLOListElement | null>(null)
|
|
174
189
|
const positionedRef = useRef(false)
|
|
175
190
|
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
191
|
+
const copyAliveRef = useRef(true)
|
|
176
192
|
|
|
177
193
|
const status = deriveDetailStatus(props.header.status)
|
|
178
194
|
const sourceBadges = deriveSourceBadges(props.timeline.sources)
|
|
@@ -209,12 +225,16 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
209
225
|
}
|
|
210
226
|
}, [listening, entryCount])
|
|
211
227
|
|
|
212
|
-
useEffect(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
copyAliveRef.current = true
|
|
230
|
+
return () => {
|
|
231
|
+
copyAliveRef.current = false
|
|
232
|
+
if (copyTimerRef.current !== null) {
|
|
233
|
+
clearTimeout(copyTimerRef.current)
|
|
234
|
+
copyTimerRef.current = null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}, [])
|
|
218
238
|
|
|
219
239
|
const toggleExpand = (key: string): void => {
|
|
220
240
|
setExpandedKeys((prev) => {
|
|
@@ -234,31 +254,32 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
234
254
|
})
|
|
235
255
|
}
|
|
236
256
|
|
|
237
|
-
const copySessionId = (): void => {
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}, 2000)
|
|
247
|
-
},
|
|
248
|
-
() => {
|
|
249
|
-
// Clipboard permission denied: silently keep the plain id display.
|
|
250
|
-
},
|
|
251
|
-
)
|
|
257
|
+
const copySessionId = async (): Promise<void> => {
|
|
258
|
+
if (!(await writeClipboard(props.sessionId))) return
|
|
259
|
+
if (!copyAliveRef.current) return
|
|
260
|
+
if (copyTimerRef.current !== null) clearTimeout(copyTimerRef.current)
|
|
261
|
+
setCopied(true)
|
|
262
|
+
copyTimerRef.current = setTimeout(() => {
|
|
263
|
+
copyTimerRef.current = null
|
|
264
|
+
setCopied(false)
|
|
265
|
+
}, 2000)
|
|
252
266
|
}
|
|
253
267
|
|
|
268
|
+
const statusDotState: StateDotState | null =
|
|
269
|
+
status.status === 'working'
|
|
270
|
+
? 'ongoing'
|
|
271
|
+
: status.status === 'waiting'
|
|
272
|
+
? 'warning'
|
|
273
|
+
: null
|
|
274
|
+
|
|
254
275
|
return (
|
|
255
|
-
<div
|
|
276
|
+
<div {...surfaceProps('timeline', styles['root'])} data-testid="agent-sidecar-detail">
|
|
256
277
|
<header className={styles['header']}>
|
|
257
278
|
<div className={styles['headerTop']}>
|
|
258
279
|
{props.onClose !== undefined && (
|
|
259
|
-
<
|
|
280
|
+
<Button type="button" size="sm" variant="outline" onClick={props.onClose}>
|
|
260
281
|
{DETAIL_STRINGS.header.close}
|
|
261
|
-
</
|
|
282
|
+
</Button>
|
|
262
283
|
)}
|
|
263
284
|
<span className={styles['agent']}>
|
|
264
285
|
<span className={styles['agentGlyph']} aria-hidden>
|
|
@@ -266,15 +287,18 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
266
287
|
</span>
|
|
267
288
|
{props.header.agent}
|
|
268
289
|
</span>
|
|
269
|
-
<
|
|
270
|
-
|
|
290
|
+
<StaticPill className={styles['badge']} title={DETAIL_STRINGS.header.observedDisclaimer}>
|
|
291
|
+
{statusDotState === null
|
|
292
|
+
? <span className={styles['dot']} data-tone={status.tone} />
|
|
293
|
+
: <StateDot state={statusDotState} size={8} />}
|
|
271
294
|
{status.label}
|
|
272
|
-
</
|
|
295
|
+
</StaticPill>
|
|
273
296
|
<span className={styles['spacer']} />
|
|
274
297
|
{props.onRefresh !== undefined && (
|
|
275
|
-
<
|
|
298
|
+
<Button
|
|
276
299
|
type="button"
|
|
277
|
-
|
|
300
|
+
size="sm"
|
|
301
|
+
variant="outline"
|
|
278
302
|
disabled={props.refreshing === true}
|
|
279
303
|
title={DETAIL_STRINGS.header.refreshHint}
|
|
280
304
|
onClick={props.onRefresh}
|
|
@@ -283,18 +307,17 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
283
307
|
{props.refreshing === true
|
|
284
308
|
? DETAIL_STRINGS.header.refreshing
|
|
285
309
|
: DETAIL_STRINGS.header.refresh}
|
|
286
|
-
</
|
|
310
|
+
</Button>
|
|
287
311
|
)}
|
|
288
|
-
<
|
|
312
|
+
<Pill
|
|
289
313
|
type="button"
|
|
290
|
-
|
|
314
|
+
active={props.listening}
|
|
291
315
|
aria-pressed={props.listening}
|
|
292
|
-
data-active={props.listening || undefined}
|
|
293
316
|
title={DETAIL_STRINGS.header.listenHint}
|
|
294
317
|
onClick={props.onToggleListen}
|
|
295
318
|
>
|
|
296
319
|
{props.listening ? DETAIL_STRINGS.header.listenOn : DETAIL_STRINGS.header.listenOff}
|
|
297
|
-
</
|
|
320
|
+
</Pill>
|
|
298
321
|
</div>
|
|
299
322
|
<div className={styles['title']} title={props.header.title}>
|
|
300
323
|
{props.header.title.trim() === '' ? DETAIL_STRINGS.header.untitled : props.header.title}
|
|
@@ -305,19 +328,21 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
305
328
|
? DETAIL_STRINGS.header.unknownProject
|
|
306
329
|
: props.header.project}
|
|
307
330
|
</span>
|
|
308
|
-
<
|
|
331
|
+
<Button
|
|
309
332
|
type="button"
|
|
333
|
+
size="sm"
|
|
334
|
+
variant="ghost"
|
|
310
335
|
className={styles['sessionId']}
|
|
311
336
|
title={`${props.sessionId} · ${DETAIL_STRINGS.header.copyIdTitle}`}
|
|
312
|
-
onClick={copySessionId}
|
|
337
|
+
onClick={() => { void copySessionId() }}
|
|
313
338
|
data-testid="agent-sidecar-detail-copy-id"
|
|
314
339
|
>
|
|
315
340
|
{props.sessionId}
|
|
316
|
-
</
|
|
341
|
+
</Button>
|
|
317
342
|
{copied && (
|
|
318
|
-
<
|
|
343
|
+
<StaticPill className={styles['copiedBubble']} role="status">
|
|
319
344
|
{DETAIL_STRINGS.header.copied}
|
|
320
|
-
</
|
|
345
|
+
</StaticPill>
|
|
321
346
|
)}
|
|
322
347
|
</div>
|
|
323
348
|
<div className={styles['metaRow']}>
|
|
@@ -325,9 +350,13 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
325
350
|
{sourceBadges.length > 0 && (
|
|
326
351
|
<span className={styles['sourceList']} title={DETAIL_STRINGS.sources.title}>
|
|
327
352
|
{sourceBadges.map((badge) => (
|
|
328
|
-
<
|
|
353
|
+
<StaticPill
|
|
354
|
+
key={badge.id}
|
|
355
|
+
className={styles['sourceBadge']}
|
|
356
|
+
data-tone={badge.tone}
|
|
357
|
+
>
|
|
329
358
|
{badge.label}
|
|
330
|
-
</
|
|
359
|
+
</StaticPill>
|
|
331
360
|
))}
|
|
332
361
|
</span>
|
|
333
362
|
)}
|
|
@@ -335,13 +364,17 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
335
364
|
</header>
|
|
336
365
|
|
|
337
366
|
{bodyState.errorBanner !== null && (
|
|
338
|
-
<div className={styles['banner']} role="
|
|
367
|
+
<div className={styles['banner']} role="alert">
|
|
339
368
|
{bodyState.errorBanner}
|
|
340
369
|
</div>
|
|
341
370
|
)}
|
|
342
371
|
|
|
343
372
|
{bodyState.kind !== 'list' ? (
|
|
344
|
-
<div
|
|
373
|
+
<div
|
|
374
|
+
className={styles['bodyState']}
|
|
375
|
+
data-kind={bodyState.kind}
|
|
376
|
+
role={bodyState.kind === 'error' ? 'alert' : bodyState.kind === 'loading' ? 'status' : undefined}
|
|
377
|
+
>
|
|
345
378
|
<div className={styles['bodyStateTitle']}>{bodyState.title}</div>
|
|
346
379
|
{bodyState.hint !== null && <div className={styles['bodyStateHint']}>{bodyState.hint}</div>}
|
|
347
380
|
</div>
|
|
@@ -349,18 +382,17 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
349
382
|
<>
|
|
350
383
|
<div className={styles['filterRow']} data-testid="agent-sidecar-detail-filter">
|
|
351
384
|
{(['conversation', 'all'] as const).map((mode) => (
|
|
352
|
-
<
|
|
385
|
+
<Pill
|
|
353
386
|
key={mode}
|
|
354
387
|
type="button"
|
|
355
|
-
|
|
356
|
-
data-active={filterMode === mode || undefined}
|
|
388
|
+
active={filterMode === mode}
|
|
357
389
|
aria-pressed={filterMode === mode}
|
|
358
390
|
onClick={() => {
|
|
359
391
|
setFilterMode(mode)
|
|
360
392
|
}}
|
|
361
393
|
>
|
|
362
394
|
{DETAIL_STRINGS.filter[mode]}
|
|
363
|
-
</
|
|
395
|
+
</Pill>
|
|
364
396
|
))}
|
|
365
397
|
{filtered.hiddenCount > 0 && (
|
|
366
398
|
<span className={styles['filterHiddenNote']}>
|
|
@@ -370,16 +402,17 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
370
402
|
</div>
|
|
371
403
|
<div className={styles['pager']}>
|
|
372
404
|
{props.hasMore ? (
|
|
373
|
-
<
|
|
405
|
+
<Button
|
|
374
406
|
type="button"
|
|
375
|
-
|
|
407
|
+
size="sm"
|
|
408
|
+
variant="outline"
|
|
376
409
|
disabled={props.loading}
|
|
377
410
|
onClick={props.onLoadMore}
|
|
378
411
|
>
|
|
379
412
|
{props.loading
|
|
380
413
|
? DETAIL_STRINGS.timeline.loadingMore
|
|
381
414
|
: DETAIL_STRINGS.timeline.loadMore}
|
|
382
|
-
</
|
|
415
|
+
</Button>
|
|
383
416
|
) : (
|
|
384
417
|
<span className={styles['pagerNote']}>{DETAIL_STRINGS.timeline.noMore}</span>
|
|
385
418
|
)}
|
|
@@ -387,13 +420,14 @@ export function SessionDetail(props: SessionDetailProps): ReactElement {
|
|
|
387
420
|
{limited.notice !== null && (
|
|
388
421
|
<div className={styles['hiddenNotice']}>
|
|
389
422
|
{limited.notice}
|
|
390
|
-
<
|
|
423
|
+
<Button
|
|
391
424
|
type="button"
|
|
392
|
-
|
|
425
|
+
size="sm"
|
|
426
|
+
variant="ghost"
|
|
393
427
|
onClick={() => setRenderAll(true)}
|
|
394
428
|
>
|
|
395
429
|
{DETAIL_STRINGS.timeline.showAll}
|
|
396
|
-
</
|
|
430
|
+
</Button>
|
|
397
431
|
</div>
|
|
398
432
|
)}
|
|
399
433
|
<ol className={styles['timeline']} ref={listRef}>
|