@shuind/dsh-codex-harness 0.1.22 → 0.1.24

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
@@ -10,14 +10,15 @@
10
10
  - **GPT 能力补全**:为 GPT 系列模型补充图片输入和思考强度选项;不会覆盖用户已有的显式配置。
11
11
  - **Fast**:仅在 Codex preset 的模型选择菜单中显示,开启后向 Responses 请求发送 `service_tier: "priority"`。
12
12
  - **旧版 Web 兼容**:在未提供新版模型设置/上下文设置 slot 的 DSH Web 中,Fast 和上下文容量会自动回退到旧版 composer slot;client 注入不依赖纯类型 slot 包。
13
+ - **Responses 原生 apply_patch**:在 GPT Responses 请求的插件传输边界,把普通 JSON function tool 改写为 `type: "custom"` + OpenAI `lark` grammar;下一轮历史同步改写为 `custom_tool_call`。如果中转站拒绝 custom tool,则自动回退到普通 function tool,不改变 DSH 工具执行器。
13
14
  - **远程搜索与压缩**:OpenAI Responses 请求默认优先使用 hosted `web_search` 和 `/responses/compact`;失败时回退到 DSH 的本地实现。
14
15
  - **上下文容量**:在 Web 中设置 `1K`–`1000K` tokens,设置值作用于下一次请求。
15
- - **活动状态**:Codex 等待模型响应或进行上下文压缩时,在输入框上方显示对应状态和耗时,让长时间 Deep Diving 不再像卡住。
16
+ - **活动状态**:Codex 等待模型响应或进行上下文压缩时,在 `Deep diving...` 右侧显示对应状态和耗时,让长时间 Deep Diving 不再像卡住。
16
17
 
17
18
  ## 安装
18
19
 
19
20
  ```sh
20
- dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.22
21
+ dsh plugin --profile web add @shuind/dsh-codex-harness@0.1.24
21
22
  ```
22
23
 
23
24
  重启 Web,创建新会话,在模式菜单中选择 **Codex 模式**。
@@ -56,15 +57,20 @@ Fast 设置保存在 `codex` 命名空间,只对 Codex preset 生效。
56
57
 
57
58
  ### 活动状态
58
59
 
59
- Codex 等待模型首个输出时,Web 输入框上方会显示 `正在等待模型响应... · Ns`;开始上下文压缩后会切换为 `正在压缩上下文... · Ns`。收到模型输出、压缩结束或下一轮开始后自动隐藏。该状态由 Host 的 `sessionProjections` 投影驱动,因此本地压缩和远程 `/responses/compact` 都会显示同一种状态。
60
+ Codex 等待模型首个输出时,Web 会在 `Deep diving...` 右侧显示 `正在等待模型响应... · Ns`;开始上下文压缩后会切换为 `正在压缩上下文... · Ns`。收到模型输出、压缩结束或下一轮开始后自动隐藏。该状态由 Host 的 `sessionProjections` 投影驱动,因此本地压缩和远程 `/responses/compact` 都会显示同一种状态。
60
61
 
61
62
  如果当前 DSH 组合没有加载 projection 服务,插件会跳过注册,客户端也会自动隐藏这行,不影响普通会话。
62
63
 
63
64
  ## 随包提供的 Codex preset
64
65
 
65
- 安装插件后,首次启动对应的 DSH profile 时,插件会把完整 preset 从随包提供的 `presets/codex` 安装到 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/codex/`;如果你已经创建了同名 preset,插件会保留你的文件。普通用户不需要自己创建 preset。
66
+ 安装插件后,首次启动对应的 DSH profile 时,插件会安装两套随包提供的 preset
66
67
 
67
- Web 的 **Agent 预设** 中选择 **Codex**,再新建会话即可使用随包提供的 Codex 工具、网页搜索、远程压缩和 Skills 组合。Fast、思考强度、GPT 图片输入和上下文容量可以在对应的设置位置配置。
68
+ - **Codex 模式**:使用精简版 Codex 提示词。
69
+ - **Codex 协作模式**:在精简版 Codex 提示词后追加协作提示词。
70
+
71
+ 它们分别位于 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/codex/` 和 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/codex-collaboration/`。如果对应目录已经存在,插件会保留你的文件,不会覆盖自定义内容。
72
+
73
+ 在 Web 的 **Agent 预设** 中选择 **Codex 模式** 或 **Codex 协作模式**,再新建会话即可使用随包提供的 Codex 工具、网页搜索、远程压缩和 Skills 组合。两套 preset 共用 Fast、思考强度、GPT 图片输入和上下文容量设置;只有系统提示词是否包含协作指导不同。
68
74
 
69
75
  ## 自定义 preset
70
76
 
@@ -127,6 +133,7 @@ Make things as effortless as possible for the user.
127
133
  - preset 在创建会话时确定;已有会话不会自动切换到 Codex 模式。
128
134
  - GPT 模型的图片输入和思考强度属于能力补全;用户已明确配置的字段会被保留。
129
135
  - hosted search、远程压缩以及 reasoning 参数的可用性取决于实际 Responses 中转站和模型。
136
+ - `apply_patch` 的 custom grammar 改写只作用于 GPT Responses 请求;中转站必须接受 OpenAI custom tools 和 Lark grammar。拒绝时插件会自动发送原始 JSON function-call 请求。
130
137
 
131
138
  ## License
132
139
 
package/lib/client.js CHANGED
@@ -13,6 +13,11 @@ window.__ModuleLoader__.load({
13
13
  /** User-selectable Codex context controls use whole K-token units. */
14
14
  const CODEX_CONTEXT_UNIT = 1e3;
15
15
  const CODEX_CONTEXT_MAX = 1e6;
16
+ const CODEX_PRESET_IDS = /* @__PURE__ */ new Set(["codex", "codex-collaboration"]);
17
+ /** Both shipped presets use the same Codex request controls in Web. */
18
+ function isCodexPresetId(value) {
19
+ return value !== void 0 && CODEX_PRESET_IDS.has(value);
20
+ }
16
21
  //#endregion
17
22
  //#region lib/types/client/activity.js
18
23
  /** Client-only inference used while the host activity projection frame is in flight. */
@@ -30,6 +35,8 @@ window.__ModuleLoader__.load({
30
35
  const SETTINGS_NAMESPACE = "codex";
31
36
  const MODEL_SETTINGS_SLOT = "conversation.input.model.settings";
32
37
  const CONTEXT_SETTINGS_SLOT = "conversation.input.context.settings";
38
+ const ACTIVITY_SLOT = "conversation.chat.status";
39
+ const ACTIVITY_FALLBACK_SLOT = "conversation.composer.dock";
33
40
  const FAST_FALLBACK_SLOT = "conversation.input.right";
34
41
  const CONTEXT_FALLBACK_SLOT = "conversation.input.dock";
35
42
  const DEFAULT_CONTEXT_WINDOW = 262144;
@@ -61,7 +68,7 @@ window.__ModuleLoader__.load({
61
68
  const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
62
69
  const snapshot = useSettings((state) => state);
63
70
  const fast = snapshot.value?.fast ?? false;
64
- if (agentPreset !== "codex") return null;
71
+ if (!isCodexPresetId(agentPreset)) return null;
65
72
  return (0, react_jsx_runtime.jsxs)("button", {
66
73
  type: "button",
67
74
  role: "menuitemcheckbox",
@@ -106,7 +113,7 @@ window.__ModuleLoader__.load({
106
113
  const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
107
114
  const snapshot = useSettings((state) => state);
108
115
  const fast = snapshot.value?.fast ?? false;
109
- if (agentPreset !== "codex") return null;
116
+ if (!isCodexPresetId(agentPreset)) return null;
110
117
  return (0, react_jsx_runtime.jsxs)("button", {
111
118
  type: "button",
112
119
  role: "switch",
@@ -277,34 +284,34 @@ window.__ModuleLoader__.load({
277
284
  const agentPreset = useSessions((state) => state.byId[sessionId]?.agentPreset);
278
285
  const projectedActivity = useProjection("codexActivity");
279
286
  const fallbackStartedAt = useSession((snapshot) => awaitingModelStartedAt(snapshot));
280
- const activity = projectedActivity !== void 0 ? projectedActivity : agentPreset === "codex" && fallbackStartedAt !== void 0 ? {
287
+ const activity = projectedActivity !== void 0 ? projectedActivity : isCodexPresetId(agentPreset) && fallbackStartedAt !== void 0 ? {
281
288
  activity: "awaiting-model",
282
289
  startedAt: fallbackStartedAt
283
290
  } : null;
284
291
  const [now, setNow] = (0, react.useState)(() => Date.now());
285
292
  (0, react.useEffect)(() => {
286
- if (agentPreset !== "codex" || activity === void 0 || activity === null) return void 0;
293
+ if (!isCodexPresetId(agentPreset) || activity === void 0 || activity === null) return void 0;
287
294
  setNow(Date.now());
288
295
  const timer = setInterval(() => {
289
296
  setNow(Date.now());
290
297
  }, 1e3);
291
298
  return () => clearInterval(timer);
292
299
  }, [agentPreset, activity?.startedAt]);
293
- if (agentPreset !== "codex" || activity === void 0 || activity === null) return null;
300
+ if (!isCodexPresetId(agentPreset) || activity === void 0 || activity === null) return null;
294
301
  const label = activity.activity === "awaiting-model" ? t("activityAwaitingModel") : t("activityCompacting");
295
- return (0, react_jsx_runtime.jsxs)("div", {
302
+ return (0, react_jsx_runtime.jsxs)("span", {
296
303
  id: "codex-activity",
297
304
  role: "status",
298
305
  "aria-live": "polite",
299
306
  style: {
300
- display: "flex",
307
+ display: "inline-flex",
301
308
  alignItems: "center",
302
309
  gap: 7,
303
- minHeight: 28,
304
- padding: "4px 10px",
310
+ minHeight: 26,
305
311
  color: "var(--dsw-alias-label-secondary)",
306
312
  fontSize: 12,
307
- lineHeight: "20px"
313
+ lineHeight: "20px",
314
+ whiteSpace: "nowrap"
308
315
  },
309
316
  children: [
310
317
  (0, react_jsx_runtime.jsx)("span", {
@@ -329,6 +336,18 @@ window.__ModuleLoader__.load({
329
336
  ]
330
337
  });
331
338
  }
339
+ /** Preserve the old composer-dock footprint for hosts without the chat status seat. */
340
+ function ActivityDock(props) {
341
+ return (0, react_jsx_runtime.jsx)("div", {
342
+ style: {
343
+ display: "flex",
344
+ alignItems: "center",
345
+ minHeight: 28,
346
+ padding: "4px 10px"
347
+ },
348
+ children: (0, react_jsx_runtime.jsx)(ActivityLine, { ...props })
349
+ });
350
+ }
332
351
  const inject = [
333
352
  "slots",
334
353
  "locale",
@@ -342,6 +361,16 @@ window.__ModuleLoader__.load({
342
361
  const registry = ctx.slots;
343
362
  return typeof registry.spec === "function" && registry.spec.call(ctx.slots, slot) !== void 0;
344
363
  }
364
+ /** Register an optional host slot without claiming it in older SlotMap declarations. */
365
+ function injectOptionalActivitySlot(ctx, slot) {
366
+ const registry = ctx.slots;
367
+ registry.inject(slot, () => registry.register({
368
+ name: slot,
369
+ id: "codex-activity",
370
+ order: 5,
371
+ locale: NS
372
+ }, ActivityLine));
373
+ }
345
374
  /** Mount Codex controls in the newest named seats, with legacy composer fallbacks. */
346
375
  function apply(ctx) {
347
376
  ctx.effect(() => ctx.locale.register(NS, {
@@ -382,12 +411,13 @@ window.__ModuleLoader__.load({
382
411
  locale: NS,
383
412
  inject: injected
384
413
  }, LegacyContextSizeControl));
385
- ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
386
- name: "conversation.composer.dock",
414
+ if (chooseDeclaredSlot(ACTIVITY_SLOT, ACTIVITY_FALLBACK_SLOT, (slot) => slotIsDeclared(ctx, slot)) === ACTIVITY_SLOT) injectOptionalActivitySlot(ctx, ACTIVITY_SLOT);
415
+ else ctx.slots.inject(ACTIVITY_FALLBACK_SLOT, () => ctx.slots.register({
416
+ name: ACTIVITY_FALLBACK_SLOT,
387
417
  id: "codex-activity",
388
418
  order: 5,
389
419
  locale: NS
390
- }, ActivityLine));
420
+ }, ActivityDock));
391
421
  }
392
422
  var client_default = {
393
423
  inject,
package/lib/index.js CHANGED
@@ -8,7 +8,12 @@ import { AsyncLocalStorage } from "node:async_hooks";
8
8
  import { z as z$1 } from "zod";
9
9
  //#region lib/types/context.js
10
10
  const CODEX_CONTEXT_MAX = 1e6;
11
- String.raw`start: begin_patch environment_id? hunk+ end_patch
11
+ //#endregion
12
+ //#region lib/types/patch.js
13
+ /** Parser and line-oriented applicator for Codex's `apply_patch` language. */
14
+ 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.";
15
+ /** The grammar sent to providers that support OpenAI custom grammar tools. */
16
+ const APPLY_PATCH_GRAMMAR = String.raw`start: begin_patch environment_id? hunk+ end_patch
12
17
  begin_patch: "*** Begin Patch" LF
13
18
  environment_id: "*** Environment ID: " filename LF
14
19
  end_patch: "*** End Patch" LF?
@@ -575,6 +580,134 @@ function isWebSearchTool(tool) {
575
580
  function hasWebSearchTool(body) {
576
581
  return Array.isArray(body.tools) && body.tools.some(isWebSearchTool);
577
582
  }
583
+ function toolName(tool) {
584
+ if (typeof tool !== "object" || tool === null) return void 0;
585
+ const value = tool;
586
+ if (typeof value.name === "string") return value.name;
587
+ const fn = value.function;
588
+ return typeof fn === "object" && fn !== null && typeof fn.name === "string" ? fn.name : void 0;
589
+ }
590
+ function isApplyPatchTool(tool) {
591
+ return toolName(tool) === "apply_patch";
592
+ }
593
+ function isFunctionApplyPatchTool(tool) {
594
+ if (!isApplyPatchTool(tool) || typeof tool !== "object" || tool === null) return false;
595
+ return tool.type !== "custom";
596
+ }
597
+ function hasFunctionApplyPatchTool(body) {
598
+ return Array.isArray(body.tools) && body.tools.some(isFunctionApplyPatchTool);
599
+ }
600
+ function applyPatchTool(tool) {
601
+ const value = tool;
602
+ const fn = value.function;
603
+ const custom = {
604
+ type: "custom",
605
+ name: "apply_patch",
606
+ description: typeof value.description === "string" ? value.description : typeof fn === "object" && fn !== null && typeof fn.description === "string" ? fn.description : APPLY_PATCH_DESCRIPTION,
607
+ format: {
608
+ type: "grammar",
609
+ syntax: "lark",
610
+ definition: APPLY_PATCH_GRAMMAR
611
+ }
612
+ };
613
+ if (value.defer_loading !== void 0) custom.defer_loading = value.defer_loading;
614
+ return custom;
615
+ }
616
+ /** Replace DSH's JSON function declaration with the Responses custom grammar tool. */
617
+ function addCodexApplyPatch(body) {
618
+ if (!Array.isArray(body.tools)) return body;
619
+ let changed = false;
620
+ const tools = body.tools.map((tool) => {
621
+ if (!isApplyPatchTool(tool) || typeof tool !== "object" || tool === null) return tool;
622
+ if (tool.type === "custom") return tool;
623
+ changed = true;
624
+ return applyPatchTool(tool);
625
+ });
626
+ return changed ? {
627
+ ...body,
628
+ tools
629
+ } : body;
630
+ }
631
+ function applyPatchInputOfCall(item) {
632
+ if (item.type !== "function_call" || item.name !== "apply_patch") return void 0;
633
+ if (typeof item.call_id !== "string" || item.call_id.length === 0) return void 0;
634
+ const raw = item.arguments;
635
+ const argumentsObject = typeof raw === "string" ? (() => {
636
+ try {
637
+ return JSON.parse(raw);
638
+ } catch {
639
+ return;
640
+ }
641
+ })() : raw;
642
+ if (typeof argumentsObject !== "object" || argumentsObject === null) return void 0;
643
+ const input = argumentsObject.input;
644
+ return typeof input === "string" ? {
645
+ callId: item.call_id,
646
+ input
647
+ } : void 0;
648
+ }
649
+ function canRewriteApplyPatchHistory(body) {
650
+ if (!Array.isArray(body.input)) return true;
651
+ return body.input.every((item) => {
652
+ if (typeof item !== "object" || item === null) return true;
653
+ const value = item;
654
+ return value.type !== "function_call" || value.name !== "apply_patch" || applyPatchInputOfCall(value) !== void 0;
655
+ });
656
+ }
657
+ /** Replay apply_patch calls as custom tool items after pi-ai's generic conversion. */
658
+ function rewriteCodexApplyPatchHistory(body) {
659
+ if (!Array.isArray(body.input) || !canRewriteApplyPatchHistory(body)) return body;
660
+ const applyCallIds = /* @__PURE__ */ new Set();
661
+ for (const item of body.input) {
662
+ if (typeof item !== "object" || item === null) continue;
663
+ const value = item;
664
+ if (value.type === "custom_tool_call" && value.name === "apply_patch" && typeof value.call_id === "string") applyCallIds.add(value.call_id);
665
+ const call = applyPatchInputOfCall(value);
666
+ if (call !== void 0) applyCallIds.add(call.callId);
667
+ }
668
+ if (applyCallIds.size === 0) return body;
669
+ let changed = false;
670
+ const input = body.input.map((item) => {
671
+ if (typeof item !== "object" || item === null) return item;
672
+ const value = item;
673
+ const call = applyPatchInputOfCall(value);
674
+ if (call !== void 0) {
675
+ const next = {
676
+ type: "custom_tool_call",
677
+ call_id: call.callId,
678
+ name: "apply_patch",
679
+ input: call.input
680
+ };
681
+ if (typeof value.id === "string") next.id = value.id;
682
+ changed = true;
683
+ return next;
684
+ }
685
+ if (value.type === "function_call_output" && typeof value.call_id === "string" && applyCallIds.has(value.call_id)) {
686
+ changed = true;
687
+ return {
688
+ type: "custom_tool_call_output",
689
+ call_id: value.call_id,
690
+ output: value.output
691
+ };
692
+ }
693
+ return item;
694
+ });
695
+ return changed ? {
696
+ ...body,
697
+ input
698
+ } : body;
699
+ }
700
+ /** Compose the plugin-only Responses rewrites without changing DSH's tool API. */
701
+ function rewriteCodexResponsesBody(body) {
702
+ let next = body;
703
+ if (hasWebSearchTool(next)) next = addHostedWebSearch(next);
704
+ else if (hasRemoteCompaction(next)) next = replaceRemoteCompactions(next);
705
+ if (hasFunctionApplyPatchTool(next) && canRewriteApplyPatchHistory(next)) {
706
+ next = addCodexApplyPatch(next);
707
+ next = rewriteCodexApplyPatchHistory(next);
708
+ }
709
+ return next;
710
+ }
578
711
  /** Convert a generic DSH Responses tool list to the hosted Codex variant. */
579
712
  function addHostedWebSearch(body) {
580
713
  const tools = Array.isArray(body.tools) ? body.tools.filter((tool) => !isWebSearchTool(tool)) : [];
@@ -618,12 +751,13 @@ function hasRemoteCompaction(body) {
618
751
  });
619
752
  }
620
753
  function canPatchBody(body) {
621
- return isGptModel$1(body.model) && (hasWebSearchTool(body) || hasRemoteCompaction(body));
754
+ return isGptModel$1(body.model) && (hasWebSearchTool(body) || hasRemoteCompaction(body) || hasFunctionApplyPatchTool(body) && canRewriteApplyPatchHistory(body));
622
755
  }
623
756
  /**
624
757
  * Install a scoped fetch shim for pi-ai's already-built Responses request.
625
758
  * The generic DSH adapter remains the owner of auth, streaming, replay, and
626
- * attachments; this shim changes only the hosted-tool portion of the wire body.
759
+ * attachments; this shim changes only Codex's Responses tool and compaction
760
+ * representation at the wire boundary.
627
761
  */
628
762
  function installGlobalHostedWebSearchPatch() {
629
763
  const original = globalThis.fetch;
@@ -642,7 +776,7 @@ function installGlobalHostedWebSearchPatch() {
642
776
  const headers = new Headers(request.headers);
643
777
  headers.delete("content-length");
644
778
  const hostedRequest = new Request(request, {
645
- body: JSON.stringify(hasWebSearchTool(body) ? addHostedWebSearch(body) : replaceRemoteCompactions(body)),
779
+ body: JSON.stringify(rewriteCodexResponsesBody(body)),
646
780
  headers
647
781
  });
648
782
  try {
@@ -1063,7 +1197,6 @@ function buildCodexSystemPrompt(config = {}) {
1063
1197
  }
1064
1198
  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.";
1065
1199
  const WRITE_STDIN_DESCRIPTION = "Writes characters to an existing unified exec session and returns recent output.";
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.";
1067
1200
  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.";
1068
1201
  const PLAN_STATUSES = [
1069
1202
  "pending",
@@ -1445,13 +1578,13 @@ function apply(ctx, config = {}) {
1445
1578
  ctx.on("agent/request", async (_payload, next) => {
1446
1579
  return applyCodexRequestSettings(await next(), codexSettings.current());
1447
1580
  });
1448
- if ((resolved.hostedWebSearch || resolved.remoteCompact) && typeof ctx.effect === "function") {
1581
+ if (typeof ctx.effect === "function") {
1449
1582
  const disposeHostedWebSearch = installHostedWebSearch();
1450
1583
  ctx.effect(() => disposeHostedWebSearch, "codex: Responses transport wrapper");
1451
1584
  }
1452
- if (resolved.hostedWebSearch || resolved.remoteCompact) ctx.on("llm/stream", ((options, next) => {
1585
+ ctx.on("llm/stream", ((options, next) => {
1453
1586
  if (resolved.remoteCompact && options.purpose === "compaction" && isGptModel(options.model)) return remoteCompactStream(ctx, options, next);
1454
- if ((resolved.hostedWebSearch || resolved.remoteCompact) && options.purpose === void 0 && isGptModel(options.model)) return hostedWebSearchStream(next);
1587
+ if (options.purpose === void 0 && isGptModel(options.model)) return hostedWebSearchStream(next);
1455
1588
  return next();
1456
1589
  }));
1457
1590
  ctx.on("system-prompt/assemble", async (_assembly, _context, next) => normalizeCodexPromptAssembly(await next()));
package/lib/installer.js CHANGED
@@ -4,9 +4,9 @@ import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region lib/types/installer.js
6
6
  /** Install the user-visible Codex agent preset supplied by this bundle. */
7
- const PRESET_ID = "codex";
8
7
  const PRESET_FILES = ["agent.cordis.yml", "preset.yml"];
9
8
  const SOURCE_PRESET_DIR = fileURLToPath(new URL("../presets/codex/", import.meta.url));
9
+ const SOURCE_COLLABORATION_PRESET_DIR = fileURLToPath(new URL("../presets/codex-collaboration/", import.meta.url));
10
10
  function dshHomePath(...segments) {
11
11
  const configured = process.env.DSH_HOME?.trim();
12
12
  const expanded = configured === void 0 || configured.length === 0 ? join(homedir(), ".dsh") : configured === "~" ? homedir() : configured.startsWith("~/") || configured.startsWith("~\\") ? join(homedir(), configured.slice(2)) : configured;
@@ -14,21 +14,11 @@ function dshHomePath(...segments) {
14
14
  }
15
15
  /** Bundle plugin name for the preset installer. */
16
16
  const name = "codex-preset-installer";
17
- /**
18
- * Install the shipped Codex preset only when the user has not authored one.
19
- *
20
- * The directory is committed with a staging rename so a failed copy cannot
21
- * leave a half-written preset that hides the mode from the roster. Existing
22
- * directories are intentionally preserved, including user customizations.
23
- *
24
- * @param targetDir - destination preset directory.
25
- * @param sourceDir - directory containing the packaged preset files.
26
- */
27
- function installCodexPreset(targetDir = dshHomePath(".agent-presets", PRESET_ID), sourceDir = SOURCE_PRESET_DIR) {
17
+ function installPreset(targetDir, sourceDir, presetId) {
28
18
  if (existsSync(targetDir)) return;
29
19
  const parentDir = dirname(targetDir);
30
20
  mkdirSync(parentDir, { recursive: true });
31
- const stagingDir = mkdtempSync(join(parentDir, `.${PRESET_ID}-`));
21
+ const stagingDir = mkdtempSync(join(parentDir, `.${presetId}-`));
32
22
  try {
33
23
  for (const file of PRESET_FILES) copyFileSync(join(sourceDir, file), join(stagingDir, file));
34
24
  try {
@@ -43,12 +33,30 @@ function installCodexPreset(targetDir = dshHomePath(".agent-presets", PRESET_ID)
43
33
  });
44
34
  }
45
35
  }
36
+ /**
37
+ * Install the shipped Codex preset only when the user has not authored one.
38
+ *
39
+ * The directory is committed with a staging rename so a failed copy cannot
40
+ * leave a half-written preset that hides the mode from the roster. Existing
41
+ * directories are intentionally preserved, including user customizations.
42
+ *
43
+ * @param targetDir - destination preset directory.
44
+ * @param sourceDir - directory containing the packaged preset files.
45
+ */
46
+ function installCodexPreset(targetDir = dshHomePath(".agent-presets", "codex"), sourceDir = SOURCE_PRESET_DIR) {
47
+ installPreset(targetDir, sourceDir, "codex");
48
+ }
49
+ /** Install the shipped Codex preset with the optional collaboration guidance enabled. */
50
+ function installCodexCollaborationPreset(targetDir = dshHomePath(".agent-presets", "codex-collaboration"), sourceDir = SOURCE_COLLABORATION_PRESET_DIR) {
51
+ installPreset(targetDir, sourceDir, "codex-collaboration");
52
+ }
46
53
  /** Install the preset during profile boot without changing the host tool catalog. */
47
54
  function apply(ctx) {
48
55
  try {
49
56
  installCodexPreset();
57
+ installCodexCollaborationPreset();
50
58
  } catch (error) {
51
- ctx.logger.warn(`dsh-codex: could not install the Codex preset: ${String(error)}`);
59
+ ctx.logger.warn(`dsh-codex: could not install the Codex presets: ${String(error)}`);
52
60
  }
53
61
  }
54
62
  var installer_default = {
@@ -56,4 +64,4 @@ var installer_default = {
56
64
  apply
57
65
  };
58
66
  //#endregion
59
- export { apply, installer_default as default, installCodexPreset, name };
67
+ export { apply, installer_default as default, installCodexCollaborationPreset, installCodexPreset, name };
@@ -32,7 +32,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
32
32
  };
33
33
  }
34
34
  }
35
- type ActivityProps = PropsRuntime<'conversation.composer.dock'> & PropsLocale<'codex'>;
35
+ type ActivityDockProps = PropsRuntime<'conversation.composer.dock'> & PropsLocale<'codex'>;
36
+ type ActivityProps = ActivityDockProps;
36
37
  export declare function ActivityLine({ sessionId, useSession, useSessions, useProjection, t }: ActivityProps): import("react").JSX.Element | null;
37
38
  export declare const inject: string[];
38
39
  /** Select a preferred slot when the host declares it, otherwise use a stable legacy composer seat. */
@@ -1,12 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Browser controls for the Codex request settings mounted by the Host plugin. */
3
3
  import { useEffect, useState } from 'react';
4
- import { CODEX_CONTEXT_MAX, CODEX_CONTEXT_UNIT, CODEX_PRESET_ID } from "../context.js";
4
+ import { CODEX_CONTEXT_MAX, CODEX_CONTEXT_UNIT, isCodexPresetId } from "../context.js";
5
5
  import { awaitingModelStartedAt } from "./activity.js";
6
6
  const NS = 'codex';
7
7
  const SETTINGS_NAMESPACE = 'codex';
8
8
  const MODEL_SETTINGS_SLOT = 'conversation.input.model.settings';
9
9
  const CONTEXT_SETTINGS_SLOT = 'conversation.input.context.settings';
10
+ // Prefer a dedicated status seat when a newer host declares it; the current
11
+ // conversation contract falls back to the ambient composer dock below.
12
+ const ACTIVITY_SLOT = 'conversation.chat.status';
13
+ const ACTIVITY_FALLBACK_SLOT = 'conversation.composer.dock';
10
14
  const FAST_FALLBACK_SLOT = 'conversation.input.right';
11
15
  const CONTEXT_FALLBACK_SLOT = 'conversation.input.dock';
12
16
  const DEFAULT_CONTEXT_WINDOW = 262_144;
@@ -38,7 +42,7 @@ function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t })
38
42
  const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
39
43
  const snapshot = useSettings(state => state);
40
44
  const fast = snapshot.value?.fast ?? false;
41
- if (agentPreset !== CODEX_PRESET_ID)
45
+ if (!isCodexPresetId(agentPreset))
42
46
  return null;
43
47
  return (_jsxs("button", { type: "button", role: "menuitemcheckbox", disabled: snapshot.writable === false, "aria-pressed": fast, "aria-label": fast ? t('fastOn') : t('fastOff'), title: fast ? t('fastOn') : t('fastOff'), onClick: () => { void setSetting('fast', !fast); }, style: {
44
48
  boxSizing: 'border-box',
@@ -66,7 +70,7 @@ function FastModeFallback(props) {
66
70
  const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
67
71
  const snapshot = useSettings(state => state);
68
72
  const fast = snapshot.value?.fast ?? false;
69
- if (agentPreset !== CODEX_PRESET_ID)
73
+ if (!isCodexPresetId(agentPreset))
70
74
  return null;
71
75
  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
76
  boxSizing: 'border-box',
@@ -145,31 +149,31 @@ export function ActivityLine({ sessionId, useSession, useSessions, useProjection
145
149
  const fallbackStartedAt = useSession((snapshot) => awaitingModelStartedAt(snapshot));
146
150
  // `undefined` means the key has not arrived yet; `null` is an authoritative
147
151
  // clear and must not be resurrected by the local snapshot fallback.
148
- const activity = projectedActivity !== undefined ? projectedActivity : (agentPreset === CODEX_PRESET_ID && fallbackStartedAt !== undefined
152
+ const activity = projectedActivity !== undefined ? projectedActivity : (isCodexPresetId(agentPreset) && fallbackStartedAt !== undefined
149
153
  ? { activity: 'awaiting-model', startedAt: fallbackStartedAt }
150
154
  : null);
151
155
  const [now, setNow] = useState(() => Date.now());
152
156
  useEffect(() => {
153
- if (agentPreset !== CODEX_PRESET_ID || activity === undefined || activity === null)
157
+ if (!isCodexPresetId(agentPreset) || activity === undefined || activity === null)
154
158
  return undefined;
155
159
  setNow(Date.now());
156
160
  const timer = setInterval(() => { setNow(Date.now()); }, 1_000);
157
161
  return () => clearInterval(timer);
158
162
  }, [agentPreset, activity?.startedAt]);
159
- if (agentPreset !== CODEX_PRESET_ID || activity === undefined || activity === null)
163
+ if (!isCodexPresetId(agentPreset) || activity === undefined || activity === null)
160
164
  return null;
161
165
  const label = activity.activity === 'awaiting-model'
162
166
  ? t('activityAwaitingModel')
163
167
  : t('activityCompacting');
164
- return (_jsxs("div", { id: "codex-activity", role: "status", "aria-live": "polite", style: {
165
- display: 'flex',
168
+ return (_jsxs("span", { id: "codex-activity", role: "status", "aria-live": "polite", style: {
169
+ display: 'inline-flex',
166
170
  alignItems: 'center',
167
171
  gap: 7,
168
- minHeight: 28,
169
- padding: '4px 10px',
172
+ minHeight: 26,
170
173
  color: 'var(--dsw-alias-label-secondary)',
171
174
  fontSize: 12,
172
175
  lineHeight: '20px',
176
+ whiteSpace: 'nowrap',
173
177
  }, children: [_jsx("span", { "aria-hidden": "true", style: {
174
178
  width: 7,
175
179
  height: 7,
@@ -178,6 +182,10 @@ export function ActivityLine({ sessionId, useSession, useSessions, useProjection
178
182
  background: 'var(--dsw-static-blue-500)',
179
183
  } }), _jsx("span", { children: label }), _jsxs("span", { style: { color: 'var(--dsw-alias-label-tertiary)' }, children: ["\u00B7 ", elapsedSeconds(activity.startedAt, now), "s"] })] }));
180
184
  }
185
+ /** Preserve the old composer-dock footprint for hosts without the chat status seat. */
186
+ function ActivityDock(props) {
187
+ return (_jsx("div", { style: { display: 'flex', alignItems: 'center', minHeight: 28, padding: '4px 10px' }, children: _jsx(ActivityLine, { ...props }) }));
188
+ }
181
189
  export const inject = ['slots', 'locale', 'settingsScope'];
182
190
  /** Select a preferred slot when the host declares it, otherwise use a stable legacy composer seat. */
183
191
  export function chooseDeclaredSlot(preferred, fallback, isDeclared) {
@@ -187,6 +195,16 @@ function slotIsDeclared(ctx, slot) {
187
195
  const registry = ctx.slots;
188
196
  return typeof registry.spec === 'function' && registry.spec.call(ctx.slots, slot) !== undefined;
189
197
  }
198
+ /** Register an optional host slot without claiming it in older SlotMap declarations. */
199
+ function injectOptionalActivitySlot(ctx, slot) {
200
+ const registry = ctx.slots;
201
+ registry.inject(slot, () => registry.register({
202
+ name: slot,
203
+ id: 'codex-activity',
204
+ order: 5,
205
+ locale: NS,
206
+ }, ActivityLine));
207
+ }
190
208
  /** Mount Codex controls in the newest named seats, with legacy composer fallbacks. */
191
209
  export function apply(ctx) {
192
210
  ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'codex client: dictionaries');
@@ -234,12 +252,18 @@ export function apply(ctx) {
234
252
  inject: injected,
235
253
  }, LegacyContextSizeControl));
236
254
  }
237
- ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
238
- name: 'conversation.composer.dock',
239
- id: 'codex-activity',
240
- order: 5,
241
- locale: NS,
242
- }, ActivityLine));
255
+ const activitySlot = chooseDeclaredSlot(ACTIVITY_SLOT, ACTIVITY_FALLBACK_SLOT, slot => slotIsDeclared(ctx, slot));
256
+ if (activitySlot === ACTIVITY_SLOT) {
257
+ injectOptionalActivitySlot(ctx, ACTIVITY_SLOT);
258
+ }
259
+ else {
260
+ ctx.slots.inject(ACTIVITY_FALLBACK_SLOT, () => ctx.slots.register({
261
+ name: ACTIVITY_FALLBACK_SLOT,
262
+ id: 'codex-activity',
263
+ order: 5,
264
+ locale: NS,
265
+ }, ActivityDock));
266
+ }
243
267
  }
244
268
  export default { inject, apply };
245
269
  //# sourceMappingURL=index.js.map
@@ -2,4 +2,7 @@
2
2
  export declare const CODEX_CONTEXT_UNIT = 1000;
3
3
  export declare const CODEX_CONTEXT_MAX = 1000000;
4
4
  export declare const CODEX_PRESET_ID = "codex";
5
+ export declare const CODEX_COLLABORATION_PRESET_ID = "codex-collaboration";
6
+ /** Both shipped presets use the same Codex request controls in Web. */
7
+ export declare function isCodexPresetId(value: string | undefined): boolean;
5
8
  //# sourceMappingURL=context.d.ts.map
@@ -2,4 +2,13 @@
2
2
  export const CODEX_CONTEXT_UNIT = 1_000;
3
3
  export const CODEX_CONTEXT_MAX = 1_000_000;
4
4
  export const CODEX_PRESET_ID = 'codex';
5
+ export const CODEX_COLLABORATION_PRESET_ID = 'codex-collaboration';
6
+ const CODEX_PRESET_IDS = new Set([
7
+ CODEX_PRESET_ID,
8
+ CODEX_COLLABORATION_PRESET_ID,
9
+ ]);
10
+ /** Both shipped presets use the same Codex request controls in Web. */
11
+ export function isCodexPresetId(value) {
12
+ return value !== undefined && CODEX_PRESET_IDS.has(value);
13
+ }
5
14
  //# sourceMappingURL=context.js.map
@@ -3,7 +3,7 @@ import z from '@deepseek-ai/schemastery';
3
3
  import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools';
5
5
  import { CODEX_CONTEXT_MAX } from "./context.js";
6
- import { applyPatchHunks, parsePatch } from "./patch.js";
6
+ import { APPLY_PATCH_DESCRIPTION, applyPatchHunks, parsePatch } from "./patch.js";
7
7
  import { renderExecResult, runExecCommand, runWriteStdin } from "./exec.js";
8
8
  import { hostedWebSearchStream, installHostedWebSearch, remoteCompactStream } from "./remote.js";
9
9
  import { registerCodexActivityProjection } from "./activity.js";
@@ -196,7 +196,6 @@ export function buildCodexSystemPrompt(config = {}) {
196
196
  }
197
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 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
199
  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
200
  const PLAN_STATUSES = ['pending', 'in_progress', 'completed'];
202
201
  function sessionCwd(exec) {
@@ -473,23 +472,22 @@ export function apply(ctx, config = {}) {
473
472
  const request = await next();
474
473
  return applyCodexRequestSettings(request, codexSettings.current());
475
474
  });
476
- if ((resolved.hostedWebSearch || resolved.remoteCompact) && typeof ctx.effect === 'function') {
475
+ if (typeof ctx.effect === 'function') {
477
476
  const disposeHostedWebSearch = installHostedWebSearch();
478
477
  ctx.effect(() => disposeHostedWebSearch, 'codex: Responses transport wrapper');
479
478
  }
480
- if (resolved.hostedWebSearch || resolved.remoteCompact) {
481
- ctx.on('llm/stream', ((options, next) => {
482
- if (resolved.remoteCompact && options.purpose === 'compaction' && isGptModel(options.model)) {
483
- return remoteCompactStream(ctx, options, next);
484
- }
485
- if ((resolved.hostedWebSearch || resolved.remoteCompact)
486
- && options.purpose === undefined
487
- && isGptModel(options.model)) {
488
- return hostedWebSearchStream(next);
489
- }
490
- return next();
491
- }));
492
- }
479
+ ctx.on('llm/stream', ((options, next) => {
480
+ if (resolved.remoteCompact && options.purpose === 'compaction' && isGptModel(options.model)) {
481
+ return remoteCompactStream(ctx, options, next);
482
+ }
483
+ // Keep all ordinary GPT Responses calls inside the plugin-owned transport
484
+ // scope so apply_patch can use the same wire bridge even when hosted search
485
+ // and remote compaction are disabled.
486
+ if (options.purpose === undefined && isGptModel(options.model)) {
487
+ return hostedWebSearchStream(next);
488
+ }
489
+ return next();
490
+ }));
493
491
  ctx.on('system-prompt/assemble', async (_assembly, _context, next) => normalizeCodexPromptAssembly(await next()));
494
492
  ctx.systemPrompt.section({ name: 'codex:base', order: 10, text: buildCodexSystemPrompt(resolved) });
495
493
  registerExecTools(ctx, resolved);
@@ -13,6 +13,8 @@ export declare const name = "codex-preset-installer";
13
13
  * @param sourceDir - directory containing the packaged preset files.
14
14
  */
15
15
  export declare function installCodexPreset(targetDir?: string, sourceDir?: string): void;
16
+ /** Install the shipped Codex preset with the optional collaboration guidance enabled. */
17
+ export declare function installCodexCollaborationPreset(targetDir?: string, sourceDir?: string): void;
16
18
  /** Install the preset during profile boot without changing the host tool catalog. */
17
19
  export declare function apply(ctx: Context): void;
18
20
  declare const _default: {
@@ -3,9 +3,9 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync }
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- const PRESET_ID = 'codex';
7
6
  const PRESET_FILES = ['agent.cordis.yml', 'preset.yml'];
8
7
  const SOURCE_PRESET_DIR = fileURLToPath(new URL('../presets/codex/', import.meta.url));
8
+ const SOURCE_COLLABORATION_PRESET_DIR = fileURLToPath(new URL('../presets/codex-collaboration/', import.meta.url));
9
9
  function dshHomePath(...segments) {
10
10
  const configured = process.env.DSH_HOME?.trim();
11
11
  const expanded = configured === undefined || configured.length === 0
@@ -19,22 +19,12 @@ function dshHomePath(...segments) {
19
19
  }
20
20
  /** Bundle plugin name for the preset installer. */
21
21
  export const name = 'codex-preset-installer';
22
- /**
23
- * Install the shipped Codex preset only when the user has not authored one.
24
- *
25
- * The directory is committed with a staging rename so a failed copy cannot
26
- * leave a half-written preset that hides the mode from the roster. Existing
27
- * directories are intentionally preserved, including user customizations.
28
- *
29
- * @param targetDir - destination preset directory.
30
- * @param sourceDir - directory containing the packaged preset files.
31
- */
32
- export function installCodexPreset(targetDir = dshHomePath('.agent-presets', PRESET_ID), sourceDir = SOURCE_PRESET_DIR) {
22
+ function installPreset(targetDir, sourceDir, presetId) {
33
23
  if (existsSync(targetDir))
34
24
  return;
35
25
  const parentDir = dirname(targetDir);
36
26
  mkdirSync(parentDir, { recursive: true });
37
- const stagingDir = mkdtempSync(join(parentDir, `.${PRESET_ID}-`));
27
+ const stagingDir = mkdtempSync(join(parentDir, `.${presetId}-`));
38
28
  try {
39
29
  for (const file of PRESET_FILES)
40
30
  copyFileSync(join(sourceDir, file), join(stagingDir, file));
@@ -51,13 +41,31 @@ export function installCodexPreset(targetDir = dshHomePath('.agent-presets', PRE
51
41
  rmSync(stagingDir, { recursive: true, force: true });
52
42
  }
53
43
  }
44
+ /**
45
+ * Install the shipped Codex preset only when the user has not authored one.
46
+ *
47
+ * The directory is committed with a staging rename so a failed copy cannot
48
+ * leave a half-written preset that hides the mode from the roster. Existing
49
+ * directories are intentionally preserved, including user customizations.
50
+ *
51
+ * @param targetDir - destination preset directory.
52
+ * @param sourceDir - directory containing the packaged preset files.
53
+ */
54
+ export function installCodexPreset(targetDir = dshHomePath('.agent-presets', 'codex'), sourceDir = SOURCE_PRESET_DIR) {
55
+ installPreset(targetDir, sourceDir, 'codex');
56
+ }
57
+ /** Install the shipped Codex preset with the optional collaboration guidance enabled. */
58
+ export function installCodexCollaborationPreset(targetDir = dshHomePath('.agent-presets', 'codex-collaboration'), sourceDir = SOURCE_COLLABORATION_PRESET_DIR) {
59
+ installPreset(targetDir, sourceDir, 'codex-collaboration');
60
+ }
54
61
  /** Install the preset during profile boot without changing the host tool catalog. */
55
62
  export function apply(ctx) {
56
63
  try {
57
64
  installCodexPreset();
65
+ installCodexCollaborationPreset();
58
66
  }
59
67
  catch (error) {
60
- ctx.logger.warn(`dsh-codex: could not install the Codex preset: ${String(error)}`);
68
+ ctx.logger.warn(`dsh-codex: could not install the Codex presets: ${String(error)}`);
61
69
  }
62
70
  }
63
71
  export default { name, apply };
@@ -1,4 +1,5 @@
1
1
  /** Parser and line-oriented applicator for Codex's `apply_patch` language. */
2
+ export declare 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.";
2
3
  /** The grammar sent to providers that support OpenAI custom grammar tools. */
3
4
  export declare const APPLY_PATCH_GRAMMAR: string;
4
5
  export type PatchLine = {
@@ -1,4 +1,5 @@
1
1
  /** Parser and line-oriented applicator for Codex's `apply_patch` language. */
2
+ export 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.';
2
3
  /** The grammar sent to providers that support OpenAI custom grammar tools. */
3
4
  export const APPLY_PATCH_GRAMMAR = String.raw `start: begin_patch environment_id? hunk+ end_patch
4
5
  begin_patch: "*** Begin Patch" LF
@@ -7,6 +7,12 @@ export interface RemoteCodexConfig {
7
7
  interface JsonObject {
8
8
  [key: string]: unknown;
9
9
  }
10
+ /** Replace DSH's JSON function declaration with the Responses custom grammar tool. */
11
+ export declare function addCodexApplyPatch(body: JsonObject): JsonObject;
12
+ /** Replay apply_patch calls as custom tool items after pi-ai's generic conversion. */
13
+ export declare function rewriteCodexApplyPatchHistory(body: JsonObject): JsonObject;
14
+ /** Compose the plugin-only Responses rewrites without changing DSH's tool API. */
15
+ export declare function rewriteCodexResponsesBody(body: JsonObject): JsonObject;
10
16
  /** Convert a generic DSH Responses tool list to the hosted Codex variant. */
11
17
  export declare function addHostedWebSearch(body: JsonObject): JsonObject;
12
18
  /** Replace the text placeholder written by dsh-compaction-basic with the native item. */
@@ -1,5 +1,6 @@
1
1
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { APPLY_PATCH_DESCRIPTION, APPLY_PATCH_GRAMMAR } from "./patch.js";
3
4
  const SETTINGS_NAMESPACE = 'llm-pi-ai';
4
5
  const REMOTE_COMPACTION_OPEN = '<codex-remote-compaction>';
5
6
  const REMOTE_COMPACTION_CLOSE = '</codex-remote-compaction>';
@@ -59,6 +60,162 @@ function isWebSearchTool(tool) {
59
60
  function hasWebSearchTool(body) {
60
61
  return Array.isArray(body.tools) && body.tools.some(isWebSearchTool);
61
62
  }
63
+ function toolName(tool) {
64
+ if (typeof tool !== 'object' || tool === null)
65
+ return undefined;
66
+ const value = tool;
67
+ if (typeof value.name === 'string')
68
+ return value.name;
69
+ const fn = value.function;
70
+ return typeof fn === 'object' && fn !== null && typeof fn.name === 'string'
71
+ ? fn.name
72
+ : undefined;
73
+ }
74
+ function isApplyPatchTool(tool) {
75
+ return toolName(tool) === 'apply_patch';
76
+ }
77
+ function isFunctionApplyPatchTool(tool) {
78
+ if (!isApplyPatchTool(tool) || typeof tool !== 'object' || tool === null)
79
+ return false;
80
+ return tool.type !== 'custom';
81
+ }
82
+ function hasFunctionApplyPatchTool(body) {
83
+ return Array.isArray(body.tools) && body.tools.some(isFunctionApplyPatchTool);
84
+ }
85
+ function applyPatchTool(tool) {
86
+ const value = tool;
87
+ const fn = value.function;
88
+ const description = typeof value.description === 'string'
89
+ ? value.description
90
+ : typeof fn === 'object' && fn !== null && typeof fn.description === 'string'
91
+ ? fn.description
92
+ : APPLY_PATCH_DESCRIPTION;
93
+ const custom = {
94
+ type: 'custom',
95
+ name: 'apply_patch',
96
+ description,
97
+ format: {
98
+ type: 'grammar',
99
+ syntax: 'lark',
100
+ definition: APPLY_PATCH_GRAMMAR,
101
+ },
102
+ };
103
+ if (value.defer_loading !== undefined)
104
+ custom.defer_loading = value.defer_loading;
105
+ return custom;
106
+ }
107
+ /** Replace DSH's JSON function declaration with the Responses custom grammar tool. */
108
+ export function addCodexApplyPatch(body) {
109
+ if (!Array.isArray(body.tools))
110
+ return body;
111
+ let changed = false;
112
+ const tools = body.tools.map(tool => {
113
+ if (!isApplyPatchTool(tool) || typeof tool !== 'object' || tool === null)
114
+ return tool;
115
+ if (tool.type === 'custom')
116
+ return tool;
117
+ changed = true;
118
+ return applyPatchTool(tool);
119
+ });
120
+ return changed ? { ...body, tools } : body;
121
+ }
122
+ function applyPatchInputOfCall(item) {
123
+ if (item.type !== 'function_call' || item.name !== 'apply_patch')
124
+ return undefined;
125
+ if (typeof item.call_id !== 'string' || item.call_id.length === 0)
126
+ return undefined;
127
+ const raw = item.arguments;
128
+ const argumentsObject = typeof raw === 'string'
129
+ ? (() => {
130
+ try {
131
+ return JSON.parse(raw);
132
+ }
133
+ catch {
134
+ return undefined;
135
+ }
136
+ })()
137
+ : raw;
138
+ if (typeof argumentsObject !== 'object' || argumentsObject === null)
139
+ return undefined;
140
+ const input = argumentsObject.input;
141
+ return typeof input === 'string' ? { callId: item.call_id, input } : undefined;
142
+ }
143
+ function canRewriteApplyPatchHistory(body) {
144
+ if (!Array.isArray(body.input))
145
+ return true;
146
+ return body.input.every(item => {
147
+ if (typeof item !== 'object' || item === null)
148
+ return true;
149
+ const value = item;
150
+ return value.type !== 'function_call' || value.name !== 'apply_patch'
151
+ || applyPatchInputOfCall(value) !== undefined;
152
+ });
153
+ }
154
+ /** Replay apply_patch calls as custom tool items after pi-ai's generic conversion. */
155
+ export function rewriteCodexApplyPatchHistory(body) {
156
+ if (!Array.isArray(body.input) || !canRewriteApplyPatchHistory(body))
157
+ return body;
158
+ const applyCallIds = new Set();
159
+ for (const item of body.input) {
160
+ if (typeof item !== 'object' || item === null)
161
+ continue;
162
+ const value = item;
163
+ if (value.type === 'custom_tool_call' && value.name === 'apply_patch' && typeof value.call_id === 'string') {
164
+ applyCallIds.add(value.call_id);
165
+ }
166
+ const call = applyPatchInputOfCall(value);
167
+ if (call !== undefined)
168
+ applyCallIds.add(call.callId);
169
+ }
170
+ if (applyCallIds.size === 0)
171
+ return body;
172
+ let changed = false;
173
+ const input = body.input.map(item => {
174
+ if (typeof item !== 'object' || item === null)
175
+ return item;
176
+ const value = item;
177
+ const call = applyPatchInputOfCall(value);
178
+ if (call !== undefined) {
179
+ const next = {
180
+ type: 'custom_tool_call',
181
+ call_id: call.callId,
182
+ name: 'apply_patch',
183
+ input: call.input,
184
+ };
185
+ if (typeof value.id === 'string')
186
+ next.id = value.id;
187
+ changed = true;
188
+ return next;
189
+ }
190
+ if (value.type === 'function_call_output'
191
+ && typeof value.call_id === 'string'
192
+ && applyCallIds.has(value.call_id)) {
193
+ changed = true;
194
+ return {
195
+ type: 'custom_tool_call_output',
196
+ call_id: value.call_id,
197
+ output: value.output,
198
+ };
199
+ }
200
+ return item;
201
+ });
202
+ return changed ? { ...body, input } : body;
203
+ }
204
+ /** Compose the plugin-only Responses rewrites without changing DSH's tool API. */
205
+ export function rewriteCodexResponsesBody(body) {
206
+ let next = body;
207
+ if (hasWebSearchTool(next)) {
208
+ next = addHostedWebSearch(next);
209
+ }
210
+ else if (hasRemoteCompaction(next)) {
211
+ next = replaceRemoteCompactions(next);
212
+ }
213
+ if (hasFunctionApplyPatchTool(next) && canRewriteApplyPatchHistory(next)) {
214
+ next = addCodexApplyPatch(next);
215
+ next = rewriteCodexApplyPatchHistory(next);
216
+ }
217
+ return next;
218
+ }
62
219
  /** Convert a generic DSH Responses tool list to the hosted Codex variant. */
63
220
  export function addHostedWebSearch(body) {
64
221
  const tools = Array.isArray(body.tools) ? body.tools.filter(tool => !isWebSearchTool(tool)) : [];
@@ -107,12 +264,15 @@ function hasRemoteCompaction(body) {
107
264
  }
108
265
  function canPatchBody(body) {
109
266
  return isGptModel(body.model)
110
- && (hasWebSearchTool(body) || hasRemoteCompaction(body));
267
+ && (hasWebSearchTool(body)
268
+ || hasRemoteCompaction(body)
269
+ || (hasFunctionApplyPatchTool(body) && canRewriteApplyPatchHistory(body)));
111
270
  }
112
271
  /**
113
272
  * Install a scoped fetch shim for pi-ai's already-built Responses request.
114
273
  * The generic DSH adapter remains the owner of auth, streaming, replay, and
115
- * attachments; this shim changes only the hosted-tool portion of the wire body.
274
+ * attachments; this shim changes only Codex's Responses tool and compaction
275
+ * representation at the wire boundary.
116
276
  */
117
277
  function installGlobalHostedWebSearchPatch() {
118
278
  const original = globalThis.fetch;
@@ -135,7 +295,7 @@ function installGlobalHostedWebSearchPatch() {
135
295
  const headers = new Headers(request.headers);
136
296
  headers.delete('content-length');
137
297
  const hostedRequest = new Request(request, {
138
- body: JSON.stringify(hasWebSearchTool(body) ? addHostedWebSearch(body) : replaceRemoteCompactions(body)),
298
+ body: JSON.stringify(rewriteCodexResponsesBody(body)),
139
299
  headers,
140
300
  });
141
301
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shuind/dsh-codex-harness",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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",
@@ -35,6 +35,7 @@
35
35
  "lib/client.js",
36
36
  "cordis.patch.yml",
37
37
  "presets/codex",
38
+ "presets/codex-collaboration",
38
39
  "README.md",
39
40
  "README.zh.md",
40
41
  "LICENSE"
@@ -0,0 +1,91 @@
1
+ # The Codex collaboration preset. It shares the standard Codex composition
2
+ # while enabling the optional collaboration guidance in the prompt layer.
3
+
4
+ - id: persona
5
+ name: '@deepseek-ai/dsh-persona'
6
+ config:
7
+ # Keep identity, model, cwd, and runtime context in one persona section.
8
+ text: >-
9
+ You are Codex, a coding agent based on the {{model}} model. Your working directory is {{cwd}}.
10
+ includeRuntimeContext: true
11
+
12
+ - id: agent-instructions
13
+ name: '@deepseek-ai/dsh-agent-instructions'
14
+ config:
15
+ maxBytes: 65536
16
+
17
+ # The collaboration variant only changes this flag; execution remains
18
+ # delegated to the same dsh Codex tool layer.
19
+ - id: codex-tools
20
+ name: '@shuind/dsh-codex-harness'
21
+ config:
22
+ collaborationPrompt: true
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
+
32
+ # The generic DSH pi-ai adapter still owns the user's configured provider,
33
+ # endpoint, API key, and model. Codex's remote-first transport wrapper is
34
+ # installed only in this Codex scope and falls back to the generic DSH path.
35
+
36
+ # Keep the local tool mounted as the fallback. Codex rewrites the final GPT
37
+ # Responses request to a hosted web_search tool first; a failed remote request
38
+ # is retried through this unchanged local function-tool path.
39
+ - id: codex-web-search
40
+ name: '@deepseek-ai/dsh-tool-web'
41
+ config:
42
+ search: true
43
+ fetch: false
44
+
45
+ # PTY execution is optional. Pipe-backed exec_command remains available on all
46
+ # platforms through the shell capability consumed by the Codex tool layer.
47
+ - id: codex-terminal
48
+ name: cordis:group
49
+ group: true
50
+ isolate:
51
+ terminals: true
52
+ config:
53
+ - id: terminals
54
+ name: '@deepseek-ai/dsh-terminal'
55
+
56
+ - id: terminal-bash
57
+ name: '@deepseek-ai/dsh-terminal-bash'
58
+ disabled: !!js process.platform === 'win32'
59
+ config:
60
+ timeoutMs: 300000
61
+
62
+ # Context compaction is agent-scoped in Web. The token meter stays on the host
63
+ # plane, while this group owns the backend, `/compact`, and optional tool-result
64
+ # pruning for Codex sessions.
65
+ - id: compaction
66
+ name: cordis:group
67
+ group: true
68
+ isolate:
69
+ compaction: true
70
+ toolResultPruner: true
71
+ config:
72
+ - id: compaction-basic
73
+ name: '@deepseek-ai/dsh-compaction-basic'
74
+
75
+ - id: command-compact
76
+ name: '@deepseek-ai/dsh-command-compact'
77
+
78
+ - id: tool-result-pruner
79
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
80
+ config:
81
+ thresholdChars: 8192
82
+ headChars: 4096
83
+ tailChars: 1024
84
+
85
+ # Skills are the dsh extensibility seam and do not change the four Codex core
86
+ # tool names.
87
+ - id: skill-filesystem
88
+ name: '@deepseek-ai/dsh-skill-filesystem'
89
+
90
+ - id: tool-skill
91
+ name: '@deepseek-ai/dsh-tool-skill'
@@ -0,0 +1,3 @@
1
+ name: Codex 协作模式
2
+ description: 使用精简版 Codex 提示词,并加入协作提示词、统一执行工具、后台任务控制和 dsh Skills。
3
+ order: 6