dsh-code 1.0.6 → 1.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.
Files changed (86) hide show
  1. package/README.en.md +123 -26
  2. package/README.md +124 -27
  3. package/bin/deepseek.mjs +283 -35
  4. package/cordis.patch.yml +97 -0
  5. package/lib/index.mjs +5008 -881
  6. package/lib/session-query.mjs +150 -0
  7. package/lib/startup.mjs +4 -4
  8. package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
  9. package/lib/types/app.d.ts +106 -62
  10. package/lib/types/authorization-panel.d.ts +3 -3
  11. package/lib/types/git-workflow.d.ts +91 -2
  12. package/lib/types/i18n.d.ts +39 -0
  13. package/lib/types/index.d.ts +100 -1
  14. package/lib/types/input-split.d.ts +1 -1
  15. package/lib/types/kernel-panels.d.ts +107 -29
  16. package/lib/types/language-panel.d.ts +12 -0
  17. package/lib/types/locales/en.d.ts +450 -0
  18. package/lib/types/locales/zh.d.ts +9 -0
  19. package/lib/types/mentions.d.ts +7 -3
  20. package/lib/types/models.d.ts +14 -0
  21. package/lib/types/panel-accent.d.ts +28 -0
  22. package/lib/types/rainbow.d.ts +69 -0
  23. package/lib/types/render/animations.d.ts +42 -0
  24. package/lib/types/render/editor.d.ts +4 -3
  25. package/lib/types/render/ime-cursor.d.ts +60 -0
  26. package/lib/types/render/inspector.d.ts +26 -0
  27. package/lib/types/render/lines.d.ts +21 -1
  28. package/lib/types/render/markdown.d.ts +1 -1
  29. package/lib/types/render/projection.d.ts +130 -4
  30. package/lib/types/render/status.d.ts +9 -9
  31. package/lib/types/render/text.d.ts +6 -0
  32. package/lib/types/render/usage.d.ts +113 -0
  33. package/lib/types/session-directory.d.ts +17 -0
  34. package/lib/types/session-query.d.ts +92 -0
  35. package/lib/types/startup.d.ts +1 -1
  36. package/lib/types/terminal-title.d.ts +66 -0
  37. package/lib/types/theme-panel.d.ts +2 -2
  38. package/lib/types/theme.d.ts +271 -52
  39. package/lib/types/update-panel.d.ts +49 -0
  40. package/lib/types/update.d.ts +75 -0
  41. package/lib/types/version.d.ts +4 -3
  42. package/package.json +246 -90
  43. package/src/app.ts +1369 -509
  44. package/src/approval.ts +166 -166
  45. package/src/authorization-panel.ts +19 -16
  46. package/src/editor-keys.ts +371 -371
  47. package/src/git-workflow.ts +229 -3
  48. package/src/i18n.ts +68 -0
  49. package/src/index.ts +534 -80
  50. package/src/input-split.ts +3 -3
  51. package/src/internals.ts +5 -0
  52. package/src/kernel-panels.ts +554 -86
  53. package/src/keyboard.ts +5 -4
  54. package/src/language-panel.ts +53 -0
  55. package/src/locales/en.ts +489 -0
  56. package/src/locales/zh.ts +488 -0
  57. package/src/mentions.ts +8 -4
  58. package/src/models.ts +264 -212
  59. package/src/panel-accent.ts +41 -0
  60. package/src/presets.ts +1 -1
  61. package/src/provider-settings.ts +1 -1
  62. package/src/rainbow.ts +208 -0
  63. package/src/render/animations.ts +104 -6
  64. package/src/render/editor.ts +25 -24
  65. package/src/render/export.ts +116 -95
  66. package/src/render/ime-cursor.ts +147 -0
  67. package/src/render/inspector.ts +42 -0
  68. package/src/render/lines.ts +628 -415
  69. package/src/render/markdown.ts +15 -3
  70. package/src/render/projection.ts +572 -21
  71. package/src/render/status.ts +59 -39
  72. package/src/render/text.ts +14 -0
  73. package/src/render/tool-preview.ts +77 -77
  74. package/src/render/usage.ts +430 -0
  75. package/src/render/width.ts +2 -2
  76. package/src/session-directory.ts +8 -6
  77. package/src/session-query.ts +239 -0
  78. package/src/startup.ts +3 -3
  79. package/src/subagents.ts +229 -229
  80. package/src/terminal-title.ts +190 -0
  81. package/src/theme-panel.ts +17 -21
  82. package/src/theme.ts +281 -33
  83. package/src/update-panel.ts +256 -0
  84. package/src/update.ts +126 -0
  85. package/src/version.ts +58 -20
  86. package/src/whale-glyph.ts +23 -23
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Skip-tolerant session-query engine for this terminal.
3
+ *
4
+ * The upstream SqliteSessionQueryEngine reconciliation observes EVERY
5
+ * persisted session before each search, and ONE unreadable source (for
6
+ * example a pre-release session artifact the frozen format codecs reject)
7
+ * fails the whole pass with SESSION_QUERY_PERSISTENCE_FAILED — every
8
+ * cross-session search dies because of a single old file nobody opened
9
+ * otherwise. This subclass overrides only the observation loop so an
10
+ * unreadable source is skipped with a warning and the rest of the corpus
11
+ * indexes normally; skipped sessions are retried on later reconciliations
12
+ * and rejoin automatically once a host that can read them is installed.
13
+ *
14
+ * Vendored surface note: `_observeStable` and its module-local helpers are
15
+ * private upstream; this file re-declares the observation loop against the
16
+ * pinned @deepseek-ai line (see package.json peers) and must be re-checked
17
+ * whenever that line moves. The engine class and the tool boundary also
18
+ * share ONE physical parent-package instance through this bundle, which
19
+ * restores instanceof-based typed error messages on the search path.
20
+ */
21
+
22
+ import { createHash } from 'node:crypto'
23
+ import SqliteSessionQueryEngine, { type Config } from '@deepseek-ai/dsh-session-query-sqlite'
24
+ import {
25
+ assertSessionHeadersCompatible,
26
+ buildSessionEventSearchDocuments,
27
+ readColdSessionLog,
28
+ SessionQueryError,
29
+ } from '@deepseek-ai/dsh-session-query'
30
+ import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
31
+ import type { SessionPersistenceRevision, SessionPersistenceSnapshot } from '@deepseek-ai/dsh-session-persistence'
32
+
33
+ /** One observed session: detached header plus its derived search documents. */
34
+ interface ObservedSession {
35
+ header: SessionHeader
36
+ inheritedEventCount: SessionLogOffset
37
+ documents: readonly ReturnType<typeof buildSessionEventSearchDocuments>[number][]
38
+ fingerprint: string
39
+ }
40
+
41
+ /** One persisted snapshot as the reconciliation sees it (loaded once readable). */
42
+ interface ObservedPersistedSession {
43
+ header: SessionHeader
44
+ revision: SessionPersistenceRevision
45
+ loaded?: ObservedSession
46
+ /** Diagnosis for a cold read that failed; the session stays unindexed. */
47
+ unreadable?: string
48
+ }
49
+
50
+ /** The engine-internal state the observation loop touches. */
51
+ export interface EngineSurface {
52
+ readonly ctx: {
53
+ sessions: {
54
+ list(): readonly { header: SessionHeader; inheritedEventCount: SessionLogOffset; snapshotEvents(): readonly SessionEvent[]; id: SessionId }[]
55
+ get(id: SessionId): unknown
56
+ }
57
+ logger?: { warn(format: string, ...args: readonly unknown[]): void }
58
+ }
59
+ readonly _persistenceBinding: {
60
+ readonly identity: symbol
61
+ readonly service?: {
62
+ list(options?: { readonly signal?: AbortSignal }): Promise<readonly SessionPersistenceSnapshot[]>
63
+ }
64
+ }
65
+ _lastPersistenceIdentity: symbol | undefined
66
+ }
67
+
68
+ type ColdRead = (persistence: NonNullable<EngineSurface['_persistenceBinding']['service']>, id: SessionId, signal: AbortSignal | undefined) => Promise<{ header: SessionHeader; inheritedEventCount: SessionLogOffset; events: readonly SessionEvent[] }>
69
+
70
+ const STABLE_OBSERVATION_ATTEMPTS = 2
71
+
72
+ function assertNotAborted(signal: AbortSignal | undefined): void {
73
+ if (signal?.aborted) {
74
+ throw new SessionQueryError('session-search aborted', 'SESSION_QUERY_ABORTED')
75
+ }
76
+ }
77
+
78
+ function isAbort(error: unknown): boolean {
79
+ return error instanceof SessionQueryError && error.code === 'SESSION_QUERY_ABORTED'
80
+ }
81
+
82
+ function errorMessage(error: unknown): string {
83
+ return error instanceof Error ? error.message : 'unknown error'
84
+ }
85
+
86
+ function observeSession(header: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[]): ObservedSession {
87
+ const detachedHeader = structuredClone(header)
88
+ const detachedEvents = events.map(event => structuredClone(event))
89
+ return {
90
+ header: detachedHeader,
91
+ inheritedEventCount,
92
+ documents: buildSessionEventSearchDocuments(detachedHeader.id, detachedEvents),
93
+ fingerprint: createHash('sha256')
94
+ .update(JSON.stringify({ header: detachedHeader, inheritedEventCount, events: detachedEvents }))
95
+ .digest('base64url'),
96
+ }
97
+ }
98
+
99
+ function sameHeader(a: SessionHeader, b: SessionHeader): boolean {
100
+ return a.id === b.id
101
+ && a.createdAt === b.createdAt
102
+ && a.cwd === b.cwd
103
+ && a.parentSession === b.parentSession
104
+ && a.isSeeded === b.isSeeded
105
+ && (a.delegationDepth ?? 0) === (b.delegationDepth ?? 0)
106
+ && a.agentPreset === b.agentPreset
107
+ }
108
+
109
+ function materializePersistenceSnapshots(snapshots: readonly SessionPersistenceSnapshot[]): Map<SessionId, ObservedPersistedSession> {
110
+ // `Array.isArray` narrows a declared array type to `any[]`, which would let
111
+ // every later access escape the type system. Step through `unknown` so the
112
+ // guard stays a runtime assertion and the element type stays declared.
113
+ const candidate: unknown = snapshots
114
+ if (!Array.isArray(candidate)) throw new Error('persistence snapshots must be an array')
115
+ const result = new Map<SessionId, ObservedPersistedSession>()
116
+ for (const snapshot of candidate as readonly SessionPersistenceSnapshot[]) {
117
+ if (typeof snapshot.revision !== 'string') {
118
+ throw new Error('persistence snapshot revision must be a string')
119
+ }
120
+ const header = structuredClone(snapshot.header)
121
+ if (result.has(header.id)) {
122
+ throw new Error(`persistence listed duplicate session "${header.id}"`)
123
+ }
124
+ result.set(header.id, { header, revision: snapshot.revision })
125
+ }
126
+ return result
127
+ }
128
+
129
+ function samePersistenceSnapshots(before: ReadonlyMap<SessionId, ObservedPersistedSession>, after: ReadonlyMap<SessionId, ObservedPersistedSession>): boolean {
130
+ if (before.size !== after.size) return false
131
+ for (const [id, first] of before) {
132
+ const second = after.get(id)
133
+ if (second === undefined || first.revision !== second.revision || !sameHeader(first.header, second.header)) return false
134
+ }
135
+ return true
136
+ }
137
+
138
+ /**
139
+ * The skip-tolerant observation pass: structurally the upstream loop, with
140
+ * the per-source cold read wrapped so one unreadable session degrades to a
141
+ * warning instead of failing every search. Exported for unit tests with an
142
+ * injectable cold reader.
143
+ */
144
+ export async function observeStableWithSkip(
145
+ engine: EngineSurface,
146
+ indexed: ReadonlyMap<SessionId, { revision: SessionPersistenceRevision }>,
147
+ signal: AbortSignal | undefined,
148
+ readCold: ColdRead = readColdSessionLog as unknown as ColdRead,
149
+ ): Promise<{ persistenceBinding: EngineSurface['_persistenceBinding']; persisted: Map<SessionId, ObservedPersistedSession>; live: Map<SessionId, ObservedSession> }> {
150
+ for (let attempt = 0; attempt < STABLE_OBSERVATION_ATTEMPTS; attempt += 1) {
151
+ assertNotAborted(signal)
152
+ const persistenceBinding = engine._persistenceBinding
153
+ const persistence = persistenceBinding.service
154
+ const initiallyLive = new Set(engine.ctx.sessions.list().map(session => session.id))
155
+ let persisted = new Map<SessionId, ObservedPersistedSession>()
156
+ if (persistence !== undefined) {
157
+ try {
158
+ const canReuseIndexed = engine._lastPersistenceIdentity === undefined
159
+ || engine._lastPersistenceIdentity === persistenceBinding.identity
160
+ const listOptions = signal === undefined ? undefined : { signal }
161
+ const before = await persistence.list(listOptions)
162
+ assertNotAborted(signal)
163
+ persisted = materializePersistenceSnapshots(before)
164
+ for (const entry of persisted.values()) {
165
+ if (canReuseIndexed && indexed.get(entry.header.id)?.revision === entry.revision) continue
166
+ if (initiallyLive.has(entry.header.id) || engine.ctx.sessions.get(entry.header.id) !== undefined) continue
167
+ assertNotAborted(signal)
168
+ // The skip: a session whose cold log fails to migrate or decode
169
+ // stays OUT of the index with one warning; the remaining corpus
170
+ // indexes normally. `loaded` stays undefined exactly like a
171
+ // not-yet-read entry, so the stable-snapshot comparison and the
172
+ // live-preferred merge below are unaffected.
173
+ try {
174
+ const loaded = await readCold(persistence, entry.header.id, signal)
175
+ assertNotAborted(signal)
176
+ assertSessionHeadersCompatible(entry.header, loaded.header)
177
+ entry.loaded = observeSession(loaded.header, loaded.inheritedEventCount, loaded.events)
178
+ } catch (error: unknown) {
179
+ if (isAbort(error) || signal?.aborted) throw error
180
+ if (engine._persistenceBinding !== persistenceBinding) break
181
+ entry.unreadable = errorMessage(error)
182
+ engine.ctx.logger?.warn(
183
+ 'session-search skipped unreadable session %s: %s',
184
+ entry.header.id,
185
+ entry.unreadable,
186
+ )
187
+ }
188
+ }
189
+ assertNotAborted(signal)
190
+ const afterSnapshots = await persistence.list(listOptions)
191
+ assertNotAborted(signal)
192
+ const after = materializePersistenceSnapshots(afterSnapshots)
193
+ if (!samePersistenceSnapshots(persisted, after)) continue
194
+ if (engine._persistenceBinding !== persistenceBinding) continue
195
+ } catch (error: unknown) {
196
+ if (isAbort(error) || signal?.aborted) {
197
+ throw new SessionQueryError('session-search aborted', 'SESSION_QUERY_ABORTED', { cause: error })
198
+ }
199
+ if (engine._persistenceBinding !== persistenceBinding) continue
200
+ if (error instanceof SessionQueryError) throw error
201
+ throw new SessionQueryError(
202
+ `session-search persistence observation failed: ${errorMessage(error)}`,
203
+ 'SESSION_QUERY_PERSISTENCE_FAILED',
204
+ { cause: error },
205
+ )
206
+ }
207
+ }
208
+ const live = new Map<SessionId, ObservedSession>()
209
+ for (const session of engine.ctx.sessions.list()) {
210
+ const observed = observeSession(session.header, session.inheritedEventCount, session.snapshotEvents())
211
+ const durable = persisted.get(session.id)
212
+ if (durable !== undefined && durable.loaded === undefined) {
213
+ // A live owner always wins over a skipped durable copy.
214
+ live.set(session.id, observed)
215
+ continue
216
+ }
217
+ if (durable !== undefined) assertSessionHeadersCompatible(observed.header, durable.header)
218
+ live.set(session.id, observed)
219
+ }
220
+ const sameLive = initiallyLive.size === live.size && [...initiallyLive].every(id => live.has(id))
221
+ if (!sameLive) continue
222
+ return { persistenceBinding, persisted, live }
223
+ }
224
+ throw new SessionQueryError('session-search persistence observation did not stabilize after one retry', 'SESSION_QUERY_PERSISTENCE_FAILED')
225
+ }
226
+
227
+ // The opaque-base cast keeps the private `_observeStable` override legal in
228
+ // TypeScript while inheriting every runtime static (inject, Config, Service
229
+ // metadata) from the real engine class.
230
+ const EngineBase = SqliteSessionQueryEngine as unknown as abstract new (ctx: never, config: Config) => EngineSurface & object
231
+
232
+ /** The engine this bundle mounts in place of the base `session-query-sqlite` row. */
233
+ export class SkipTolerantSessionQueryEngine extends EngineBase {
234
+ async _observeStable(indexed: ReadonlyMap<SessionId, { revision: SessionPersistenceRevision }>, signal: AbortSignal | undefined): Promise<unknown> {
235
+ return await observeStableWithSkip(this, indexed, signal)
236
+ }
237
+ }
238
+
239
+ export default SkipTolerantSessionQueryEngine as unknown as typeof SqliteSessionQueryEngine
package/src/startup.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * whose project directory matches the current working directory.
15
15
  * - `--session <id>` — create a new session under an explicit identity (the
16
16
  * id must not exist yet).
17
- * - `--theme <dark|light|auto>` — the color palette; auto follows the
17
+ * - `--theme <dark|light|prismatic|rainbow|auto>` — the color palette; auto follows the
18
18
  * terminal (dark fallback until OSC-11 detection lands).
19
19
  * - no flags — a fresh session with a minted id.
20
20
  *
@@ -69,7 +69,7 @@ export function resolveTuiStartup(options: TuiStartupOptions): TuiStartup {
69
69
  throw new Error('--mode applies only to a new session; it cannot be combined with --resume or --continue')
70
70
  }
71
71
  if (options.theme !== undefined && !THEME_NAMES.includes(options.theme)) {
72
- throw new Error('--theme must be dark, light, or auto')
72
+ throw new Error(`--theme must be one of: ${THEME_NAMES.join(', ')}`)
73
73
  }
74
74
  const input = {
75
75
  ...(options.theme === undefined ? {} : { theme: options.theme }),
@@ -99,7 +99,7 @@ function tuiCommand(): Command {
99
99
  .option('-c, --continue', 'resume the most recent persisted session for this working directory')
100
100
  .option('--session <id>', 'create a new session under this explicit id')
101
101
  .option('--mode <preset>', 'agent preset for a newly created session')
102
- .option('--theme <name>', 'color theme: dark (default), light, or auto')
102
+ .option('--theme <name>', 'color theme: dark (default), light, prismatic, rainbow, or auto')
103
103
  .option('-i, --image <path>', 'attach an image to the initial prompt (repeatable)', (path, paths: string[]) => [...paths, path], [])
104
104
  .argument('[prompt...]', 'initial prompt; sends immediately after startup')
105
105
  .addHelpText('after', `