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
@@ -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 };
@@ -15,9 +15,10 @@
15
15
  */
16
16
  import { type ReactElement } from 'react';
17
17
  import type { CommandDescriptor } from '@deepseek-ai/dsh-commands';
18
- import type { ImageBlock } from '@deepseek-ai/dsh-llm';
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';
@@ -35,7 +36,7 @@ import type { PluginRow } from './plugin-inventory.ts';
35
36
  import type { SessionDirectoryOptions, SessionRow } from './session-directory.ts';
36
37
  import type { GitDiffView } from './git-workflow.ts';
37
38
  import { type ProviderAuthorizationDirectory, type ProviderAuthorizationRow } from './authorization.ts';
38
- import { type ImagePathInspection } from './attachments.ts';
39
+ import { type FilePathInspection, type ImagePathInspection } from './attachments.ts';
39
40
  /** Visual priority for one bounded local notice. */
40
41
  export type NoticeTone = 'info' | 'warning' | 'error';
41
42
  /** Props the runner hands the app; callbacks stay owned by the runner. */
@@ -70,10 +71,20 @@ export interface AppProps {
70
71
  mode: string;
71
72
  /** Permission preset selected for the current or pending first session. */
72
73
  permission: string;
73
- /** Submit one line: slash commands to the registry, other text to the agent. */
74
- dispatch(text: string, images?: readonly ImageBlock[]): void;
75
- /** Submit steering: consumed at the running turn's next step boundary. */
76
- steer(text: string, images?: readonly ImageBlock[]): void;
74
+ /**
75
+ * Submit one line: slash commands to the registry, other text to the agent.
76
+ * The optional origin names the session the submission was composed for —
77
+ * an attachment prepare resolves after the app remounted onto another
78
+ * session, and the runner drops the stale delivery then.
79
+ */
80
+ dispatch(text: string, attachments?: readonly ContentBlock[], origin?: string): void;
81
+ /** Submit steering, with the same stale-delivery guard as {@link dispatch}. */
82
+ steer(text: string, attachments?: readonly ContentBlock[], origin?: string): void;
83
+ /**
84
+ * The FULL current session identity ('' while the first session is pending)
85
+ * — the stale-delivery origin above. Distinct from the short display id.
86
+ */
87
+ sessionKey: string;
77
88
  /** Interrupt the running turn (Esc); true when a turn was cancelled. */
78
89
  interrupt(): boolean;
79
90
  /** Quit: unmount, flush, and request process exit. */
@@ -86,6 +97,10 @@ export interface AppProps {
86
97
  inspectImages(paths: readonly string[]): Promise<readonly ImagePathInspection[]>;
87
98
  /** Validate, normalize and persist images immediately before submission. */
88
99
  prepareImages(paths: readonly string[], signal?: AbortSignal): Promise<readonly ImageBlock[]>;
100
+ /** Validate draft non-image file paths without committing attachment objects. */
101
+ inspectFiles(paths: readonly string[]): Promise<readonly FilePathInspection[]>;
102
+ /** Persist non-image files immediately before submission as durable file blocks. */
103
+ prepareFiles(paths: readonly string[], signal?: AbortSignal): Promise<readonly FileBlock[]>;
89
104
  /** Apply one /model selection (with an advertised reasoning effort, when picked); returns the display label. */
90
105
  selectModel(row: ModelRow, effortId?: string): string;
91
106
  /** The /subagent override label, '' when delegated agents follow the current model. */
@@ -125,8 +140,10 @@ export interface AppProps {
125
140
  logoutProviderAuthorization?(row: ProviderAuthorizationRow): Promise<void>;
126
141
  openAuthorizationUrl?(url: string): boolean;
127
142
  copyTextValue?(text: string): Promise<void>;
128
- /** Cycle to the next permission preset (Shift+Tab); returns the new label. */
129
- 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;
130
147
  /** Select or inspect a permission preset without requiring a pre-existing session. */
131
148
  setPermission(id: string): string;
132
149
  /** Export the transcript to a markdown file (/export [path]); reports via notices. */
@@ -156,6 +173,10 @@ export interface AppProps {
156
173
  loadPlugins(): readonly PluginRow[];
157
174
  /** Caller-visible background jobs (the host jobs registry, read-only). */
158
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>;
159
180
  /** Registers the app's notice channel with the runner (called once on mount). */
160
181
  onBridgeReady(bridge: {
161
182
  notify(text: string, tone?: NoticeTone): void;
@@ -166,6 +187,11 @@ export interface AppProps {
166
187
  saveStatusline(items: readonly string[]): void;
167
188
  /** Apply and persist one /theme selection; the runner owns the theme.json file. */
168
189
  saveTheme?(name: ThemeName): void;
190
+ /** Whether timed animations run at startup (animations.json; on by default
191
+ * — like parseAnimationsPref, only an explicit false disables them). */
192
+ animations?: boolean;
193
+ /** Apply and persist one /animation toggle; the runner owns the file. */
194
+ saveAnimations?(enabled: boolean): void;
169
195
  /** Persistent cross-session input history (oldest first); the runner owns the file. */
170
196
  history: readonly string[];
171
197
  /** Persist one submitted prompt to the global history file. */
@@ -1,6 +1,6 @@
1
- /** Terminal image-file adapter over the Harness durable attachment service. */
1
+ /** Terminal image- and file-attachment adapter over the Harness durable attachment service. */
2
2
  import type { AttachmentStore, ImageMediaType } from '@deepseek-ai/dsh-attachment';
3
- import type { ImageBlock } from '@deepseek-ai/dsh-llm';
3
+ import type { FileBlock, ImageBlock } from '@deepseek-ai/dsh-llm';
4
4
  /** A validated path retained in the editor until submission persists it. */
5
5
  export interface ImagePathInspection {
6
6
  readonly path: string;
@@ -8,13 +8,45 @@ export interface ImagePathInspection {
8
8
  readonly mediaType: ImageMediaType;
9
9
  readonly bytes: number;
10
10
  }
11
+ /** A validated non-image file path retained the same way (0.1.5 file blocks). */
12
+ export interface FilePathInspection {
13
+ readonly path: string;
14
+ readonly name: string;
15
+ readonly bytes: number;
16
+ }
17
+ /**
18
+ * Terminal-side file admission bounds. Upstream exposes image limits through
19
+ * the attachment service but no file limits (files ride verbatim storage);
20
+ * these keep a dragged file from silently ingesting a disk-sized blob and
21
+ * bound one message the way the image batch is bounded.
22
+ */
23
+ export declare const MAX_FILE_BYTES: number;
24
+ export declare const MAX_FILES_PER_MESSAGE = 8;
11
25
  /** Detect the supported encoded raster formats from bytes, never from a path suffix. */
12
26
  export declare function detectImageMediaType(data: Uint8Array): ImageMediaType | undefined;
13
27
  /** Whether a path-like token is worth probing as an image attachment. */
14
28
  export declare function looksLikeImagePath(path: string): boolean;
15
- /** Parse a terminal paste/drop containing only one or more image paths. */
16
- export declare function parsePastedImagePaths(input: string): readonly string[];
29
+ /**
30
+ * Parse a paste/drop into its image and file paths: image-suffixed tokens
31
+ * stay images, other path-shaped tokens ride as file attachments (0.1.5
32
+ * file blocks), and anything that is neither leaves both empty — the caller
33
+ * then treats the paste as plain text.
34
+ *
35
+ * File tokens are held to an absolute-path-with-shape bar (drive/backslash
36
+ * or a dot-suffixed leaf after a separator): a dropped terminal path always
37
+ * carries one of those, while prose, slash commands, and option flags never
38
+ * do. A POSIX absolute path without any dot-suffixed leaf falls through as
39
+ * text — the @ mention route still attaches such files deliberately.
40
+ */
41
+ export declare function parsePastedAttachmentPaths(input: string): {
42
+ readonly images: readonly string[];
43
+ readonly files: readonly string[];
44
+ };
17
45
  /** Validate path, byte size and encoded signature without writing an attachment object. */
18
46
  export declare function inspectImagePaths(paths: readonly string[], attachments: AttachmentStore | undefined, cwd?: string): Promise<readonly ImagePathInspection[]>;
19
47
  /** Read, validate, and persist an ordered image path list as model content blocks. */
20
48
  export declare function saveImagePaths(paths: readonly string[], attachments: AttachmentStore | undefined, signal?: AbortSignal): Promise<readonly ImageBlock[]>;
49
+ /** Validate path and byte size for non-image file attachments without writing. */
50
+ export declare function inspectFilePaths(paths: readonly string[], attachments: AttachmentStore | undefined, cwd?: string): Promise<readonly FilePathInspection[]>;
51
+ /** Read and persist an ordered non-image file path list as model file blocks. */
52
+ export declare function saveFilePaths(paths: readonly string[], attachments: AttachmentStore | undefined, signal?: AbortSignal): Promise<readonly FileBlock[]>;
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import type { Context } from '@deepseek-ai/cordis';
12
12
  import z from '@deepseek-ai/schemastery';
13
- import { type ImageBlock } from '@deepseek-ai/dsh-llm';
13
+ import { type ContentBlock } from '@deepseek-ai/dsh-llm';
14
14
  import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session';
15
15
  import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence';
16
16
  import type { TuiStartup } from './startup.ts';
@@ -74,8 +74,44 @@ export declare function runQuitSequence(steps: readonly QuitCleanupStep[], exit:
74
74
  export interface QueuedSubmission {
75
75
  readonly text: string;
76
76
  readonly mode: 'followup' | 'steer';
77
- readonly images: readonly ImageBlock[];
77
+ readonly images: readonly ContentBlock[];
78
78
  }
79
+ /**
80
+ * Whether a tagged submission still belongs to the active session. Attachment
81
+ * prepares resolve on the microtask timeline, while a queued session switch
82
+ * remounts the app asynchronously — the composing instance's unmount cleanup
83
+ * runs too late to abort, so the delivery itself carries the composing
84
+ * session's full id and the runner drops it here when the world moved on.
85
+ * An untagged (synchronous) or pending-session ('') submission always passes.
86
+ */
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;
79
115
  /**
80
116
  * Order-preserving gate for composer input while the startup prompt/images
81
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;
@@ -78,17 +78,6 @@ export interface DiscoveredModelView {
78
78
  /** Output cap when disclosed. */
79
79
  readonly maxTokens?: number;
80
80
  }
81
- /** One model an endpoint reported about itself (mirrors LlmDiscoveredModel). */
82
- export interface DiscoveredModelView {
83
- /** Model id the endpoint accepts. */
84
- readonly id: string;
85
- /** Human-readable name when the endpoint supplies one. */
86
- readonly name?: string;
87
- /** Context window when disclosed; adoption still owes it if absent. */
88
- readonly contextWindow?: number;
89
- /** Output cap when disclosed. */
90
- readonly maxTokens?: number;
91
- }
92
81
  /**
93
82
  * The seven canonical reasoning levels a reasoningEfforts key may name -
94
83
  * pi-ai's THINKING_LEVELS. A pi-ai upgrade that adds or removes one fails
@@ -153,6 +142,12 @@ export interface ProviderTargetView {
153
142
  readonly configuration: ProviderConfiguration;
154
143
  /** The owning adapter reports this route as hand-declared (absent when it draws no distinction). */
155
144
  readonly declared?: boolean;
145
+ /**
146
+ * Configuration diagnostic the adapter reported for this route (catalog or
147
+ * profile damage): the row stays listed and repairable instead of the whole
148
+ * provider vanishing; absent when the route reads clean.
149
+ */
150
+ readonly diagnostic?: string;
156
151
  }
157
152
  /** The resolved provider/settings/credential join. */
158
153
  export interface ProviderSettingsDirectory {
@@ -13,6 +13,19 @@
13
13
  * marker keeps the tier accent afterwards (persistent, like Codex's prompt
14
14
  * charge). Pure functions only — the Ink layer owns timers and colors.
15
15
  *
16
+ * Wave and Pulse deliberately extend the Codex port after in-terminal
17
+ * testing: the per-row phase cascade was removed (Codex tints each column
18
+ * across the whole band), Wave became a WATER SURFACE — one continuous sine
19
+ * swell spanning the band, mirror-symmetric about the center column, its
20
+ * crests flowing outward from the center with a symmetric fade envelope
21
+ * (the deepseek tier adds one faster harmonic crossing it), painted with
22
+ * Aurora's recipe: wide soft gradients, mirrored second-hue mixing, and a
23
+ * low alpha cap — no hard core line — while Pulse became true
24
+ * two-dimensional, cell-aspect-corrected detonations: soft wide rings whose
25
+ * color grades across their width, expanding outward through a symmetric
26
+ * fade envelope and each trailing an echo ripple in the next blue. Aurora
27
+ * keeps Codex's geometry verbatim.
28
+ *
16
29
  * @module @deepseek-ai/dsh-code/render/animations
17
30
  */
18
31
  import type { RgbTriple } from '../theme.ts';
@@ -41,6 +54,8 @@ export declare function deepDivingGradientColor(index: number, tick: number, gra
41
54
  export declare function deepDivingSparkIntensity(tick: number): number;
42
55
  /** Blue RGB color for the breathing Deep diving sparkle. */
43
56
  export declare function deepDivingSparkColor(tick: number, base: RgbTriple, highlight: RgbTriple): RgbTriple;
57
+ /** Caret blink cadence: one blink step (on or off) per tick. */
58
+ export declare const CARET_BLINK_TICK_MS = 530;
44
59
  /** Caret visibility: half the ticks on, half off (530ms blink). */
45
60
  export declare function caretVisible(tick: number): boolean;
46
61
  /**
@@ -71,8 +86,34 @@ export type DeepseekWaveTier = 'flash' | 'deepseek' | 'unknown';
71
86
  * One style is picked at random per trigger and never repeats the previous.
72
87
  */
73
88
  export type DeepseekWaveStyle = 'wave' | 'aurora' | 'pulse';
74
- /** Wave half-width in columns — Codex WAVE_HALF_WIDTH (9). */
75
- export declare const WAVE_HALF_WIDTH = 9;
89
+ /**
90
+ * The water surface: ONE continuous sine line spanning the whole band,
91
+ * mirror-symmetric about the center column, its crests flowing OUTWARD from
92
+ * the center (phase k·|x − center| − ω·t). No sweep window, no return trip —
93
+ * the surface fades in, flows, and fades out, symmetric in both space and
94
+ * time. The deepseek tier adds one faster, finer HARMONIC line whose crests
95
+ * cross the fundamental's: interleaved richness with both lines still
96
+ * symmetric and still only ever flowing outward.
97
+ */
98
+ export declare const WAVE_SURFACE_AMPLITUDE = 0.8;
99
+ export declare const WAVE_SURFACE_HARMONIC = 0.45;
100
+ export declare const WAVE_SURFACE_WAVELENGTH = 40;
101
+ export declare const WAVE_SURFACE_OMEGA = 9;
102
+ /** Vertical thickness in lane units — Aurora-wide: soft gradients, no hard edges. */
103
+ export declare const WAVE_SURFACE_THICKNESS = 1.2;
104
+ /**
105
+ * The mirrored second-hue profile: the space BELOW the surface carries a
106
+ * second blue at this strength, so color (not just brightness) varies
107
+ * continuously across the wave — Aurora-style hue mixing instead of a
108
+ * single flat tint.
109
+ */
110
+ export declare const WAVE_SURFACE_MIRROR = 0.6;
111
+ /** Aurora-style soft alpha: low gain, capped well under the pulse ring's. */
112
+ export declare const WAVE_SURFACE_ALPHA_GAIN = 0.45;
113
+ export declare const WAVE_SURFACE_ALPHA_CAP = 0.68;
114
+ /** Aurora-grade soft alpha for the detonation — a notch above the swell. */
115
+ export declare const PULSE_ALPHA_GAIN = 0.45;
116
+ export declare const PULSE_ALPHA_CAP = 0.72;
76
117
  /** Sparkle glyphs in frame order — Codex SPARK_GLYPHS (`· ✦ ✧`). */
77
118
  export declare const SPARK_GLYPHS: readonly ["·", "✦", "✧"];
78
119
  /**
@@ -98,9 +139,11 @@ export declare function deepseekWaveDuration(tier: DeepseekWaveTier, style?: Dee
98
139
  */
99
140
  export declare function deepseekWaveStyleRandom(previous: DeepseekWaveStyle | undefined): DeepseekWaveStyle;
100
141
  /**
101
- * Tier for a `provider/model` label: a model id containing `flash` runs the
142
+ * Tier for a `provider/model` label: a MODEL ID containing `flash` runs the
102
143
  * single-band flash tier; everything else (pro/reasoner/chat) runs the
103
- * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
144
+ * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping. Only the model
145
+ * segment (after the `/`) is matched, so a provider whose name contains
146
+ * `flash` cannot flip an unrelated model onto the flash tier.
104
147
  * @param model - the `provider/model` label of the applied model.
105
148
  * @returns the wave tier for that model.
106
149
  */
@@ -139,9 +182,13 @@ export declare function envelope(elapsed: number, total: number, fadeIn: number,
139
182
  * blends the mixed hue toward the blank-cell base at the style's alpha cap,
140
183
  * and Aurora applies its own fade envelope. Returns `null` when the column
141
184
  * should stay transparent, so the row returns to no `backgroundColor` on
142
- * both ends. With `rows > 1` each row samples the same timeline shifted by a
143
- * per-row phase offset, so the crest cascades down the band instead of
144
- * painting every row identically.
185
+ * both ends. With `rows > 1`: Wave is a water surface every column
186
+ * lights the row nearest the surface's current height, so the light reads
187
+ * as ONE continuous wavy line spanning the band, symmetric about the center
188
+ * column and flowing outward (a single-row band falls back to a flat glow);
189
+ * Pulse rings in two dimensions around the band's center cell with trailing
190
+ * echo ripples; only Aurora samples the timeline shifted by a per-row phase
191
+ * offset.
145
192
  * @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
146
193
  * @param column - column index in the content row (0..width-1).
147
194
  * @param width - content-row width in columns.
@@ -196,3 +243,23 @@ export declare function isOfficialDeepSeekLabel(label: string): boolean;
196
243
  * @returns whether the effort ranks above high.
197
244
  */
198
245
  export declare function effortAboveHigh(effort: string | undefined): boolean;
246
+ /**
247
+ * Parse a persisted animations preference (`animations.json`): timed
248
+ * animations are on by default and only an explicit `false` disables them —
249
+ * a missing key, corrupt value, or absent file all mean enabled, so the
250
+ * /animation toggle degrades exactly like every other user preference.
251
+ * @param value - the raw parsed JSON value (expected boolean).
252
+ * @returns whether timed animations should run.
253
+ */
254
+ export declare function parseAnimationsPref(value: unknown): boolean;
255
+ /**
256
+ * One parsed `/animation` argument: '' toggles, `on|true|1` enables,
257
+ * `off|false|0` disables (case-insensitive, surrounding whitespace ignored),
258
+ * and anything else is a usage error the caller surfaces. Kept pure so the
259
+ * command's entire decision table is unit-testable.
260
+ * @param argument - the raw text after `/animation`.
261
+ * @returns `{ enabled }`, `'toggle'`, or `'usage'`.
262
+ */
263
+ export declare function parseAnimationsArgument(argument: string): {
264
+ enabled: boolean;
265
+ } | 'toggle' | 'usage';
@@ -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;
@@ -6,10 +6,4 @@
6
6
  * @module @deepseek-ai/dsh-code/render/export
7
7
  */
8
8
  import { type TranscriptView } from './projection.ts';
9
- /**
10
- * Render the transcript as a standalone markdown document.
11
- * @param view - the folded transcript view to export.
12
- * @param sessionId - the full session identity for the header.
13
- * @returns the complete markdown text.
14
- */
15
9
  export declare function buildExportMarkdown(view: TranscriptView, sessionId: string): string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared fuzzy ranking for `/` and `@` menu candidates: the query must be a
3
+ * case-insensitive ordered subsequence of the candidate name. Prefix hits
4
+ * rank first, then the strongest alignment score, then the source order of
5
+ * the input. Ported from the upstream web client's ui-primitives
6
+ * (rank-by-name.ts) so the terminal matches the web menu's discovery feel;
7
+ * the algorithm is unchanged, only the module's home moved.
8
+ *
9
+ * @module @deepseek-ai/dsh-code/render/fuzzy
10
+ */
11
+ /**
12
+ * Rank named items by a menu query.
13
+ * @param items - candidates in source order (the caller's composition order
14
+ * is the final tie-breaker, e.g. local commands before registry entries).
15
+ * @param rawQuery - the text typed after the trigger, matched case-insensitively.
16
+ * @returns the matching items: prefix hits first, then by alignment score,
17
+ * then in source order. The input list itself for an empty query.
18
+ */
19
+ export declare function rankByName<T extends {
20
+ readonly name: string;
21
+ }>(items: readonly T[], rawQuery: string): readonly T[];
@@ -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;