dsh-code 0.4.0 → 0.6.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 (49) hide show
  1. package/README.en.md +217 -0
  2. package/README.md +216 -67
  3. package/bin/deepseek.mjs +70 -0
  4. package/cordis.patch.yml +62 -6
  5. package/lib/devtools-CdTl3MNy.mjs +3643 -0
  6. package/lib/index.mjs +27467 -673
  7. package/lib/rolldown-runtime-CMFfr-1z.mjs +26 -0
  8. package/lib/startup.mjs +34 -17
  9. package/lib/types/app.d.ts +29 -1
  10. package/lib/types/commands.d.ts +2 -0
  11. package/lib/types/history.d.ts +79 -0
  12. package/lib/types/index.d.ts +5 -5
  13. package/lib/types/internals.d.ts +2 -0
  14. package/lib/types/kernel-panels.d.ts +48 -0
  15. package/lib/types/plugin-inventory.d.ts +11 -0
  16. package/lib/types/presets.d.ts +32 -0
  17. package/lib/types/render/animations.d.ts +10 -1
  18. package/lib/types/render/inspector.d.ts +6 -0
  19. package/lib/types/render/projection.d.ts +21 -1
  20. package/lib/types/render/status.d.ts +131 -14
  21. package/lib/types/render/text.d.ts +9 -0
  22. package/lib/types/session-directory.d.ts +54 -0
  23. package/lib/types/session-switch.d.ts +17 -0
  24. package/lib/types/skills.d.ts +2 -0
  25. package/lib/types/startup.d.ts +11 -1
  26. package/package.json +117 -112
  27. package/src/app.ts +2543 -1969
  28. package/src/commands.ts +15 -1
  29. package/src/history.ts +136 -0
  30. package/src/index.ts +550 -155
  31. package/src/internals.ts +5 -0
  32. package/src/kernel-panels.ts +419 -0
  33. package/src/plugin-inventory.ts +47 -0
  34. package/src/presets.ts +64 -0
  35. package/src/render/animations.ts +14 -1
  36. package/src/render/export.ts +4 -0
  37. package/src/render/inspector.ts +23 -5
  38. package/src/render/lines.ts +21 -10
  39. package/src/render/markdown.ts +15 -1
  40. package/src/render/projection.ts +71 -8
  41. package/src/render/status.ts +522 -65
  42. package/src/render/text.ts +34 -6
  43. package/src/session-directory.ts +102 -0
  44. package/src/session-switch.ts +58 -0
  45. package/src/skills.ts +20 -7
  46. package/src/startup.ts +38 -20
  47. package/src/whale-glyph.ts +23 -23
  48. package/README.zh.md +0 -65
  49. package/src/pictures/1.png +0 -0
@@ -143,16 +143,27 @@ export function transcriptEntryLines(entry: TranscriptEntry, columns: number): r
143
143
  lineSegment(entry.notice ? '⤷ ' : '❯ ', entry.notice ? 'dim' : 'brand'),
144
144
  lineSegment(entry.text, entry.notice ? 'dim' : 'plain'),
145
145
  ], width)
146
- case 'assistant':
147
- return [
148
- ...(entry.reasoning === ''
149
- ? []
150
- : styledLines([
151
- lineSegment(' ✻ ', 'dimItalic'),
152
- lineSegment(entry.reasoning, 'dimItalic'),
153
- ], width)),
154
- ...markdownLines(entry.text, width),
155
- ]
146
+ case 'pending':
147
+ // Codex PendingSteer: a queued prompt renders exactly like an ordinary
148
+ // user row, so the durable user/message retires it without any flicker.
149
+ return styledLines([
150
+ lineSegment('❯ ', 'brand'),
151
+ lineSegment(entry.text, 'plain'),
152
+ ], width)
153
+ case 'assistant': {
154
+ const reasoning = entry.reasoning === ''
155
+ ? []
156
+ : styledLines([
157
+ lineSegment(' ✻ ', 'dimItalic'),
158
+ lineSegment(entry.reasoning, 'dimItalic'),
159
+ ], width)
160
+ // Every reply row carries the composer's two-column gutter, so reply
161
+ // text aligns with the input cursor (Codex LIVE_PREFIX alignment); the
162
+ // wrap budget shrinks by the same amount so no line double-wraps.
163
+ const body = markdownLines(entry.text, Math.max(10, width - 2))
164
+ .map(line => ({ segments: [{ text: ' ', style: 'plain' as const }, ...line.segments] }))
165
+ return [...reasoning, ...body]
166
+ }
156
167
  case 'tool': {
157
168
  const mark = entry.state === 'running' ? '●' : entry.state === 'error' ? '⨯' : '⏺'
158
169
  const markStyle: LineStyle = entry.state === 'running' ? 'brand' : entry.state === 'error' ? 'error' : 'success'
@@ -166,11 +166,18 @@ const ORDERED = /^\s*(\d+)[.)]\s+(.*)$/u
166
166
  /** Render markdown text into styled lines of at most `width` columns. */
167
167
  export function renderMarkdown(text: string, width: number): readonly MdLine[] {
168
168
  const lines: MdLine[] = []
169
+ let separatorPending = false
169
170
  const push = (segments: readonly MdSegment[]): void => {
170
171
  for (const wrapped of wrapSegments(segments, Math.max(10, width))) {
171
172
  lines.push({ segments: merge(wrapped) })
172
173
  }
173
174
  }
175
+ const startBlock = (): void => {
176
+ if (separatorPending && lines.length > 0 && lines.at(-1)?.segments.length !== 0) {
177
+ lines.push({ segments: [] })
178
+ }
179
+ separatorPending = false
180
+ }
174
181
  const raw = text.replaceAll('\r', '')
175
182
  const source = raw.split('\n')
176
183
  let index = 0
@@ -178,6 +185,14 @@ export function renderMarkdown(text: string, width: number): readonly MdLine[] {
178
185
  const line = source[index] ?? ''
179
186
  index += 1
180
187
 
188
+ // Preserve one deliberate row between source blocks. Repeated blank
189
+ // lines collapse, and leading/trailing whitespace never grows output.
190
+ if (line.trim() === '') {
191
+ separatorPending = lines.length > 0
192
+ continue
193
+ }
194
+ startBlock()
195
+
181
196
  // Fenced code block: verbatim lines in code style, language label first.
182
197
  const fence = FENCE.exec(line)
183
198
  if (fence !== null) {
@@ -191,7 +206,6 @@ export function renderMarkdown(text: string, width: number): readonly MdLine[] {
191
206
  continue
192
207
  }
193
208
 
194
- if (line.trim() === '') continue
195
209
  if (RULE.test(line.trim())) {
196
210
  push([seg(` ${'─'.repeat(Math.max(1, Math.floor(width / 4)))}`, 'dim')])
197
211
  continue
@@ -7,12 +7,13 @@
7
7
  * @module @deepseek-ai/dsh-tui/render/projection
8
8
  */
9
9
 
10
- import { boundContextSummary, type ContentBlock } from '@deepseek-ai/dsh-llm'
10
+ import { boundContextSummary, type ContentBlock, type MessageId } from '@deepseek-ai/dsh-llm'
11
11
  import type { SessionEvent, TodoItem } from '@deepseek-ai/dsh-session'
12
12
  // Type-only imports merge the plugin-owned SessionEventMap variants
13
- // (command/*, compaction/*, goal/change, llm/retry*, plan/mode,
14
- // permission/preset, sandbox/mode, session/title) into the union this
15
- // reducer switches on.
13
+ // (agent/inbox/spliced, command/*, compaction/*, goal/change, llm/retry*,
14
+ // plan/mode, permission/preset, sandbox/mode, session/title) into the union
15
+ // this reducer switches on.
16
+ import type {} from '@deepseek-ai/dsh-agent'
16
17
  import type {} from '@deepseek-ai/dsh-commands'
17
18
  import type {} from '@deepseek-ai/dsh-compaction'
18
19
  import type {} from '@deepseek-ai/dsh-goal'
@@ -43,6 +44,17 @@ export interface UserEntry {
43
44
  notice: boolean
44
45
  }
45
46
 
47
+ /** One user message waiting in the agent inbox (the web's queued-message row). */
48
+ export interface PendingEntry {
49
+ kind: 'pending'
50
+ /** Stable message identity shared with the durable `user/message` that retires it. */
51
+ messageId: MessageId
52
+ /** Which inbox list holds the message: steering is consumed at the next step boundary. */
53
+ target: 'next-turn' | 'next-step'
54
+ /** Full message text — Codex PendingSteer renders queued prompts exactly like user rows. */
55
+ text: string
56
+ }
57
+
46
58
  /** One assembled assistant reply. */
47
59
  export interface AssistantEntry {
48
60
  kind: 'assistant'
@@ -139,7 +151,7 @@ export interface FilesEntry {
139
151
  }
140
152
 
141
153
  /** Ordered transcript items the renderer draws. */
142
- export type TranscriptEntry = UserEntry | AssistantEntry | ToolEntry | CommandEntry | ErrorEntry | TurnMarkerEntry | CompactionEntry | RetryEntry | FilesEntry
154
+ export type TranscriptEntry = UserEntry | PendingEntry | AssistantEntry | ToolEntry | CommandEntry | ErrorEntry | TurnMarkerEntry | CompactionEntry | RetryEntry | FilesEntry
143
155
 
144
156
  /** The live goal the status line badges, folded from `goal/change`. */
145
157
  export interface GoalFold {
@@ -223,6 +235,12 @@ export interface TranscriptView {
223
235
  sandbox: string
224
236
  /** Current long-running goal folded from the last `goal/change`, undefined when cleared. */
225
237
  goal: GoalFold | undefined
238
+ /**
239
+ * Ordered live message ids per inbox target, mirrored from
240
+ * `agent/inbox/spliced` exactly like the upstream Inbox projection — the
241
+ * coordinates later removals resolve against.
242
+ */
243
+ pending: { 'next-turn': readonly string[]; 'next-step': readonly string[] }
226
244
  /**
227
245
  * Fold-internal timing anchors, never rendered: open step and tool-call
228
246
  * start timestamps the next `assistant/message` / `tool/result` resolves
@@ -256,11 +274,17 @@ export function createTranscriptView(): TranscriptView {
256
274
  title: '',
257
275
  sandbox: '',
258
276
  goal: undefined,
277
+ pending: { 'next-turn': [], 'next-step': [] },
259
278
  stats: { turns: 0, steps: 0, llmMs: 0, toolMs: 0, usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 }, lastPromptTokens: 0, contextWindow: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0 },
260
279
  anchors: { stepStart: new Map(), toolStart: new Map(), firstChunkAt: new Map(), compactionTokens: new Map(), lastPruneTokens: 0, turnFiles: new Map() },
261
280
  }
262
281
  }
263
282
 
283
+ /** Full prompt text of a queued message (identical to the durable user row it retires into). */
284
+ function pendingText(content: readonly ContentBlock[]): string {
285
+ return textOf(content)
286
+ }
287
+
264
288
  /**
265
289
  * Fold one session event into an updated view (copy-on-write).
266
290
  * @param view - the view before the event.
@@ -270,17 +294,56 @@ export function createTranscriptView(): TranscriptView {
270
294
  export function projectEvent(view: TranscriptView, event: SessionEvent): TranscriptView {
271
295
  switch (event.type) {
272
296
  case 'user/message': {
297
+ // A queued row retires when its durable user message lands (the agent
298
+ // claims the inbox and logs the same message identity) — the transient
299
+ // steering/queued preview yields to the real transcript entry.
300
+ const message = event.data
301
+ let entries = view.entries
302
+ let pending = view.pending
303
+ for (const target of ['next-turn', 'next-step'] as const) {
304
+ const index = pending[target].indexOf(message.id)
305
+ if (index < 0) continue
306
+ pending = { ...pending, [target]: pending[target].filter((_, i) => i !== index) }
307
+ entries = entries.filter(entry => !(entry.kind === 'pending' && entry.messageId === message.id))
308
+ }
273
309
  // Injected context (plugin/model-continuation sources) stays collapsed
274
310
  // to a bounded notice row, exactly like collapsed transcript context
275
311
  // elsewhere in the product; only direct human prompts render in full.
276
- const message = event.data
277
312
  if (message.source.kind === 'user') {
278
- return { ...view, entries: [...view.entries, { kind: 'user', text: textOf(message.content), notice: false }] }
313
+ return { ...view, pending, entries: [...entries, { kind: 'user', text: textOf(message.content), notice: false }] }
279
314
  }
280
315
  const notice = message.source.kind === 'plugin' && message.source.form === 'notice'
281
316
  ? message.source.summary
282
317
  : message.source.kind
283
- return { ...view, entries: [...view.entries, { kind: 'user', text: boundContextSummary(notice), notice: true }] }
318
+ return { ...view, pending, entries: [...entries, { kind: 'user', text: boundContextSummary(notice), notice: true }] }
319
+ }
320
+ case 'agent/inbox/spliced': {
321
+ // The durable inbox mutation (web queue-mirror contract, event-sourced):
322
+ // removals drop the projected rows at their inbox coordinates, inserted
323
+ // messages gain a pending row at their log position.
324
+ const { target, start, removedCount = 0, inserted } = event.data
325
+ const ids = view.pending[target]
326
+ const removed = ids.slice(start, start + removedCount)
327
+ const nextIds = [
328
+ ...ids.slice(0, start),
329
+ ...ids.slice(start + removedCount),
330
+ ...inserted.map(message => message.id),
331
+ ]
332
+ let entries = view.entries
333
+ if (removed.length > 0) {
334
+ const removedSet = new Set(removed)
335
+ entries = entries.filter(entry =>
336
+ !(entry.kind === 'pending' && entry.target === target && removedSet.has(entry.messageId)))
337
+ }
338
+ for (const message of inserted) {
339
+ entries = [...entries, {
340
+ kind: 'pending',
341
+ messageId: message.id,
342
+ target,
343
+ text: pendingText(message.content),
344
+ }]
345
+ }
346
+ return { ...view, entries, pending: { ...view.pending, [target]: nextIds } }
284
347
  }
285
348
  case 'assistant/chunk': {
286
349
  const chunk = event.data.chunk