dsh-code 1.0.4 → 1.0.5
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.md +9 -7
- package/bin/deepseek.mjs +221 -11
- package/cordis.patch.yml +12 -14
- package/lib/index.mjs +164 -94
- package/lib/types/git-workflow.d.ts +7 -2
- package/lib/types/history.d.ts +18 -11
- package/lib/types/presets.d.ts +4 -1
- package/lib/types/questions.d.ts +16 -12
- package/lib/types/render/projection.d.ts +2 -1
- package/lib/types/settings-file.d.ts +8 -0
- package/package.json +49 -46
- package/src/app.ts +1 -1
- package/src/git-workflow.ts +29 -10
- package/src/history.ts +22 -13
- package/src/index.ts +50 -23
- package/src/permissions.ts +1 -1
- package/src/presets.ts +19 -6
- package/src/questions.ts +57 -74
- package/src/render/export.ts +7 -7
- package/src/render/projection.ts +2 -1
- package/src/settings-file.ts +38 -6
package/lib/index.mjs
CHANGED
|
@@ -4,11 +4,11 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import { readFileSync, realpathSync } from "node:fs";
|
|
6
6
|
import os, { homedir } from "node:os";
|
|
7
|
-
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
9
9
|
import z from "@deepseek-ai/schemastery";
|
|
10
10
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
11
|
-
import { MessageId, ReasoningEffortId,
|
|
11
|
+
import { MessageId, ReasoningEffortId, boundContextSummary, createUserMessage, normalizeApiKey } from "@deepseek-ai/dsh-llm";
|
|
12
12
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
13
13
|
import { PassThrough, Stream } from "node:stream";
|
|
14
14
|
import process$1, { cwd, env } from "node:process";
|
|
@@ -22,6 +22,7 @@ import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
|
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
|
|
24
24
|
import { isUserInvocable } from "@deepseek-ai/dsh-skill";
|
|
25
|
+
import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
25
26
|
//#region node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
|
|
26
27
|
/**
|
|
27
28
|
* @license React
|
|
@@ -30061,6 +30062,26 @@ function parseHistoryFile(raw, max = 100) {
|
|
|
30061
30062
|
return kept.slice(-max);
|
|
30062
30063
|
}
|
|
30063
30064
|
/**
|
|
30065
|
+
* The append unit for the persistent file: one JSON line, so a multi-line
|
|
30066
|
+
* draft still occupies exactly one physical line. Each submission appends
|
|
30067
|
+
* this unit at the end of the file, so concurrent terminals add entries
|
|
30068
|
+
* after each other. Node chunks one append at 512 KiB: a pasted entry
|
|
30069
|
+
* beyond that size could interleave mid-line with another writer's
|
|
30070
|
+
* chunks, and the damaged line then drops out at the next parse —
|
|
30071
|
+
* recall tolerates the loss by design.
|
|
30072
|
+
*/
|
|
30073
|
+
function historyLine(text) {
|
|
30074
|
+
return serializeHistoryEntry(text) + "\n";
|
|
30075
|
+
}
|
|
30076
|
+
/**
|
|
30077
|
+
* Whether the file on disk differs from its canonical form (deduped and
|
|
30078
|
+
* capped). True means stale lines have accumulated and the next boot
|
|
30079
|
+
* should rewrite it once, atomically.
|
|
30080
|
+
*/
|
|
30081
|
+
function needsCompaction(raw, max = 100) {
|
|
30082
|
+
return serializeHistoryList(parseHistoryFile(raw, max)) !== raw;
|
|
30083
|
+
}
|
|
30084
|
+
/**
|
|
30064
30085
|
* Record one in-session submission: empty text is ignored and an adjacent
|
|
30065
30086
|
* duplicate collapses (Codex `record_local_submission` semantics). The local
|
|
30066
30087
|
* pool shares the persistent pool's cap so the recall space stays bounded.
|
|
@@ -30075,10 +30096,10 @@ function recordLocalEntry(local, text, max = 100) {
|
|
|
30075
30096
|
return [...local, text].slice(-max);
|
|
30076
30097
|
}
|
|
30077
30098
|
/**
|
|
30078
|
-
* Serialize a capped entry list to the history file format (one JSON line
|
|
30079
|
-
* entry, trailing newline). The
|
|
30080
|
-
*
|
|
30081
|
-
*
|
|
30099
|
+
* Serialize a capped entry list to the history file format (one JSON line
|
|
30100
|
+
* per entry, trailing newline). The boot-time compaction writes this
|
|
30101
|
+
* canonical form once when stale lines have accumulated; submissions
|
|
30102
|
+
* themselves only ever append a single line.
|
|
30082
30103
|
* @param entries - the entries to persist, oldest first.
|
|
30083
30104
|
* @returns the file content, '' for an empty list.
|
|
30084
30105
|
*/
|
|
@@ -35789,13 +35810,15 @@ async function syncModelCapabilities(ctx, notify) {
|
|
|
35789
35810
|
//#region src/questions.ts
|
|
35790
35811
|
const ABORT_ERROR = new UserQuestionError("ask_user_question was interrupted before the user answered", "ASK_ABORTED");
|
|
35791
35812
|
/**
|
|
35792
|
-
* Mount the
|
|
35793
|
-
* @param ctx - context
|
|
35794
|
-
* @
|
|
35795
|
-
*
|
|
35813
|
+
* Mount the `user-questions/request` answerer over a FIFO queue.
|
|
35814
|
+
* @param ctx - plugin context whose event bus carries the waterfall.
|
|
35815
|
+
* @param owns - agents this terminal answers for; every other request is
|
|
35816
|
+
* deferred back into the waterfall (`next()`), so sibling answerers stay
|
|
35817
|
+
* usable. Agent-less asks are claimed: this TUI is the only human surface
|
|
35818
|
+
* in the process.
|
|
35819
|
+
* @returns the store the renderer subscribes to.
|
|
35796
35820
|
*/
|
|
35797
|
-
function mountQuestionProvider(ctx) {
|
|
35798
|
-
const service = ctx.get("userQuestions");
|
|
35821
|
+
function mountQuestionProvider(ctx, owns) {
|
|
35799
35822
|
let snapshot = { pending: void 0 };
|
|
35800
35823
|
let active;
|
|
35801
35824
|
const queue = [];
|
|
@@ -35810,54 +35833,40 @@ function mountQuestionProvider(ctx) {
|
|
|
35810
35833
|
active = next;
|
|
35811
35834
|
set({ pending: next });
|
|
35812
35835
|
};
|
|
35813
|
-
|
|
35814
|
-
if (
|
|
35815
|
-
|
|
35816
|
-
|
|
35817
|
-
|
|
35818
|
-
|
|
35819
|
-
|
|
35820
|
-
|
|
35821
|
-
|
|
35822
|
-
|
|
35823
|
-
|
|
35824
|
-
submit() {},
|
|
35825
|
-
cancel() {}
|
|
35826
|
-
};
|
|
35827
|
-
service.registerProvider({ ask(request) {
|
|
35828
|
-
return new Promise((resolve, reject) => {
|
|
35829
|
-
const onAbort = () => {
|
|
35830
|
-
if (active === pending) {
|
|
35831
|
-
active = void 0;
|
|
35832
|
-
set({ pending: void 0 });
|
|
35833
|
-
advance();
|
|
35834
|
-
} else {
|
|
35835
|
-
const at = queue.indexOf(pending);
|
|
35836
|
-
if (at >= 0) queue.splice(at, 1);
|
|
35837
|
-
}
|
|
35838
|
-
reject(ABORT_ERROR);
|
|
35839
|
-
};
|
|
35840
|
-
const detachAbort = () => {
|
|
35841
|
-
if (request.signal !== void 0) request.signal.removeEventListener("abort", onAbort);
|
|
35842
|
-
};
|
|
35843
|
-
const pending = {
|
|
35844
|
-
request,
|
|
35845
|
-
resolve,
|
|
35846
|
-
reject,
|
|
35847
|
-
detachAbort
|
|
35848
|
-
};
|
|
35849
|
-
if (request.signal?.aborted === true) {
|
|
35850
|
-
reject(ABORT_ERROR);
|
|
35851
|
-
return;
|
|
35836
|
+
ctx.on("user-questions/request", (request, next) => {
|
|
35837
|
+
if (request.agent !== void 0 && !owns(request.agent)) return next();
|
|
35838
|
+
return new Promise((resolve, reject) => {
|
|
35839
|
+
const onAbort = () => {
|
|
35840
|
+
if (active === pending) {
|
|
35841
|
+
active = void 0;
|
|
35842
|
+
set({ pending: void 0 });
|
|
35843
|
+
advance();
|
|
35844
|
+
} else {
|
|
35845
|
+
const at = queue.indexOf(pending);
|
|
35846
|
+
if (at >= 0) queue.splice(at, 1);
|
|
35852
35847
|
}
|
|
35853
|
-
|
|
35854
|
-
|
|
35855
|
-
|
|
35856
|
-
|
|
35857
|
-
|
|
35858
|
-
|
|
35859
|
-
|
|
35860
|
-
|
|
35848
|
+
reject(ABORT_ERROR);
|
|
35849
|
+
};
|
|
35850
|
+
const detachAbort = () => {
|
|
35851
|
+
if (request.signal !== void 0) request.signal.removeEventListener("abort", onAbort);
|
|
35852
|
+
};
|
|
35853
|
+
const pending = {
|
|
35854
|
+
request,
|
|
35855
|
+
resolve,
|
|
35856
|
+
reject,
|
|
35857
|
+
detachAbort
|
|
35858
|
+
};
|
|
35859
|
+
if (request.signal?.aborted === true) {
|
|
35860
|
+
reject(ABORT_ERROR);
|
|
35861
|
+
return;
|
|
35862
|
+
}
|
|
35863
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
35864
|
+
if (active === void 0) {
|
|
35865
|
+
active = pending;
|
|
35866
|
+
set({ pending });
|
|
35867
|
+
} else queue.push(pending);
|
|
35868
|
+
});
|
|
35869
|
+
});
|
|
35861
35870
|
return {
|
|
35862
35871
|
subscribe(listener) {
|
|
35863
35872
|
listeners.add(listener);
|
|
@@ -36633,7 +36642,12 @@ function selectForkSeed(events, atSeq) {
|
|
|
36633
36642
|
}
|
|
36634
36643
|
//#endregion
|
|
36635
36644
|
//#region src/git-workflow.ts
|
|
36636
|
-
/**
|
|
36645
|
+
/**
|
|
36646
|
+
* Read-only Git inspection used by /diff and /review. Every diff
|
|
36647
|
+
* invocation carries --no-ext-diff and --no-textconv, so configured
|
|
36648
|
+
* external diff drivers and text converters can never execute as a
|
|
36649
|
+
* side effect of reading a diff.
|
|
36650
|
+
*/
|
|
36637
36651
|
/** Split Git's stable `diff --git` framing without interpreting patch content. */
|
|
36638
36652
|
function parseGitDiffFiles(text) {
|
|
36639
36653
|
if (text === "") return [];
|
|
@@ -36656,6 +36670,7 @@ function parseGitDiffSpec(argument) {
|
|
|
36656
36670
|
args: [
|
|
36657
36671
|
"diff",
|
|
36658
36672
|
"--no-ext-diff",
|
|
36673
|
+
"--no-textconv",
|
|
36659
36674
|
"--unified=3",
|
|
36660
36675
|
"HEAD",
|
|
36661
36676
|
"--"
|
|
@@ -36666,6 +36681,7 @@ function parseGitDiffSpec(argument) {
|
|
|
36666
36681
|
args: [
|
|
36667
36682
|
"diff",
|
|
36668
36683
|
"--no-ext-diff",
|
|
36684
|
+
"--no-textconv",
|
|
36669
36685
|
"--unified=3",
|
|
36670
36686
|
"--cached",
|
|
36671
36687
|
"--"
|
|
@@ -36677,6 +36693,7 @@ function parseGitDiffSpec(argument) {
|
|
|
36677
36693
|
args: [
|
|
36678
36694
|
"diff",
|
|
36679
36695
|
"--no-ext-diff",
|
|
36696
|
+
"--no-textconv",
|
|
36680
36697
|
"--unified=3",
|
|
36681
36698
|
value,
|
|
36682
36699
|
"--"
|
|
@@ -36700,8 +36717,25 @@ function executeGit(cwd, args, signal) {
|
|
|
36700
36717
|
});
|
|
36701
36718
|
});
|
|
36702
36719
|
}
|
|
36720
|
+
/** Arguments for the unstaged-only fallback below. */
|
|
36721
|
+
const UNSTAGED_DIFF_ARGS = [
|
|
36722
|
+
"diff",
|
|
36723
|
+
"--no-ext-diff",
|
|
36724
|
+
"--no-textconv",
|
|
36725
|
+
"--unified=3",
|
|
36726
|
+
"--"
|
|
36727
|
+
];
|
|
36728
|
+
/** Whether the repository has at least one commit (a HEAD revision). */
|
|
36729
|
+
function hasHeadRevision(cwd, signal) {
|
|
36730
|
+
return executeGit(cwd, [
|
|
36731
|
+
"rev-parse",
|
|
36732
|
+
"--verify",
|
|
36733
|
+
"--quiet",
|
|
36734
|
+
"HEAD"
|
|
36735
|
+
], signal).then(() => true).catch(() => false);
|
|
36736
|
+
}
|
|
36703
36737
|
/**
|
|
36704
|
-
* Load one complete textual diff without invoking external
|
|
36738
|
+
* Load one complete textual diff without invoking external programs.
|
|
36705
36739
|
* @param signal - aborted by the caller on session switches/quit, killing the
|
|
36706
36740
|
* git subprocess instead of letting a stale repository's diff land later.
|
|
36707
36741
|
*/
|
|
@@ -36714,15 +36748,10 @@ async function loadGitDiff(cwd, argument, signal) {
|
|
|
36714
36748
|
files: parseGitDiffFiles(text)
|
|
36715
36749
|
};
|
|
36716
36750
|
} catch (error) {
|
|
36717
|
-
if (argument.trim() !== "") throw error;
|
|
36751
|
+
if (argument.trim() !== "" || signal?.aborted === true || await hasHeadRevision(cwd, signal)) throw error;
|
|
36718
36752
|
return {
|
|
36719
|
-
title: "git diff - working tree",
|
|
36720
|
-
files: parseGitDiffFiles(await executeGit(cwd,
|
|
36721
|
-
"diff",
|
|
36722
|
-
"--no-ext-diff",
|
|
36723
|
-
"--unified=3",
|
|
36724
|
-
"--"
|
|
36725
|
-
], signal))
|
|
36753
|
+
title: "git diff - working tree (no commits yet)",
|
|
36754
|
+
files: parseGitDiffFiles(await executeGit(cwd, UNSTAGED_DIFF_ARGS, signal))
|
|
36726
36755
|
};
|
|
36727
36756
|
}
|
|
36728
36757
|
}
|
|
@@ -36797,16 +36826,24 @@ function agentPresetsFrom(ctx) {
|
|
|
36797
36826
|
function isBlankSession(events) {
|
|
36798
36827
|
return !events.some((event) => event.type === "turn/start");
|
|
36799
36828
|
}
|
|
36829
|
+
/** Upstream renamed the shipped `code` preset to `ptc` in 0.1.2-rc.1; sessions
|
|
36830
|
+
* and CLI choices recorded before the rename keep resolving through this map. */
|
|
36831
|
+
const LEGACY_PRESET_IDS = { code: "ptc" };
|
|
36832
|
+
function normalizePresetId(id) {
|
|
36833
|
+
return id === void 0 ? void 0 : LEGACY_PRESET_IDS[id] ?? id;
|
|
36834
|
+
}
|
|
36800
36835
|
/** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
|
|
36801
36836
|
function resolvePreset(session) {
|
|
36802
|
-
|
|
36803
|
-
|
|
36804
|
-
|
|
36837
|
+
const events = session.snapshotEvents();
|
|
36838
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
36839
|
+
const event = events[index];
|
|
36840
|
+
if (event.type === "agent-preset/selected" && event.data?.agentPreset !== void 0) return normalizePresetId(event.data.agentPreset);
|
|
36805
36841
|
}
|
|
36806
|
-
return session.header.agentPreset ?? "standard";
|
|
36842
|
+
return normalizePresetId(session.header.agentPreset) ?? "standard";
|
|
36807
36843
|
}
|
|
36808
36844
|
/** Resolve a pre-session choice, or recompose an active blank Agent. */
|
|
36809
36845
|
async function selectPreset(service, agent, presetId) {
|
|
36846
|
+
presetId = normalizePresetId(presetId);
|
|
36810
36847
|
if (agent !== void 0) return switchPreset(service, agent, presetId);
|
|
36811
36848
|
const preset = await service.resolve(presetId);
|
|
36812
36849
|
if (preset.broken !== void 0) throw new Error(preset.broken);
|
|
@@ -36814,7 +36851,7 @@ async function selectPreset(service, agent, presetId) {
|
|
|
36814
36851
|
}
|
|
36815
36852
|
/** Recompose atomically from the caller's perspective, logging only success. */
|
|
36816
36853
|
async function switchPreset(service, agent, presetId) {
|
|
36817
|
-
if (!isBlankSession(agent.session.
|
|
36854
|
+
if (!isBlankSession(agent.session.snapshotEvents())) throw new Error("mode is locked after the first turn; use /new <mode>");
|
|
36818
36855
|
const preset = await service.recompose(agent.ctx, presetId);
|
|
36819
36856
|
agent.session.append("agent-preset/selected", { agentPreset: preset.id });
|
|
36820
36857
|
return preset;
|
|
@@ -36827,7 +36864,7 @@ function permissionPresetsFrom(ctx) {
|
|
|
36827
36864
|
}
|
|
36828
36865
|
/** Effective label for either an active session or the not-yet-created first one. */
|
|
36829
36866
|
function effectivePermission(service, session, pending) {
|
|
36830
|
-
return session === void 0 ? pending ?? service.defaultPreset : service.current(session
|
|
36867
|
+
return session === void 0 ? pending ?? service.defaultPreset : service.current(session);
|
|
36831
36868
|
}
|
|
36832
36869
|
/** Validate a preset and write it only when a durable session already exists. */
|
|
36833
36870
|
function selectPermission(service, session, preset) {
|
|
@@ -36909,6 +36946,38 @@ function listPluginRows(ctx) {
|
|
|
36909
36946
|
* @module @deepseek-ai/dsh-code/settings-file
|
|
36910
36947
|
*/
|
|
36911
36948
|
/**
|
|
36949
|
+
* Run one file operation with a bounded retry: one initial try plus at
|
|
36950
|
+
* most `retries` more. Creating or replacing a file can fail transiently
|
|
36951
|
+
* with EPERM/EACCES while an antivirus scanner or search indexer holds
|
|
36952
|
+
* it — the standard graceful-fs remedy, not a workaround for a
|
|
36953
|
+
* persistent permission problem. A save that still fails leaves its
|
|
36954
|
+
* uniquely named temp file behind, so repeated crashed saves accumulate
|
|
36955
|
+
* distinct leftovers rather than corrupting a shared one.
|
|
36956
|
+
*/
|
|
36957
|
+
async function withTransientRetry(operation, retries = 5) {
|
|
36958
|
+
for (let attempt = 0;; attempt += 1) try {
|
|
36959
|
+
await operation();
|
|
36960
|
+
return;
|
|
36961
|
+
} catch (error) {
|
|
36962
|
+
const code = error.code;
|
|
36963
|
+
if (attempt >= retries || code !== "EPERM" && code !== "EACCES") throw error;
|
|
36964
|
+
await new Promise((resolve) => setTimeout(resolve, 30 * (attempt + 1)));
|
|
36965
|
+
}
|
|
36966
|
+
}
|
|
36967
|
+
/**
|
|
36968
|
+
* Write one file atomically: create the parent directory, write to a
|
|
36969
|
+
* uniquely named temp file, and rename it into place. A crash midway
|
|
36970
|
+
* can never leave a half-written document behind. Unique temp names
|
|
36971
|
+
* keep concurrent writers (two terminals, two chains in one process)
|
|
36972
|
+
* from sharing one temp path.
|
|
36973
|
+
*/
|
|
36974
|
+
async function writeFileAtomically(path, text) {
|
|
36975
|
+
await mkdir(dirname(path), { recursive: true });
|
|
36976
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
36977
|
+
await withTransientRetry(() => writeFile(temp, text, "utf8"));
|
|
36978
|
+
await withTransientRetry(() => rename(temp, path));
|
|
36979
|
+
}
|
|
36980
|
+
/**
|
|
36912
36981
|
* Create the shared settings-write chain. One instance per process keeps
|
|
36913
36982
|
* every user-level JSON file mutually serialized.
|
|
36914
36983
|
* @returns the persistence handle.
|
|
@@ -36917,12 +36986,7 @@ function createUserSettingsPersistence() {
|
|
|
36917
36986
|
let chain = Promise.resolve();
|
|
36918
36987
|
return {
|
|
36919
36988
|
save(path, text) {
|
|
36920
|
-
const write = chain.then(
|
|
36921
|
-
await mkdir(dirname(path), { recursive: true });
|
|
36922
|
-
const temp = `${path}.tmp`;
|
|
36923
|
-
await writeFile(temp, text, "utf8");
|
|
36924
|
-
await rename(temp, path);
|
|
36925
|
-
});
|
|
36989
|
+
const write = chain.then(() => writeFileAtomically(path, text));
|
|
36926
36990
|
chain = write.catch(() => {});
|
|
36927
36991
|
return write;
|
|
36928
36992
|
},
|
|
@@ -37161,7 +37225,7 @@ async function run(ctx, startup, io) {
|
|
|
37161
37225
|
const nextCwd = next.cwd ?? cwd;
|
|
37162
37226
|
const selectionState = pendingSelection === void 0 ? {} : { picked: pendingSelection };
|
|
37163
37227
|
let mode = next.resume ? next.mode : next.mode ?? pendingMode;
|
|
37164
|
-
if (!next.resume) mode = (await presets.resolve(mode)).id;
|
|
37228
|
+
if (!next.resume) mode = (await presets.resolve(normalizePresetId(mode ?? presets.defaultId))).id;
|
|
37165
37229
|
const setup = async (agentCtx) => {
|
|
37166
37230
|
const sessionPreset = next.resume ? resolvePreset(agentCtx.agent.session) : mode;
|
|
37167
37231
|
mode = (await presets.mount(agentCtx, sessionPreset)).id;
|
|
@@ -37206,7 +37270,7 @@ async function run(ctx, startup, io) {
|
|
|
37206
37270
|
handle,
|
|
37207
37271
|
agent: handle.agent,
|
|
37208
37272
|
session,
|
|
37209
|
-
store: createTranscriptStore(session.
|
|
37273
|
+
store: createTranscriptStore(session.snapshotEvents()),
|
|
37210
37274
|
mentions: createMentions(ctx, handle.agent, session.header.cwd ?? nextCwd),
|
|
37211
37275
|
mode: mode ?? "standard",
|
|
37212
37276
|
selection: selectionState,
|
|
@@ -37290,7 +37354,7 @@ async function run(ctx, startup, io) {
|
|
|
37290
37354
|
const picked = subagentOverride ?? resolveEffectiveSelection(belongsToActive && activeAgent !== void 0 ? activeAgent.selection.picked ?? pendingSelection : void 0, subject.session.requestHeader()?.config, currentDefaults());
|
|
37291
37355
|
return next().then((resolved) => applyModelSelectionToConfig(resolved, picked));
|
|
37292
37356
|
});
|
|
37293
|
-
const questions = mountQuestionProvider(ctx);
|
|
37357
|
+
const questions = mountQuestionProvider(ctx, (candidate) => agent !== void 0 && candidate.id === agent.id);
|
|
37294
37358
|
const bridge = { notify: () => {} };
|
|
37295
37359
|
const capabilitySyncDebounceMs = 400;
|
|
37296
37360
|
const runCapabilitySync = () => {
|
|
@@ -37348,17 +37412,27 @@ async function run(ctx, startup, io) {
|
|
|
37348
37412
|
};
|
|
37349
37413
|
const historyPath = join(homedir(), ".dsh", "dsh-code", "history.jsonl");
|
|
37350
37414
|
let inputHistory = [];
|
|
37415
|
+
let historyWriteChain = Promise.resolve();
|
|
37351
37416
|
try {
|
|
37352
|
-
|
|
37417
|
+
const rawHistory = readFileSync(historyPath, "utf8");
|
|
37418
|
+
inputHistory = parseHistoryFile(rawHistory);
|
|
37419
|
+
if (needsCompaction(rawHistory)) historyWriteChain = historyWriteChain.then(() => writeFileAtomically(historyPath, serializeHistoryList(inputHistory))).catch(() => {});
|
|
37353
37420
|
} catch {
|
|
37354
37421
|
inputHistory = [];
|
|
37355
37422
|
}
|
|
37356
|
-
/**
|
|
37357
|
-
|
|
37423
|
+
/**
|
|
37424
|
+
* Serialized history writes: each submission appends one JSON line at the
|
|
37425
|
+
* end of the file, so concurrent terminals add entries after each other
|
|
37426
|
+
* instead of overwriting snapshots they read at their own boot. A
|
|
37427
|
+
* multi-line draft still occupies one physical line (JSON escapes the
|
|
37428
|
+
* newline), and a regular-length line reaches the disk as one positioned
|
|
37429
|
+
* write; an oversized paste may interleave mid-line, which the next
|
|
37430
|
+
* parse simply drops.
|
|
37431
|
+
*/
|
|
37358
37432
|
const recordHistory = (text) => {
|
|
37359
37433
|
if (text === "") return;
|
|
37360
37434
|
inputHistory = [...inputHistory, text].slice(-100);
|
|
37361
|
-
historyWriteChain = historyWriteChain.then(() => mkdir(dirname(historyPath), { recursive: true })).then(() =>
|
|
37435
|
+
historyWriteChain = historyWriteChain.then(() => mkdir(dirname(historyPath), { recursive: true })).then(() => appendFile(historyPath, historyLine(text), "utf8")).catch((writeError) => {
|
|
37362
37436
|
bridge.notify("history save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
|
|
37363
37437
|
});
|
|
37364
37438
|
};
|
|
@@ -37977,10 +38051,6 @@ async function run(ctx, startup, io) {
|
|
|
37977
38051
|
};
|
|
37978
38052
|
const reviewChanges = (argument) => {
|
|
37979
38053
|
const currentAgent = agent;
|
|
37980
|
-
if (currentAgent === void 0) {
|
|
37981
|
-
bridge.notify("no session yet - submit a message to start", "warning");
|
|
37982
|
-
return;
|
|
37983
|
-
}
|
|
37984
38054
|
const atEpoch = epoch;
|
|
37985
38055
|
const reviewCwd = session?.header.cwd ?? cwd;
|
|
37986
38056
|
const controller = new AbortController();
|
|
@@ -38014,7 +38084,7 @@ async function run(ctx, startup, io) {
|
|
|
38014
38084
|
const text = argument.trim();
|
|
38015
38085
|
const atSeq = text === "" ? void 0 : Number(text);
|
|
38016
38086
|
if (text !== "" && (!Number.isSafeInteger(atSeq) || (atSeq ?? -1) < 0)) throw new Error("usage: /fork [event-seq]");
|
|
38017
|
-
const seed = selectForkSeed(session.
|
|
38087
|
+
const seed = selectForkSeed(session.snapshotEvents(), atSeq);
|
|
38018
38088
|
const id = `session-${randomUUID()}`;
|
|
38019
38089
|
requestSwitch({
|
|
38020
38090
|
target: {
|
|
@@ -38070,7 +38140,7 @@ async function run(ctx, startup, io) {
|
|
|
38070
38140
|
branch: gitBranch(sessionCwd),
|
|
38071
38141
|
sessionId: session === void 0 ? "" : session.id.slice(-8),
|
|
38072
38142
|
resumed: active?.resumed ?? false,
|
|
38073
|
-
mode: active?.mode ?? pendingMode ?? presets.defaultId,
|
|
38143
|
+
mode: active?.mode ?? pendingMode ?? normalizePresetId(presets.defaultId),
|
|
38074
38144
|
permission,
|
|
38075
38145
|
dispatch,
|
|
38076
38146
|
steer,
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Git inspection used by /diff and /review. Every diff
|
|
3
|
+
* invocation carries --no-ext-diff and --no-textconv, so configured
|
|
4
|
+
* external diff drivers and text converters can never execute as a
|
|
5
|
+
* side effect of reading a diff.
|
|
6
|
+
*/
|
|
2
7
|
export interface GitDiffSpec {
|
|
3
8
|
readonly label: string;
|
|
4
9
|
readonly args: readonly string[];
|
|
@@ -18,7 +23,7 @@ export declare function parseGitDiffFiles(text: string): readonly GitDiffFile[];
|
|
|
18
23
|
/** Parse the intentionally small, option-safe /diff argument vocabulary. */
|
|
19
24
|
export declare function parseGitDiffSpec(argument: string): GitDiffSpec;
|
|
20
25
|
/**
|
|
21
|
-
* Load one complete textual diff without invoking external
|
|
26
|
+
* Load one complete textual diff without invoking external programs.
|
|
22
27
|
* @param signal - aborted by the caller on session switches/quit, killing the
|
|
23
28
|
* git subprocess instead of letting a stale repository's diff land later.
|
|
24
29
|
*/
|
package/lib/types/history.d.ts
CHANGED
|
@@ -21,14 +21,21 @@ export declare function serializeHistoryEntry(text: string): string;
|
|
|
21
21
|
*/
|
|
22
22
|
export declare function parseHistoryFile(raw: string, max?: number): readonly string[];
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* The append unit for the persistent file: one JSON line, so a multi-line
|
|
25
|
+
* draft still occupies exactly one physical line. Each submission appends
|
|
26
|
+
* this unit at the end of the file, so concurrent terminals add entries
|
|
27
|
+
* after each other. Node chunks one append at 512 KiB: a pasted entry
|
|
28
|
+
* beyond that size could interleave mid-line with another writer's
|
|
29
|
+
* chunks, and the damaged line then drops out at the next parse —
|
|
30
|
+
* recall tolerates the loss by design.
|
|
31
|
+
*/
|
|
32
|
+
export declare function historyLine(text: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the file on disk differs from its canonical form (deduped and
|
|
35
|
+
* capped). True means stale lines have accumulated and the next boot
|
|
36
|
+
* should rewrite it once, atomically.
|
|
30
37
|
*/
|
|
31
|
-
export declare function
|
|
38
|
+
export declare function needsCompaction(raw: string, max?: number): boolean;
|
|
32
39
|
/**
|
|
33
40
|
* Record one in-session submission: empty text is ignored and an adjacent
|
|
34
41
|
* duplicate collapses (Codex `record_local_submission` semantics). The local
|
|
@@ -40,10 +47,10 @@ export declare function appendHistoryContent(current: string, text: string, max?
|
|
|
40
47
|
*/
|
|
41
48
|
export declare function recordLocalEntry(local: readonly string[], text: string, max?: number): readonly string[];
|
|
42
49
|
/**
|
|
43
|
-
* Serialize a capped entry list to the history file format (one JSON line
|
|
44
|
-
* entry, trailing newline). The
|
|
45
|
-
*
|
|
46
|
-
*
|
|
50
|
+
* Serialize a capped entry list to the history file format (one JSON line
|
|
51
|
+
* per entry, trailing newline). The boot-time compaction writes this
|
|
52
|
+
* canonical form once when stale lines have accumulated; submissions
|
|
53
|
+
* themselves only ever append a single line.
|
|
47
54
|
* @param entries - the entries to persist, oldest first.
|
|
48
55
|
* @returns the file content, '' for an empty list.
|
|
49
56
|
*/
|
package/lib/types/presets.d.ts
CHANGED
|
@@ -11,8 +11,11 @@ export type AgentPresetsService = AgentPresets;
|
|
|
11
11
|
export declare function agentPresetsFrom(ctx: Context): AgentPresetsService | undefined;
|
|
12
12
|
/** A preset may change only before the first durable turn begins. */
|
|
13
13
|
export declare function isBlankSession(events: readonly SessionEvent[]): boolean;
|
|
14
|
+
/** Translate a preset id recorded before an upstream rename to its current id. */
|
|
15
|
+
export declare function normalizePresetId(id: string): string;
|
|
16
|
+
export declare function normalizePresetId(id: string | undefined): string | undefined;
|
|
14
17
|
/** Latest logged selection wins; legacy sessions deliberately fall back to standard. */
|
|
15
|
-
export declare function resolvePreset(session: Pick<Session, 'header' | '
|
|
18
|
+
export declare function resolvePreset(session: Pick<Session, 'header' | 'snapshotEvents'>): string;
|
|
16
19
|
/** Resolve a pre-session choice, or recompose an active blank Agent. */
|
|
17
20
|
export declare function selectPreset(service: AgentPresetsService, agent: Agent | undefined, presetId: string): Promise<PresetRow>;
|
|
18
21
|
/** Recompose atomically from the caller's perspective, logging only success. */
|
package/lib/types/questions.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The terminal ask_user_question
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The terminal ask_user_question answerer: one `user-questions/request`
|
|
3
|
+
* waterfall listener that drives a FIFO queue — one question request on
|
|
4
|
+
* screen at a time, everything else waiting — then resolves the collected
|
|
5
|
+
* answers back into the waterfall. Mirrors the approval answerer's claim/
|
|
6
|
+
* defer split: only agents this TUI owns are answered, every other request
|
|
7
|
+
* falls through to the next answerer.
|
|
8
8
|
*
|
|
9
|
-
* Plan reviews (`exit_plan_mode`) arrive through the same
|
|
9
|
+
* Plan reviews (`exit_plan_mode`) arrive through the same waterfall with an
|
|
10
10
|
* `intent: { kind: 'plan-review' }` — the renderer highlights the approve
|
|
11
11
|
* option; the answer encoding is identical either way.
|
|
12
12
|
*
|
|
13
13
|
* @module @deepseek-ai/dsh-code/questions
|
|
14
14
|
*/
|
|
15
15
|
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
16
17
|
import { type AskUserQuestionAnswer, type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-questions';
|
|
17
18
|
/** One question request waiting on the human, with its settle channels. */
|
|
18
19
|
export interface PendingQuestion {
|
|
@@ -42,9 +43,12 @@ export interface QuestionStore {
|
|
|
42
43
|
cancel(pending: PendingQuestion): void;
|
|
43
44
|
}
|
|
44
45
|
/**
|
|
45
|
-
* Mount the
|
|
46
|
-
* @param ctx - context
|
|
47
|
-
* @
|
|
48
|
-
*
|
|
46
|
+
* Mount the `user-questions/request` answerer over a FIFO queue.
|
|
47
|
+
* @param ctx - plugin context whose event bus carries the waterfall.
|
|
48
|
+
* @param owns - agents this terminal answers for; every other request is
|
|
49
|
+
* deferred back into the waterfall (`next()`), so sibling answerers stay
|
|
50
|
+
* usable. Agent-less asks are claimed: this TUI is the only human surface
|
|
51
|
+
* in the process.
|
|
52
|
+
* @returns the store the renderer subscribes to.
|
|
49
53
|
*/
|
|
50
|
-
export declare function mountQuestionProvider(ctx: Context): QuestionStore;
|
|
54
|
+
export declare function mountQuestionProvider(ctx: Context, owns: (agent: Agent) => boolean): QuestionStore;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* @module @deepseek-ai/dsh-tui/render/projection
|
|
8
8
|
*/
|
|
9
9
|
import { type ImageBlock, type MessageId } from '@deepseek-ai/dsh-llm';
|
|
10
|
-
import type { SessionEvent
|
|
10
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
11
|
+
import type { TodoItem } from '@deepseek-ai/dsh-tool-todo';
|
|
11
12
|
import { type ToolDetail } from './tool-detail.ts';
|
|
12
13
|
/** One user prompt line. */
|
|
13
14
|
export interface UserEntry {
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @module @deepseek-ai/dsh-code/settings-file
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* Write one file atomically: create the parent directory, write to a
|
|
20
|
+
* uniquely named temp file, and rename it into place. A crash midway
|
|
21
|
+
* can never leave a half-written document behind. Unique temp names
|
|
22
|
+
* keep concurrent writers (two terminals, two chains in one process)
|
|
23
|
+
* from sharing one temp path.
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeFileAtomically(path: string, text: string): Promise<void>;
|
|
18
26
|
/** The serialized persistence surface; flush() is handed to the quit sequence. */
|
|
19
27
|
export interface UserSettingsPersistence {
|
|
20
28
|
/**
|