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.
- package/README.en.md +60 -9
- package/README.md +60 -9
- package/bin/deepseek.mjs +86 -1
- package/cordis.patch.yml +88 -0
- package/lib/index.mjs +1002 -94
- package/lib/session-query.mjs +149 -0
- package/lib/types/app.d.ts +9 -2
- package/lib/types/index.d.ts +27 -0
- package/lib/types/kernel-panels.d.ts +23 -0
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/projection.d.ts +35 -0
- package/lib/types/render/status.d.ts +1 -1
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/terminal-title.d.ts +58 -0
- package/lib/types/update-panel.d.ts +49 -0
- package/lib/types/update.d.ts +66 -0
- package/package.json +228 -89
- package/src/app.ts +252 -69
- package/src/index.ts +129 -11
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +89 -3
- package/src/render/editor.ts +5 -4
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/projection.ts +144 -3
- package/src/render/status.ts +18 -4
- package/src/session-query.ts +235 -0
- package/src/terminal-title.ts +173 -0
- package/src/update-panel.ts +246 -0
- 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 };
|
package/lib/types/app.d.ts
CHANGED
|
@@ -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
|
|
143
|
-
|
|
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;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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>;
|