@shuind/dsh-codex-harness 0.1.20 → 0.1.22
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 +10 -3
- package/lib/client.js +87 -5
- package/lib/index.js +73 -11
- package/lib/types/client/index.d.ts +3 -1
- package/lib/types/client/index.js +88 -15
- package/lib/types/exec.d.ts +2 -0
- package/lib/types/exec.js +53 -2
- package/lib/types/index.js +5 -3
- package/lib/types/patch.d.ts +1 -1
- package/lib/types/patch.js +27 -4
- package/package.json +12 -3
- package/presets/codex/agent.cordis.yml +8 -0
- package/presets/codex/preset.yml +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
7
|
- **精简 Codex coding-agent 提示词**:使用精简版系统提示词;身份、模型和工作目录由 `dsh-persona` 提供,避免重复的 Harness 身份说明,同时保留必要的工具契约和编码工作流。
|
|
8
|
-
- **Codex 工具**:`exec_command`、`write_stdin`、`apply_patch`、`update_plan
|
|
8
|
+
- **Codex 工具**:`exec_command`、`write_stdin`、`apply_patch`、`update_plan`;`apply_patch` 匹配失败时会显示文件、hunk、行号、可见空白和附近文件内容,便于修正上下文。
|
|
9
|
+
- **后台任务**:`exec_command` 支持 `run_in_background: true`,立即返回 DSH `job_id`;使用 `job_output` 读取输出、`job_kill` 停止任务。任务完成时,忙碌中的 agent 会在下一步收到 inbox 注入;空闲 agent 默认保持安静,等下一次唤醒时再处理完成消息。
|
|
9
10
|
- **GPT 能力补全**:为 GPT 系列模型补充图片输入和思考强度选项;不会覆盖用户已有的显式配置。
|
|
10
11
|
- **Fast**:仅在 Codex preset 的模型选择菜单中显示,开启后向 Responses 请求发送 `service_tier: "priority"`。
|
|
12
|
+
- **旧版 Web 兼容**:在未提供新版模型设置/上下文设置 slot 的 DSH Web 中,Fast 和上下文容量会自动回退到旧版 composer slot;client 注入不依赖纯类型 slot 包。
|
|
11
13
|
- **远程搜索与压缩**:OpenAI Responses 请求默认优先使用 hosted `web_search` 和 `/responses/compact`;失败时回退到 DSH 的本地实现。
|
|
12
14
|
- **上下文容量**:在 Web 中设置 `1K`–`1000K` tokens,设置值作用于下一次请求。
|
|
13
15
|
- **活动状态**:Codex 等待模型响应或进行上下文压缩时,在输入框上方显示对应状态和耗时,让长时间 Deep Diving 不再像卡住。
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
## 安装
|
|
16
18
|
|
|
17
19
|
```sh
|
|
18
|
-
dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.
|
|
20
|
+
dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.22
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
重启 Web,创建新会话,在模式菜单中选择 **Codex 模式**。
|
|
@@ -82,6 +84,11 @@ Codex 等待模型首个输出时,Web 输入框上方会显示 `正在等待
|
|
|
82
84
|
name: '@shuind/dsh-codex-harness'
|
|
83
85
|
config:
|
|
84
86
|
collaborationPrompt: true
|
|
87
|
+
|
|
88
|
+
- id: codex-jobs
|
|
89
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
90
|
+
config:
|
|
91
|
+
completionDelivery: quiet
|
|
85
92
|
```
|
|
86
93
|
|
|
87
94
|
再写入 `preset.yml`,让 Web 中显示更清晰的名称:
|
|
@@ -92,7 +99,7 @@ description: 使用 Codex 工具并开启可选协作提示词。
|
|
|
92
99
|
order: 10
|
|
93
100
|
```
|
|
94
101
|
|
|
95
|
-
然后在 Web 的 **Agent 预设** 中选择 `My Codex`,再新建会话。这个 preset 会加载 Codex
|
|
102
|
+
然后在 Web 的 **Agent 预设** 中选择 `My Codex`,再新建会话。这个 preset 会加载 Codex 工具,并开启协作提示词;网页搜索、远程压缩、后台任务控制和 Skills 等其他功能需要在同一个 `agent.cordis.yml` 中按需添加。
|
|
96
103
|
|
|
97
104
|
### 可选协作提示词(私货)
|
|
98
105
|
|
package/lib/client.js
CHANGED
|
@@ -28,6 +28,11 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
/** Browser controls for the Codex request settings mounted by the Host plugin. */
|
|
29
29
|
const NS = "codex";
|
|
30
30
|
const SETTINGS_NAMESPACE = "codex";
|
|
31
|
+
const MODEL_SETTINGS_SLOT = "conversation.input.model.settings";
|
|
32
|
+
const CONTEXT_SETTINGS_SLOT = "conversation.input.context.settings";
|
|
33
|
+
const FAST_FALLBACK_SLOT = "conversation.input.right";
|
|
34
|
+
const CONTEXT_FALLBACK_SLOT = "conversation.input.dock";
|
|
35
|
+
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
31
36
|
const en = {
|
|
32
37
|
fast: "Fast mode",
|
|
33
38
|
fastOn: "Fast mode on (priority tier)",
|
|
@@ -95,6 +100,50 @@ window.__ModuleLoader__.load({
|
|
|
95
100
|
})]
|
|
96
101
|
});
|
|
97
102
|
}
|
|
103
|
+
/** The legacy composer seat is a compact inline row, not a menu panel. */
|
|
104
|
+
function FastModeFallback(props) {
|
|
105
|
+
const { sessionId, useSessions, useSettings, setSetting, t } = props;
|
|
106
|
+
const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
|
|
107
|
+
const snapshot = useSettings((state) => state);
|
|
108
|
+
const fast = snapshot.value?.fast ?? false;
|
|
109
|
+
if (agentPreset !== "codex") return null;
|
|
110
|
+
return (0, react_jsx_runtime.jsxs)("button", {
|
|
111
|
+
type: "button",
|
|
112
|
+
role: "switch",
|
|
113
|
+
disabled: snapshot.writable === false,
|
|
114
|
+
"aria-checked": fast,
|
|
115
|
+
"aria-label": fast ? t("fastOn") : t("fastOff"),
|
|
116
|
+
title: fast ? t("fastOn") : t("fastOff"),
|
|
117
|
+
onClick: () => {
|
|
118
|
+
setSetting("fast", !fast);
|
|
119
|
+
},
|
|
120
|
+
style: {
|
|
121
|
+
boxSizing: "border-box",
|
|
122
|
+
display: "inline-flex",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
gap: 5,
|
|
125
|
+
height: 28,
|
|
126
|
+
border: "1px solid var(--dsw-alias-border-primary)",
|
|
127
|
+
borderRadius: 7,
|
|
128
|
+
padding: "0 8px",
|
|
129
|
+
color: fast ? "var(--dsw-static-blue-500)" : "var(--dsw-alias-label-secondary)",
|
|
130
|
+
background: fast ? "var(--dsw-alias-interactive-bg-selected)" : "transparent",
|
|
131
|
+
cursor: snapshot.writable === false ? "default" : "pointer",
|
|
132
|
+
font: "inherit",
|
|
133
|
+
fontSize: 12,
|
|
134
|
+
lineHeight: "20px",
|
|
135
|
+
fontWeight: fast ? 600 : 400,
|
|
136
|
+
whiteSpace: "nowrap"
|
|
137
|
+
},
|
|
138
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t("fast") }), (0, react_jsx_runtime.jsx)("span", {
|
|
139
|
+
style: {
|
|
140
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
141
|
+
fontSize: 11
|
|
142
|
+
},
|
|
143
|
+
children: fast ? t("fastStateOn") : t("fastStateOff")
|
|
144
|
+
})]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
98
147
|
function ContextSizeControl({ contextWindow, useSettings, setSetting, unsetSetting, t }) {
|
|
99
148
|
const snapshot = useSettings((state) => state);
|
|
100
149
|
const configured = snapshot.value?.contextWindow;
|
|
@@ -211,6 +260,16 @@ window.__ModuleLoader__.load({
|
|
|
211
260
|
]
|
|
212
261
|
});
|
|
213
262
|
}
|
|
263
|
+
function contextWindowFromProjection(useProjection) {
|
|
264
|
+
const pressure = useProjection("contextPressure");
|
|
265
|
+
return pressure?.contextWindow !== void 0 && pressure.contextWindow > 0 ? pressure.contextWindow : DEFAULT_CONTEXT_WINDOW;
|
|
266
|
+
}
|
|
267
|
+
function LegacyContextSizeControl({ useProjection, ...props }) {
|
|
268
|
+
return (0, react_jsx_runtime.jsx)(ContextSizeControl, {
|
|
269
|
+
...props,
|
|
270
|
+
contextWindow: contextWindowFromProjection(useProjection)
|
|
271
|
+
});
|
|
272
|
+
}
|
|
214
273
|
function elapsedSeconds(startedAt, now) {
|
|
215
274
|
return Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
216
275
|
}
|
|
@@ -275,7 +334,15 @@ window.__ModuleLoader__.load({
|
|
|
275
334
|
"locale",
|
|
276
335
|
"settingsScope"
|
|
277
336
|
];
|
|
278
|
-
/**
|
|
337
|
+
/** Select a preferred slot when the host declares it, otherwise use a stable legacy composer seat. */
|
|
338
|
+
function chooseDeclaredSlot(preferred, fallback, isDeclared) {
|
|
339
|
+
return isDeclared(preferred) ? preferred : fallback;
|
|
340
|
+
}
|
|
341
|
+
function slotIsDeclared(ctx, slot) {
|
|
342
|
+
const registry = ctx.slots;
|
|
343
|
+
return typeof registry.spec === "function" && registry.spec.call(ctx.slots, slot) !== void 0;
|
|
344
|
+
}
|
|
345
|
+
/** Mount Codex controls in the newest named seats, with legacy composer fallbacks. */
|
|
279
346
|
function apply(ctx) {
|
|
280
347
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
281
348
|
en,
|
|
@@ -287,20 +354,34 @@ window.__ModuleLoader__.load({
|
|
|
287
354
|
setSetting: (field, value) => settings.set(field, value),
|
|
288
355
|
unsetSetting: (field) => settings.unset(field)
|
|
289
356
|
});
|
|
290
|
-
ctx.slots.inject(
|
|
291
|
-
name:
|
|
357
|
+
if (chooseDeclaredSlot(MODEL_SETTINGS_SLOT, FAST_FALLBACK_SLOT, (slot) => slotIsDeclared(ctx, slot)) === MODEL_SETTINGS_SLOT) ctx.slots.inject(MODEL_SETTINGS_SLOT, () => ctx.slots.register({
|
|
358
|
+
name: MODEL_SETTINGS_SLOT,
|
|
292
359
|
id: "codex-fast",
|
|
293
360
|
order: 0,
|
|
294
361
|
locale: NS,
|
|
295
362
|
inject: injected
|
|
296
363
|
}, FastModeButton));
|
|
297
|
-
ctx.slots.inject(
|
|
298
|
-
name:
|
|
364
|
+
else ctx.slots.inject(FAST_FALLBACK_SLOT, () => ctx.slots.register({
|
|
365
|
+
name: FAST_FALLBACK_SLOT,
|
|
366
|
+
id: "codex-fast-fallback",
|
|
367
|
+
order: 0,
|
|
368
|
+
locale: NS,
|
|
369
|
+
inject: injected
|
|
370
|
+
}, FastModeFallback));
|
|
371
|
+
if (chooseDeclaredSlot(CONTEXT_SETTINGS_SLOT, CONTEXT_FALLBACK_SLOT, (slot) => slotIsDeclared(ctx, slot)) === CONTEXT_SETTINGS_SLOT) ctx.slots.inject(CONTEXT_SETTINGS_SLOT, () => ctx.slots.register({
|
|
372
|
+
name: CONTEXT_SETTINGS_SLOT,
|
|
299
373
|
id: "codex-context-size",
|
|
300
374
|
order: 0,
|
|
301
375
|
locale: NS,
|
|
302
376
|
inject: injected
|
|
303
377
|
}, ContextSizeControl));
|
|
378
|
+
else ctx.slots.inject(CONTEXT_FALLBACK_SLOT, () => ctx.slots.register({
|
|
379
|
+
name: CONTEXT_FALLBACK_SLOT,
|
|
380
|
+
id: "codex-context-size-fallback",
|
|
381
|
+
order: 0,
|
|
382
|
+
locale: NS,
|
|
383
|
+
inject: injected
|
|
384
|
+
}, LegacyContextSizeControl));
|
|
304
385
|
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
305
386
|
name: "conversation.composer.dock",
|
|
306
387
|
id: "codex-activity",
|
|
@@ -315,6 +396,7 @@ window.__ModuleLoader__.load({
|
|
|
315
396
|
//#endregion
|
|
316
397
|
exports.ActivityLine = ActivityLine;
|
|
317
398
|
exports.apply = apply;
|
|
399
|
+
exports.chooseDeclaredSlot = chooseDeclaredSlot;
|
|
318
400
|
exports.default = client_default;
|
|
319
401
|
exports.inject = inject;
|
|
320
402
|
return module.exports;
|
package/lib/index.js
CHANGED
|
@@ -177,17 +177,30 @@ function findSequence(lines, expected, from, endOfFile) {
|
|
|
177
177
|
}
|
|
178
178
|
return -1;
|
|
179
179
|
}
|
|
180
|
+
function displayPatchLine(line) {
|
|
181
|
+
if (line.length === 0) return "<empty>";
|
|
182
|
+
return line.replaceAll(" ", "[space]").replaceAll(" ", "[tab]");
|
|
183
|
+
}
|
|
184
|
+
function formatPatchExcerpt(lines, start, count) {
|
|
185
|
+
if (lines.length === 0) return " <empty file>";
|
|
186
|
+
const first = Math.max(0, Math.min(start, lines.length - 1));
|
|
187
|
+
return lines.slice(first, first + count).map((line, offset) => ` ${first + offset + 1}: ${displayPatchLine(line)}`).join("\n");
|
|
188
|
+
}
|
|
189
|
+
function mismatchMessage(label, hunkIndex, expected, actual, from, endOfFile) {
|
|
190
|
+
const expectedPreview = expected.slice(0, 12).map((line, index) => ` ${index + 1}: ${displayPatchLine(line)}`).join("\n");
|
|
191
|
+
const expectedSuffix = expected.length > 12 ? "\n ..." : "";
|
|
192
|
+
const excerptStart = endOfFile ? Math.max(0, actual.length - 6) : Math.max(0, from - 2);
|
|
193
|
+
const markerHint = expected.some((wanted) => wanted.length > 0 && actual.includes(`-${wanted}`)) ? "\nHint: if a source line starts with a patch marker, repeat that marker after the operation marker; for example, `-- item` deletes the source line `- item`." : "";
|
|
194
|
+
return `could not find expected lines in ${label} hunk ${hunkIndex + 1} (search starts at line ${from + 1})\nExpected:\n${expectedPreview}${expectedSuffix}\nFile excerpt:\n${formatPatchExcerpt(actual, excerptStart, 10)}${markerHint}`;
|
|
195
|
+
}
|
|
180
196
|
/** Apply parsed update hunks and return LF-normalized text. */
|
|
181
|
-
function applyPatchHunks(original, hunks) {
|
|
197
|
+
function applyPatchHunks(original, hunks, label = "file") {
|
|
182
198
|
const value = splitText(original.replaceAll("\r\n", "\n"));
|
|
183
199
|
let cursor = 0;
|
|
184
|
-
for (const hunk of hunks) {
|
|
200
|
+
for (const [hunkIndex, hunk] of hunks.entries()) {
|
|
185
201
|
const expected = hunk.lines.filter((line) => line.kind !== "add").map((line) => line.text);
|
|
186
202
|
const start = findSequence(value.lines, expected, cursor, hunk.endOfFile);
|
|
187
|
-
if (start < 0)
|
|
188
|
-
const detail = expected.join("\n");
|
|
189
|
-
invalid(`could not find expected lines${detail.length === 0 ? "" : `:\n${detail}`}`);
|
|
190
|
-
}
|
|
203
|
+
if (start < 0) invalid(mismatchMessage(label, hunkIndex, expected, value.lines, cursor, hunk.endOfFile));
|
|
191
204
|
const replacement = hunk.lines.filter((line) => line.kind !== "delete").map((line) => line.text);
|
|
192
205
|
value.lines.splice(start, expected.length, ...replacement);
|
|
193
206
|
cursor = start + replacement.length;
|
|
@@ -248,6 +261,7 @@ function renderExecResult(result) {
|
|
|
248
261
|
sections.push(`Wall time: ${result.wall_time_seconds.toFixed(4)} seconds`);
|
|
249
262
|
if (result.exit_code !== void 0) sections.push(`Process exited with code ${result.exit_code}`);
|
|
250
263
|
if (result.session_id !== void 0) sections.push(`Process running with session ID ${result.session_id}`);
|
|
264
|
+
if (result.job_id !== void 0) sections.push(`Background job ID: ${result.job_id}`);
|
|
251
265
|
if (result.original_token_count !== void 0) sections.push(`Original token count: ${result.original_token_count}`);
|
|
252
266
|
sections.push("Output:", result.output);
|
|
253
267
|
return sections.join("\n");
|
|
@@ -268,6 +282,17 @@ function terminalResult(result, maxBytes, startedAt) {
|
|
|
268
282
|
...result.sessionStatus.kind === "exited" ? { ...result.sessionStatus.exitCode === null ? {} : { exit_code: result.sessionStatus.exitCode } } : {}
|
|
269
283
|
});
|
|
270
284
|
}
|
|
285
|
+
/** Map a detached shell process onto the generic DSH job outcome contract. */
|
|
286
|
+
function backgroundOutcome(process) {
|
|
287
|
+
if (process.status === "killed") return {
|
|
288
|
+
status: "killed",
|
|
289
|
+
detail: process.signal !== null ? `signal: ${process.signal}` : "killed before exit"
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
status: "completed",
|
|
293
|
+
detail: `exit code: ${process.exitCode ?? 0}`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
271
296
|
function sleep(ms, signal) {
|
|
272
297
|
return new Promise((resolve) => {
|
|
273
298
|
let timer;
|
|
@@ -321,6 +346,7 @@ async function runExecCommand(ctx, args, exec, config) {
|
|
|
321
346
|
const maxBytes = outputLimit(config.maxOutputBytes, args.max_output_tokens);
|
|
322
347
|
const workdir = sessionCwd$1(exec, args.workdir);
|
|
323
348
|
const startedAt = performance.now();
|
|
349
|
+
if (args.run_in_background === true && args.tty === true) throw new Error("run_in_background is only supported for pipe-backed commands; omit tty");
|
|
324
350
|
if (args.tty === true) {
|
|
325
351
|
const agent = exec.agent;
|
|
326
352
|
const terminals = ctx.get("terminals");
|
|
@@ -364,11 +390,42 @@ async function runExecCommand(ctx, args, exec, config) {
|
|
|
364
390
|
login: args.login ?? true,
|
|
365
391
|
...workdir === void 0 ? {} : { workdir },
|
|
366
392
|
stdoutMaxBytes: maxBytes,
|
|
367
|
-
signal: exec.signal,
|
|
368
393
|
...dshEnv === void 0 ? {} : { dshEnv },
|
|
369
394
|
...policy === void 0 ? {} : { sandboxPolicy: policy }
|
|
370
395
|
};
|
|
371
|
-
|
|
396
|
+
if (args.run_in_background === true) {
|
|
397
|
+
const owner = exec.agent;
|
|
398
|
+
if (owner === void 0) throw new Error("run_in_background requires an owning agent session");
|
|
399
|
+
const jobs = ctx.get("jobs");
|
|
400
|
+
if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
|
|
401
|
+
if (exec.signal.aborted) {
|
|
402
|
+
exec.signal.throwIfAborted();
|
|
403
|
+
throw new Error("tool call aborted");
|
|
404
|
+
}
|
|
405
|
+
const id = jobs.start({
|
|
406
|
+
kind: "bash",
|
|
407
|
+
label: args.cmd,
|
|
408
|
+
owner,
|
|
409
|
+
outputLimitBytes: maxBytes,
|
|
410
|
+
run: () => {
|
|
411
|
+
const process = ctx.shell.start(ctx.shell.resolve(shellRequest));
|
|
412
|
+
return {
|
|
413
|
+
cancel: () => void process.kill(),
|
|
414
|
+
done: process.done.then(() => backgroundOutcome(process)),
|
|
415
|
+
readOutput: () => readShellOutput(process.readOutput(), maxBytes)
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
return withChunkId({
|
|
420
|
+
wall_time_seconds: (performance.now() - startedAt) / 1e3,
|
|
421
|
+
job_id: id,
|
|
422
|
+
output: `Started background job ${id}. Use job_output with job_id "${id}" to read output.`
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const process = ctx.shell.start(ctx.shell.resolve({
|
|
426
|
+
...shellRequest,
|
|
427
|
+
signal: exec.signal
|
|
428
|
+
}));
|
|
372
429
|
try {
|
|
373
430
|
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
|
|
374
431
|
const output = readShellOutput(process.readOutput(), maxBytes);
|
|
@@ -1004,9 +1061,9 @@ function normalizeCodexPromptAssembly(assembly) {
|
|
|
1004
1061
|
function buildCodexSystemPrompt(config = {}) {
|
|
1005
1062
|
return config.collaborationPrompt === true ? `${CODEX_BASE_PROMPT}\n\n${CODEX_COLLABORATION_PROMPT}` : CODEX_BASE_PROMPT;
|
|
1006
1063
|
}
|
|
1007
|
-
const EXEC_COMMAND_DESCRIPTION = "Runs a command in a PTY, returning output
|
|
1064
|
+
const EXEC_COMMAND_DESCRIPTION = "Runs a command in a PTY, returning output, a session ID for ongoing interaction, or a background job ID when requested.";
|
|
1008
1065
|
const WRITE_STDIN_DESCRIPTION = "Writes characters to an existing unified exec session and returns recent output.";
|
|
1009
|
-
const APPLY_PATCH_DESCRIPTION = "Edits files using
|
|
1066
|
+
const APPLY_PATCH_DESCRIPTION = "Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.";
|
|
1010
1067
|
const UPDATE_PLAN_DESCRIPTION = "Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.";
|
|
1011
1068
|
const PLAN_STATUSES = [
|
|
1012
1069
|
"pending",
|
|
@@ -1064,7 +1121,7 @@ async function applyOnePatch(ctx, file, exec, policy) {
|
|
|
1064
1121
|
}
|
|
1065
1122
|
const sourceInfo = await observedTarget(ctx, target, exec);
|
|
1066
1123
|
const original = await ctx.fs.readText(target, exec.signal);
|
|
1067
|
-
const updated = file.kind === "delete" ? void 0 : applyPatchHunks(original, file.hunks);
|
|
1124
|
+
const updated = file.kind === "delete" ? void 0 : applyPatchHunks(original, file.hunks, file.path);
|
|
1068
1125
|
if (file.kind === "delete") {
|
|
1069
1126
|
await deletePatchedFile(ctx, target, sourceInfo.version, exec, policy);
|
|
1070
1127
|
return {
|
|
@@ -1152,6 +1209,10 @@ function registerExecTools(ctx, config) {
|
|
|
1152
1209
|
type: "boolean",
|
|
1153
1210
|
description: "True allocates a PTY for the command; false or omitted uses plain pipes."
|
|
1154
1211
|
},
|
|
1212
|
+
run_in_background: {
|
|
1213
|
+
type: "boolean",
|
|
1214
|
+
description: "Run as a DSH background job and return its job id immediately; collect output with job_output and stop it with job_kill. Only supported for pipe-backed commands."
|
|
1215
|
+
},
|
|
1155
1216
|
yield_time_ms: {
|
|
1156
1217
|
type: "number",
|
|
1157
1218
|
description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms."
|
|
@@ -1181,6 +1242,7 @@ function registerExecTools(ctx, config) {
|
|
|
1181
1242
|
},
|
|
1182
1243
|
exit_code: { type: "number" },
|
|
1183
1244
|
session_id: { type: "number" },
|
|
1245
|
+
job_id: { type: "string" },
|
|
1184
1246
|
original_token_count: { type: "number" },
|
|
1185
1247
|
output: {
|
|
1186
1248
|
type: "string",
|
|
@@ -35,7 +35,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
35
35
|
type ActivityProps = PropsRuntime<'conversation.composer.dock'> & PropsLocale<'codex'>;
|
|
36
36
|
export declare function ActivityLine({ sessionId, useSession, useSessions, useProjection, t }: ActivityProps): import("react").JSX.Element | null;
|
|
37
37
|
export declare const inject: string[];
|
|
38
|
-
/**
|
|
38
|
+
/** Select a preferred slot when the host declares it, otherwise use a stable legacy composer seat. */
|
|
39
|
+
export declare function chooseDeclaredSlot(preferred: string, fallback: string, isDeclared: (slot: string) => boolean): string;
|
|
40
|
+
/** Mount Codex controls in the newest named seats, with legacy composer fallbacks. */
|
|
39
41
|
export declare function apply(ctx: Context): void;
|
|
40
42
|
declare const _default: {
|
|
41
43
|
inject: string[];
|
|
@@ -5,6 +5,11 @@ import { CODEX_CONTEXT_MAX, CODEX_CONTEXT_UNIT, CODEX_PRESET_ID } from "../conte
|
|
|
5
5
|
import { awaitingModelStartedAt } from "./activity.js";
|
|
6
6
|
const NS = 'codex';
|
|
7
7
|
const SETTINGS_NAMESPACE = 'codex';
|
|
8
|
+
const MODEL_SETTINGS_SLOT = 'conversation.input.model.settings';
|
|
9
|
+
const CONTEXT_SETTINGS_SLOT = 'conversation.input.context.settings';
|
|
10
|
+
const FAST_FALLBACK_SLOT = 'conversation.input.right';
|
|
11
|
+
const CONTEXT_FALLBACK_SLOT = 'conversation.input.dock';
|
|
12
|
+
const DEFAULT_CONTEXT_WINDOW = 262_144;
|
|
8
13
|
const en = {
|
|
9
14
|
fast: 'Fast mode',
|
|
10
15
|
fastOn: 'Fast mode on (priority tier)',
|
|
@@ -55,6 +60,33 @@ function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t })
|
|
|
55
60
|
textAlign: 'left',
|
|
56
61
|
}, children: [_jsx("span", { children: t('fast') }), _jsx("span", { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 }, children: fast ? t('fastStateOn') : t('fastStateOff') })] }));
|
|
57
62
|
}
|
|
63
|
+
/** The legacy composer seat is a compact inline row, not a menu panel. */
|
|
64
|
+
function FastModeFallback(props) {
|
|
65
|
+
const { sessionId, useSessions, useSettings, setSetting, t } = props;
|
|
66
|
+
const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
|
|
67
|
+
const snapshot = useSettings(state => state);
|
|
68
|
+
const fast = snapshot.value?.fast ?? false;
|
|
69
|
+
if (agentPreset !== CODEX_PRESET_ID)
|
|
70
|
+
return null;
|
|
71
|
+
return (_jsxs("button", { type: "button", role: "switch", disabled: snapshot.writable === false, "aria-checked": fast, "aria-label": fast ? t('fastOn') : t('fastOff'), title: fast ? t('fastOn') : t('fastOff'), onClick: () => { void setSetting('fast', !fast); }, style: {
|
|
72
|
+
boxSizing: 'border-box',
|
|
73
|
+
display: 'inline-flex',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
gap: 5,
|
|
76
|
+
height: 28,
|
|
77
|
+
border: '1px solid var(--dsw-alias-border-primary)',
|
|
78
|
+
borderRadius: 7,
|
|
79
|
+
padding: '0 8px',
|
|
80
|
+
color: fast ? 'var(--dsw-static-blue-500)' : 'var(--dsw-alias-label-secondary)',
|
|
81
|
+
background: fast ? 'var(--dsw-alias-interactive-bg-selected)' : 'transparent',
|
|
82
|
+
cursor: snapshot.writable === false ? 'default' : 'pointer',
|
|
83
|
+
font: 'inherit',
|
|
84
|
+
fontSize: 12,
|
|
85
|
+
lineHeight: '20px',
|
|
86
|
+
fontWeight: fast ? 600 : 400,
|
|
87
|
+
whiteSpace: 'nowrap',
|
|
88
|
+
}, children: [_jsx("span", { children: t('fast') }), _jsx("span", { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 }, children: fast ? t('fastStateOn') : t('fastStateOff') })] }));
|
|
89
|
+
}
|
|
58
90
|
function ContextSizeControl({ contextWindow, useSettings, setSetting, unsetSetting, t }) {
|
|
59
91
|
const snapshot = useSettings(state => state);
|
|
60
92
|
const configured = snapshot.value?.contextWindow;
|
|
@@ -90,6 +122,15 @@ function ContextSizeControl({ contextWindow, useSettings, setSetting, unsetSetti
|
|
|
90
122
|
fontSize: 11,
|
|
91
123
|
}, children: t('contextRestore') }))] }));
|
|
92
124
|
}
|
|
125
|
+
function contextWindowFromProjection(useProjection) {
|
|
126
|
+
const pressure = useProjection('contextPressure');
|
|
127
|
+
return pressure?.contextWindow !== undefined && pressure.contextWindow > 0
|
|
128
|
+
? pressure.contextWindow
|
|
129
|
+
: DEFAULT_CONTEXT_WINDOW;
|
|
130
|
+
}
|
|
131
|
+
function LegacyContextSizeControl({ useProjection, ...props }) {
|
|
132
|
+
return _jsx(ContextSizeControl, { ...props, contextWindow: contextWindowFromProjection(useProjection) });
|
|
133
|
+
}
|
|
93
134
|
function elapsedSeconds(startedAt, now) {
|
|
94
135
|
return Math.max(0, Math.floor((now - startedAt) / 1_000));
|
|
95
136
|
}
|
|
@@ -138,7 +179,15 @@ export function ActivityLine({ sessionId, useSession, useSessions, useProjection
|
|
|
138
179
|
} }), _jsx("span", { children: label }), _jsxs("span", { style: { color: 'var(--dsw-alias-label-tertiary)' }, children: ["\u00B7 ", elapsedSeconds(activity.startedAt, now), "s"] })] }));
|
|
139
180
|
}
|
|
140
181
|
export const inject = ['slots', 'locale', 'settingsScope'];
|
|
141
|
-
/**
|
|
182
|
+
/** Select a preferred slot when the host declares it, otherwise use a stable legacy composer seat. */
|
|
183
|
+
export function chooseDeclaredSlot(preferred, fallback, isDeclared) {
|
|
184
|
+
return isDeclared(preferred) ? preferred : fallback;
|
|
185
|
+
}
|
|
186
|
+
function slotIsDeclared(ctx, slot) {
|
|
187
|
+
const registry = ctx.slots;
|
|
188
|
+
return typeof registry.spec === 'function' && registry.spec.call(ctx.slots, slot) !== undefined;
|
|
189
|
+
}
|
|
190
|
+
/** Mount Codex controls in the newest named seats, with legacy composer fallbacks. */
|
|
142
191
|
export function apply(ctx) {
|
|
143
192
|
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'codex client: dictionaries');
|
|
144
193
|
const settings = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
|
|
@@ -147,20 +196,44 @@ export function apply(ctx) {
|
|
|
147
196
|
setSetting: (field, value) => settings.set(field, value),
|
|
148
197
|
unsetSetting: (field) => settings.unset(field),
|
|
149
198
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
199
|
+
const fastSlot = chooseDeclaredSlot(MODEL_SETTINGS_SLOT, FAST_FALLBACK_SLOT, slot => slotIsDeclared(ctx, slot));
|
|
200
|
+
if (fastSlot === MODEL_SETTINGS_SLOT) {
|
|
201
|
+
ctx.slots.inject(MODEL_SETTINGS_SLOT, () => ctx.slots.register({
|
|
202
|
+
name: MODEL_SETTINGS_SLOT,
|
|
203
|
+
id: 'codex-fast',
|
|
204
|
+
order: 0,
|
|
205
|
+
locale: NS,
|
|
206
|
+
inject: injected,
|
|
207
|
+
}, FastModeButton));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
ctx.slots.inject(FAST_FALLBACK_SLOT, () => ctx.slots.register({
|
|
211
|
+
name: FAST_FALLBACK_SLOT,
|
|
212
|
+
id: 'codex-fast-fallback',
|
|
213
|
+
order: 0,
|
|
214
|
+
locale: NS,
|
|
215
|
+
inject: injected,
|
|
216
|
+
}, FastModeFallback));
|
|
217
|
+
}
|
|
218
|
+
const contextSlot = chooseDeclaredSlot(CONTEXT_SETTINGS_SLOT, CONTEXT_FALLBACK_SLOT, slot => slotIsDeclared(ctx, slot));
|
|
219
|
+
if (contextSlot === CONTEXT_SETTINGS_SLOT) {
|
|
220
|
+
ctx.slots.inject(CONTEXT_SETTINGS_SLOT, () => ctx.slots.register({
|
|
221
|
+
name: CONTEXT_SETTINGS_SLOT,
|
|
222
|
+
id: 'codex-context-size',
|
|
223
|
+
order: 0,
|
|
224
|
+
locale: NS,
|
|
225
|
+
inject: injected,
|
|
226
|
+
}, ContextSizeControl));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
ctx.slots.inject(CONTEXT_FALLBACK_SLOT, () => ctx.slots.register({
|
|
230
|
+
name: CONTEXT_FALLBACK_SLOT,
|
|
231
|
+
id: 'codex-context-size-fallback',
|
|
232
|
+
order: 0,
|
|
233
|
+
locale: NS,
|
|
234
|
+
inject: injected,
|
|
235
|
+
}, LegacyContextSizeControl));
|
|
236
|
+
}
|
|
164
237
|
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
165
238
|
name: 'conversation.composer.dock',
|
|
166
239
|
id: 'codex-activity',
|
package/lib/types/exec.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface ExecCommandArgs {
|
|
|
6
6
|
cmd: string;
|
|
7
7
|
workdir?: string;
|
|
8
8
|
tty?: boolean;
|
|
9
|
+
run_in_background?: boolean;
|
|
9
10
|
yield_time_ms?: number;
|
|
10
11
|
max_output_tokens?: number;
|
|
11
12
|
shell?: string;
|
|
@@ -24,6 +25,7 @@ export interface ExecResult {
|
|
|
24
25
|
wall_time_seconds: number;
|
|
25
26
|
output: string;
|
|
26
27
|
session_id?: number;
|
|
28
|
+
job_id?: string;
|
|
27
29
|
exit_code?: number;
|
|
28
30
|
original_token_count?: number;
|
|
29
31
|
}
|
package/lib/types/exec.js
CHANGED
|
@@ -56,6 +56,8 @@ export function renderExecResult(result) {
|
|
|
56
56
|
sections.push(`Process exited with code ${result.exit_code}`);
|
|
57
57
|
if (result.session_id !== undefined)
|
|
58
58
|
sections.push(`Process running with session ID ${result.session_id}`);
|
|
59
|
+
if (result.job_id !== undefined)
|
|
60
|
+
sections.push(`Background job ID: ${result.job_id}`);
|
|
59
61
|
if (result.original_token_count !== undefined)
|
|
60
62
|
sections.push(`Original token count: ${result.original_token_count}`);
|
|
61
63
|
sections.push('Output:', result.output);
|
|
@@ -80,6 +82,16 @@ function terminalResult(result, maxBytes, startedAt) {
|
|
|
80
82
|
} : {},
|
|
81
83
|
});
|
|
82
84
|
}
|
|
85
|
+
/** Map a detached shell process onto the generic DSH job outcome contract. */
|
|
86
|
+
function backgroundOutcome(process) {
|
|
87
|
+
if (process.status === 'killed') {
|
|
88
|
+
return {
|
|
89
|
+
status: 'killed',
|
|
90
|
+
detail: process.signal !== null ? `signal: ${process.signal}` : 'killed before exit',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { status: 'completed', detail: `exit code: ${process.exitCode ?? 0}` };
|
|
94
|
+
}
|
|
83
95
|
function sleep(ms, signal) {
|
|
84
96
|
return new Promise(resolve => {
|
|
85
97
|
let timer;
|
|
@@ -144,6 +156,9 @@ export async function runExecCommand(ctx, args, exec, config) {
|
|
|
144
156
|
const maxBytes = outputLimit(config.maxOutputBytes, args.max_output_tokens);
|
|
145
157
|
const workdir = sessionCwd(exec, args.workdir);
|
|
146
158
|
const startedAt = performance.now();
|
|
159
|
+
if (args.run_in_background === true && args.tty === true) {
|
|
160
|
+
throw new Error('run_in_background is only supported for pipe-backed commands; omit tty');
|
|
161
|
+
}
|
|
147
162
|
if (args.tty === true) {
|
|
148
163
|
const agent = exec.agent;
|
|
149
164
|
const terminals = ctx.get('terminals');
|
|
@@ -187,17 +202,53 @@ export async function runExecCommand(ctx, args, exec, config) {
|
|
|
187
202
|
}
|
|
188
203
|
const policy = ctx.get('sandboxPolicy')?.resolve(exec.agent === undefined ? {} : { session: exec.agent.session });
|
|
189
204
|
const dshEnv = ctx.get('shellEnv')?.collect(exec);
|
|
205
|
+
// Background jobs intentionally omit the tool-call signal. Once this call
|
|
206
|
+
// returns, the job lifetime belongs to the jobs registry, not the request.
|
|
190
207
|
const shellRequest = {
|
|
191
208
|
command: commandFor(args),
|
|
192
209
|
...args.shell === undefined ? {} : { shell: args.shell },
|
|
193
210
|
login: args.login ?? true,
|
|
194
211
|
...workdir === undefined ? {} : { workdir },
|
|
195
212
|
stdoutMaxBytes: maxBytes,
|
|
196
|
-
signal: exec.signal,
|
|
197
213
|
...dshEnv === undefined ? {} : { dshEnv },
|
|
198
214
|
...policy === undefined ? {} : { sandboxPolicy: policy },
|
|
199
215
|
};
|
|
200
|
-
|
|
216
|
+
if (args.run_in_background === true) {
|
|
217
|
+
const owner = exec.agent;
|
|
218
|
+
if (owner === undefined)
|
|
219
|
+
throw new Error('run_in_background requires an owning agent session');
|
|
220
|
+
const jobs = ctx.get('jobs');
|
|
221
|
+
if (jobs === undefined) {
|
|
222
|
+
throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs');
|
|
223
|
+
}
|
|
224
|
+
if (exec.signal.aborted) {
|
|
225
|
+
exec.signal.throwIfAborted();
|
|
226
|
+
throw new Error('tool call aborted');
|
|
227
|
+
}
|
|
228
|
+
const id = jobs.start({
|
|
229
|
+
kind: 'bash',
|
|
230
|
+
label: args.cmd,
|
|
231
|
+
owner,
|
|
232
|
+
outputLimitBytes: maxBytes,
|
|
233
|
+
run: () => {
|
|
234
|
+
const process = ctx.shell.start(ctx.shell.resolve(shellRequest));
|
|
235
|
+
return {
|
|
236
|
+
cancel: () => void process.kill(),
|
|
237
|
+
done: process.done.then(() => backgroundOutcome(process)),
|
|
238
|
+
readOutput: () => readShellOutput(process.readOutput(), maxBytes),
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
return withChunkId({
|
|
243
|
+
wall_time_seconds: (performance.now() - startedAt) / 1000,
|
|
244
|
+
job_id: id,
|
|
245
|
+
output: `Started background job ${id}. Use job_output with job_id "${id}" to read output.`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const process = ctx.shell.start(ctx.shell.resolve({
|
|
249
|
+
...shellRequest,
|
|
250
|
+
signal: exec.signal,
|
|
251
|
+
}));
|
|
201
252
|
try {
|
|
202
253
|
await waitForShell(process, normalizeWaitMs(args.yield_time_ms, config.defaultYieldTimeMs), exec.signal);
|
|
203
254
|
const output = readShellOutput(process.readOutput(), maxBytes);
|
package/lib/types/index.js
CHANGED
|
@@ -194,9 +194,9 @@ export function buildCodexSystemPrompt(config = {}) {
|
|
|
194
194
|
? `${CODEX_BASE_PROMPT}\n\n${CODEX_COLLABORATION_PROMPT}`
|
|
195
195
|
: CODEX_BASE_PROMPT;
|
|
196
196
|
}
|
|
197
|
-
const EXEC_COMMAND_DESCRIPTION = 'Runs a command in a PTY, returning output
|
|
197
|
+
const EXEC_COMMAND_DESCRIPTION = 'Runs a command in a PTY, returning output, a session ID for ongoing interaction, or a background job ID when requested.';
|
|
198
198
|
const WRITE_STDIN_DESCRIPTION = 'Writes characters to an existing unified exec session and returns recent output.';
|
|
199
|
-
const APPLY_PATCH_DESCRIPTION = 'Edits files using
|
|
199
|
+
const APPLY_PATCH_DESCRIPTION = 'Edits files using Codex patch syntax with Begin/End Patch markers and file update directives. In hunk lines, the first character is the operation marker; repeat a source-leading marker when the source line itself starts with one.';
|
|
200
200
|
const UPDATE_PLAN_DESCRIPTION = 'Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.';
|
|
201
201
|
const PLAN_STATUSES = ['pending', 'in_progress', 'completed'];
|
|
202
202
|
function sessionCwd(exec) {
|
|
@@ -245,7 +245,7 @@ async function applyOnePatch(ctx, file, exec, policy) {
|
|
|
245
245
|
}
|
|
246
246
|
const sourceInfo = await observedTarget(ctx, target, exec);
|
|
247
247
|
const original = await ctx.fs.readText(target, exec.signal);
|
|
248
|
-
const updated = file.kind === 'delete' ? undefined : applyPatchHunks(original, file.hunks);
|
|
248
|
+
const updated = file.kind === 'delete' ? undefined : applyPatchHunks(original, file.hunks, file.path);
|
|
249
249
|
if (file.kind === 'delete') {
|
|
250
250
|
await deletePatchedFile(ctx, target, sourceInfo.version, exec, policy);
|
|
251
251
|
return { path: file.path, operation: 'deleted' };
|
|
@@ -306,6 +306,7 @@ function registerExecTools(ctx, config) {
|
|
|
306
306
|
cmd: { type: 'string', required: true, description: 'Shell command to execute.' },
|
|
307
307
|
workdir: { type: 'string', description: 'Working directory for the command. Defaults to the turn cwd.' },
|
|
308
308
|
tty: { type: 'boolean', description: 'True allocates a PTY for the command; false or omitted uses plain pipes.' },
|
|
309
|
+
run_in_background: { type: 'boolean', description: 'Run as a DSH background job and return its job id immediately; collect output with job_output and stop it with job_kill. Only supported for pipe-backed commands.' },
|
|
309
310
|
yield_time_ms: { type: 'number', description: 'Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms.' },
|
|
310
311
|
max_output_tokens: { type: 'number', description: 'Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy.' },
|
|
311
312
|
shell: { type: 'string', description: "Shell binary to launch. Defaults to the user's default shell." },
|
|
@@ -320,6 +321,7 @@ function registerExecTools(ctx, config) {
|
|
|
320
321
|
wall_time_seconds: { type: 'number', required: true },
|
|
321
322
|
exit_code: { type: 'number' },
|
|
322
323
|
session_id: { type: 'number' },
|
|
324
|
+
job_id: { type: 'string' },
|
|
323
325
|
original_token_count: { type: 'number' },
|
|
324
326
|
output: { type: 'string', required: true },
|
|
325
327
|
},
|
package/lib/types/patch.d.ts
CHANGED
|
@@ -32,5 +32,5 @@ export type PatchFile = {
|
|
|
32
32
|
/** Parse one complete Codex patch after normalizing CRLF input to LF. */
|
|
33
33
|
export declare function parsePatch(input: string): PatchFile[];
|
|
34
34
|
/** Apply parsed update hunks and return LF-normalized text. */
|
|
35
|
-
export declare function applyPatchHunks(original: string, hunks: readonly PatchHunk[]): string;
|
|
35
|
+
export declare function applyPatchHunks(original: string, hunks: readonly PatchHunk[], label?: string): string;
|
|
36
36
|
//# sourceMappingURL=patch.d.ts.map
|
package/lib/types/patch.js
CHANGED
|
@@ -188,16 +188,39 @@ function findSequence(lines, expected, from, endOfFile) {
|
|
|
188
188
|
}
|
|
189
189
|
return -1;
|
|
190
190
|
}
|
|
191
|
+
function displayPatchLine(line) {
|
|
192
|
+
if (line.length === 0)
|
|
193
|
+
return '<empty>';
|
|
194
|
+
return line.replaceAll(' ', '[space]').replaceAll('\t', '[tab]');
|
|
195
|
+
}
|
|
196
|
+
function formatPatchExcerpt(lines, start, count) {
|
|
197
|
+
if (lines.length === 0)
|
|
198
|
+
return ' <empty file>';
|
|
199
|
+
const first = Math.max(0, Math.min(start, lines.length - 1));
|
|
200
|
+
return lines.slice(first, first + count)
|
|
201
|
+
.map((line, offset) => ` ${first + offset + 1}: ${displayPatchLine(line)}`)
|
|
202
|
+
.join('\n');
|
|
203
|
+
}
|
|
204
|
+
function mismatchMessage(label, hunkIndex, expected, actual, from, endOfFile) {
|
|
205
|
+
const expectedPreview = expected.slice(0, 12)
|
|
206
|
+
.map((line, index) => ` ${index + 1}: ${displayPatchLine(line)}`)
|
|
207
|
+
.join('\n');
|
|
208
|
+
const expectedSuffix = expected.length > 12 ? '\n ...' : '';
|
|
209
|
+
const excerptStart = endOfFile ? Math.max(0, actual.length - 6) : Math.max(0, from - 2);
|
|
210
|
+
const markerHint = expected.some(wanted => wanted.length > 0 && actual.includes(`-${wanted}`))
|
|
211
|
+
? '\nHint: if a source line starts with a patch marker, repeat that marker after the operation marker; for example, `-- item` deletes the source line `- item`.'
|
|
212
|
+
: '';
|
|
213
|
+
return `could not find expected lines in ${label} hunk ${hunkIndex + 1} (search starts at line ${from + 1})\nExpected:\n${expectedPreview}${expectedSuffix}\nFile excerpt:\n${formatPatchExcerpt(actual, excerptStart, 10)}${markerHint}`;
|
|
214
|
+
}
|
|
191
215
|
/** Apply parsed update hunks and return LF-normalized text. */
|
|
192
|
-
export function applyPatchHunks(original, hunks) {
|
|
216
|
+
export function applyPatchHunks(original, hunks, label = 'file') {
|
|
193
217
|
const value = splitText(original.replaceAll('\r\n', '\n'));
|
|
194
218
|
let cursor = 0;
|
|
195
|
-
for (const hunk of hunks) {
|
|
219
|
+
for (const [hunkIndex, hunk] of hunks.entries()) {
|
|
196
220
|
const expected = hunk.lines.filter(line => line.kind !== 'add').map(line => line.text);
|
|
197
221
|
const start = findSequence(value.lines, expected, cursor, hunk.endOfFile);
|
|
198
222
|
if (start < 0) {
|
|
199
|
-
|
|
200
|
-
invalid(`could not find expected lines${detail.length === 0 ? '' : `:\n${detail}`}`);
|
|
223
|
+
invalid(mismatchMessage(label, hunkIndex, expected, value.lines, cursor, hunk.endOfFile));
|
|
201
224
|
}
|
|
202
225
|
const replacement = hunk.lines.filter(line => line.kind !== 'delete').map(line => line.text);
|
|
203
226
|
value.lines.splice(start, expected.length, ...replacement);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shuind/dsh-codex-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "A Codex harness with a streamlined system prompt for GPT models that do not fit DSH's native interface, while remaining compatible with the DSH plugin ecosystem.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -69,8 +69,7 @@
|
|
|
69
69
|
"@deepseek-ai/dsh-client-locale",
|
|
70
70
|
"@deepseek-ai/dsh-client-runtime",
|
|
71
71
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
72
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
73
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
74
73
|
],
|
|
75
74
|
"platform": "web"
|
|
76
75
|
}
|
|
@@ -90,6 +89,7 @@
|
|
|
90
89
|
"@deepseek-ai/dsh-credentials": ">=0.1.0-rc.8",
|
|
91
90
|
"@deepseek-ai/dsh-fs": ">=0.0.1-rc.1",
|
|
92
91
|
"@deepseek-ai/dsh-invariants": ">=0.0.1-rc.1",
|
|
92
|
+
"@deepseek-ai/dsh-jobs": ">=0.1.0-rc.8",
|
|
93
93
|
"@deepseek-ai/dsh-llm": ">=0.1.0-rc.8",
|
|
94
94
|
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.8",
|
|
95
95
|
"@deepseek-ai/dsh-sandbox": ">=0.0.1-rc.1",
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"@deepseek-ai/dsh-tool-web": ">=0.1.0-rc.8",
|
|
101
101
|
"@deepseek-ai/dsh-tool-todo": ">=0.0.1-rc.1",
|
|
102
102
|
"@deepseek-ai/dsh-terminal": ">=0.0.1-rc.3",
|
|
103
|
+
"@deepseek-ai/dsh-tool-jobs": ">=0.1.0-rc.8",
|
|
103
104
|
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1"
|
|
104
105
|
},
|
|
105
106
|
"peerDependenciesMeta": {
|
|
@@ -133,6 +134,9 @@
|
|
|
133
134
|
"@deepseek-ai/dsh-invariants": {
|
|
134
135
|
"optional": true
|
|
135
136
|
},
|
|
137
|
+
"@deepseek-ai/dsh-jobs": {
|
|
138
|
+
"optional": true
|
|
139
|
+
},
|
|
136
140
|
"@deepseek-ai/dsh-llm": {
|
|
137
141
|
"optional": true
|
|
138
142
|
},
|
|
@@ -163,6 +167,9 @@
|
|
|
163
167
|
"@deepseek-ai/dsh-terminal": {
|
|
164
168
|
"optional": true
|
|
165
169
|
},
|
|
170
|
+
"@deepseek-ai/dsh-tool-jobs": {
|
|
171
|
+
"optional": true
|
|
172
|
+
},
|
|
166
173
|
"@deepseek-ai/dsh-tools": {
|
|
167
174
|
"optional": true
|
|
168
175
|
}
|
|
@@ -178,6 +185,7 @@
|
|
|
178
185
|
"@deepseek-ai/dsh-credentials": "0.1.0-rc.8",
|
|
179
186
|
"@deepseek-ai/dsh-fs": "0.1.0-rc.6",
|
|
180
187
|
"@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
|
|
188
|
+
"@deepseek-ai/dsh-jobs": "0.1.0-rc.8",
|
|
181
189
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
182
190
|
"@deepseek-ai/dsh-settings": "0.1.0-rc.8",
|
|
183
191
|
"@deepseek-ai/dsh-sandbox": "0.1.0-rc.6",
|
|
@@ -188,6 +196,7 @@
|
|
|
188
196
|
"@deepseek-ai/dsh-tool-web": "0.1.0-rc.8",
|
|
189
197
|
"@deepseek-ai/dsh-tool-todo": "0.1.0-rc.6",
|
|
190
198
|
"@deepseek-ai/dsh-terminal": "0.1.0-rc.6",
|
|
199
|
+
"@deepseek-ai/dsh-tool-jobs": "0.1.0-rc.8",
|
|
191
200
|
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
192
201
|
"@types/node": "^22.20.0",
|
|
193
202
|
"@types/react": "~18.3.1",
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
config:
|
|
22
22
|
collaborationPrompt: false
|
|
23
23
|
|
|
24
|
+
# Background jobs use the host-plane registry. A busy agent receives a
|
|
25
|
+
# completion in its next step; an idle agent keeps it pending until the next
|
|
26
|
+
# user/tool wake, so finishing work never opens an unsolicited model turn.
|
|
27
|
+
- id: codex-jobs
|
|
28
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
29
|
+
config:
|
|
30
|
+
completionDelivery: quiet
|
|
31
|
+
|
|
24
32
|
# The generic DSH pi-ai adapter still owns the user's configured provider,
|
|
25
33
|
# endpoint, API key, and model. Codex's remote-first transport wrapper is
|
|
26
34
|
# installed only in this Codex scope and falls back to the generic DSH path.
|
package/presets/codex/preset.yml
CHANGED