@tarquinen/opencode-dcp 3.2.0-beta0 → 3.2.2-beta0
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/lib/analysis/tokens.ts +225 -0
- package/lib/config.ts +1071 -0
- package/lib/logger.ts +235 -0
- package/lib/messages/query.ts +56 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +260 -0
- package/lib/state/state.ts +180 -0
- package/lib/state/tool-cache.ts +98 -0
- package/lib/state/types.ts +108 -0
- package/lib/state/utils.ts +310 -0
- package/lib/token-utils.ts +162 -0
- package/package.json +17 -20
- package/tui/data/context.ts +177 -0
- package/tui/index.tsx +34 -0
- package/tui/routes/summary.tsx +175 -0
- package/tui/shared/names.ts +9 -0
- package/tui/shared/theme.ts +58 -0
- package/tui/shared/types.ts +38 -0
- package/tui/slots/sidebar-content.tsx +502 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import type {
|
|
3
|
+
MessageStatus as DcpMessageStatus,
|
|
4
|
+
TokenBreakdown as DcpContextBreakdown,
|
|
5
|
+
} from "../../lib/analysis/tokens"
|
|
6
|
+
|
|
7
|
+
export type DcpTuiApi = TuiPluginApi
|
|
8
|
+
export type DcpTuiClient = DcpTuiApi["client"]
|
|
9
|
+
|
|
10
|
+
export type { DcpMessageStatus }
|
|
11
|
+
|
|
12
|
+
export interface DcpActiveBlockInfo {
|
|
13
|
+
topic: string
|
|
14
|
+
summary: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DcpPersistedSummary {
|
|
18
|
+
available: boolean
|
|
19
|
+
activeBlockCount: number
|
|
20
|
+
activeBlocks: DcpActiveBlockInfo[]
|
|
21
|
+
lastUpdated?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DcpAllTimeStats {
|
|
25
|
+
totalTokensSaved: number
|
|
26
|
+
sessionCount: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DcpContextSnapshot {
|
|
30
|
+
sessionID?: string
|
|
31
|
+
breakdown: DcpContextBreakdown
|
|
32
|
+
activeSummaryTokens: number
|
|
33
|
+
persisted: DcpPersistedSummary
|
|
34
|
+
messageStatuses: DcpMessageStatus[]
|
|
35
|
+
allTimeStats: DcpAllTimeStats
|
|
36
|
+
notes: string[]
|
|
37
|
+
loadedAt: number
|
|
38
|
+
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { createEffect, createMemo, createSignal, on, onCleanup, untrack } from "solid-js"
|
|
3
|
+
import type { TuiSlotPlugin } from "@opencode-ai/plugin/tui"
|
|
4
|
+
import { Logger } from "../../lib/logger"
|
|
5
|
+
import {
|
|
6
|
+
createPlaceholderContextSnapshot,
|
|
7
|
+
invalidateContextSnapshot,
|
|
8
|
+
loadContextSnapshotCached,
|
|
9
|
+
peekContextSnapshot,
|
|
10
|
+
} from "../data/context"
|
|
11
|
+
import { getPalette, toneColor, type DcpColor, type DcpPalette } from "../shared/theme"
|
|
12
|
+
import { LABEL, type DcpRouteNames } from "../shared/names"
|
|
13
|
+
import type { DcpActiveBlockInfo, DcpMessageStatus, DcpTuiApi } from "../shared/types"
|
|
14
|
+
|
|
15
|
+
const SINGLE_BORDER = { type: "single" } as any
|
|
16
|
+
const DIM_TEXT = { dim: true } as any
|
|
17
|
+
|
|
18
|
+
const REFRESH_DEBOUNCE_MS = 100
|
|
19
|
+
const MAX_TOPIC_LEN = 30
|
|
20
|
+
|
|
21
|
+
const truncateTopic = (topic: string): string =>
|
|
22
|
+
topic.length > MAX_TOPIC_LEN ? topic.slice(0, MAX_TOPIC_LEN - 3) + "..." : topic
|
|
23
|
+
|
|
24
|
+
const compactTokenCount = (value: number): string => {
|
|
25
|
+
if (value >= 1_000_000) {
|
|
26
|
+
const m = (value / 1_000_000).toFixed(2)
|
|
27
|
+
return `${m}M`
|
|
28
|
+
}
|
|
29
|
+
if (value >= 100_000) return `${Math.round(value / 1000)}K`
|
|
30
|
+
if (value >= 1_000) {
|
|
31
|
+
const k = (value / 1000).toFixed(1)
|
|
32
|
+
return k.endsWith(".0") ? `${Math.round(value / 1000)}K` : `${k}K`
|
|
33
|
+
}
|
|
34
|
+
const d = Math.round(value / 100)
|
|
35
|
+
if (d >= 10) return `${Math.round(value / 1000)}K`
|
|
36
|
+
if (d > 0) return `.${d}K`
|
|
37
|
+
return "0"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const buildMessageRuns = (
|
|
41
|
+
statuses: DcpMessageStatus[],
|
|
42
|
+
): { count: number; status: DcpMessageStatus }[] => {
|
|
43
|
+
if (statuses.length === 0) return []
|
|
44
|
+
|
|
45
|
+
// Group consecutive same-status messages into runs
|
|
46
|
+
const runs: { count: number; status: DcpMessageStatus }[] = []
|
|
47
|
+
let runStart = 0
|
|
48
|
+
for (let i = 1; i <= statuses.length; i++) {
|
|
49
|
+
if (i === statuses.length || statuses[i] !== statuses[runStart]) {
|
|
50
|
+
runs.push({ count: i - runStart, status: statuses[runStart] })
|
|
51
|
+
runStart = i
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return runs
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const SummaryRow = (props: {
|
|
58
|
+
palette: DcpPalette
|
|
59
|
+
label: string
|
|
60
|
+
value: string
|
|
61
|
+
tone?: "text" | "muted" | "accent" | "success" | "warning"
|
|
62
|
+
swatch?: DcpColor
|
|
63
|
+
marginTop?: number
|
|
64
|
+
}) => {
|
|
65
|
+
return (
|
|
66
|
+
<box
|
|
67
|
+
width="100%"
|
|
68
|
+
flexDirection="row"
|
|
69
|
+
justifyContent="space-between"
|
|
70
|
+
marginTop={props.marginTop}
|
|
71
|
+
>
|
|
72
|
+
<box flexDirection="row">
|
|
73
|
+
{props.swatch && <text fg={props.swatch}>{"█ "}</text>}
|
|
74
|
+
<text fg={props.palette.text}>{props.label}</text>
|
|
75
|
+
</box>
|
|
76
|
+
<text fg={toneColor(props.palette, props.tone)}>
|
|
77
|
+
<b>{props.value}</b>
|
|
78
|
+
</text>
|
|
79
|
+
</box>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const SidebarContextBar = (props: {
|
|
84
|
+
palette: DcpPalette
|
|
85
|
+
label: string
|
|
86
|
+
value: number
|
|
87
|
+
total: number
|
|
88
|
+
tone?: "text" | "muted" | "accent" | "success" | "warning"
|
|
89
|
+
}) => {
|
|
90
|
+
const percent = createMemo(() =>
|
|
91
|
+
props.total > 0 ? `${Math.round((props.value / props.total) * 100)}%` : "0%",
|
|
92
|
+
)
|
|
93
|
+
const label = createMemo(() => props.label.padEnd(9, " "))
|
|
94
|
+
return (
|
|
95
|
+
<box width="100%" flexDirection="row">
|
|
96
|
+
<text fg={props.palette.text}>
|
|
97
|
+
{label()}
|
|
98
|
+
{` ${percent().padStart(4, " ")} |`}
|
|
99
|
+
</text>
|
|
100
|
+
<box flexGrow={1} flexDirection="row" height={1}>
|
|
101
|
+
{props.value > 0 && (
|
|
102
|
+
<box
|
|
103
|
+
flexGrow={props.value}
|
|
104
|
+
backgroundColor={toneColor(props.palette, props.tone)}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
{props.total > props.value && <box flexGrow={props.total - props.value} />}
|
|
108
|
+
</box>
|
|
109
|
+
<text fg={props.palette.text}>
|
|
110
|
+
{`| ${compactTokenCount(props.value).padStart(5, " ")}`}
|
|
111
|
+
</text>
|
|
112
|
+
</box>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const SidebarContext = (props: {
|
|
117
|
+
api: DcpTuiApi
|
|
118
|
+
names: DcpRouteNames
|
|
119
|
+
palette: DcpPalette
|
|
120
|
+
sessionID: () => string
|
|
121
|
+
logger: Logger
|
|
122
|
+
}) => {
|
|
123
|
+
const initialSnapshot = peekContextSnapshot(props.sessionID())
|
|
124
|
+
const [snapshot, setSnapshot] = createSignal(
|
|
125
|
+
initialSnapshot ?? createPlaceholderContextSnapshot(props.sessionID()),
|
|
126
|
+
)
|
|
127
|
+
const [loading, setLoading] = createSignal(!initialSnapshot)
|
|
128
|
+
const [error, setError] = createSignal<string>()
|
|
129
|
+
let requestVersion = 0
|
|
130
|
+
let renderTimeout: ReturnType<typeof setTimeout> | undefined
|
|
131
|
+
|
|
132
|
+
const requestRender = () => {
|
|
133
|
+
if (renderTimeout) clearTimeout(renderTimeout)
|
|
134
|
+
renderTimeout = setTimeout(() => {
|
|
135
|
+
renderTimeout = undefined
|
|
136
|
+
try {
|
|
137
|
+
props.api.renderer.requestRender()
|
|
138
|
+
} catch (error) {
|
|
139
|
+
props.logger.warn("Failed to request TUI render", {
|
|
140
|
+
error: error instanceof Error ? error.message : String(error),
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}, 0)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onCleanup(() => {
|
|
147
|
+
if (renderTimeout) clearTimeout(renderTimeout)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const refreshSnapshot = async (
|
|
151
|
+
sessionID: string,
|
|
152
|
+
options?: { invalidate?: boolean; preserveSnapshot?: boolean },
|
|
153
|
+
) => {
|
|
154
|
+
if (options?.invalidate) {
|
|
155
|
+
invalidateContextSnapshot(sessionID)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cached = peekContextSnapshot(sessionID)
|
|
159
|
+
let silentRefresh = false
|
|
160
|
+
if (cached) {
|
|
161
|
+
setSnapshot(cached)
|
|
162
|
+
setLoading(false)
|
|
163
|
+
} else {
|
|
164
|
+
const current = untrack(snapshot)
|
|
165
|
+
if (options?.preserveSnapshot && current?.sessionID === sessionID) {
|
|
166
|
+
silentRefresh = true
|
|
167
|
+
} else {
|
|
168
|
+
setSnapshot(createPlaceholderContextSnapshot(sessionID, ["Loading DCP context..."]))
|
|
169
|
+
setLoading(true)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
setError(undefined)
|
|
173
|
+
if (!silentRefresh) {
|
|
174
|
+
requestRender()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const currentRequest = ++requestVersion
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const value = await loadContextSnapshotCached(props.api.client, props.logger, sessionID)
|
|
181
|
+
if (currentRequest !== requestVersion || props.sessionID() !== sessionID) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
setSnapshot(value)
|
|
185
|
+
setLoading(false)
|
|
186
|
+
requestRender()
|
|
187
|
+
} catch (cause) {
|
|
188
|
+
if (currentRequest !== requestVersion || props.sessionID() !== sessionID) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
props.logger.warn("Failed to refresh sidebar snapshot", {
|
|
192
|
+
sessionID,
|
|
193
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
194
|
+
})
|
|
195
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
196
|
+
setLoading(false)
|
|
197
|
+
requestRender()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
createEffect(
|
|
202
|
+
on(
|
|
203
|
+
props.sessionID,
|
|
204
|
+
(sessionID) => {
|
|
205
|
+
void refreshSnapshot(sessionID)
|
|
206
|
+
},
|
|
207
|
+
{ defer: false },
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
createEffect(
|
|
212
|
+
on(
|
|
213
|
+
props.sessionID,
|
|
214
|
+
(sessionID) => {
|
|
215
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
216
|
+
|
|
217
|
+
const scheduleRefresh = () => {
|
|
218
|
+
if (!sessionID) return
|
|
219
|
+
if (timeout) clearTimeout(timeout)
|
|
220
|
+
timeout = setTimeout(() => {
|
|
221
|
+
timeout = undefined
|
|
222
|
+
void refreshSnapshot(sessionID, {
|
|
223
|
+
invalidate: true,
|
|
224
|
+
preserveSnapshot: true,
|
|
225
|
+
})
|
|
226
|
+
}, REFRESH_DEBOUNCE_MS)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const unsubs = [
|
|
230
|
+
props.api.event.on("message.updated", (event) => {
|
|
231
|
+
if (event.properties.info.sessionID !== sessionID) return
|
|
232
|
+
scheduleRefresh()
|
|
233
|
+
}),
|
|
234
|
+
props.api.event.on("message.removed", (event) => {
|
|
235
|
+
if (event.properties.sessionID !== sessionID) return
|
|
236
|
+
scheduleRefresh()
|
|
237
|
+
}),
|
|
238
|
+
props.api.event.on("message.part.updated", (event) => {
|
|
239
|
+
if (event.properties.part.sessionID !== sessionID) return
|
|
240
|
+
scheduleRefresh()
|
|
241
|
+
}),
|
|
242
|
+
props.api.event.on("message.part.delta", (event) => {
|
|
243
|
+
if (event.properties.sessionID !== sessionID) return
|
|
244
|
+
scheduleRefresh()
|
|
245
|
+
}),
|
|
246
|
+
props.api.event.on("message.part.removed", (event) => {
|
|
247
|
+
if (event.properties.sessionID !== sessionID) return
|
|
248
|
+
scheduleRefresh()
|
|
249
|
+
}),
|
|
250
|
+
props.api.event.on("session.updated", (event) => {
|
|
251
|
+
if (event.properties.info.id !== sessionID) return
|
|
252
|
+
scheduleRefresh()
|
|
253
|
+
}),
|
|
254
|
+
props.api.event.on("session.deleted", (event) => {
|
|
255
|
+
if (event.properties.info.id !== sessionID) return
|
|
256
|
+
scheduleRefresh()
|
|
257
|
+
}),
|
|
258
|
+
props.api.event.on("session.diff", (event) => {
|
|
259
|
+
if (event.properties.sessionID !== sessionID) return
|
|
260
|
+
scheduleRefresh()
|
|
261
|
+
}),
|
|
262
|
+
props.api.event.on("session.error", (event) => {
|
|
263
|
+
if (event.properties.sessionID !== sessionID) return
|
|
264
|
+
scheduleRefresh()
|
|
265
|
+
}),
|
|
266
|
+
props.api.event.on("session.status", (event) => {
|
|
267
|
+
if (event.properties.sessionID !== sessionID) return
|
|
268
|
+
scheduleRefresh()
|
|
269
|
+
}),
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
onCleanup(() => {
|
|
273
|
+
if (timeout) clearTimeout(timeout)
|
|
274
|
+
for (const unsub of unsubs) {
|
|
275
|
+
unsub()
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
},
|
|
279
|
+
{ defer: false },
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
const TOPIC_LIMIT = 3
|
|
284
|
+
const allBlocks = createMemo(() => snapshot().persisted.activeBlocks)
|
|
285
|
+
const [topicsExpanded, setTopicsExpanded] = createSignal(false)
|
|
286
|
+
const blocks = createMemo(() =>
|
|
287
|
+
topicsExpanded() ? allBlocks() : allBlocks().slice(0, TOPIC_LIMIT),
|
|
288
|
+
)
|
|
289
|
+
const topicOverflow = createMemo(() => allBlocks().length - TOPIC_LIMIT)
|
|
290
|
+
|
|
291
|
+
const navigateToSummary = (block: DcpActiveBlockInfo) => {
|
|
292
|
+
props.api.route.navigate(props.names.routes.summary, {
|
|
293
|
+
topic: block.topic,
|
|
294
|
+
summary: block.summary,
|
|
295
|
+
sessionID: props.sessionID(),
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
const fallbackNote = createMemo(() => snapshot().notes[0] ?? "")
|
|
299
|
+
|
|
300
|
+
const messageBarRuns = createMemo(() => buildMessageRuns(snapshot().messageStatuses))
|
|
301
|
+
|
|
302
|
+
const status = createMemo(() => {
|
|
303
|
+
if (error() && snapshot().breakdown.total > 0)
|
|
304
|
+
return { label: "cached", tone: "warning" as const }
|
|
305
|
+
if (error()) return { label: "error", tone: "warning" as const }
|
|
306
|
+
if (loading() && snapshot().breakdown.total > 0)
|
|
307
|
+
return { label: "refreshing", tone: "warning" as const }
|
|
308
|
+
if (loading()) return { label: "loading", tone: "warning" as const }
|
|
309
|
+
return { label: "loaded", tone: "success" as const }
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<box
|
|
314
|
+
width="100%"
|
|
315
|
+
flexDirection="column"
|
|
316
|
+
backgroundColor={props.palette.surface}
|
|
317
|
+
border={SINGLE_BORDER}
|
|
318
|
+
borderColor={props.palette.accent}
|
|
319
|
+
paddingTop={1}
|
|
320
|
+
paddingBottom={1}
|
|
321
|
+
paddingLeft={1}
|
|
322
|
+
paddingRight={1}
|
|
323
|
+
>
|
|
324
|
+
<box flexDirection="row" justifyContent="space-between" alignItems="center">
|
|
325
|
+
<box flexDirection="row" gap={1} alignItems="center">
|
|
326
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={props.palette.accent}>
|
|
327
|
+
<text fg={props.palette.panel}>
|
|
328
|
+
<b>{LABEL}</b>
|
|
329
|
+
</text>
|
|
330
|
+
</box>
|
|
331
|
+
</box>
|
|
332
|
+
<text fg={toneColor(props.palette, status().tone)}>{status().label}</text>
|
|
333
|
+
</box>
|
|
334
|
+
|
|
335
|
+
<SummaryRow
|
|
336
|
+
palette={props.palette}
|
|
337
|
+
label="Current Messages"
|
|
338
|
+
value={`~${compactTokenCount(snapshot().breakdown.total)}`}
|
|
339
|
+
tone="accent"
|
|
340
|
+
swatch={props.palette.accent}
|
|
341
|
+
marginTop={1}
|
|
342
|
+
/>
|
|
343
|
+
<SummaryRow
|
|
344
|
+
palette={props.palette}
|
|
345
|
+
label="Compressed Messages"
|
|
346
|
+
value={`~${compactTokenCount(snapshot().breakdown.prunedTokens)}`}
|
|
347
|
+
tone="accent"
|
|
348
|
+
swatch={props.palette.muted}
|
|
349
|
+
/>
|
|
350
|
+
|
|
351
|
+
{snapshot().messageStatuses.length > 0 && (
|
|
352
|
+
<box width="100%" flexDirection="row" height={1} marginTop={1}>
|
|
353
|
+
{messageBarRuns().map((run) => (
|
|
354
|
+
<box
|
|
355
|
+
flexGrow={run.count}
|
|
356
|
+
backgroundColor={
|
|
357
|
+
run.status === "active" ? props.palette.accent : props.palette.muted
|
|
358
|
+
}
|
|
359
|
+
/>
|
|
360
|
+
))}
|
|
361
|
+
</box>
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
<box width="100%" flexDirection="column" paddingTop={1}>
|
|
365
|
+
<SidebarContextBar
|
|
366
|
+
palette={props.palette}
|
|
367
|
+
label="System"
|
|
368
|
+
value={snapshot().breakdown.system}
|
|
369
|
+
total={snapshot().breakdown.total}
|
|
370
|
+
tone="accent"
|
|
371
|
+
/>
|
|
372
|
+
<SidebarContextBar
|
|
373
|
+
palette={props.palette}
|
|
374
|
+
label="User"
|
|
375
|
+
value={snapshot().breakdown.user}
|
|
376
|
+
total={snapshot().breakdown.total}
|
|
377
|
+
tone="accent"
|
|
378
|
+
/>
|
|
379
|
+
<SidebarContextBar
|
|
380
|
+
palette={props.palette}
|
|
381
|
+
label="Assistant"
|
|
382
|
+
value={snapshot().breakdown.assistant}
|
|
383
|
+
total={snapshot().breakdown.total}
|
|
384
|
+
tone="accent"
|
|
385
|
+
/>
|
|
386
|
+
<SidebarContextBar
|
|
387
|
+
palette={props.palette}
|
|
388
|
+
label="Tools"
|
|
389
|
+
value={snapshot().breakdown.tools}
|
|
390
|
+
total={snapshot().breakdown.total}
|
|
391
|
+
tone="accent"
|
|
392
|
+
/>
|
|
393
|
+
</box>
|
|
394
|
+
|
|
395
|
+
<box width="100%" flexDirection="column" gap={0} paddingTop={1}>
|
|
396
|
+
{blocks().length > 0 ? (
|
|
397
|
+
<>
|
|
398
|
+
<box width="100%" flexDirection="row" justifyContent="space-between">
|
|
399
|
+
<text fg={props.palette.text}>
|
|
400
|
+
<b>Compressed Summaries</b>
|
|
401
|
+
</text>
|
|
402
|
+
<text fg={toneColor(props.palette, "accent")}>
|
|
403
|
+
<b>{`~${compactTokenCount(snapshot().activeSummaryTokens)}`}</b>
|
|
404
|
+
</text>
|
|
405
|
+
</box>
|
|
406
|
+
{blocks().map((block) => (
|
|
407
|
+
<box flexDirection="row" width="100%" height={1}>
|
|
408
|
+
<box flexGrow={1} flexShrink={1} overflow="hidden" height={1}>
|
|
409
|
+
<text fg={props.palette.muted}>
|
|
410
|
+
{truncateTopic(block.topic)}
|
|
411
|
+
</text>
|
|
412
|
+
</box>
|
|
413
|
+
<box flexShrink={0} height={1} paddingLeft={1}>
|
|
414
|
+
<box
|
|
415
|
+
backgroundColor={props.palette.base}
|
|
416
|
+
height={1}
|
|
417
|
+
onMouseUp={() => navigateToSummary(block)}
|
|
418
|
+
>
|
|
419
|
+
<text fg={props.palette.accent}> ▶ </text>
|
|
420
|
+
</box>
|
|
421
|
+
</box>
|
|
422
|
+
</box>
|
|
423
|
+
))}
|
|
424
|
+
{topicOverflow() > 0 ? (
|
|
425
|
+
<box flexDirection="row" width="100%" height={1}>
|
|
426
|
+
<box flexGrow={1} flexShrink={1} height={1}>
|
|
427
|
+
<text {...DIM_TEXT} fg={props.palette.muted}>
|
|
428
|
+
{topicsExpanded()
|
|
429
|
+
? `showing all ${allBlocks().length} topics`
|
|
430
|
+
: `... ${topicOverflow()} more topics`}
|
|
431
|
+
</text>
|
|
432
|
+
</box>
|
|
433
|
+
<box flexShrink={0} height={1} paddingLeft={1}>
|
|
434
|
+
<box
|
|
435
|
+
backgroundColor={props.palette.base}
|
|
436
|
+
height={1}
|
|
437
|
+
onMouseUp={() => setTopicsExpanded(!topicsExpanded())}
|
|
438
|
+
>
|
|
439
|
+
<text fg={props.palette.accent}>
|
|
440
|
+
{topicsExpanded() ? " ▲ " : " ▼ "}
|
|
441
|
+
</text>
|
|
442
|
+
</box>
|
|
443
|
+
</box>
|
|
444
|
+
</box>
|
|
445
|
+
) : null}
|
|
446
|
+
</>
|
|
447
|
+
) : fallbackNote() ? (
|
|
448
|
+
<text fg={props.palette.muted}>{fallbackNote()}</text>
|
|
449
|
+
) : null}
|
|
450
|
+
</box>
|
|
451
|
+
|
|
452
|
+
{snapshot().allTimeStats.sessionCount > 0 && (
|
|
453
|
+
<box width="100%" flexDirection="column" paddingTop={1}>
|
|
454
|
+
<text fg={props.palette.text}>
|
|
455
|
+
<b>All Time</b>
|
|
456
|
+
</text>
|
|
457
|
+
<SummaryRow
|
|
458
|
+
palette={props.palette}
|
|
459
|
+
label="Tokens Saved"
|
|
460
|
+
value={`~${compactTokenCount(snapshot().allTimeStats.totalTokensSaved)}`}
|
|
461
|
+
tone="accent"
|
|
462
|
+
/>
|
|
463
|
+
<SummaryRow
|
|
464
|
+
palette={props.palette}
|
|
465
|
+
label="Sessions"
|
|
466
|
+
value={`${snapshot().allTimeStats.sessionCount}`}
|
|
467
|
+
tone="accent"
|
|
468
|
+
/>
|
|
469
|
+
</box>
|
|
470
|
+
)}
|
|
471
|
+
</box>
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export const createSidebarContentSlot = (
|
|
476
|
+
api: DcpTuiApi,
|
|
477
|
+
names: DcpRouteNames,
|
|
478
|
+
logger: Logger,
|
|
479
|
+
): TuiSlotPlugin => {
|
|
480
|
+
const renderSidebar = (
|
|
481
|
+
ctx: { theme: { current: Record<string, unknown> } },
|
|
482
|
+
value: { session_id: string },
|
|
483
|
+
) => {
|
|
484
|
+
const palette = createMemo(() => getPalette(ctx.theme.current as Record<string, unknown>))
|
|
485
|
+
return (
|
|
486
|
+
<SidebarContext
|
|
487
|
+
api={api}
|
|
488
|
+
names={names}
|
|
489
|
+
palette={palette()}
|
|
490
|
+
sessionID={() => value.session_id}
|
|
491
|
+
logger={logger}
|
|
492
|
+
/>
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
order: 90,
|
|
498
|
+
slots: {
|
|
499
|
+
sidebar_content: renderSidebar,
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
}
|