dsh-code 1.0.5 → 1.0.7

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 (47) hide show
  1. package/README.en.md +338 -286
  2. package/README.md +68 -16
  3. package/bin/deepseek.mjs +204 -4
  4. package/cordis.patch.yml +105 -7
  5. package/lib/index.mjs +2148 -439
  6. package/lib/session-query.mjs +149 -0
  7. package/lib/types/app.d.ts +34 -8
  8. package/lib/types/attachments.d.ts +36 -4
  9. package/lib/types/index.d.ts +38 -2
  10. package/lib/types/kernel-panels.d.ts +23 -0
  11. package/lib/types/provider-settings.d.ts +6 -11
  12. package/lib/types/render/animations.d.ts +74 -7
  13. package/lib/types/render/editor.d.ts +4 -3
  14. package/lib/types/render/export.d.ts +0 -6
  15. package/lib/types/render/fuzzy.d.ts +21 -0
  16. package/lib/types/render/ime-cursor.d.ts +60 -0
  17. package/lib/types/render/projection.d.ts +80 -4
  18. package/lib/types/render/status.d.ts +1 -1
  19. package/lib/types/session-directory.d.ts +48 -13
  20. package/lib/types/session-query.d.ts +92 -0
  21. package/lib/types/store.d.ts +3 -0
  22. package/lib/types/terminal-title.d.ts +58 -0
  23. package/lib/types/update-panel.d.ts +49 -0
  24. package/lib/types/update.d.ts +66 -0
  25. package/package.json +307 -162
  26. package/src/app.ts +730 -266
  27. package/src/attachments.ts +110 -11
  28. package/src/commands.ts +35 -5
  29. package/src/index.ts +1986 -1779
  30. package/src/internals.ts +66 -40
  31. package/src/kernel-panels.ts +89 -3
  32. package/src/provider-settings.ts +12 -12
  33. package/src/render/animations.ts +606 -403
  34. package/src/render/editor.ts +5 -4
  35. package/src/render/export.ts +13 -3
  36. package/src/render/fuzzy.ts +83 -0
  37. package/src/render/ime-cursor.ts +147 -0
  38. package/src/render/projection.ts +1974 -1621
  39. package/src/render/status.ts +18 -4
  40. package/src/session-directory.ts +94 -16
  41. package/src/session-query.ts +235 -0
  42. package/src/skills.ts +23 -9
  43. package/src/store.ts +39 -1
  44. package/src/subagents.ts +26 -3
  45. package/src/terminal-title.ts +173 -0
  46. package/src/update-panel.ts +246 -0
  47. package/src/update.ts +110 -0
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * @module @deepseek-ai/dsh-tui/render/projection
8
8
  */
9
- import { type ImageBlock, type MessageId } from '@deepseek-ai/dsh-llm';
9
+ import { type ImageBlock, type MessageId, type StreamChunk } from '@deepseek-ai/dsh-llm';
10
+ import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment';
10
11
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
11
12
  import type { TodoItem } from '@deepseek-ai/dsh-tool-todo';
12
13
  import { type ToolDetail } from './tool-detail.ts';
@@ -20,6 +21,8 @@ export interface UserEntry {
20
21
  notice: boolean;
21
22
  /** Durable image references carried by this prompt. */
22
23
  images?: readonly ImageBlock['attachment'][];
24
+ /** Durable file references carried by this prompt (0.1.5 file blocks). */
25
+ files?: readonly FileAttachmentRef[];
23
26
  }
24
27
  /** One user message waiting in the agent inbox (the web's queued-message row). */
25
28
  export interface PendingEntry {
@@ -32,6 +35,8 @@ export interface PendingEntry {
32
35
  text: string;
33
36
  /** Durable image references queued with this prompt. */
34
37
  images?: readonly ImageBlock['attachment'][];
38
+ /** Durable file references queued with this prompt (0.1.5 file blocks). */
39
+ files?: readonly FileAttachmentRef[];
35
40
  }
36
41
  /** One authoritative assembled assistant reply. */
37
42
  export interface AssistantEntry {
@@ -214,11 +219,43 @@ export interface TranscriptStats {
214
219
  */
215
220
  reasoningEffort: string;
216
221
  }
222
+ /** One active reminder folded from durable `schedule/change` events. */
223
+ export interface ScheduleRow {
224
+ readonly id: string;
225
+ readonly kind: 'after' | 'at' | 'every';
226
+ readonly prompt: string;
227
+ /** Next due time (epoch ms); the /schedule panel derives overdue/relative labels. */
228
+ readonly targetAt: number;
229
+ /** Recurrence seconds for 'every' rows, undefined otherwise. */
230
+ readonly everySeconds?: number;
231
+ }
232
+ /**
233
+ * The durable `schedule/change` payload shape this fold consumes. Upstream
234
+ * strict-decodes the whole transition stream before appending, so unknown
235
+ * ids here are corrupt-input edges that degrade to a no-op.
236
+ */
237
+ export interface ScheduleChangeLike {
238
+ readonly operation: 'create' | 'delete' | 'dispatch';
239
+ readonly schedule?: {
240
+ readonly id: string;
241
+ readonly kind: 'after' | 'at' | 'every';
242
+ readonly prompt: string;
243
+ readonly afterSeconds?: number;
244
+ readonly everySeconds?: number;
245
+ readonly scheduledAt: string;
246
+ };
247
+ readonly id?: string;
248
+ readonly acceptedAt?: string;
249
+ }
250
+ /** Fold one `schedule/change` into the active-reminder list (create/delete/dispatch). */
251
+ export declare function applyScheduleChange(rows: readonly ScheduleRow[], data: ScheduleChangeLike): readonly ScheduleRow[];
252
+ /** First anchor-aligned target after `acceptedAt`, stepping from the previous aligned target. */
253
+ export declare function nextEveryTarget(previousTarget: number, acceptedAt: number, everySeconds: number): number;
217
254
  /** The complete TUI transcript view for one session. */
218
255
  export interface TranscriptView {
219
256
  /** Settled entries in log order. */
220
257
  entries: readonly TranscriptEntry[];
221
- /** Bounded text tail accumulated from `assistant/chunk` deltas since the last flush. */
258
+ /** Bounded text tail accumulated from live stream frames since the last settlement. */
222
259
  streaming: string;
223
260
  /** Bounded thinking tail accumulated from reasoning deltas since the last flush. */
224
261
  streamingReasoning: string;
@@ -249,10 +286,19 @@ export interface TranscriptView {
249
286
  permission: string;
250
287
  /** Latest session title folded from the last `session/title` event, empty before one. */
251
288
  title: string;
289
+ /**
290
+ * Effective system prompt assembled from `system/message` surface nodes
291
+ * (v3): the head node's text joined with every later non-empty node, blank
292
+ * lines between. Empty before the first system node or when every node is
293
+ * empty ("no system prompt").
294
+ */
295
+ systemPrompt: string;
252
296
  /** Sandbox-mode override folded from the last `sandbox/mode` event, empty when never switched. */
253
297
  sandbox: string;
254
298
  /** Current long-running goal folded from the last `goal/change`, undefined when cleared. */
255
299
  goal: GoalFold | undefined;
300
+ /** Active reminders folded from `schedule/change` events, oldest target first at render. */
301
+ schedules: readonly ScheduleRow[];
256
302
  /**
257
303
  * Ordered live message ids per inbox target, mirrored from
258
304
  * `agent/inbox/spliced` exactly like the upstream Inbox projection — the
@@ -279,12 +325,16 @@ export interface TranscriptView {
279
325
  turnFiles: Map<number, Set<string>>;
280
326
  turnSteps: Map<number, string>;
281
327
  turnTools: Map<number, Set<string>>;
328
+ /** Live `system/message` surface nodes by event seq (empty string = an empty node). */
329
+ systemNodes: Map<number, string>;
282
330
  };
283
331
  }
284
332
  /** Human-readable bounded image labels for transcript, inspector, and export surfaces. */
285
333
  export declare function imageLabels(images: readonly ImageBlock['attachment'][] | undefined): string;
286
- /** Prompt text with its durable image labels, without exposing local paths or bytes. */
287
- export declare function promptDisplayText(entry: Pick<UserEntry | PendingEntry, 'text' | 'images'>): string;
334
+ /** Human-readable bounded file labels for the same surfaces (0.1.5 file blocks). */
335
+ export declare function fileLabels(files: readonly FileAttachmentRef[] | undefined): string;
336
+ /** Prompt text with its durable image and file labels, without exposing local paths or bytes. */
337
+ export declare function promptDisplayText(entry: Pick<UserEntry | PendingEntry, 'text' | 'images' | 'files'>): string;
288
338
  /** A fresh, empty transcript view. */
289
339
  export declare function createTranscriptView(): TranscriptView;
290
340
  /**
@@ -341,8 +391,10 @@ export interface ReplayAccumulator {
341
391
  plan: boolean;
342
392
  permission: string;
343
393
  title: string;
394
+ systemPrompt: string;
344
395
  sandbox: string;
345
396
  goal: GoalFold | undefined;
397
+ schedules: readonly ScheduleRow[];
346
398
  stats: TranscriptStats;
347
399
  stepStart: Map<string, number>;
348
400
  toolStart: Map<string, number>;
@@ -352,6 +404,8 @@ export interface ReplayAccumulator {
352
404
  turnFiles: Map<number, Set<string>>;
353
405
  turnSteps: Map<number, string>;
354
406
  turnTools: Map<number, Set<string>>;
407
+ /** Live `system/message` surface nodes by event seq (empty string = an empty node). */
408
+ systemNodes: Map<number, string>;
355
409
  /** Entry-level container operations performed so far (test instrumentation). */
356
410
  ops: number;
357
411
  }
@@ -400,6 +454,28 @@ export declare function snapshotReplayView(acc: ReplayAccumulator): TranscriptVi
400
454
  * @returns the folded view.
401
455
  */
402
456
  export declare function projectEvents(events: readonly SessionEvent[]): TranscriptView;
457
+ /**
458
+ * Fold one process-local assistant-stream chunk frame (session-log v2+ keeps
459
+ * durable logs settlement-only; live typing rides the `agent/assistant-stream`
460
+ * agent event). Same first-token anchoring the durable `assistant/chunk` event
461
+ * used to carry: the first non-empty delta anchors the TTFT and empty
462
+ * keep-alive deltas do not count. The caller maps the frame's attempt to the
463
+ * `turn:step` key (the start frame owns turn/step; chunk frames do not).
464
+ * @param acc - the live replay accumulator.
465
+ * @param key - the `turn:step` key the attempt's start frame declared.
466
+ * @param time - the frame's safe-integer timestamp.
467
+ * @param chunk - the model chunk the frame carries.
468
+ * @returns whether the accumulator changed (the store stays silent otherwise).
469
+ */
470
+ export declare function applyAssistantStreamChunk(acc: ReplayAccumulator, key: string, time: number, chunk: StreamChunk): boolean;
471
+ /**
472
+ * Drop the live streaming tails without a settlement (an `agent/assistant-stream`
473
+ * end frame with an `abandoned` outcome, or a session switch). The next start
474
+ * frame rebuilds from scratch.
475
+ * @param acc - the live replay accumulator.
476
+ * @returns whether any tail text was discarded.
477
+ */
478
+ export declare function clearAssistantStream(acc: ReplayAccumulator): boolean;
403
479
  /**
404
480
  * The append-only flush boundary for a transcript view: the count of entries
405
481
  * no later event can remove. Entries at or beyond this index are mutable and
@@ -33,7 +33,7 @@ export declare function cacheHitPercent(usage: TranscriptStats['usage']): number
33
33
  * Presentation tones for status spans; the footer maps each to a theme color
34
34
  * (Codex status-line accents: model/path/branch/state/usage categories).
35
35
  */
36
- export type StatusTone = 'model' | 'live' | 'path' | 'branch' | 'value' | 'label' | 'meta' | 'accent' | 'success' | 'warn' | 'error' | 'ctxFill';
36
+ export type StatusTone = 'model' | 'live' | 'path' | 'branch' | 'value' | 'label' | 'meta' | 'accent' | 'success' | 'plan' | 'warn' | 'error' | 'ctxFill';
37
37
  /** One colored run inside the status bar. */
38
38
  export interface StatusSpan {
39
39
  text: string;
@@ -1,5 +1,5 @@
1
1
  /** Lightweight session-directory projection for the /resume picker. */
2
- import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session';
2
+ import { type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session';
3
3
  export interface SessionRecord {
4
4
  readonly header: SessionHeader;
5
5
  readonly live: boolean;
@@ -77,23 +77,58 @@ export declare function projectSessionRows(records: readonly SessionRecord[], op
77
77
  export declare function mergeSessionTitles(rows: readonly SessionRow[], observations: readonly TitleObservationResult[]): SessionRow[];
78
78
  /**
79
79
  * Encode a session id the way the JSONL backend does for its on-disk layout
80
- * (`encodeSegment`: safe units literal, everything else `~XXXX`). Used ONLY to
81
- * validate that a `locate()` path really is this session's directory before
82
- * any deletion touches the filesystem — a local copy of the pure upstream
80
+ * (`encodeSegment`: safe units literal, everything else `~XXXX`). Used to
81
+ * validate and derive session directories a local copy of the pure upstream
83
82
  * contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
84
83
  */
85
84
  export declare function encodeSessionSegment(raw: string): string;
86
- /** The session-log artifact names the JSONL backend may create. */
87
- export declare const SESSION_ARTIFACT_NAMES: readonly string[];
88
85
  /**
89
- * Guard one `locate()` artifact path before deletion (codex's scoped-path
90
- * check, adapted to the JSONL layout): the file must be a `session.jsonl`
91
- * artifact sitting in the directory named exactly `encodeSegment(id)`.
92
- * @param artifact - the path the persistence backend located.
93
- * @param id - the session id the artifact claims to belong to.
94
- * @returns the owning session directory, or undefined when the layout is unexpected.
86
+ * Encode a project cwd the way the JSONL backend groups sessions on disk
87
+ * (`projectKey`: separators collapse to one `-`, everything else mirrors
88
+ * `encodeSegment`, bounded to 251 chars). A local copy of the pure upstream
89
+ * contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
90
+ */
91
+ export declare function encodeProjectKey(cwd: string): string;
92
+ /**
93
+ * Derive one session's artifact directory under the JSONL backend root,
94
+ * mirroring the upstream `<root>/<projectKey(cwd)>/<encodeSegment(id)>/`
95
+ * layout (0.1.5 `sessionDir`/`projectDir`).
96
+ * @param root - the JSONL backend's configured session root.
97
+ * @param cwd - the session's pinned working directory, when the header has one.
98
+ * @param id - the session id.
99
+ * @returns the absolute session directory path.
100
+ */
101
+ export declare function sessionDirectoryFor(root: string, cwd: string | undefined, id: string): string;
102
+ /**
103
+ * The canonical session-log artifact filenames the JSONL backend may create:
104
+ * format v0 writes the bare `session.jsonl` name; v1+ write
105
+ * `session.vN.jsonl`, each generation optionally zstd-compressed. Multiple
106
+ * immutable generations may coexist in one session directory (0.1.5). The
107
+ * range follows the installed session package's `SESSION_FORMAT_VERSION`, so
108
+ * a future generation joins the enumeration with the dependency bump.
109
+ */
110
+ export declare function sessionArtifactNames(): readonly string[];
111
+ /** True for one canonical session-log artifact filename the backend may own. */
112
+ export declare function isSessionArtifactName(name: string): boolean;
113
+ /**
114
+ * Guard a derived session directory before deletion (codex's scoped-path
115
+ * check, adapted to the JSONL layout): the directory's base name must be
116
+ * exactly `encodeSegment(id)` beneath its project grouping.
117
+ * @param dir - the derived session artifact directory.
118
+ * @param id - the session id the directory claims to belong to.
119
+ * @returns the guarded directory, or undefined when the layout is unexpected.
120
+ */
121
+ export declare function sessionArtifactDirectory(dir: string, id: string): string | undefined;
122
+ /**
123
+ * The JSONL backend's configured session root, when the mounted backend
124
+ * exposes one. The upstream service contract dropped `locate()` in 0.1.5
125
+ * (artifact paths are backend-private; only refusal diagnostics carry them),
126
+ * so the TUI derives artifact paths from the backend's public plugin config.
127
+ * Backends without a JSONL-style config (or a foreign shape) yield undefined
128
+ * and callers degrade: mtime sorting falls back to createdAt and /delete
129
+ * refuses, exactly as before.
95
130
  */
96
- export declare function sessionArtifactDirectory(artifact: string, id: string): string | undefined;
131
+ export declare function jsonlSessionRoot(persistence: unknown): string | undefined;
97
132
  /**
98
133
  * Collect one session's deletion subtree: the id plus every record whose
99
134
  * parent chain leads to it (codex deletes subagent threads with their root).
@@ -0,0 +1,92 @@
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
+ import SqliteSessionQueryEngine, { type Config } from '@deepseek-ai/dsh-session-query-sqlite';
22
+ import { buildSessionEventSearchDocuments } from '@deepseek-ai/dsh-session-query';
23
+ import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
24
+ import type { SessionPersistenceRevision, SessionPersistenceSnapshot } from '@deepseek-ai/dsh-session-persistence';
25
+ /** One observed session: detached header plus its derived search documents. */
26
+ interface ObservedSession {
27
+ header: SessionHeader;
28
+ inheritedEventCount: SessionLogOffset;
29
+ documents: readonly ReturnType<typeof buildSessionEventSearchDocuments>[number][];
30
+ fingerprint: string;
31
+ }
32
+ /** One persisted snapshot as the reconciliation sees it (loaded once readable). */
33
+ interface ObservedPersistedSession {
34
+ header: SessionHeader;
35
+ revision: SessionPersistenceRevision;
36
+ loaded?: ObservedSession;
37
+ /** Diagnosis for a cold read that failed; the session stays unindexed. */
38
+ unreadable?: string;
39
+ }
40
+ /** The engine-internal state the observation loop touches. */
41
+ export interface EngineSurface {
42
+ readonly ctx: {
43
+ sessions: {
44
+ list(): readonly {
45
+ header: SessionHeader;
46
+ inheritedEventCount: SessionLogOffset;
47
+ snapshotEvents(): readonly SessionEvent[];
48
+ id: SessionId;
49
+ }[];
50
+ get(id: SessionId): unknown;
51
+ };
52
+ logger?: {
53
+ warn(format: string, ...args: readonly unknown[]): void;
54
+ };
55
+ };
56
+ readonly _persistenceBinding: {
57
+ readonly identity: symbol;
58
+ readonly service?: {
59
+ list(options?: {
60
+ readonly signal?: AbortSignal;
61
+ }): Promise<readonly SessionPersistenceSnapshot[]>;
62
+ };
63
+ };
64
+ _lastPersistenceIdentity: symbol | undefined;
65
+ }
66
+ type ColdRead = (persistence: NonNullable<EngineSurface['_persistenceBinding']['service']>, id: SessionId, signal: AbortSignal | undefined) => Promise<{
67
+ header: SessionHeader;
68
+ inheritedEventCount: SessionLogOffset;
69
+ events: readonly SessionEvent[];
70
+ }>;
71
+ /**
72
+ * The skip-tolerant observation pass: structurally the upstream loop, with
73
+ * the per-source cold read wrapped so one unreadable session degrades to a
74
+ * warning instead of failing every search. Exported for unit tests with an
75
+ * injectable cold reader.
76
+ */
77
+ export declare function observeStableWithSkip(engine: EngineSurface, indexed: ReadonlyMap<SessionId, {
78
+ revision: SessionPersistenceRevision;
79
+ }>, signal: AbortSignal | undefined, readCold?: ColdRead): Promise<{
80
+ persistenceBinding: EngineSurface['_persistenceBinding'];
81
+ persisted: Map<SessionId, ObservedPersistedSession>;
82
+ live: Map<SessionId, ObservedSession>;
83
+ }>;
84
+ declare const EngineBase: abstract new (ctx: never, config: Config) => EngineSurface & object;
85
+ /** The engine this bundle mounts in place of the base `session-query-sqlite` row. */
86
+ export declare class SkipTolerantSessionQueryEngine extends EngineBase {
87
+ _observeStable(indexed: ReadonlyMap<SessionId, {
88
+ revision: SessionPersistenceRevision;
89
+ }>, signal: AbortSignal | undefined): Promise<unknown>;
90
+ }
91
+ declare const _default: typeof SqliteSessionQueryEngine;
92
+ export default _default;
@@ -29,6 +29,7 @@
29
29
  *
30
30
  * @module @deepseek-ai/dsh-tui/store
31
31
  */
32
+ import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent';
32
33
  import type { SessionEvent } from '@deepseek-ai/dsh-session';
33
34
  import { type TranscriptView } from './render/projection.ts';
34
35
  /** The externally readable, event-fed transcript store for one session. */
@@ -39,6 +40,8 @@ export interface TranscriptStore {
39
40
  subscribe(listener: () => void): () => void;
40
41
  /** Fold one session event; ignored events change nothing and notify nobody. */
41
42
  apply(event: SessionEvent): void;
43
+ /** Fold one live assistant-stream frame; frames without visible deltas stay silent. */
44
+ applyStreamFrame(frame: AssistantStreamFrame): void;
42
45
  /** Drop the folded view entirely (/clear): the next event starts a fresh one. */
43
46
  reset(): void;
44
47
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Terminal tab/window title management for the TUI.
3
+ *
4
+ * Terminals label their tab from the window title, which an application sets
5
+ * with an OSC 0 sequence; without one the tab shows the process name ("node").
6
+ * The title text is untrusted display content (session names arrive through
7
+ * events and user input), so it is sanitized before emission: control
8
+ * characters and bidi/invisible formatting codepoints are stripped, whitespace
9
+ * runs collapse to single spaces, and the result is bounded. Clearing writes
10
+ * an empty OSC payload and the terminal falls back to its own default; the
11
+ * previously set title is not portable to read back and is never restored.
12
+ *
13
+ * @module @deepseek-ai/dsh-code/terminal-title
14
+ */
15
+ /** Tab label before a session carries a name. */
16
+ export declare const DEFAULT_TERMINAL_TITLE = "deepseek";
17
+ /** Practical upper bound on title length, in visible characters: long enough
18
+ * for session names, short enough for tab bars and window managers. */
19
+ export declare const MAX_TERMINAL_TITLE_CHARS = 240;
20
+ /** Normalize untrusted title text into one bounded display line: disallowed
21
+ * codepoints dropped, whitespace runs collapsed to single spaces, leading and
22
+ * trailing whitespace removed, length bounded. */
23
+ export declare function sanitizeTerminalTitle(text: string): string;
24
+ /** Build one OSC 0 title sequence. An empty sanitized title yields an empty
25
+ * string: emitting nothing is distinct from clearing, which is a separate
26
+ * policy decision made by the caller. */
27
+ export declare function terminalTitleSequence(text: string): string;
28
+ /** Clear the managed title with an empty OSC payload; the terminal falls back
29
+ * to its own default label. */
30
+ export declare function clearTerminalTitleSequence(): string;
31
+ /** Outcome of the VS Code settings alignment. */
32
+ export interface VsCodeTitleSettingResult {
33
+ wrote: boolean;
34
+ reason?: 'not-vscode' | 'unparseable' | 'key-present' | 'error';
35
+ }
36
+ /** VS Code renders an application-set tab title only when
37
+ * "terminal.integrated.tabs.title" maps to the sequence variable; the editor
38
+ * default shows the process name instead ("node" for a Node CLI). Inside a VS
39
+ * Code integrated terminal, align the user settings once: if the key is
40
+ * absent, insert it and keep a one-shot backup of the original file. A value
41
+ * the user already set is never overwritten, an unparseable file is never
42
+ * touched, and every failure degrades to a no-op - the OSC and process-title
43
+ * channels keep working everywhere else. */
44
+ export declare function ensureVsCodeTabTitleSetting(options?: {
45
+ env?: NodeJS.ProcessEnv;
46
+ settingsFile?: string;
47
+ isTTY?: boolean;
48
+ }): VsCodeTitleSettingResult;
49
+ /**
50
+ * Keep the terminal tab label on `title` (sanitized; empty titles leave the
51
+ * current label alone). Two delivery channels run in parallel: the OSC 0
52
+ * sequence to stdout, and the host process title. Writes are deduplicated by
53
+ * title, and on unmount the managed title is cleared and the process title
54
+ * restored so the host shell regains its default label.
55
+ */
56
+ export declare function useTerminalTitle(title: string, options?: {
57
+ clearOnUnmount?: boolean;
58
+ }): void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The /update panel: one bounded surface over the launcher update
3
+ * pipeline. The panel never decides versions itself — it renders the
4
+ * launcher `update --json` probe (plan plus refusals), takes one
5
+ * confirmation, then streams `update --apply` progress and reports the
6
+ * result with a restart hint. Every alignment guarantee (host pinned to
7
+ * the release peers line, companion plugins carried, downgrade and
8
+ * local-checkout refusals) lives in the launcher and is only displayed
9
+ * here.
10
+ */
11
+ import { type ReactElement } from 'react';
12
+ import type { LauncherUpdateStatus } from './update.ts';
13
+ /** Retained apply-progress lines (ring tail; npm output is ephemeral). */
14
+ export declare const UPDATE_OUTPUT_CAP = 800;
15
+ /** Keep the newest UPDATE_OUTPUT_CAP lines of streamed update output. */
16
+ export declare function clipUpdateLines(lines: readonly string[]): readonly string[];
17
+ /** One display row of the update surface. */
18
+ export interface UpdateRow {
19
+ readonly key: string;
20
+ readonly text: string;
21
+ readonly tone?: 'ok' | 'warn' | 'error' | 'dim';
22
+ }
23
+ /** The plan view derived from one probe: facts to show and whether apply may run. */
24
+ export interface UpdatePlanView {
25
+ readonly rows: readonly UpdateRow[];
26
+ readonly runnable: boolean;
27
+ }
28
+ /** Rendered facts of one probe: current/target versions, actions, refusals. */
29
+ export declare function updatePlanView(status: LauncherUpdateStatus): UpdatePlanView;
30
+ /** Panel lifecycle phases; the footer names the keys each phase accepts. */
31
+ export type UpdatePhase = 'probe' | 'error' | 'plan' | 'apply' | 'done';
32
+ /** Footer hint line per phase; the plan phase names the confirm key only when runnable. */
33
+ export declare function updateFooter(phase: UpdatePhase, runnable: boolean, upToDate: boolean): string;
34
+ /**
35
+ * The /update surface: probe on open (and on r), confirm with enter/y,
36
+ * stream the aligned apply, and land on a bounded result view. Escape is
37
+ * locked while the apply child runs — killing npm mid-install is exactly
38
+ * the half-updated state this command exists to prevent.
39
+ */
40
+ export declare function UpdatePanel({ probe, apply, close, notify }: {
41
+ /** Read-only probe of the launcher update status (never installs). */
42
+ probe(): Promise<LauncherUpdateStatus>;
43
+ /** Run the aligned update; streams sanitized progress lines. */
44
+ apply(onLine: (line: string) => void): Promise<number>;
45
+ /** Close the panel (App keeps ownership of the flag). */
46
+ close(): void;
47
+ /** One bounded cross-surface notice (phase completions). */
48
+ notify(text: string, tone?: 'info' | 'warning' | 'error'): void;
49
+ }): ReactElement;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Child-process adapter for the launcher update pipeline. The launcher
3
+ * (bin/deepseek.mjs) stays the single owner of update semantics — plan,
4
+ * guards, and the aligned install sequence — so the TUI only spawns
5
+ * `update --json` (read-only probe) and `update --apply` (streamed run)
6
+ * and never re-implements version-line decisions.
7
+ */
8
+ import { type ChildProcess } from 'node:child_process';
9
+ /** Spawn double acceptable to the adapter (tests inject an EventEmitter). */
10
+ export type SpawnLike = (command: string, args: readonly string[], options: {
11
+ readonly stdio: readonly string[];
12
+ readonly windowsHide: boolean;
13
+ }) => ChildProcess;
14
+ /** Structured `update --json` payload; mirrors the launcher buildUpdateStatus. */
15
+ export interface LauncherUpdateStatus {
16
+ readonly code: {
17
+ readonly running: string;
18
+ readonly latest: string | null;
19
+ };
20
+ readonly host: {
21
+ readonly installed: string | null;
22
+ readonly targetLine: string | null;
23
+ };
24
+ readonly profile: {
25
+ readonly spec: string | null;
26
+ readonly mounted: string | null;
27
+ readonly localCheckout: boolean;
28
+ };
29
+ readonly plan: {
30
+ readonly dshSpec: string;
31
+ readonly codeSpec: string;
32
+ readonly pluginSpecs: readonly string[];
33
+ };
34
+ readonly blockers: {
35
+ readonly registry: string | null;
36
+ readonly downgrade: boolean;
37
+ readonly localCheckout: readonly string[] | null;
38
+ };
39
+ readonly upToDate: boolean;
40
+ }
41
+ /** The launcher entrypoint that ships beside this bundle (lib/../bin). */
42
+ export declare function launcherUpdateCommand(args: readonly string[], moduleUrl?: string): {
43
+ readonly command: string;
44
+ readonly args: string[];
45
+ };
46
+ /**
47
+ * Split a streamed chunk sequence into complete display lines: CR is
48
+ * stripped, a chunk boundary may split a line, and the trailing partial
49
+ * stays pending until its newline arrives (npm writes whole lines, but a
50
+ * pipe may cut anywhere). Blank lines carry no progress information and
51
+ * are dropped so the panel budget is not spent on gaps.
52
+ */
53
+ export declare function createLineSplitter(onLine: (line: string) => void): (chunk: string) => void;
54
+ /**
55
+ * Probe the aligned update status. Read-only: `update --json` never
56
+ * installs anything. The probe is bounded (npm view may hang on a broken
57
+ * network) and resolves with the parsed status.
58
+ */
59
+ export declare function probeLauncherUpdate(spawnProcess?: SpawnLike): Promise<LauncherUpdateStatus>;
60
+ /**
61
+ * Run the aligned update (`update --apply`) as a child process and stream
62
+ * its sanitized progress lines to the caller. Resolves with the child
63
+ * exit code (0 success); rejects only when the process could not start.
64
+ * No timeout: an npm install may legitimately take minutes.
65
+ */
66
+ export declare function applyLauncherUpdate(onLine: (line: string) => void, spawnProcess?: SpawnLike): Promise<number>;