@xp266/dshtui 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.
- package/bin/dshtui.js +65 -0
- package/cordis.patch.yml +81 -0
- package/lib/contract/index.d.mts +2 -0
- package/lib/contract/index.mjs +5 -0
- package/lib/dialog.d.mts +156 -0
- package/lib/dialog.mjs +3 -0
- package/lib/index-DqnaSXqP.d.mts +823 -0
- package/lib/index.d.mts +17 -0
- package/lib/index.mjs +11404 -0
- package/lib/prod-react-Dzof4qJx.mjs +4 -0
- package/lib/surface-DdxzIgIY.mjs +3126 -0
- package/lib/vendor.d.mts +3 -0
- package/lib/vendor.mjs +4 -0
- package/package.json +128 -0
- package/src/apply-theme.ts +8 -0
- package/src/boot-log.ts +35 -0
- package/src/chat/blocks.ts +41 -0
- package/src/chat/bridge.ts +1076 -0
- package/src/chat/builtin-tool-views.ts +53 -0
- package/src/chat/chat-face.ts +16 -0
- package/src/chat/chat-nodes.ts +53 -0
- package/src/chat/efforts.ts +4 -0
- package/src/chat/interactions.ts +329 -0
- package/src/chat/models.ts +263 -0
- package/src/chat/partial-json.ts +125 -0
- package/src/chat/presets.ts +26 -0
- package/src/chat/question-view.ts +106 -0
- package/src/chat/retry-status.ts +33 -0
- package/src/chat/session-groups.ts +34 -0
- package/src/chat/session-list.ts +311 -0
- package/src/chat/store.ts +636 -0
- package/src/chat/todo-view.ts +66 -0
- package/src/chat/tool-view.ts +311 -0
- package/src/chat/tool-views.ts +114 -0
- package/src/contract/index.ts +704 -0
- package/src/contract/upstream.ts +165 -0
- package/src/core/caret-nonce.ts +19 -0
- package/src/core/composer-layout.ts +65 -0
- package/src/core/edit.ts +152 -0
- package/src/core/field-view.ts +52 -0
- package/src/core/fields.ts +185 -0
- package/src/core/metrics.ts +26 -0
- package/src/core/paste.ts +149 -0
- package/src/core/segments.ts +112 -0
- package/src/core/text.ts +290 -0
- package/src/dialog.ts +11 -0
- package/src/env.ts +38 -0
- package/src/harness-home.ts +22 -0
- package/src/hot-theme.ts +56 -0
- package/src/index.tsx +185 -0
- package/src/kernel/registry.ts +102 -0
- package/src/kernel/surface.ts +33 -0
- package/src/log.ts +249 -0
- package/src/model/message.ts +89 -0
- package/src/model/selection.ts +63 -0
- package/src/performance-guard.ts +28 -0
- package/src/runtime/prod-react.ts +10 -0
- package/src/terminal/background.ts +66 -0
- package/src/terminal/capabilities.ts +54 -0
- package/src/terminal/clipboard-backends.ts +69 -0
- package/src/terminal/clipboard.ts +161 -0
- package/src/terminal/cursor-shape.ts +13 -0
- package/src/terminal/glyphs.ts +44 -0
- package/src/terminal/mouse.ts +154 -0
- package/src/terminal/probe.ts +61 -0
- package/src/terminal/screen.ts +349 -0
- package/src/theme-settings.ts +31 -0
- package/src/theme.ts +319 -0
- package/src/ui/app.tsx +454 -0
- package/src/ui/chrome/caps.tsx +9 -0
- package/src/ui/chrome/hint-service.ts +87 -0
- package/src/ui/chrome/input-status.ts +27 -0
- package/src/ui/chrome/key-gate.tsx +29 -0
- package/src/ui/chrome/status-bar.tsx +147 -0
- package/src/ui/contributions.ts +47 -0
- package/src/ui/dialog/defaults-dialog.tsx +152 -0
- package/src/ui/dialog/dialog-item.tsx +38 -0
- package/src/ui/dialog/dialog.tsx +386 -0
- package/src/ui/dialog/effort-dialog.tsx +45 -0
- package/src/ui/dialog/geometry.ts +131 -0
- package/src/ui/dialog/items.ts +124 -0
- package/src/ui/dialog/list-dialog.tsx +63 -0
- package/src/ui/dialog/models-dialog.tsx +203 -0
- package/src/ui/dialog/presets-dialog.tsx +46 -0
- package/src/ui/dialog/providers-dialog.tsx +313 -0
- package/src/ui/dialog/sessions-dialog.tsx +163 -0
- package/src/ui/dialog/sizes.ts +7 -0
- package/src/ui/dialog/status-lines.ts +11 -0
- package/src/ui/dialog/todo-dialog.tsx +29 -0
- package/src/ui/dialog/use-dialog-input.ts +186 -0
- package/src/ui/extension-point.ts +188 -0
- package/src/ui/home-logo.ts +132 -0
- package/src/ui/hooks/use-async-action.ts +25 -0
- package/src/ui/hooks/use-async-list.ts +71 -0
- package/src/ui/hooks/use-caret.ts +24 -0
- package/src/ui/hooks/use-chat-events.ts +153 -0
- package/src/ui/hooks/use-mouse-selection.ts +352 -0
- package/src/ui/hooks/use-scroll.ts +49 -0
- package/src/ui/hooks/use-terminal-size.ts +20 -0
- package/src/ui/input/commands.ts +118 -0
- package/src/ui/input/composer-bus.ts +14 -0
- package/src/ui/input/composer-fields.ts +111 -0
- package/src/ui/input/composer-keys.ts +21 -0
- package/src/ui/input/composer-paste.ts +20 -0
- package/src/ui/input/input-bar.tsx +279 -0
- package/src/ui/input/use-composer.ts +495 -0
- package/src/ui/key-arbiter.ts +37 -0
- package/src/ui/keymap.ts +15 -0
- package/src/ui/layout-service.ts +77 -0
- package/src/ui/message/layout.ts +763 -0
- package/src/ui/message/md/block.ts +247 -0
- package/src/ui/message/md/engine.ts +430 -0
- package/src/ui/message/md/extensions.ts +58 -0
- package/src/ui/message/md/highlight.ts +626 -0
- package/src/ui/message/md/index.ts +14 -0
- package/src/ui/message/md/inline.ts +97 -0
- package/src/ui/message/md/palette.ts +87 -0
- package/src/ui/message/message-list.tsx +69 -0
- package/src/ui/message/message-row.tsx +137 -0
- package/src/ui/message/message-views.ts +16 -0
- package/src/ui/message/renderers.ts +39 -0
- package/src/ui/message/tool-diff.ts +188 -0
- package/src/ui/message/warmup.ts +41 -0
- package/src/ui/message/wave.ts +98 -0
- package/src/ui/overlay.ts +22 -0
- package/src/ui/panels/approval-panel.tsx +193 -0
- package/src/ui/panels/exports.ts +3 -0
- package/src/ui/panels/question-model.ts +424 -0
- package/src/ui/panels/question-panel.tsx +154 -0
- package/src/ui/panels/surface.tsx +106 -0
- package/src/ui/panels-builtin.tsx +86 -0
- package/src/ui/pointer/builtins.ts +281 -0
- package/src/ui/pointer/registry.ts +25 -0
- package/src/ui/region.tsx +31 -0
- package/src/ui/selection/service.ts +92 -0
- package/src/ui/selection-registry.ts +88 -0
- package/src/ui/selection.tsx +148 -0
- package/src/ui/spinner-tick.tsx +24 -0
- package/src/ui/use-available-windows.ts +31 -0
- package/src/ui/widgets/actions.tsx +84 -0
- package/src/ui/widgets/button.tsx +57 -0
- package/src/ui/widgets/checkbox.tsx +58 -0
- package/src/ui/widgets/header.tsx +36 -0
- package/src/ui/widgets/index.ts +8 -0
- package/src/ui/widgets/input.tsx +76 -0
- package/src/ui/widgets/registry.ts +32 -0
- package/src/ui/widgets/search.tsx +59 -0
- package/src/ui/widgets/select.tsx +97 -0
- package/src/ui/widgets/static.tsx +37 -0
- package/src/ui/widgets/types.ts +1 -0
- package/src/ui/window-services-bridge.ts +71 -0
- package/src/ui/window-services.ts +20 -0
- package/src/ui/windows/models-window.tsx +15 -0
- package/src/ui/windows/providers-window.tsx +14 -0
- package/src/ui/windows/service-window.tsx +18 -0
- package/src/ui/windows/sessions-window.tsx +25 -0
- package/src/ui/windows-builtin.tsx +94 -0
- package/src/ui/windows.ts +18 -0
- package/src/vendor.ts +12 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises'
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
3
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
4
|
+
import { textFromBlocks } from './blocks.ts'
|
|
5
|
+
import { isBlankSession } from './presets.ts'
|
|
6
|
+
|
|
7
|
+
export interface SessionSummary {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
directory: string
|
|
11
|
+
ungrouped: boolean
|
|
12
|
+
updatedAt: number
|
|
13
|
+
modifiedAt?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function sessionTime(session: SessionSummary): number {
|
|
17
|
+
return session.modifiedAt ?? session.updatedAt
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Session picker data, mirroring the Host's `listVisibleSessionSummaries`
|
|
22
|
+
* with the client-side visibility rules: live sessions plus persisted cold
|
|
23
|
+
* sessions with a cwd, blank and subagent-origin rows hidden, newest-first,
|
|
24
|
+
* without any workspace filtering (the picker labels ungrouped sessions).
|
|
25
|
+
* Cold rows read projection-cache hints or a size-gated small-artifact
|
|
26
|
+
* probe; a hint-less conversation title comes from the cache's cold ladder
|
|
27
|
+
* (one log read, then persisted), never a repeated full read.
|
|
28
|
+
*/
|
|
29
|
+
export async function computeSessionList(ctx: Context): Promise<SessionSummary[]> {
|
|
30
|
+
const workspacePaths = collectWorkspacePaths(ctx)
|
|
31
|
+
const archived = collectArchivedIds(ctx)
|
|
32
|
+
const summaries: SessionSummary[] = []
|
|
33
|
+
const attached = new Set<string>()
|
|
34
|
+
const titleService = ctx.get('sessionTitle') as
|
|
35
|
+
| { get?(session: { id: string; events: readonly SessionEvent[] }): { title?: string } | undefined }
|
|
36
|
+
| undefined
|
|
37
|
+
for (const session of ctx.sessions.list()) {
|
|
38
|
+
const id = String(session.id)
|
|
39
|
+
attached.add(id)
|
|
40
|
+
if (session.header.origin === 'subagent') continue
|
|
41
|
+
if (archived.has(id)) continue
|
|
42
|
+
if (isBlankSession(session.events)) continue
|
|
43
|
+
const title = titleService?.get?.(session)?.title ?? firstUserText(session.events)
|
|
44
|
+
summaries.push(toSummary(id, title, session.header.cwd, session.header.createdAt, lastPromptAt(session.events), workspacePaths))
|
|
45
|
+
}
|
|
46
|
+
const persistence = ctx.get('sessionPersistence') as PersistenceLike | undefined
|
|
47
|
+
const cold = await listColdHeaders(ctx, persistence, attached)
|
|
48
|
+
await mergeColdSummaries(ctx, persistence, cold, archived, workspacePaths, summaries)
|
|
49
|
+
await attachModifiedTimes(ctx, summaries)
|
|
50
|
+
return summaries.sort((a, b) => sessionTime(b) - sessionTime(a))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ColdHeader {
|
|
54
|
+
id: string
|
|
55
|
+
cwd?: string
|
|
56
|
+
createdAt: number
|
|
57
|
+
origin?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface SessionQueryLike {
|
|
61
|
+
listSessions?(signal?: AbortSignal): Promise<Array<{ header: ColdHeader; live: boolean; persisted: boolean }>>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ProjectionCacheLike {
|
|
65
|
+
cachedSnapshot?(meta: ColdHeader): { asOfSeq: number; values: Record<string, unknown> } | undefined
|
|
66
|
+
coldSnapshot?(id: string, signal?: AbortSignal): Promise<{ asOfSeq: number; values: Record<string, unknown> } | undefined>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Cold-session headers from the official query corpus (`sessionQuery`,
|
|
71
|
+
* live-preferred, zero log reads), falling back to `persistence.list()`
|
|
72
|
+
* (first-line header reads) when the query service is not mounted.
|
|
73
|
+
*/
|
|
74
|
+
async function listColdHeaders(ctx: Context, persistence: PersistenceLike | undefined, attached: ReadonlySet<string>): Promise<ColdHeader[]> {
|
|
75
|
+
const query = ctx.get('sessionQuery') as SessionQueryLike | undefined
|
|
76
|
+
if (query?.listSessions !== undefined) {
|
|
77
|
+
const records = await query.listSessions()
|
|
78
|
+
return records
|
|
79
|
+
.filter(record => !record.live && record.persisted && !attached.has(String(record.header.id)))
|
|
80
|
+
.filter(record => record.header.cwd !== undefined && record.header.origin !== 'subagent')
|
|
81
|
+
.map(record => record.header)
|
|
82
|
+
}
|
|
83
|
+
if (persistence?.list !== undefined) {
|
|
84
|
+
const headers = await persistence.list()
|
|
85
|
+
return headers.filter(header => !attached.has(String(header.id)) && header.cwd !== undefined && header.origin !== 'subagent')
|
|
86
|
+
}
|
|
87
|
+
return []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function attachModifiedTimes(ctx: Context, summaries: SessionSummary[]): Promise<void> {
|
|
91
|
+
const persistence = ctx.get('sessionPersistence') as PersistenceLike | undefined
|
|
92
|
+
const locate = persistence?.locate
|
|
93
|
+
if (locate === undefined) return
|
|
94
|
+
await Promise.all(summaries.map(async summary => {
|
|
95
|
+
try {
|
|
96
|
+
const location = locate({ id: summary.id, cwd: summary.directory === '' ? undefined : summary.directory })
|
|
97
|
+
if (location === undefined) return
|
|
98
|
+
const info = await stat(location.path)
|
|
99
|
+
summary.modifiedAt = Math.floor(info.mtimeMs)
|
|
100
|
+
} catch {
|
|
101
|
+
// A missing or unreadable session artifact keeps the updatedAt timestamp.
|
|
102
|
+
}
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toSummary(
|
|
107
|
+
id: string,
|
|
108
|
+
title: string | undefined,
|
|
109
|
+
cwd: string | undefined,
|
|
110
|
+
createdAt: number,
|
|
111
|
+
updatedAt: number,
|
|
112
|
+
workspacePaths: ReadonlySet<string>,
|
|
113
|
+
): SessionSummary {
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
name: title ?? fallbackName(id, cwd),
|
|
117
|
+
directory: cwd ?? '',
|
|
118
|
+
ungrouped: cwd === undefined || !workspacePaths.has(cwd),
|
|
119
|
+
updatedAt: Math.max(createdAt, updatedAt),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function collectWorkspacePaths(ctx: Context): Set<string> {
|
|
124
|
+
const workspace = ctx.get('workspaceRegistry') as { list?(): Array<{ path: string }> } | undefined
|
|
125
|
+
try {
|
|
126
|
+
const paths = new Set(workspace?.list?.().map(entry => entry.path) ?? [])
|
|
127
|
+
return paths.size > 0 ? paths : new Set([process.cwd()])
|
|
128
|
+
} catch {
|
|
129
|
+
// Without a readable workspace list every session is treated as ungrouped.
|
|
130
|
+
return new Set([process.cwd()])
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function collectArchivedIds(ctx: Context): Set<string> {
|
|
135
|
+
const workspace = ctx.get('workspaceRegistry') as { archivedSessionIds?: readonly unknown[] } | undefined
|
|
136
|
+
try {
|
|
137
|
+
return new Set((workspace?.archivedSessionIds ?? []).map(String))
|
|
138
|
+
} catch {
|
|
139
|
+
// Without a readable archive list no session is hidden.
|
|
140
|
+
return new Set()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function lastPromptAt(events: readonly SessionEvent[]): number {
|
|
145
|
+
let last = 0
|
|
146
|
+
for (const event of events) {
|
|
147
|
+
if (event.type === 'user/message' && event.data.source.kind === 'user') last = event.time
|
|
148
|
+
}
|
|
149
|
+
return last
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface PersistenceLike {
|
|
153
|
+
list(signal?: AbortSignal): Promise<ColdHeader[]>
|
|
154
|
+
locate?(header: { cwd?: string; id: string }): { path: string } | undefined
|
|
155
|
+
readFrom?(id: string, offset: number, signal?: AbortSignal): Promise<{ events: SessionEvent[] }>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface SessionMetaCacheEntry {
|
|
159
|
+
title: string | undefined
|
|
160
|
+
blank: boolean | undefined
|
|
161
|
+
promptAt: number
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const SESSION_META_CACHE_MAX = 512
|
|
165
|
+
const sessionMetaCache = new Map<string, SessionMetaCacheEntry>()
|
|
166
|
+
|
|
167
|
+
function cacheSessionMeta(id: string, entry: SessionMetaCacheEntry): void {
|
|
168
|
+
while (sessionMetaCache.size >= SESSION_META_CACHE_MAX) {
|
|
169
|
+
const oldest = sessionMetaCache.keys().next()
|
|
170
|
+
if (oldest.done) break
|
|
171
|
+
sessionMetaCache.delete(oldest.value)
|
|
172
|
+
}
|
|
173
|
+
sessionMetaCache.set(id, entry)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const PROBE_BATCH_SIZE = 16
|
|
177
|
+
|
|
178
|
+
/** Mirrors the host's cold blank-probe gate: only small artifacts are read. */
|
|
179
|
+
const COLD_PROBE_MAX_BYTES = 1024
|
|
180
|
+
|
|
181
|
+
interface ColdMeta {
|
|
182
|
+
title: string | undefined
|
|
183
|
+
blank: boolean
|
|
184
|
+
promptAt: number
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function listMetadataHint(values: Record<string, unknown>): { blank: boolean; lastPromptAt: number | null } | undefined {
|
|
188
|
+
const hint = values.sessionListMetadata as { blank?: unknown; lastPromptAt?: unknown } | undefined
|
|
189
|
+
if (typeof hint?.blank !== 'boolean') return undefined
|
|
190
|
+
return {
|
|
191
|
+
blank: hint.blank,
|
|
192
|
+
lastPromptAt: typeof hint.lastPromptAt === 'number' ? hint.lastPromptAt : null,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Cold-session metadata without unconditional full-log reads: projection
|
|
198
|
+
* hints first (zero I/O), then a direct read only when the artifact is small
|
|
199
|
+
* enough to be a blank-session log. A hint-less oversized artifact is
|
|
200
|
+
* conservatively visible; its title comes from the projection cache's cold
|
|
201
|
+
* ladder, which reads the log once and writes the row back durably.
|
|
202
|
+
*/
|
|
203
|
+
async function resolveColdMeta(cache: ProjectionCacheLike | undefined, persistence: PersistenceLike, header: ColdHeader): Promise<ColdMeta> {
|
|
204
|
+
let titleHint: string | undefined
|
|
205
|
+
let metadata: { blank: boolean; lastPromptAt: number | null } | undefined
|
|
206
|
+
try {
|
|
207
|
+
const values = cache?.cachedSnapshot?.(header)?.values ?? {}
|
|
208
|
+
if (typeof values.title === 'string' && values.title !== '') titleHint = values.title
|
|
209
|
+
metadata = listMetadataHint(values)
|
|
210
|
+
} catch {
|
|
211
|
+
// A broken projection cache must not block the session picker; fall back to probing.
|
|
212
|
+
}
|
|
213
|
+
if (metadata?.blank === false) {
|
|
214
|
+
return { title: await coldTitle(cache, header, titleHint), blank: false, promptAt: metadata.lastPromptAt ?? 0 }
|
|
215
|
+
}
|
|
216
|
+
const probed = await probeSmallArtifact(persistence, header)
|
|
217
|
+
if (probed !== undefined) {
|
|
218
|
+
return { title: probed.title ?? titleHint, blank: probed.blank, promptAt: probed.promptAt }
|
|
219
|
+
}
|
|
220
|
+
return { title: await coldTitle(cache, header, titleHint), blank: false, promptAt: metadata?.lastPromptAt ?? 0 }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function coldTitle(cache: ProjectionCacheLike | undefined, header: ColdHeader, titleHint: string | undefined): Promise<string | undefined> {
|
|
224
|
+
if (titleHint !== undefined || cache?.coldSnapshot === undefined) return titleHint
|
|
225
|
+
try {
|
|
226
|
+
const snapshot = await cache.coldSnapshot(String(header.id))
|
|
227
|
+
const title = snapshot?.values?.title
|
|
228
|
+
return typeof title === 'string' && title !== '' ? title : undefined
|
|
229
|
+
} catch {
|
|
230
|
+
// A failed cold-snapshot read leaves the session without a title.
|
|
231
|
+
return undefined
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function probeSmallArtifact(persistence: PersistenceLike, header: ColdHeader): Promise<ColdMeta | undefined> {
|
|
236
|
+
if (persistence.locate === undefined || persistence.readFrom === undefined) return undefined
|
|
237
|
+
const location = persistence.locate(header)
|
|
238
|
+
if (location === undefined) return undefined
|
|
239
|
+
try {
|
|
240
|
+
const info = await stat(location.path)
|
|
241
|
+
if (info.size > COLD_PROBE_MAX_BYTES) return undefined
|
|
242
|
+
const { events } = await persistence.readFrom(String(header.id), 0)
|
|
243
|
+
return { blank: isBlankSession(events), title: titleFromEvents(events) ?? firstUserText(events), promptAt: lastPromptAt(events) }
|
|
244
|
+
} catch {
|
|
245
|
+
// An unreadable artifact is treated as a blank session (skipped in the list).
|
|
246
|
+
return undefined
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function mergeColdSummaries(
|
|
251
|
+
ctx: Context,
|
|
252
|
+
persistence: PersistenceLike | undefined,
|
|
253
|
+
cold: ColdHeader[],
|
|
254
|
+
archived: ReadonlySet<string>,
|
|
255
|
+
workspacePaths: ReadonlySet<string>,
|
|
256
|
+
summaries: SessionSummary[],
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
if (persistence === undefined) return
|
|
259
|
+
const cache = ctx.get('sessionProjectionCache') as ProjectionCacheLike | undefined
|
|
260
|
+
const pending: ColdHeader[] = []
|
|
261
|
+
for (const header of cold) {
|
|
262
|
+
if (archived.has(String(header.id))) continue
|
|
263
|
+
const cached = sessionMetaCache.get(String(header.id))
|
|
264
|
+
if (cached !== undefined && cached.blank !== undefined) {
|
|
265
|
+
if (!cached.blank) {
|
|
266
|
+
summaries.push(toSummary(String(header.id), cached.title, header.cwd, header.createdAt, cached.promptAt, workspacePaths))
|
|
267
|
+
}
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
pending.push(header)
|
|
271
|
+
}
|
|
272
|
+
for (let index = 0; index < pending.length; index += PROBE_BATCH_SIZE) {
|
|
273
|
+
await Promise.all(pending.slice(index, index + PROBE_BATCH_SIZE).map(async header => {
|
|
274
|
+
const meta = await resolveColdMeta(cache, persistence, header)
|
|
275
|
+
cacheSessionMeta(String(header.id), meta)
|
|
276
|
+
if (!meta.blank) {
|
|
277
|
+
summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths))
|
|
278
|
+
}
|
|
279
|
+
}))
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function fallbackName(id: string, directory?: string): string {
|
|
284
|
+
if (directory !== undefined && directory !== '') {
|
|
285
|
+
const base = directory.replace(/[/\\]+$/, '').split(/[/\\]/).pop()
|
|
286
|
+
if (base !== undefined && base !== '') return base
|
|
287
|
+
}
|
|
288
|
+
const match = id.match(/[^/]+$/)
|
|
289
|
+
return match?.[0] ?? id
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function titleFromEvents(events: readonly SessionEvent[]): string | undefined {
|
|
293
|
+
let title: string | undefined
|
|
294
|
+
for (const event of events) {
|
|
295
|
+
if ((event as { type?: string }).type === 'session/title') {
|
|
296
|
+
const data = (event as { data?: { title?: string } }).data
|
|
297
|
+
if (data?.title !== undefined && data.title !== '') title = data.title
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return title
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function firstUserText(events: readonly SessionEvent[]): string | undefined {
|
|
304
|
+
for (const event of events) {
|
|
305
|
+
if (event.type !== 'user/message') continue
|
|
306
|
+
if (event.data.source.kind !== 'user') continue
|
|
307
|
+
const text = textFromBlocks(event.data.content)
|
|
308
|
+
if (text.trim() !== '') return text
|
|
309
|
+
}
|
|
310
|
+
return undefined
|
|
311
|
+
}
|