dsh-loop-engine 1.0.0-rc2 → 1.0.0-rc4
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 +35 -126
- package/README.zh.md +17 -73
- package/cordis.patch.yml +3 -0
- package/lib/client.js +212 -3
- package/lib/index.js +309 -125
- package/lib/invariant.js +2 -1
- package/lib/types/client/LoopEngineComposerSelect.d.ts +40 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/store.d.ts +13 -1
- package/lib/types/commands.d.ts +43 -6
- package/lib/types/driver-core/context-files.d.ts +62 -0
- package/lib/types/engine-codex/skills.d.ts +13 -10
- package/lib/types/engine-pi/skills.d.ts +40 -11
- package/lib/types/settings.d.ts +2 -0
- package/lib/types/skills.d.ts +16 -0
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -45,10 +45,10 @@ var __callDispose = (stack, error, hasError) => {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
// src/index.ts
|
|
48
|
-
import { mkdirSync, readFileSync as
|
|
48
|
+
import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
|
|
49
49
|
import { mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
|
|
50
50
|
import { randomUUID } from "node:crypto";
|
|
51
|
-
import { dirname as
|
|
51
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
52
52
|
import z5 from "@deepseek-ai/schemastery";
|
|
53
53
|
import { installSettingsSection } from "@deepseek-ai/dsh-settings";
|
|
54
54
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -3650,7 +3650,8 @@ var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
|
|
|
3650
3650
|
// src/settings.ts
|
|
3651
3651
|
var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi"];
|
|
3652
3652
|
var LOOP_ENGINE_SETTINGS_SCHEMA = z4.object({
|
|
3653
|
-
engine: z4.union([z4.const("in-process"), z4.const("claude-code"), z4.const("codex"), z4.const("pi")]).default("in-process")
|
|
3653
|
+
engine: z4.union([z4.const("in-process"), z4.const("claude-code"), z4.const("codex"), z4.const("pi")]).default("in-process"),
|
|
3654
|
+
showInComposer: z4.boolean().default(true)
|
|
3654
3655
|
});
|
|
3655
3656
|
function loopEngineSettingsNamespace() {
|
|
3656
3657
|
return settingsNamespace(LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL);
|
|
@@ -3710,48 +3711,89 @@ ${block}`;
|
|
|
3710
3711
|
}
|
|
3711
3712
|
|
|
3712
3713
|
// src/commands.ts
|
|
3714
|
+
import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
3715
|
+
import { homedir } from "node:os";
|
|
3716
|
+
import { join as join3 } from "node:path";
|
|
3717
|
+
import { createUserMessage as createUserMessage4 } from "@deepseek-ai/dsh-llm";
|
|
3718
|
+
var COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
3719
|
+
function forwardClaudeCodeCommand(name2) {
|
|
3720
|
+
return (invocation) => {
|
|
3721
|
+
invocation.agent.followup(createUserMessage4({
|
|
3722
|
+
content: [{ type: "text", text: `/${name2}${invocation.rawInput}` }],
|
|
3723
|
+
source: { kind: "user" }
|
|
3724
|
+
}));
|
|
3725
|
+
return { kind: "success" };
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
function builtin(name2, description) {
|
|
3729
|
+
return { name: name2, description, handler: forwardClaudeCodeCommand(name2) };
|
|
3730
|
+
}
|
|
3713
3731
|
var CLAUDE_CODE_COMMANDS = [
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
description: "Compact the conversation to reduce context usage",
|
|
3722
|
-
handler: async () => ({ kind: "success" })
|
|
3723
|
-
},
|
|
3724
|
-
{
|
|
3725
|
-
name: "clear",
|
|
3726
|
-
description: "Clear the conversation and start fresh",
|
|
3727
|
-
handler: async () => ({ kind: "success" })
|
|
3728
|
-
},
|
|
3729
|
-
{
|
|
3730
|
-
name: "review",
|
|
3731
|
-
description: "Review recent changes (git diff)",
|
|
3732
|
-
handler: async () => ({ kind: "success" })
|
|
3733
|
-
},
|
|
3734
|
-
{
|
|
3735
|
-
name: "explain",
|
|
3736
|
-
description: "Explain the selected code",
|
|
3737
|
-
handler: async () => ({ kind: "success" })
|
|
3738
|
-
},
|
|
3739
|
-
{
|
|
3740
|
-
name: "fix",
|
|
3741
|
-
description: "Fix issues in the code",
|
|
3742
|
-
handler: async () => ({ kind: "success" })
|
|
3743
|
-
},
|
|
3744
|
-
{
|
|
3745
|
-
name: "tests",
|
|
3746
|
-
description: "Add tests for the selected code",
|
|
3747
|
-
handler: async () => ({ kind: "success" })
|
|
3748
|
-
}
|
|
3732
|
+
builtin("help", "Show help about Claude Code commands"),
|
|
3733
|
+
builtin("compact", "Compact the conversation to reduce context usage"),
|
|
3734
|
+
builtin("clear", "Clear the conversation and start fresh"),
|
|
3735
|
+
builtin("review", "Review recent changes (git diff)"),
|
|
3736
|
+
builtin("explain", "Explain the selected code"),
|
|
3737
|
+
builtin("fix", "Fix issues in the code"),
|
|
3738
|
+
builtin("tests", "Add tests for the selected code")
|
|
3749
3739
|
];
|
|
3740
|
+
function discoverUserSlashCommands() {
|
|
3741
|
+
let entries;
|
|
3742
|
+
try {
|
|
3743
|
+
entries = readdirSync(userCommandsDir(), { encoding: "utf8" });
|
|
3744
|
+
} catch {
|
|
3745
|
+
return [];
|
|
3746
|
+
}
|
|
3747
|
+
const definitions = [];
|
|
3748
|
+
const seen = new Set(CLAUDE_CODE_COMMANDS.map((command) => command.name));
|
|
3749
|
+
for (const entry of entries.sort()) {
|
|
3750
|
+
if (!entry.endsWith(".md")) continue;
|
|
3751
|
+
const name2 = entry.slice(0, -".md".length);
|
|
3752
|
+
if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
|
|
3753
|
+
const path = join3(userCommandsDir(), entry);
|
|
3754
|
+
let raw;
|
|
3755
|
+
try {
|
|
3756
|
+
raw = readFileSync2(path, "utf8");
|
|
3757
|
+
} catch {
|
|
3758
|
+
continue;
|
|
3759
|
+
}
|
|
3760
|
+
const description = commandDescription(raw);
|
|
3761
|
+
if (description === void 0) continue;
|
|
3762
|
+
seen.add(name2);
|
|
3763
|
+
definitions.push({ name: name2, description, handler: forwardClaudeCodeCommand(name2) });
|
|
3764
|
+
}
|
|
3765
|
+
return definitions;
|
|
3766
|
+
}
|
|
3767
|
+
function userCommandsDir() {
|
|
3768
|
+
return join3(homedir(), ".claude", "commands");
|
|
3769
|
+
}
|
|
3770
|
+
function commandDescription(raw) {
|
|
3771
|
+
const trimmed = raw.trim();
|
|
3772
|
+
if (trimmed.length === 0) return void 0;
|
|
3773
|
+
let body = trimmed;
|
|
3774
|
+
if (trimmed.startsWith("---\n")) {
|
|
3775
|
+
const closing = trimmed.indexOf("\n---");
|
|
3776
|
+
if (closing <= 0) return void 0;
|
|
3777
|
+
for (const line of trimmed.slice(4, closing).split("\n")) {
|
|
3778
|
+
const colon = line.indexOf(":");
|
|
3779
|
+
if (colon < 0 || line.slice(0, colon).trim() !== "description") continue;
|
|
3780
|
+
const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
3781
|
+
if (value.length > 0) return value;
|
|
3782
|
+
}
|
|
3783
|
+
body = trimmed.slice(closing + 4);
|
|
3784
|
+
}
|
|
3785
|
+
for (const line of body.split("\n")) {
|
|
3786
|
+
const candidate = line.trim();
|
|
3787
|
+
if (candidate.length === 0 || candidate.startsWith("#")) continue;
|
|
3788
|
+
return candidate.length > 120 ? `${candidate.slice(0, 119)}\u2026` : candidate;
|
|
3789
|
+
}
|
|
3790
|
+
return void 0;
|
|
3791
|
+
}
|
|
3750
3792
|
|
|
3751
3793
|
// src/skills.ts
|
|
3752
3794
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
3753
|
-
import { homedir } from "node:os";
|
|
3754
|
-
import { join as
|
|
3795
|
+
import { homedir as homedir2 } from "node:os";
|
|
3796
|
+
import { join as join4, resolve } from "node:path";
|
|
3755
3797
|
var SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3756
3798
|
var PROVIDER_NAME = "claude-code";
|
|
3757
3799
|
var CLAUDE_CODE_RANK = 150;
|
|
@@ -3861,10 +3903,10 @@ var ClaudeCodeSkillProvider = class {
|
|
|
3861
3903
|
const cwd = options.cwd;
|
|
3862
3904
|
if (cwd !== void 0) {
|
|
3863
3905
|
const projectRoot = await findProjectRoot(resolve(cwd));
|
|
3864
|
-
await collectSkillsDir(
|
|
3906
|
+
await collectSkillsDir(join4(projectRoot, ".claude", "skills"), CLAUDE_CODE_RANK, candidates);
|
|
3865
3907
|
await collectClaudeMd(projectRoot, candidates);
|
|
3866
3908
|
}
|
|
3867
|
-
await collectSkillsDir(
|
|
3909
|
+
await collectSkillsDir(join4(homedir2(), ".claude", "skills"), CLAUDE_CODE_USER_RANK, candidates);
|
|
3868
3910
|
if (this.control.signal.aborted) return [];
|
|
3869
3911
|
return candidates;
|
|
3870
3912
|
}
|
|
@@ -3898,11 +3940,11 @@ async function collectSkillsDir(skillsDir, rank, candidates) {
|
|
|
3898
3940
|
return;
|
|
3899
3941
|
}
|
|
3900
3942
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3901
|
-
const entryPath =
|
|
3943
|
+
const entryPath = join4(skillsDir, entry.name);
|
|
3902
3944
|
const info = await stat(entryPath).catch(() => void 0);
|
|
3903
3945
|
if (info === void 0) continue;
|
|
3904
3946
|
if (info.isDirectory()) {
|
|
3905
|
-
const path =
|
|
3947
|
+
const path = join4(entryPath, "SKILL.md");
|
|
3906
3948
|
const skill2 = await tryParseSkill(path);
|
|
3907
3949
|
if (skill2 === void 0) continue;
|
|
3908
3950
|
candidates.push(toCandidate(skill2, path, rank, entryPath));
|
|
@@ -3915,7 +3957,7 @@ async function collectSkillsDir(skillsDir, rank, candidates) {
|
|
|
3915
3957
|
}
|
|
3916
3958
|
}
|
|
3917
3959
|
async function collectClaudeMd(projectRoot, candidates) {
|
|
3918
|
-
const claudeMd =
|
|
3960
|
+
const claudeMd = join4(projectRoot, "CLAUDE.md");
|
|
3919
3961
|
try {
|
|
3920
3962
|
const info = await stat(claudeMd);
|
|
3921
3963
|
if (!info.isFile()) return;
|
|
@@ -3952,7 +3994,7 @@ async function findProjectRoot(cwd) {
|
|
|
3952
3994
|
let current = cwd;
|
|
3953
3995
|
while (true) {
|
|
3954
3996
|
try {
|
|
3955
|
-
await stat(
|
|
3997
|
+
await stat(join4(current, ".git"));
|
|
3956
3998
|
return current;
|
|
3957
3999
|
} catch {
|
|
3958
4000
|
}
|
|
@@ -3963,12 +4005,81 @@ async function findProjectRoot(cwd) {
|
|
|
3963
4005
|
}
|
|
3964
4006
|
|
|
3965
4007
|
// src/engine-codex/skills.ts
|
|
3966
|
-
import {
|
|
3967
|
-
import {
|
|
3968
|
-
|
|
4008
|
+
import { homedir as homedir3 } from "node:os";
|
|
4009
|
+
import { join as join6 } from "node:path";
|
|
4010
|
+
|
|
4011
|
+
// src/driver-core/context-files.ts
|
|
4012
|
+
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
4013
|
+
import { join as join5, resolve as resolve2 } from "node:path";
|
|
4014
|
+
async function projectAncestors(cwd) {
|
|
4015
|
+
const root = await findProjectRoot(resolve2(cwd));
|
|
4016
|
+
const dirs = [];
|
|
4017
|
+
let current = resolve2(cwd);
|
|
4018
|
+
while (true) {
|
|
4019
|
+
dirs.push(current);
|
|
4020
|
+
if (current === root) return dirs;
|
|
4021
|
+
current = resolve2(current, "..");
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
async function collectProjectContextFiles(cwd, policy) {
|
|
4025
|
+
const files = [];
|
|
4026
|
+
for (const dir of await projectAncestors(cwd)) {
|
|
4027
|
+
const chosen = await dirContextFile(dir, policy);
|
|
4028
|
+
if (chosen !== void 0) files.push(chosen);
|
|
4029
|
+
}
|
|
4030
|
+
return files;
|
|
4031
|
+
}
|
|
4032
|
+
async function dirContextFile(dir, policy) {
|
|
4033
|
+
if (policy.override !== void 0) {
|
|
4034
|
+
const override = join5(dir, policy.override);
|
|
4035
|
+
if (await pathExists(override)) return override;
|
|
4036
|
+
}
|
|
4037
|
+
for (const name2 of policy.primary) {
|
|
4038
|
+
const candidate = join5(dir, name2);
|
|
4039
|
+
if (await pathExists(candidate)) return candidate;
|
|
4040
|
+
}
|
|
4041
|
+
return void 0;
|
|
4042
|
+
}
|
|
4043
|
+
async function pathExists(path) {
|
|
4044
|
+
try {
|
|
4045
|
+
await stat2(path);
|
|
4046
|
+
return true;
|
|
4047
|
+
} catch {
|
|
4048
|
+
return false;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
async function readOptionalFile(path) {
|
|
4052
|
+
try {
|
|
4053
|
+
return await readFile2(path, { encoding: "utf8" });
|
|
4054
|
+
} catch {
|
|
4055
|
+
return void 0;
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
async function anySourceNonEmpty(paths) {
|
|
4059
|
+
for (const path of paths) {
|
|
4060
|
+
const raw = await readOptionalFile(path);
|
|
4061
|
+
if (raw !== void 0 && raw.trim().length > 0) return true;
|
|
4062
|
+
}
|
|
4063
|
+
return false;
|
|
4064
|
+
}
|
|
4065
|
+
async function fileNonEmpty(path) {
|
|
4066
|
+
const raw = await readOptionalFile(path);
|
|
4067
|
+
return raw !== void 0 && raw.trim().length > 0;
|
|
4068
|
+
}
|
|
4069
|
+
async function readSources(paths) {
|
|
4070
|
+
const parts = [];
|
|
4071
|
+
for (const path of paths) {
|
|
4072
|
+
const raw = await readOptionalFile(path);
|
|
4073
|
+
if (raw !== void 0 && raw.trim().length > 0) parts.push(raw);
|
|
4074
|
+
}
|
|
4075
|
+
return parts.length > 0 ? parts.join("\n\n") : void 0;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
// src/engine-codex/skills.ts
|
|
3969
4079
|
var PROVIDER_NAME2 = "codex";
|
|
3970
4080
|
var CODEX_PROJECT_RANK = 140;
|
|
3971
4081
|
var CODEX_USER_RANK = 160;
|
|
4082
|
+
var CODEX_CONTEXT_POLICY = { primary: ["AGENTS.md"] };
|
|
3972
4083
|
var CodexSkillProvider = class {
|
|
3973
4084
|
constructor(control) {
|
|
3974
4085
|
this.control = control;
|
|
@@ -3979,60 +4090,65 @@ var CodexSkillProvider = class {
|
|
|
3979
4090
|
const candidates = [];
|
|
3980
4091
|
const cwd = options.cwd;
|
|
3981
4092
|
if (cwd !== void 0) {
|
|
3982
|
-
const
|
|
3983
|
-
await this.
|
|
4093
|
+
const paths = await collectProjectContextFiles(cwd, CODEX_CONTEXT_POLICY);
|
|
4094
|
+
if (await anySourceNonEmpty(paths)) candidates.push(this.agentsCandidate(paths, CODEX_PROJECT_RANK));
|
|
3984
4095
|
}
|
|
3985
|
-
|
|
4096
|
+
const userPath = join6(homedir3(), ".codex", "AGENTS.md");
|
|
4097
|
+
if (await fileNonEmpty(userPath)) candidates.push(this.agentsCandidate([userPath], CODEX_USER_RANK));
|
|
3986
4098
|
if (this.control.signal.aborted) return [];
|
|
3987
4099
|
return candidates;
|
|
3988
4100
|
}
|
|
3989
4101
|
async get(candidate, _options) {
|
|
3990
4102
|
const locator = candidate.locator;
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
} catch {
|
|
4005
|
-
return void 0;
|
|
4006
|
-
}
|
|
4103
|
+
const content = await readSources(locator.paths);
|
|
4104
|
+
if (content === void 0) return void 0;
|
|
4105
|
+
const first = locator.paths[0];
|
|
4106
|
+
return {
|
|
4107
|
+
name: candidate.name,
|
|
4108
|
+
description: candidate.description,
|
|
4109
|
+
invocation: candidate.invocation,
|
|
4110
|
+
source: candidate.source,
|
|
4111
|
+
provider: this.name,
|
|
4112
|
+
content,
|
|
4113
|
+
path: first,
|
|
4114
|
+
resourceBase: { kind: "file", path: first }
|
|
4115
|
+
};
|
|
4007
4116
|
}
|
|
4008
|
-
/**
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
resourceBase: { kind: "file", path }
|
|
4023
|
-
});
|
|
4024
|
-
} catch {
|
|
4025
|
-
}
|
|
4117
|
+
/** One merged `agents-md` candidate for a ranked file set. */
|
|
4118
|
+
agentsCandidate(paths, rank) {
|
|
4119
|
+
const first = paths[0];
|
|
4120
|
+
return {
|
|
4121
|
+
name: "agents-md",
|
|
4122
|
+
description: "Codex project/user instructions (AGENTS.md)",
|
|
4123
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
4124
|
+
source: "custom",
|
|
4125
|
+
provider: this.name,
|
|
4126
|
+
rank,
|
|
4127
|
+
locator: { kind: "agents-md", paths },
|
|
4128
|
+
path: first,
|
|
4129
|
+
resourceBase: { kind: "file", path: first }
|
|
4130
|
+
};
|
|
4026
4131
|
}
|
|
4027
4132
|
};
|
|
4028
4133
|
|
|
4029
4134
|
// src/engine-pi/skills.ts
|
|
4030
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
4031
|
-
import { homedir as
|
|
4032
|
-
import { join as
|
|
4135
|
+
import { readdir as readdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
|
|
4136
|
+
import { homedir as homedir4 } from "node:os";
|
|
4137
|
+
import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
|
|
4033
4138
|
var PROVIDER_NAME3 = "pi";
|
|
4034
|
-
var
|
|
4035
|
-
var
|
|
4139
|
+
var PI_AGENTS_PROJECT_RANK = 140;
|
|
4140
|
+
var PI_SKILL_PROJECT_RANK = 150;
|
|
4141
|
+
var PI_AGENTS_USER_RANK = 160;
|
|
4142
|
+
var PI_SKILL_USER_RANK = 170;
|
|
4143
|
+
var PI_CONTEXT_POLICY = {
|
|
4144
|
+
override: "AGENTS.override.md",
|
|
4145
|
+
primary: ["AGENTS.md", "CLAUDE.md"]
|
|
4146
|
+
};
|
|
4147
|
+
function piAgentDir() {
|
|
4148
|
+
const override = process.env.PI_CODING_AGENT_DIR;
|
|
4149
|
+
if (override !== void 0 && override.length > 0) return resolve3(override);
|
|
4150
|
+
return join7(homedir4(), ".pi", "agent");
|
|
4151
|
+
}
|
|
4036
4152
|
var PiSkillProvider = class {
|
|
4037
4153
|
constructor(control) {
|
|
4038
4154
|
this.control = control;
|
|
@@ -4043,49 +4159,113 @@ var PiSkillProvider = class {
|
|
|
4043
4159
|
const candidates = [];
|
|
4044
4160
|
const cwd = options.cwd;
|
|
4045
4161
|
if (cwd !== void 0) {
|
|
4046
|
-
const
|
|
4047
|
-
await
|
|
4162
|
+
const projectDirs = await projectAncestors(cwd);
|
|
4163
|
+
const contextPaths = await collectProjectContextFiles(cwd, PI_CONTEXT_POLICY);
|
|
4164
|
+
if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, PI_AGENTS_PROJECT_RANK));
|
|
4165
|
+
for (const dir of projectDirs) {
|
|
4166
|
+
await this.collectSkillsDir(join7(dir, ".pi", "skills"), PI_SKILL_PROJECT_RANK, candidates);
|
|
4167
|
+
}
|
|
4048
4168
|
}
|
|
4049
|
-
|
|
4169
|
+
const userAgentDir = piAgentDir();
|
|
4170
|
+
const userContext = join7(userAgentDir, "AGENTS.md");
|
|
4171
|
+
if (await fileNonEmpty(userContext)) candidates.push(this.agentsCandidate([userContext], PI_AGENTS_USER_RANK));
|
|
4172
|
+
await this.collectSkillsDir(join7(userAgentDir, "skills"), PI_SKILL_USER_RANK, candidates);
|
|
4050
4173
|
if (this.control.signal.aborted) return [];
|
|
4051
4174
|
return candidates;
|
|
4052
4175
|
}
|
|
4053
4176
|
async get(candidate, _options) {
|
|
4054
4177
|
const locator = candidate.locator;
|
|
4055
|
-
|
|
4056
|
-
const
|
|
4178
|
+
if (locator.kind === "skill-file") {
|
|
4179
|
+
const parsed = await this.tryParse(locator.path);
|
|
4180
|
+
if (parsed === void 0) return void 0;
|
|
4057
4181
|
return {
|
|
4058
|
-
name:
|
|
4059
|
-
description:
|
|
4060
|
-
|
|
4182
|
+
name: parsed.name,
|
|
4183
|
+
description: parsed.description,
|
|
4184
|
+
...parsed.whenToUse === void 0 ? {} : { whenToUse: parsed.whenToUse },
|
|
4185
|
+
invocation: parsed.invocation,
|
|
4061
4186
|
source: candidate.source,
|
|
4062
4187
|
provider: this.name,
|
|
4063
|
-
content,
|
|
4188
|
+
content: parsed.content,
|
|
4064
4189
|
path: locator.path,
|
|
4065
|
-
|
|
4066
|
-
...candidate.resourceBase !== void 0 ? { resourceBase: candidate.resourceBase } : {}
|
|
4190
|
+
resourceBase: { kind: "directory", path: dirname3(locator.path) }
|
|
4067
4191
|
};
|
|
4192
|
+
}
|
|
4193
|
+
const content = await readSources(locator.paths);
|
|
4194
|
+
if (content === void 0) return void 0;
|
|
4195
|
+
const first = locator.paths[0];
|
|
4196
|
+
return {
|
|
4197
|
+
name: candidate.name,
|
|
4198
|
+
description: candidate.description,
|
|
4199
|
+
invocation: candidate.invocation,
|
|
4200
|
+
source: candidate.source,
|
|
4201
|
+
provider: this.name,
|
|
4202
|
+
content,
|
|
4203
|
+
path: first,
|
|
4204
|
+
resourceBase: { kind: "file", path: first }
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
/** One merged `agents-md` candidate for a ranked file set. */
|
|
4208
|
+
agentsCandidate(paths, rank) {
|
|
4209
|
+
const first = paths[0];
|
|
4210
|
+
return {
|
|
4211
|
+
name: "agents-md",
|
|
4212
|
+
description: "Pi project/user instructions (AGENTS.md / CLAUDE.md)",
|
|
4213
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
4214
|
+
source: "custom",
|
|
4215
|
+
provider: this.name,
|
|
4216
|
+
rank,
|
|
4217
|
+
locator: { kind: "agents-md", paths },
|
|
4218
|
+
path: first,
|
|
4219
|
+
resourceBase: { kind: "file", path: first }
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
/** Collect every skill in one skills directory, both pi layouts. */
|
|
4223
|
+
async collectSkillsDir(skillsDir, rank, candidates) {
|
|
4224
|
+
let entries;
|
|
4225
|
+
try {
|
|
4226
|
+
entries = await readdir2(skillsDir, { withFileTypes: true, encoding: "utf8" });
|
|
4068
4227
|
} catch {
|
|
4069
|
-
return
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
4231
|
+
const entryPath = join7(skillsDir, entry.name);
|
|
4232
|
+
const info = await stat3(entryPath).catch(() => void 0);
|
|
4233
|
+
if (info === void 0) continue;
|
|
4234
|
+
if (info.isDirectory()) {
|
|
4235
|
+
const path = join7(entryPath, "SKILL.md");
|
|
4236
|
+
const parsed2 = await this.tryParse(path);
|
|
4237
|
+
if (parsed2 === void 0) continue;
|
|
4238
|
+
candidates.push(this.skillCandidate(parsed2, path, rank, entryPath));
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
4242
|
+
const parsed = await this.tryParse(entryPath);
|
|
4243
|
+
if (parsed === void 0) continue;
|
|
4244
|
+
candidates.push(this.skillCandidate(parsed, entryPath, rank, skillsDir));
|
|
4070
4245
|
}
|
|
4071
4246
|
}
|
|
4072
|
-
/**
|
|
4073
|
-
|
|
4247
|
+
/** One parsed skill as a ranked candidate. */
|
|
4248
|
+
skillCandidate(skill, path, rank, resourceDir) {
|
|
4249
|
+
return {
|
|
4250
|
+
name: skill.name,
|
|
4251
|
+
description: skill.description,
|
|
4252
|
+
...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
|
|
4253
|
+
invocation: skill.invocation,
|
|
4254
|
+
source: "custom",
|
|
4255
|
+
provider: this.name,
|
|
4256
|
+
rank,
|
|
4257
|
+
locator: { kind: "skill-file", path },
|
|
4258
|
+
path,
|
|
4259
|
+
resourceBase: { kind: "directory", path: resourceDir }
|
|
4260
|
+
};
|
|
4261
|
+
}
|
|
4262
|
+
/** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
|
|
4263
|
+
async tryParse(path) {
|
|
4074
4264
|
try {
|
|
4075
|
-
const
|
|
4076
|
-
|
|
4077
|
-
candidates.push({
|
|
4078
|
-
name: "agents-md",
|
|
4079
|
-
description: "Pi project/user instructions (AGENTS.md)",
|
|
4080
|
-
invocation: { modelInvocable: true, userInvocable: true },
|
|
4081
|
-
source: "custom",
|
|
4082
|
-
provider: this.name,
|
|
4083
|
-
rank,
|
|
4084
|
-
locator: { kind: "agents-md", path },
|
|
4085
|
-
path,
|
|
4086
|
-
resourceBase: { kind: "file", path }
|
|
4087
|
-
});
|
|
4265
|
+
const raw = await readFile3(path, { encoding: "utf8" });
|
|
4266
|
+
return parseSkillFile(raw);
|
|
4088
4267
|
} catch {
|
|
4268
|
+
return void 0;
|
|
4089
4269
|
}
|
|
4090
4270
|
}
|
|
4091
4271
|
};
|
|
@@ -4111,7 +4291,7 @@ var Config4 = z5.object({
|
|
|
4111
4291
|
});
|
|
4112
4292
|
function resolvePatchPath(config) {
|
|
4113
4293
|
if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
|
|
4114
|
-
return
|
|
4294
|
+
return join8(
|
|
4115
4295
|
resolveDshHome(),
|
|
4116
4296
|
"profiles",
|
|
4117
4297
|
config.profile ?? "web",
|
|
@@ -4130,13 +4310,13 @@ async function readPatchOrUndefined(path) {
|
|
|
4130
4310
|
}
|
|
4131
4311
|
}
|
|
4132
4312
|
async function writePatchFile(path, text) {
|
|
4133
|
-
await mkdir(
|
|
4313
|
+
await mkdir(dirname4(path), { recursive: true });
|
|
4134
4314
|
const tmp = `${path}.tmp-${randomUUID()}`;
|
|
4135
4315
|
await writeFile(tmp, text, "utf8");
|
|
4136
4316
|
await rename(tmp, path);
|
|
4137
4317
|
}
|
|
4138
4318
|
function writePatchFileSync(path, text) {
|
|
4139
|
-
mkdirSync(
|
|
4319
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
4140
4320
|
const tmp = `${path}.tmp-${randomUUID()}`;
|
|
4141
4321
|
writeFileSync(tmp, text, "utf8");
|
|
4142
4322
|
renameSync(tmp, path);
|
|
@@ -4150,7 +4330,7 @@ async function syncManagedBlock(path, engine) {
|
|
|
4150
4330
|
}
|
|
4151
4331
|
function readPatchFileSync(path) {
|
|
4152
4332
|
try {
|
|
4153
|
-
return
|
|
4333
|
+
return readFileSync3(path, "utf8");
|
|
4154
4334
|
} catch (error) {
|
|
4155
4335
|
if (isMissing(error)) return "";
|
|
4156
4336
|
throw error;
|
|
@@ -4231,8 +4411,12 @@ function apply(ctx, config) {
|
|
|
4231
4411
|
const commands = ctx.get("commands");
|
|
4232
4412
|
if (commands !== void 0) {
|
|
4233
4413
|
const disposers = [];
|
|
4234
|
-
for (const
|
|
4235
|
-
|
|
4414
|
+
for (const command of [...CLAUDE_CODE_COMMANDS, ...discoverUserSlashCommands()]) {
|
|
4415
|
+
try {
|
|
4416
|
+
disposers.push(commands.register(command));
|
|
4417
|
+
} catch (error) {
|
|
4418
|
+
ctx.logger.warn(`loop-engine: skip claude-code command /${command.name}: ${String(error)}`);
|
|
4419
|
+
}
|
|
4236
4420
|
}
|
|
4237
4421
|
commandDisposers = disposers;
|
|
4238
4422
|
}
|
|
@@ -4276,7 +4460,7 @@ function apply(ctx, config) {
|
|
|
4276
4460
|
mountEngine(fileEngine);
|
|
4277
4461
|
ctx.effect(() => () => CLEAR_RETRY(), "loop-engine: mount retry cleanup");
|
|
4278
4462
|
let source;
|
|
4279
|
-
installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine }, {
|
|
4463
|
+
installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine, showInComposer: true }, {
|
|
4280
4464
|
setSource: (current) => {
|
|
4281
4465
|
source = current;
|
|
4282
4466
|
},
|
package/lib/invariant.js
CHANGED
|
@@ -3,7 +3,8 @@ import z from "@deepseek-ai/schemastery";
|
|
|
3
3
|
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
4
4
|
var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi"];
|
|
5
5
|
var LOOP_ENGINE_SETTINGS_SCHEMA = z.object({
|
|
6
|
-
engine: z.union([z.const("in-process"), z.const("claude-code"), z.const("codex"), z.const("pi")]).default("in-process")
|
|
6
|
+
engine: z.union([z.const("in-process"), z.const("claude-code"), z.const("codex"), z.const("pi")]).default("in-process"),
|
|
7
|
+
showInComposer: z.boolean().default(true)
|
|
7
8
|
});
|
|
8
9
|
|
|
9
10
|
// src/patch-manager.ts
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer loop-engine picker: a compact dropdown registered at the
|
|
3
|
+
* `conversation.input.right` seat, so it sits immediately left of the model
|
|
4
|
+
* select in the composer's tool row. The engine is a deployment-level choice,
|
|
5
|
+
* so this surface shares the same settings-backed {@link LoopEngineStore} as
|
|
6
|
+
* the settings section and the header badge — a change in any one is what the
|
|
7
|
+
* others show next. Switching still asks for confirmation first (it interrupts
|
|
8
|
+
* sessions still running on the previous engine) and reloads the page once the
|
|
9
|
+
* commit lands, matching the settings section's semantics.
|
|
10
|
+
*
|
|
11
|
+
* Styling is token-driven inline styles like the badge and section (the
|
|
12
|
+
* client-module bundle is esbuild-built without a CSS loader).
|
|
13
|
+
* @module dsh-loop-engine/client/composer
|
|
14
|
+
*/
|
|
15
|
+
import { type JSX } from 'react';
|
|
16
|
+
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
17
|
+
import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
|
|
18
|
+
import type { LoopEngineStore, LoopEngineState } from './store.ts';
|
|
19
|
+
import type { en } from './locales.ts';
|
|
20
|
+
/** Injected dependencies of {@link LoopEngineComposerSelect} (slot `inject`). */
|
|
21
|
+
export interface LoopEngineComposerSelectInjected {
|
|
22
|
+
/** The selection store (loaded on mount, refreshed by scope pushes). */
|
|
23
|
+
controller: LoopEngineStore;
|
|
24
|
+
hooks: {
|
|
25
|
+
/** Engine snapshot bound by the UI renderer as useSnapshot. */
|
|
26
|
+
snapshot: SnapshotStore<LoopEngineState>;
|
|
27
|
+
};
|
|
28
|
+
/** Composer copy bound to the loop engine dictionaries. */
|
|
29
|
+
t: (key: keyof typeof en) => string;
|
|
30
|
+
}
|
|
31
|
+
/** Props delivered by the slot outlet (the renderer erases the share boundary). */
|
|
32
|
+
export type LoopEngineComposerSelectProps = Partial<InjectFace<LoopEngineComposerSelectInjected>>;
|
|
33
|
+
/**
|
|
34
|
+
* Render the composer's loop-engine dropdown. Hides until the settings scope
|
|
35
|
+
* settles, so the composer never flashes a provisional engine.
|
|
36
|
+
* @param props - composed slot props.
|
|
37
|
+
* @returns the picker, or null while the engine is unknown.
|
|
38
|
+
*/
|
|
39
|
+
export declare function LoopEngineComposerSelect(props: LoopEngineComposerSelectProps): JSX.Element | null;
|
|
40
|
+
//# sourceMappingURL=LoopEngineComposerSelect.d.ts.map
|
|
@@ -9,6 +9,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
|
9
9
|
import { type LoopEngineKey } from './locales.ts';
|
|
10
10
|
export type { LoopEngineSectionInjected, LoopEngineSectionProps } from './LoopEngineSection.tsx';
|
|
11
11
|
export type { LoopEngineBadgeInjected, LoopEngineBadgeProps } from './LoopEngineBadge.tsx';
|
|
12
|
+
export type { LoopEngineComposerSelectInjected, LoopEngineComposerSelectProps } from './LoopEngineComposerSelect.tsx';
|
|
12
13
|
export type { LoopEngineState } from './store.ts';
|
|
13
14
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
14
15
|
interface LocaleNamespaceMap {
|
|
@@ -16,6 +16,8 @@ export interface LoopEngineKey {
|
|
|
16
16
|
engineCodex: string;
|
|
17
17
|
/** Option label: the Pi CLI driver. */
|
|
18
18
|
enginePi: string;
|
|
19
|
+
/** Settings toggle: show the engine picker in the chat page composer. */
|
|
20
|
+
showInComposerLabel: string;
|
|
19
21
|
/** Unavailable-state message. */
|
|
20
22
|
unavailable: string;
|
|
21
23
|
/** Notice shown when the selection would interrupt running agents. */
|