dsh-code 1.0.6 → 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.
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ import SqliteSessionQueryEngine from "@deepseek-ai/dsh-session-query-sqlite";
3
+ import { SessionQueryError, assertSessionHeadersCompatible, buildSessionEventSearchDocuments, readColdSessionLog } from "@deepseek-ai/dsh-session-query";
4
+ //#region src/session-query.ts
5
+ /**
6
+ * Skip-tolerant session-query engine for this terminal.
7
+ *
8
+ * The upstream SqliteSessionQueryEngine reconciliation observes EVERY
9
+ * persisted session before each search, and ONE unreadable source (for
10
+ * example a pre-release session artifact the frozen format codecs reject)
11
+ * fails the whole pass with SESSION_QUERY_PERSISTENCE_FAILED — every
12
+ * cross-session search dies because of a single old file nobody opened
13
+ * otherwise. This subclass overrides only the observation loop so an
14
+ * unreadable source is skipped with a warning and the rest of the corpus
15
+ * indexes normally; skipped sessions are retried on later reconciliations
16
+ * and rejoin automatically once a host that can read them is installed.
17
+ *
18
+ * Vendored surface note: `_observeStable` and its module-local helpers are
19
+ * private upstream; this file re-declares the observation loop against the
20
+ * pinned @deepseek-ai line (see package.json peers) and must be re-checked
21
+ * whenever that line moves. The engine class and the tool boundary also
22
+ * share ONE physical parent-package instance through this bundle, which
23
+ * restores instanceof-based typed error messages on the search path.
24
+ */
25
+ const STABLE_OBSERVATION_ATTEMPTS = 2;
26
+ function assertNotAborted(signal) {
27
+ if (signal?.aborted) throw new SessionQueryError("session-search aborted", "SESSION_QUERY_ABORTED");
28
+ }
29
+ function isAbort(error) {
30
+ return error instanceof SessionQueryError && error.code === "SESSION_QUERY_ABORTED";
31
+ }
32
+ function errorMessage(error) {
33
+ return error instanceof Error ? error.message : "unknown error";
34
+ }
35
+ function observeSession(header, inheritedEventCount, events) {
36
+ const detachedHeader = structuredClone(header);
37
+ const detachedEvents = events.map((event) => structuredClone(event));
38
+ return {
39
+ header: detachedHeader,
40
+ inheritedEventCount,
41
+ documents: buildSessionEventSearchDocuments(detachedHeader.id, detachedEvents),
42
+ fingerprint: createHash("sha256").update(JSON.stringify({
43
+ header: detachedHeader,
44
+ inheritedEventCount,
45
+ events: detachedEvents
46
+ })).digest("base64url")
47
+ };
48
+ }
49
+ function sameHeader(a, b) {
50
+ return a.id === b.id && a.createdAt === b.createdAt && a.cwd === b.cwd && a.parentSession === b.parentSession && a.isSeeded === b.isSeeded && (a.delegationDepth ?? 0) === (b.delegationDepth ?? 0) && a.agentPreset === b.agentPreset;
51
+ }
52
+ function materializePersistenceSnapshots(snapshots) {
53
+ if (!Array.isArray(snapshots)) throw new Error("persistence snapshots must be an array");
54
+ const result = /* @__PURE__ */ new Map();
55
+ for (const snapshot of snapshots) {
56
+ if (typeof snapshot.revision !== "string") throw new Error("persistence snapshot revision must be a string");
57
+ const header = structuredClone(snapshot.header);
58
+ if (result.has(header.id)) throw new Error(`persistence listed duplicate session "${header.id}"`);
59
+ result.set(header.id, {
60
+ header,
61
+ revision: snapshot.revision
62
+ });
63
+ }
64
+ return result;
65
+ }
66
+ function samePersistenceSnapshots(before, after) {
67
+ if (before.size !== after.size) return false;
68
+ for (const [id, first] of before) {
69
+ const second = after.get(id);
70
+ if (second === void 0 || first.revision !== second.revision || !sameHeader(first.header, second.header)) return false;
71
+ }
72
+ return true;
73
+ }
74
+ /**
75
+ * The skip-tolerant observation pass: structurally the upstream loop, with
76
+ * the per-source cold read wrapped so one unreadable session degrades to a
77
+ * warning instead of failing every search. Exported for unit tests with an
78
+ * injectable cold reader.
79
+ */
80
+ async function observeStableWithSkip(engine, indexed, signal, readCold = readColdSessionLog) {
81
+ for (let attempt = 0; attempt < STABLE_OBSERVATION_ATTEMPTS; attempt += 1) {
82
+ assertNotAborted(signal);
83
+ const persistenceBinding = engine._persistenceBinding;
84
+ const persistence = persistenceBinding.service;
85
+ const initiallyLive = new Set(engine.ctx.sessions.list().map((session) => session.id));
86
+ let persisted = /* @__PURE__ */ new Map();
87
+ if (persistence !== void 0) try {
88
+ const canReuseIndexed = engine._lastPersistenceIdentity === void 0 || engine._lastPersistenceIdentity === persistenceBinding.identity;
89
+ const listOptions = signal === void 0 ? void 0 : { signal };
90
+ const before = await persistence.list(listOptions);
91
+ assertNotAborted(signal);
92
+ persisted = materializePersistenceSnapshots(before);
93
+ for (const entry of persisted.values()) {
94
+ if (canReuseIndexed && indexed.get(entry.header.id)?.revision === entry.revision) continue;
95
+ if (initiallyLive.has(entry.header.id) || engine.ctx.sessions.get(entry.header.id) !== void 0) continue;
96
+ assertNotAborted(signal);
97
+ try {
98
+ const loaded = await readCold(persistence, entry.header.id, signal);
99
+ assertNotAborted(signal);
100
+ assertSessionHeadersCompatible(entry.header, loaded.header);
101
+ entry.loaded = observeSession(loaded.header, loaded.inheritedEventCount, loaded.events);
102
+ } catch (error) {
103
+ if (isAbort(error) || signal?.aborted) throw error;
104
+ if (engine._persistenceBinding !== persistenceBinding) break;
105
+ entry.unreadable = errorMessage(error);
106
+ engine.ctx.logger?.warn("session-search skipped unreadable session %s: %s", entry.header.id, entry.unreadable);
107
+ }
108
+ }
109
+ assertNotAborted(signal);
110
+ const afterSnapshots = await persistence.list(listOptions);
111
+ assertNotAborted(signal);
112
+ const after = materializePersistenceSnapshots(afterSnapshots);
113
+ if (!samePersistenceSnapshots(persisted, after)) continue;
114
+ if (engine._persistenceBinding !== persistenceBinding) continue;
115
+ } catch (error) {
116
+ if (isAbort(error) || signal?.aborted) throw new SessionQueryError("session-search aborted", "SESSION_QUERY_ABORTED", { cause: error });
117
+ if (engine._persistenceBinding !== persistenceBinding) continue;
118
+ if (error instanceof SessionQueryError) throw error;
119
+ throw new SessionQueryError(`session-search persistence observation failed: ${errorMessage(error)}`, "SESSION_QUERY_PERSISTENCE_FAILED", { cause: error });
120
+ }
121
+ const live = /* @__PURE__ */ new Map();
122
+ for (const session of engine.ctx.sessions.list()) {
123
+ const observed = observeSession(session.header, session.inheritedEventCount, session.snapshotEvents());
124
+ const durable = persisted.get(session.id);
125
+ if (durable !== void 0 && durable.loaded === void 0) {
126
+ live.set(session.id, observed);
127
+ continue;
128
+ }
129
+ if (durable !== void 0) assertSessionHeadersCompatible(observed.header, durable.header);
130
+ live.set(session.id, observed);
131
+ }
132
+ if (!(initiallyLive.size === live.size && [...initiallyLive].every((id) => live.has(id)))) continue;
133
+ return {
134
+ persistenceBinding,
135
+ persisted,
136
+ live
137
+ };
138
+ }
139
+ throw new SessionQueryError("session-search persistence observation did not stabilize after one retry", "SESSION_QUERY_PERSISTENCE_FAILED");
140
+ }
141
+ const EngineBase = SqliteSessionQueryEngine;
142
+ /** The engine this bundle mounts in place of the base `session-query-sqlite` row. */
143
+ var SkipTolerantSessionQueryEngine = class extends EngineBase {
144
+ async _observeStable(indexed, signal) {
145
+ return await observeStableWithSkip(this, indexed, signal);
146
+ }
147
+ };
148
+ //#endregion
149
+ export { SkipTolerantSessionQueryEngine, SkipTolerantSessionQueryEngine as default, observeStableWithSkip };
@@ -18,6 +18,7 @@ import type { CommandDescriptor } from '@deepseek-ai/dsh-commands';
18
18
  import type { ContentBlock, FileBlock, ImageBlock } from '@deepseek-ai/dsh-llm';
19
19
  import type { AuthorizationInteraction, AuthorizationStatus } from '@deepseek-ai/dsh-authorization';
20
20
  import { type ThemeName } from './theme.ts';
21
+ import type { LauncherUpdateStatus } from './update.ts';
21
22
  import type { TranscriptStore } from './store.ts';
22
23
  import { type TranscriptEntry } from './render/projection.ts';
23
24
  import type { ApprovalStore } from './approval.ts';
@@ -139,8 +140,10 @@ export interface AppProps {
139
140
  logoutProviderAuthorization?(row: ProviderAuthorizationRow): Promise<void>;
140
141
  openAuthorizationUrl?(url: string): boolean;
141
142
  copyTextValue?(text: string): Promise<void>;
142
- /** Cycle to the next permission preset (Shift+Tab); returns the new label. */
143
- cyclePermission(): string;
143
+ /** Cycle to the next mode station (Shift+Tab): a permission preset or a plan switch; returns the notice label. */
144
+ cycleMode(): string;
145
+ /** Pre-session plan choice: shows the plan badge before the first session exists. */
146
+ pendingPlan?: boolean;
144
147
  /** Select or inspect a permission preset without requiring a pre-existing session. */
145
148
  setPermission(id: string): string;
146
149
  /** Export the transcript to a markdown file (/export [path]); reports via notices. */
@@ -170,6 +173,10 @@ export interface AppProps {
170
173
  loadPlugins(): readonly PluginRow[];
171
174
  /** Caller-visible background jobs (the host jobs registry, read-only). */
172
175
  loadJobs(): readonly JobRow[];
176
+ /** Probe the launcher's aligned update plan (read-only; never installs). */
177
+ probeUpdate(): Promise<LauncherUpdateStatus>;
178
+ /** Run the launcher's aligned update; streams sanitized lines; resolves with the exit code. */
179
+ applyUpdate(onLine: (line: string) => void): Promise<number>;
173
180
  /** Registers the app's notice channel with the runner (called once on mount). */
174
181
  onBridgeReady(bridge: {
175
182
  notify(text: string, tone?: NoticeTone): void;
@@ -85,6 +85,33 @@ export interface QueuedSubmission {
85
85
  * An untagged (synchronous) or pending-session ('') submission always passes.
86
86
  */
87
87
  export declare function submissionBelongsToSession(origin: string | undefined, activeSessionId: string | undefined): boolean;
88
+ /** One Shift+Tab station decision for the mode cycle. */
89
+ export type ModeCycleDecision = {
90
+ readonly kind: 'permission';
91
+ readonly preset: string;
92
+ } | {
93
+ readonly kind: 'plan-on';
94
+ } | {
95
+ readonly kind: 'plan-off';
96
+ readonly preset: string;
97
+ };
98
+ /**
99
+ * Decide the next Shift+Tab station. The cycle keeps the preset table's
100
+ * own order (most restrictive first) and inserts ONE plan station between
101
+ * the most restrictive preset and the wrap target: with the shipped three
102
+ * presets the user sees workspace-write → danger-full-access → read-only
103
+ * → plan → workspace-write. Plan IS the most restrictive preset plus the
104
+ * plan prompt layer — entering it switches nothing (the cycle is already
105
+ * parked on read-only), and leaving it lands on the next preset after the
106
+ * most restrictive one. Without the /plan command the cycle is exactly the
107
+ * preset table.
108
+ */
109
+ export declare function planCycleDecision(input: {
110
+ readonly names: readonly string[];
111
+ readonly current: string;
112
+ readonly inPlan: boolean;
113
+ readonly planAvailable: boolean;
114
+ }): ModeCycleDecision | undefined;
88
115
  /**
89
116
  * Order-preserving gate for composer input while the startup prompt/images
90
117
  * are still preparing. Anything submitted before the startup delivery settles
@@ -2,6 +2,7 @@
2
2
  import { type ReactElement } from 'react';
3
3
  import type { ModelDirectory, ModelRow } from './models.ts';
4
4
  import type { SubagentRow } from './subagents.ts';
5
+ import type { ScheduleRow } from './render/projection.ts';
5
6
  import type { PermissionRow } from './permissions.ts';
6
7
  import type { PresetRow } from './presets.ts';
7
8
  import type { PluginRow } from './plugin-inventory.ts';
@@ -165,3 +166,25 @@ export declare function SubagentPanel({ current, load, pick, inherit, close }: {
165
166
  inherit(): void;
166
167
  close(): void;
167
168
  }): ReactElement;
169
+ /**
170
+ * The /schedule panel: the read-only catalog of active reminders folded from
171
+ * durable schedule/change events (the web ui-schedule contract: overdue
172
+ * first, then ascending target; the model creates and cancels through its
173
+ * schedule_* tools, the panel only shows state). A local second-hand keeps
174
+ * the relative labels live while the panel is open.
175
+ */
176
+ export interface ScheduleDisplayRow {
177
+ readonly key: string;
178
+ readonly text: string;
179
+ readonly tone?: 'error';
180
+ }
181
+ /** Human frequency label: one-shot kinds read as Once, every rows carry the interval. */
182
+ export declare function scheduleFrequency(row: ScheduleRow): string;
183
+ /** Relative label for the next target: in N unit, or N unit overdue. */
184
+ export declare function scheduleRelative(targetAt: number, now: number): string;
185
+ /** Ordered display rows: overdue first (error tone), then ascending target. */
186
+ export declare function scheduleDisplayRows(rows: readonly ScheduleRow[], now: number): readonly ScheduleDisplayRow[];
187
+ export declare function SchedulePanel({ rows, close }: {
188
+ rows(): readonly ScheduleRow[];
189
+ close(): void;
190
+ }): ReactElement;
@@ -155,8 +155,9 @@ export declare function replaceRangePreservingCursor(value: string, cursor: numb
155
155
  */
156
156
  export declare function composerMaxRows(terminalRows: number): number;
157
157
  /**
158
- * History navigation starts with Up on an empty draft, or after visual
159
- * movement has reached the directional text edge of an unchanged recalled
160
- * entry. Every other position remains under textarea movement.
158
+ * History navigation starts with Up on an empty draft, or continues from an
159
+ * unchanged recalled entry whenever the caret sits on either text edge
160
+ * (start or end) - moving the caret into the interior returns the keys to
161
+ * ordinary editing until an edge is reached again.
161
162
  */
162
163
  export declare function shouldRecallNavigate(value: string, cursor: number, lastRecalled: string | null, direction: -1 | 1): boolean;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * IME cursor anchoring for the composer.
3
+ *
4
+ * Ink keeps the real terminal cursor hidden and parks it just below the
5
+ * dynamic tree (after the status row). IME composition text and candidate
6
+ * windows anchor to that real cursor cell - VS Code's integrated terminal
7
+ * positions its hidden IME textarea there, and Windows consoles behave the
8
+ * same - so CJK input appeared at the bottom of the screen instead of at the
9
+ * caret. The anchor moves the real cursor onto the caret cell without
10
+ * touching Ink's relative erase ledger:
11
+ *
12
+ * - the displacement is owned: before any foreign write or re-anchor, the
13
+ * wrapper cancels it (cursor down, column 1), so every writer - Ink's
14
+ * log-update rewrites, the resize replay, protocol pushes - keeps seeing
15
+ * the cursor exactly where it was left;
16
+ * - log-update frame chunks (which start with the erase-line sequence) get
17
+ * the anchor re-appended inside the same write, so a repaint can never
18
+ * leave the cursor behind - in particular the caret blink keeps the anchor
19
+ * stable while an IME composition is open, because the terminal parses the
20
+ * rewrite and the re-anchor as one atomic update.
21
+ *
22
+ * @module @deepseek-ai/dsh-code/render/ime-cursor
23
+ */
24
+ /** Cancel `rows` of owned upward displacement and return to column 1. */
25
+ export declare function imeCursorRestore(rows: number): string;
26
+ /** Move onto the caret cell: `rows` up from Ink's parked row, 0-based column. */
27
+ export declare function imeCursorMove(rows: number, column: number): string;
28
+ /**
29
+ * Rows between the caret cell and Ink's parked cursor row: the band's bottom
30
+ * blank row, the editor window rows below the caret, the status footer, and
31
+ * Ink's own below-frame row. Rows above the composer (gutter, live content)
32
+ * never enter this distance.
33
+ */
34
+ export declare function imeCursorRowsUp(input: {
35
+ editorWindowRows: number;
36
+ caretRowInWindow: number;
37
+ rowsBelowComposer: number;
38
+ }): number;
39
+ /** The installed anchor handle. */
40
+ export interface ImeCursorAnchor {
41
+ /** Anchor on the caret cell: `rows` above Ink's parked row at the 0-based
42
+ * `column`; `rows <= 0` releases the anchor. */
43
+ anchor(rows: number, column: number): void;
44
+ /** Cancel the displacement, restore the original write path, and detach. */
45
+ release(): void;
46
+ }
47
+ /**
48
+ * Take over `stream.write` so the anchor displacement stays invisible to every
49
+ * other writer. Idempotent per stream: a second install returns the live
50
+ * handle. Returns `undefined` on non-TTY streams where anchoring is meaningless.
51
+ */
52
+ export declare function installImeCursorAnchor(stream: NodeJS.WriteStream): ImeCursorAnchor | undefined;
53
+ /**
54
+ * Keep the real terminal cursor on the composer's caret cell while `active`
55
+ * (the editable composer). The second effect runs after every commit without
56
+ * a dep list: frame rewrites restore the anchor themselves, but any other
57
+ * write (protocol push, resize replay) leaves the cursor at Ink's parked
58
+ * position, and the next commit re-anchors it.
59
+ */
60
+ export declare function useImeCursorAnchor(active: boolean, rows: number, column: number): void;
@@ -219,6 +219,38 @@ export interface TranscriptStats {
219
219
  */
220
220
  reasoningEffort: string;
221
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;
222
254
  /** The complete TUI transcript view for one session. */
223
255
  export interface TranscriptView {
224
256
  /** Settled entries in log order. */
@@ -265,6 +297,8 @@ export interface TranscriptView {
265
297
  sandbox: string;
266
298
  /** Current long-running goal folded from the last `goal/change`, undefined when cleared. */
267
299
  goal: GoalFold | undefined;
300
+ /** Active reminders folded from `schedule/change` events, oldest target first at render. */
301
+ schedules: readonly ScheduleRow[];
268
302
  /**
269
303
  * Ordered live message ids per inbox target, mirrored from
270
304
  * `agent/inbox/spliced` exactly like the upstream Inbox projection — the
@@ -360,6 +394,7 @@ export interface ReplayAccumulator {
360
394
  systemPrompt: string;
361
395
  sandbox: string;
362
396
  goal: GoalFold | undefined;
397
+ schedules: readonly ScheduleRow[];
363
398
  stats: TranscriptStats;
364
399
  stepStart: Map<string, number>;
365
400
  toolStart: Map<string, number>;
@@ -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;
@@ -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;
@@ -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>;