@shuind/dsh-codex-harness 0.1.19 → 0.1.21

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 CHANGED
@@ -1,13 +1,14 @@
1
1
  # @shuind/dsh-codex-harness
2
2
 
3
- 在 DSH 中提供 Codex preset:引入 Codex 风格的系统提示词与工具契约,支持图片输入、可选思考强度、Fast 优先级请求、OpenAI Responses 网页搜索与远程压缩,以及在 Web 设置上下文容量;旨在给来源于Codex的token提供适配harness,并兼容享受DSH生态。
3
+ 在 DSH 中提供 Codex preset:使用精简版 Codex 系统提示词与工具契约,支持图片输入、可选思考强度、Fast 优先级请求、OpenAI Responses 网页搜索与远程压缩,以及在 Web 设置上下文容量;旨在给来源于 Codex token 提供适配 harness,并兼容 DSH 生态。
4
4
 
5
5
  ## 功能
6
6
 
7
- - **Codex coding-agent 提示词**:提供 Codex 风格的系统提示词、工具契约和编码工作流。
8
- - **Codex 工具**:`exec_command`、`write_stdin`、`apply_patch`、`update_plan`。
7
+ - **精简 Codex coding-agent 提示词**:使用精简版系统提示词;身份、模型和工作目录由 `dsh-persona` 提供,避免重复的 Harness 身份说明,同时保留必要的工具契约和编码工作流。
8
+ - **Codex 工具**:`exec_command`、`write_stdin`、`apply_patch`、`update_plan`;`apply_patch` 匹配失败时会显示文件、hunk、行号、可见空白和附近文件内容,便于修正上下文。
9
9
  - **GPT 能力补全**:为 GPT 系列模型补充图片输入和思考强度选项;不会覆盖用户已有的显式配置。
10
10
  - **Fast**:仅在 Codex preset 的模型选择菜单中显示,开启后向 Responses 请求发送 `service_tier: "priority"`。
11
+ - **旧版 Web 兼容**:在未提供新版模型设置/上下文设置 slot 的 DSH Web 中,Fast 和上下文容量会自动回退到旧版 composer slot;client 注入不依赖纯类型 slot 包。
11
12
  - **远程搜索与压缩**:OpenAI Responses 请求默认优先使用 hosted `web_search` 和 `/responses/compact`;失败时回退到 DSH 的本地实现。
12
13
  - **上下文容量**:在 Web 中设置 `1K`–`1000K` tokens,设置值作用于下一次请求。
13
14
  - **活动状态**:Codex 等待模型响应或进行上下文压缩时,在输入框上方显示对应状态和耗时,让长时间 Deep Diving 不再像卡住。
@@ -15,7 +16,7 @@
15
16
  ## 安装
16
17
 
17
18
  ```sh
18
- dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.19
19
+ dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.21
19
20
  ```
20
21
 
21
22
  重启 Web,创建新会话,在模式菜单中选择 **Codex 模式**。
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
- /** Mount Fast inside the model selector and context size inside the meter panel. */
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("conversation.input.model.settings", () => ctx.slots.register({
291
- name: "conversation.input.model.settings",
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("conversation.input.context.settings", () => ctx.slots.register({
298
- name: "conversation.input.context.settings",
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;
@@ -949,9 +962,7 @@ function applyCodexRequestSettings(request, settings) {
949
962
  ...settings.fast ? { serviceTier: "priority" } : {}
950
963
  };
951
964
  }
952
- const CODEX_BASE_PROMPT = String.raw`You are Codex, based on {{model}}. You are running as a coding agent in dsh Web on a user's computer.
953
-
954
- ## General
965
+ const CODEX_BASE_PROMPT = String.raw`## General
955
966
 
956
967
  - When searching for text or files, prefer using rg or rg --files respectively because rg is much faster than alternatives like grep. If rg is not available, use the next best alternative.
957
968
 
@@ -959,7 +970,6 @@ const CODEX_BASE_PROMPT = String.raw`You are Codex, based on {{model}}. You are
959
970
 
960
971
  - Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
961
972
  - Add succinct code comments that explain non-obvious code. Do not add comments that merely narrate assignments or control flow.
962
- - Use apply_patch for single-file edits when practical. The apply_patch tool accepts its freeform patch language; do not wrap that patch in JSON.
963
973
  - You may be in a dirty git worktree. Never revert existing changes you did not make unless the user explicitly requests it. If unrelated files are changed, leave them alone.
964
974
 
965
975
  ## Planning
@@ -970,16 +980,10 @@ const CODEX_BASE_PROMPT = String.raw`You are Codex, based on {{model}}. You are
970
980
  ## dsh session
971
981
 
972
982
  - The user and you share one workspace. Inspect the repository and every applicable AGENTS.md before editing.
973
- - This session's preset was selected when the session was created and stays fixed for its lifetime. Do not attempt to switch the preset or replace its tool catalog while the session is running.
974
- - dsh provides the execution, filesystem, session, policy, and Skills capabilities behind these tools. Use those extension points as supplied; do not invent a second harness or bypass the filesystem service for file edits.
975
- - The core Codex tool names, arguments, and result formats are fixed: use exec_command for terminal work, write_stdin for an existing interactive command, apply_patch for file changes, and update_plan for multi-step tasks.
976
983
 
977
984
  ## Task execution
978
985
 
979
986
  - Keep the user informed with concise progress updates and lead with the result.
980
- - Prefer existing functions and extension points over new machinery.
981
- - Do not claim that a command, edit, or test succeeded unless it actually succeeded.
982
- - Use the exact tool names and argument formats supplied by this session; do not invent replacement editing tools.
983
987
 
984
988
  ## Presenting your work
985
989
 
@@ -996,13 +1000,26 @@ const CODEX_COLLABORATION_PROMPT = String.raw`## Collaboration
996
1000
  - Solve problems by thinking from first principles and at a higher level.
997
1001
  - Make things as effortless as possible for the user.
998
1002
  `;
999
- /** Build the Codex system prompt with optional, user-enabled collaboration guidance. */
1003
+ /** Put the agent persona first and omit the generic Harness identity opener. */
1004
+ function normalizeCodexPromptAssembly(assembly) {
1005
+ const sections = assembly.sections.filter((section) => section.name !== "harness:identity");
1006
+ const persona = sections.find((section) => section.name === "deployment:persona");
1007
+ if (persona === void 0) return {
1008
+ ...assembly,
1009
+ sections
1010
+ };
1011
+ return {
1012
+ ...assembly,
1013
+ sections: [persona, ...sections.filter((section) => section.name !== "deployment:persona")]
1014
+ };
1015
+ }
1016
+ /** Build the Codex operating prompt with optional, user-enabled collaboration guidance. */
1000
1017
  function buildCodexSystemPrompt(config = {}) {
1001
1018
  return config.collaborationPrompt === true ? `${CODEX_BASE_PROMPT}\n\n${CODEX_COLLABORATION_PROMPT}` : CODEX_BASE_PROMPT;
1002
1019
  }
1003
1020
  const EXEC_COMMAND_DESCRIPTION = "Runs a command in a PTY, returning output or a session ID for ongoing interaction.";
1004
1021
  const WRITE_STDIN_DESCRIPTION = "Writes characters to an existing unified exec session and returns recent output.";
1005
- const APPLY_PATCH_DESCRIPTION = "The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.";
1022
+ 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.";
1006
1023
  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.";
1007
1024
  const PLAN_STATUSES = [
1008
1025
  "pending",
@@ -1060,7 +1077,7 @@ async function applyOnePatch(ctx, file, exec, policy) {
1060
1077
  }
1061
1078
  const sourceInfo = await observedTarget(ctx, target, exec);
1062
1079
  const original = await ctx.fs.readText(target, exec.signal);
1063
- const updated = file.kind === "delete" ? void 0 : applyPatchHunks(original, file.hunks);
1080
+ const updated = file.kind === "delete" ? void 0 : applyPatchHunks(original, file.hunks, file.path);
1064
1081
  if (file.kind === "delete") {
1065
1082
  await deletePatchedFile(ctx, target, sourceInfo.version, exec, policy);
1066
1083
  return {
@@ -1388,6 +1405,7 @@ function apply(ctx, config = {}) {
1388
1405
  if ((resolved.hostedWebSearch || resolved.remoteCompact) && options.purpose === void 0 && isGptModel(options.model)) return hostedWebSearchStream(next);
1389
1406
  return next();
1390
1407
  }));
1408
+ ctx.on("system-prompt/assemble", async (_assembly, _context, next) => normalizeCodexPromptAssembly(await next()));
1391
1409
  ctx.systemPrompt.section({
1392
1410
  name: "codex:base",
1393
1411
  order: 10,
@@ -1404,4 +1422,4 @@ var types_default = {
1404
1422
  apply
1405
1423
  };
1406
1424
  //#endregion
1407
- export { CODEX_SETTINGS_NAMESPACE, CODEX_SETTINGS_SCHEMA, Config, apply, applyCodexRequestSettings, buildCodexSystemPrompt, types_default as default, enrichCodexModel, inject, name };
1425
+ export { CODEX_SETTINGS_NAMESPACE, CODEX_SETTINGS_SCHEMA, Config, apply, applyCodexRequestSettings, buildCodexSystemPrompt, types_default as default, enrichCodexModel, inject, name, normalizeCodexPromptAssembly };
@@ -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
- /** Mount Fast inside the model selector and context size inside the meter panel. */
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
- /** Mount Fast inside the model selector and context size inside the meter panel. */
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
- ctx.slots.inject('conversation.input.model.settings', () => ctx.slots.register({
151
- name: 'conversation.input.model.settings',
152
- id: 'codex-fast',
153
- order: 0,
154
- locale: NS,
155
- inject: injected,
156
- }, FastModeButton));
157
- ctx.slots.inject('conversation.input.context.settings', () => ctx.slots.register({
158
- name: 'conversation.input.context.settings',
159
- id: 'codex-context-size',
160
- order: 0,
161
- locale: NS,
162
- inject: injected,
163
- }, ContextSizeControl));
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',
@@ -2,6 +2,7 @@
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import z from '@deepseek-ai/schemastery';
4
4
  import type { LlmCallConfig } from '@deepseek-ai/dsh-llm';
5
+ import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt';
5
6
  declare module '@deepseek-ai/dsh-llm' {
6
7
  interface LlmCallConfig {
7
8
  /** Codex request context capacity override, in tokens. */
@@ -50,7 +51,9 @@ export interface CodexModelProfile {
50
51
  export declare function enrichCodexModel(model: CodexModelProfile): CodexModelProfile;
51
52
  /** Apply the live Codex controls to one agent request without leaking them to other routes. */
52
53
  export declare function applyCodexRequestSettings(request: LlmCallConfig, settings: CodexSettings): LlmCallConfig;
53
- /** Build the Codex system prompt with optional, user-enabled collaboration guidance. */
54
+ /** Put the agent persona first and omit the generic Harness identity opener. */
55
+ export declare function normalizeCodexPromptAssembly(assembly: PromptAssembly): PromptAssembly;
56
+ /** Build the Codex operating prompt with optional, user-enabled collaboration guidance. */
54
57
  export declare function buildCodexSystemPrompt(config?: Pick<Config, 'collaborationPrompt'>): string;
55
58
  /** Mount the Codex prompt/tool layer inside one fixed agent preset. */
56
59
  export declare function apply(ctx: Context, config?: Config): void;
@@ -138,9 +138,8 @@ export function applyCodexRequestSettings(request, settings) {
138
138
  ...settings.fast ? { serviceTier: 'priority' } : {},
139
139
  };
140
140
  }
141
- const CODEX_BASE_PROMPT = String.raw `You are Codex, based on {{model}}. You are running as a coding agent in dsh Web on a user's computer.
142
-
143
- ## General
141
+ // The shipped persona owns the single Codex identity line, model, and working directory.
142
+ const CODEX_BASE_PROMPT = String.raw `## General
144
143
 
145
144
  - When searching for text or files, prefer using rg or rg --files respectively because rg is much faster than alternatives like grep. If rg is not available, use the next best alternative.
146
145
 
@@ -148,7 +147,6 @@ const CODEX_BASE_PROMPT = String.raw `You are Codex, based on {{model}}. You are
148
147
 
149
148
  - Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
150
149
  - Add succinct code comments that explain non-obvious code. Do not add comments that merely narrate assignments or control flow.
151
- - Use apply_patch for single-file edits when practical. The apply_patch tool accepts its freeform patch language; do not wrap that patch in JSON.
152
150
  - You may be in a dirty git worktree. Never revert existing changes you did not make unless the user explicitly requests it. If unrelated files are changed, leave them alone.
153
151
 
154
152
  ## Planning
@@ -159,16 +157,10 @@ const CODEX_BASE_PROMPT = String.raw `You are Codex, based on {{model}}. You are
159
157
  ## dsh session
160
158
 
161
159
  - The user and you share one workspace. Inspect the repository and every applicable AGENTS.md before editing.
162
- - This session's preset was selected when the session was created and stays fixed for its lifetime. Do not attempt to switch the preset or replace its tool catalog while the session is running.
163
- - dsh provides the execution, filesystem, session, policy, and Skills capabilities behind these tools. Use those extension points as supplied; do not invent a second harness or bypass the filesystem service for file edits.
164
- - The core Codex tool names, arguments, and result formats are fixed: use exec_command for terminal work, write_stdin for an existing interactive command, apply_patch for file changes, and update_plan for multi-step tasks.
165
160
 
166
161
  ## Task execution
167
162
 
168
163
  - Keep the user informed with concise progress updates and lead with the result.
169
- - Prefer existing functions and extension points over new machinery.
170
- - Do not claim that a command, edit, or test succeeded unless it actually succeeded.
171
- - Use the exact tool names and argument formats supplied by this session; do not invent replacement editing tools.
172
164
 
173
165
  ## Presenting your work
174
166
 
@@ -185,7 +177,18 @@ const CODEX_COLLABORATION_PROMPT = String.raw `## Collaboration
185
177
  - Solve problems by thinking from first principles and at a higher level.
186
178
  - Make things as effortless as possible for the user.
187
179
  `;
188
- /** Build the Codex system prompt with optional, user-enabled collaboration guidance. */
180
+ /** Put the agent persona first and omit the generic Harness identity opener. */
181
+ export function normalizeCodexPromptAssembly(assembly) {
182
+ const sections = assembly.sections.filter(section => section.name !== 'harness:identity');
183
+ const persona = sections.find(section => section.name === 'deployment:persona');
184
+ if (persona === undefined)
185
+ return { ...assembly, sections };
186
+ return {
187
+ ...assembly,
188
+ sections: [persona, ...sections.filter(section => section.name !== 'deployment:persona')],
189
+ };
190
+ }
191
+ /** Build the Codex operating prompt with optional, user-enabled collaboration guidance. */
189
192
  export function buildCodexSystemPrompt(config = {}) {
190
193
  return config.collaborationPrompt === true
191
194
  ? `${CODEX_BASE_PROMPT}\n\n${CODEX_COLLABORATION_PROMPT}`
@@ -193,7 +196,7 @@ export function buildCodexSystemPrompt(config = {}) {
193
196
  }
194
197
  const EXEC_COMMAND_DESCRIPTION = 'Runs a command in a PTY, returning output or a session ID for ongoing interaction.';
195
198
  const WRITE_STDIN_DESCRIPTION = 'Writes characters to an existing unified exec session and returns recent output.';
196
- const APPLY_PATCH_DESCRIPTION = 'The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.';
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.';
197
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.';
198
201
  const PLAN_STATUSES = ['pending', 'in_progress', 'completed'];
199
202
  function sessionCwd(exec) {
@@ -242,7 +245,7 @@ async function applyOnePatch(ctx, file, exec, policy) {
242
245
  }
243
246
  const sourceInfo = await observedTarget(ctx, target, exec);
244
247
  const original = await ctx.fs.readText(target, exec.signal);
245
- const updated = file.kind === 'delete' ? undefined : applyPatchHunks(original, file.hunks);
248
+ const updated = file.kind === 'delete' ? undefined : applyPatchHunks(original, file.hunks, file.path);
246
249
  if (file.kind === 'delete') {
247
250
  await deletePatchedFile(ctx, target, sourceInfo.version, exec, policy);
248
251
  return { path: file.path, operation: 'deleted' };
@@ -485,6 +488,7 @@ export function apply(ctx, config = {}) {
485
488
  return next();
486
489
  }));
487
490
  }
491
+ ctx.on('system-prompt/assemble', async (_assembly, _context, next) => normalizeCodexPromptAssembly(await next()));
488
492
  ctx.systemPrompt.section({ name: 'codex:base', order: 10, text: buildCodexSystemPrompt(resolved) });
489
493
  registerExecTools(ctx, resolved);
490
494
  registerPatchTool(ctx);
@@ -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
@@ -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
- const detail = expected.join('\n');
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@shuind/dsh-codex-harness",
3
- "version": "0.1.19",
4
- "description": "A minimal Codex harness for GPT models that do not fit DSH's native interface, while remaining compatible with the DSH plugin ecosystem.",
3
+ "version": "0.1.21",
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",
7
7
  "types": "lib/types/index.d.ts",
@@ -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
  }
@@ -4,6 +4,7 @@
4
4
  - id: persona
5
5
  name: '@deepseek-ai/dsh-persona'
6
6
  config:
7
+ # Keep identity, model, cwd, and runtime context in one persona section.
7
8
  text: >-
8
9
  You are Codex, a coding agent based on the {{model}} model. Your working directory is {{cwd}}.
9
10
  includeRuntimeContext: true
@@ -1,3 +1,3 @@
1
- name: Codex 模式
2
- description: 使用 Codex 的基础提示词、exec_command、write_stdin、apply_patch 和 update_plan,并保留 dsh 的 Skills 与可插拔运行时。
3
- order: 5
1
+ name: Codex 模式
2
+ description: 使用精简版 Codex 提示词、exec_command、write_stdin、apply_patch 和 update_plan,并保留 dsh 的 Skills 与可插拔运行时。
3
+ order: 5