@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.
Files changed (159) hide show
  1. package/bin/dshtui.js +65 -0
  2. package/cordis.patch.yml +81 -0
  3. package/lib/contract/index.d.mts +2 -0
  4. package/lib/contract/index.mjs +5 -0
  5. package/lib/dialog.d.mts +156 -0
  6. package/lib/dialog.mjs +3 -0
  7. package/lib/index-DqnaSXqP.d.mts +823 -0
  8. package/lib/index.d.mts +17 -0
  9. package/lib/index.mjs +11404 -0
  10. package/lib/prod-react-Dzof4qJx.mjs +4 -0
  11. package/lib/surface-DdxzIgIY.mjs +3126 -0
  12. package/lib/vendor.d.mts +3 -0
  13. package/lib/vendor.mjs +4 -0
  14. package/package.json +128 -0
  15. package/src/apply-theme.ts +8 -0
  16. package/src/boot-log.ts +35 -0
  17. package/src/chat/blocks.ts +41 -0
  18. package/src/chat/bridge.ts +1076 -0
  19. package/src/chat/builtin-tool-views.ts +53 -0
  20. package/src/chat/chat-face.ts +16 -0
  21. package/src/chat/chat-nodes.ts +53 -0
  22. package/src/chat/efforts.ts +4 -0
  23. package/src/chat/interactions.ts +329 -0
  24. package/src/chat/models.ts +263 -0
  25. package/src/chat/partial-json.ts +125 -0
  26. package/src/chat/presets.ts +26 -0
  27. package/src/chat/question-view.ts +106 -0
  28. package/src/chat/retry-status.ts +33 -0
  29. package/src/chat/session-groups.ts +34 -0
  30. package/src/chat/session-list.ts +311 -0
  31. package/src/chat/store.ts +636 -0
  32. package/src/chat/todo-view.ts +66 -0
  33. package/src/chat/tool-view.ts +311 -0
  34. package/src/chat/tool-views.ts +114 -0
  35. package/src/contract/index.ts +704 -0
  36. package/src/contract/upstream.ts +165 -0
  37. package/src/core/caret-nonce.ts +19 -0
  38. package/src/core/composer-layout.ts +65 -0
  39. package/src/core/edit.ts +152 -0
  40. package/src/core/field-view.ts +52 -0
  41. package/src/core/fields.ts +185 -0
  42. package/src/core/metrics.ts +26 -0
  43. package/src/core/paste.ts +149 -0
  44. package/src/core/segments.ts +112 -0
  45. package/src/core/text.ts +290 -0
  46. package/src/dialog.ts +11 -0
  47. package/src/env.ts +38 -0
  48. package/src/harness-home.ts +22 -0
  49. package/src/hot-theme.ts +56 -0
  50. package/src/index.tsx +185 -0
  51. package/src/kernel/registry.ts +102 -0
  52. package/src/kernel/surface.ts +33 -0
  53. package/src/log.ts +249 -0
  54. package/src/model/message.ts +89 -0
  55. package/src/model/selection.ts +63 -0
  56. package/src/performance-guard.ts +28 -0
  57. package/src/runtime/prod-react.ts +10 -0
  58. package/src/terminal/background.ts +66 -0
  59. package/src/terminal/capabilities.ts +54 -0
  60. package/src/terminal/clipboard-backends.ts +69 -0
  61. package/src/terminal/clipboard.ts +161 -0
  62. package/src/terminal/cursor-shape.ts +13 -0
  63. package/src/terminal/glyphs.ts +44 -0
  64. package/src/terminal/mouse.ts +154 -0
  65. package/src/terminal/probe.ts +61 -0
  66. package/src/terminal/screen.ts +349 -0
  67. package/src/theme-settings.ts +31 -0
  68. package/src/theme.ts +319 -0
  69. package/src/ui/app.tsx +454 -0
  70. package/src/ui/chrome/caps.tsx +9 -0
  71. package/src/ui/chrome/hint-service.ts +87 -0
  72. package/src/ui/chrome/input-status.ts +27 -0
  73. package/src/ui/chrome/key-gate.tsx +29 -0
  74. package/src/ui/chrome/status-bar.tsx +147 -0
  75. package/src/ui/contributions.ts +47 -0
  76. package/src/ui/dialog/defaults-dialog.tsx +152 -0
  77. package/src/ui/dialog/dialog-item.tsx +38 -0
  78. package/src/ui/dialog/dialog.tsx +386 -0
  79. package/src/ui/dialog/effort-dialog.tsx +45 -0
  80. package/src/ui/dialog/geometry.ts +131 -0
  81. package/src/ui/dialog/items.ts +124 -0
  82. package/src/ui/dialog/list-dialog.tsx +63 -0
  83. package/src/ui/dialog/models-dialog.tsx +203 -0
  84. package/src/ui/dialog/presets-dialog.tsx +46 -0
  85. package/src/ui/dialog/providers-dialog.tsx +313 -0
  86. package/src/ui/dialog/sessions-dialog.tsx +163 -0
  87. package/src/ui/dialog/sizes.ts +7 -0
  88. package/src/ui/dialog/status-lines.ts +11 -0
  89. package/src/ui/dialog/todo-dialog.tsx +29 -0
  90. package/src/ui/dialog/use-dialog-input.ts +186 -0
  91. package/src/ui/extension-point.ts +188 -0
  92. package/src/ui/home-logo.ts +132 -0
  93. package/src/ui/hooks/use-async-action.ts +25 -0
  94. package/src/ui/hooks/use-async-list.ts +71 -0
  95. package/src/ui/hooks/use-caret.ts +24 -0
  96. package/src/ui/hooks/use-chat-events.ts +153 -0
  97. package/src/ui/hooks/use-mouse-selection.ts +352 -0
  98. package/src/ui/hooks/use-scroll.ts +49 -0
  99. package/src/ui/hooks/use-terminal-size.ts +20 -0
  100. package/src/ui/input/commands.ts +118 -0
  101. package/src/ui/input/composer-bus.ts +14 -0
  102. package/src/ui/input/composer-fields.ts +111 -0
  103. package/src/ui/input/composer-keys.ts +21 -0
  104. package/src/ui/input/composer-paste.ts +20 -0
  105. package/src/ui/input/input-bar.tsx +279 -0
  106. package/src/ui/input/use-composer.ts +495 -0
  107. package/src/ui/key-arbiter.ts +37 -0
  108. package/src/ui/keymap.ts +15 -0
  109. package/src/ui/layout-service.ts +77 -0
  110. package/src/ui/message/layout.ts +763 -0
  111. package/src/ui/message/md/block.ts +247 -0
  112. package/src/ui/message/md/engine.ts +430 -0
  113. package/src/ui/message/md/extensions.ts +58 -0
  114. package/src/ui/message/md/highlight.ts +626 -0
  115. package/src/ui/message/md/index.ts +14 -0
  116. package/src/ui/message/md/inline.ts +97 -0
  117. package/src/ui/message/md/palette.ts +87 -0
  118. package/src/ui/message/message-list.tsx +69 -0
  119. package/src/ui/message/message-row.tsx +137 -0
  120. package/src/ui/message/message-views.ts +16 -0
  121. package/src/ui/message/renderers.ts +39 -0
  122. package/src/ui/message/tool-diff.ts +188 -0
  123. package/src/ui/message/warmup.ts +41 -0
  124. package/src/ui/message/wave.ts +98 -0
  125. package/src/ui/overlay.ts +22 -0
  126. package/src/ui/panels/approval-panel.tsx +193 -0
  127. package/src/ui/panels/exports.ts +3 -0
  128. package/src/ui/panels/question-model.ts +424 -0
  129. package/src/ui/panels/question-panel.tsx +154 -0
  130. package/src/ui/panels/surface.tsx +106 -0
  131. package/src/ui/panels-builtin.tsx +86 -0
  132. package/src/ui/pointer/builtins.ts +281 -0
  133. package/src/ui/pointer/registry.ts +25 -0
  134. package/src/ui/region.tsx +31 -0
  135. package/src/ui/selection/service.ts +92 -0
  136. package/src/ui/selection-registry.ts +88 -0
  137. package/src/ui/selection.tsx +148 -0
  138. package/src/ui/spinner-tick.tsx +24 -0
  139. package/src/ui/use-available-windows.ts +31 -0
  140. package/src/ui/widgets/actions.tsx +84 -0
  141. package/src/ui/widgets/button.tsx +57 -0
  142. package/src/ui/widgets/checkbox.tsx +58 -0
  143. package/src/ui/widgets/header.tsx +36 -0
  144. package/src/ui/widgets/index.ts +8 -0
  145. package/src/ui/widgets/input.tsx +76 -0
  146. package/src/ui/widgets/registry.ts +32 -0
  147. package/src/ui/widgets/search.tsx +59 -0
  148. package/src/ui/widgets/select.tsx +97 -0
  149. package/src/ui/widgets/static.tsx +37 -0
  150. package/src/ui/widgets/types.ts +1 -0
  151. package/src/ui/window-services-bridge.ts +71 -0
  152. package/src/ui/window-services.ts +20 -0
  153. package/src/ui/windows/models-window.tsx +15 -0
  154. package/src/ui/windows/providers-window.tsx +14 -0
  155. package/src/ui/windows/service-window.tsx +18 -0
  156. package/src/ui/windows/sessions-window.tsx +25 -0
  157. package/src/ui/windows-builtin.tsx +94 -0
  158. package/src/ui/windows.ts +18 -0
  159. 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
+ }