@shuind/dsh-codex-harness 0.1.10 → 0.1.12

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/lib/index.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
2
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
4
  import { randomBytes } from "node:crypto";
4
5
  import { resolve } from "node:path";
6
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
+ //#region lib/types/context.js
9
+ const CODEX_CONTEXT_MAX = 1e6;
5
10
  String.raw`start: begin_patch hunk+ end_patch
6
11
  begin_patch: "*** Begin Patch" LF
7
12
  end_patch: "*** End Patch" LF?
@@ -399,6 +404,309 @@ async function runWriteStdin(ctx, args, exec, config) {
399
404
  return output;
400
405
  }
401
406
  //#endregion
407
+ //#region lib/types/remote.js
408
+ const SETTINGS_NAMESPACE = "llm-pi-ai";
409
+ const REMOTE_COMPACTION_OPEN = "<codex-remote-compaction>";
410
+ const REMOTE_COMPACTION_CLOSE = "</codex-remote-compaction>";
411
+ const HOSTED_REQUESTS = new AsyncLocalStorage();
412
+ let hostedPatchUsers = 0;
413
+ let hostedPatchRestore;
414
+ function isGptModel$1(model) {
415
+ return typeof model === "string" && /(?:^|\/)(?:gpt|chatgpt)(?:[-_.]|\d|$)/i.test(model);
416
+ }
417
+ function settingsOf(ctx) {
418
+ return ctx.get("settings")?.get?.(SETTINGS_NAMESPACE);
419
+ }
420
+ function profileOf(ctx, provider) {
421
+ return settingsOf(ctx)?.providers?.[provider];
422
+ }
423
+ function supportsResponses(profile, provider) {
424
+ return profile?.api === "openai-responses" || profile?.api === "openai-codex-responses" || profile?.api === void 0 && provider === "openai";
425
+ }
426
+ function responsesEndpoint(baseURL, suffix) {
427
+ return `${baseURL.replace(/\/+$/, "")}/${suffix}`;
428
+ }
429
+ function hasHeader(headers, name) {
430
+ return Object.keys(headers ?? {}).some((key) => key.toLowerCase() === name.toLowerCase());
431
+ }
432
+ async function apiHeaders(ctx, profile) {
433
+ const headers = {
434
+ ...profile.headers,
435
+ "content-type": "application/json"
436
+ };
437
+ if (profile.apiKeyEnv !== void 0 && !hasHeader(headers, "authorization")) {
438
+ const resolved = await ctx.get("credentials")?.resolve(credentialRef(profile.apiKeyEnv));
439
+ if (resolved?.value === void 0) return void 0;
440
+ headers.authorization = `Bearer ${resolved.value}`;
441
+ }
442
+ return headers;
443
+ }
444
+ function isResponsesRequest(url) {
445
+ try {
446
+ return new URL(url).pathname.replace(/\/+$/, "").endsWith("/responses");
447
+ } catch {
448
+ return url.replace(/\/+$/, "").endsWith("/responses");
449
+ }
450
+ }
451
+ function isWebSearchTool(tool) {
452
+ if (typeof tool !== "object" || tool === null) return false;
453
+ const value = tool;
454
+ if (value.name === "web_search") return true;
455
+ const fn = value.function;
456
+ return typeof fn === "object" && fn !== null && fn.name === "web_search";
457
+ }
458
+ function hasWebSearchTool(body) {
459
+ return Array.isArray(body.tools) && body.tools.some(isWebSearchTool);
460
+ }
461
+ /** Convert a generic DSH Responses tool list to the hosted Codex variant. */
462
+ function addHostedWebSearch(body) {
463
+ const tools = Array.isArray(body.tools) ? body.tools.filter((tool) => !isWebSearchTool(tool)) : [];
464
+ tools.push({
465
+ type: "web_search",
466
+ external_web_access: true
467
+ });
468
+ return {
469
+ ...replaceRemoteCompactions(body),
470
+ tools
471
+ };
472
+ }
473
+ /** Replace the text placeholder written by dsh-compaction-basic with the native item. */
474
+ function replaceRemoteCompactions(body) {
475
+ if (!Array.isArray(body.input)) return body;
476
+ const input = body.input.map((item) => {
477
+ if (typeof item !== "object" || item === null) return item;
478
+ const value = item;
479
+ const marker = (Array.isArray(value.content) ? value.content : []).find((part) => typeof part === "object" && part !== null && typeof part.text === "string" && String(part.text).includes(REMOTE_COMPACTION_OPEN));
480
+ if (marker === void 0) return item;
481
+ const text = String(marker.text);
482
+ const start = text.indexOf(REMOTE_COMPACTION_OPEN) + 25;
483
+ const end = text.indexOf(REMOTE_COMPACTION_CLOSE, start);
484
+ if (end < start) return item;
485
+ return {
486
+ type: "compaction",
487
+ encrypted_content: text.slice(start, end)
488
+ };
489
+ });
490
+ return {
491
+ ...body,
492
+ input
493
+ };
494
+ }
495
+ function hasRemoteCompaction(body) {
496
+ if (!Array.isArray(body.input)) return false;
497
+ return body.input.some((item) => {
498
+ if (typeof item !== "object" || item === null) return false;
499
+ const content = item.content;
500
+ return Array.isArray(content) && content.some((part) => typeof part === "object" && part !== null && typeof part.text === "string" && String(part.text).includes(REMOTE_COMPACTION_OPEN));
501
+ });
502
+ }
503
+ function canPatchBody(body) {
504
+ return isGptModel$1(body.model) && (hasWebSearchTool(body) || hasRemoteCompaction(body));
505
+ }
506
+ /**
507
+ * Install a scoped fetch shim for pi-ai's already-built Responses request.
508
+ * The generic DSH adapter remains the owner of auth, streaming, replay, and
509
+ * attachments; this shim changes only the hosted-tool portion of the wire body.
510
+ */
511
+ function installGlobalHostedWebSearchPatch() {
512
+ const original = globalThis.fetch;
513
+ const patched = async (input, init) => {
514
+ if (!HOSTED_REQUESTS.getStore()) return original(input, init);
515
+ const request = new Request(input, init);
516
+ if (!isResponsesRequest(request.url)) return original(input, init);
517
+ let body;
518
+ try {
519
+ body = JSON.parse(await request.clone().text());
520
+ } catch {
521
+ return original(input, init);
522
+ }
523
+ if (!canPatchBody(body)) return original(input, init);
524
+ const fallbackRequest = request.clone();
525
+ const headers = new Headers(request.headers);
526
+ headers.delete("content-length");
527
+ const hostedRequest = new Request(request, {
528
+ body: JSON.stringify(hasWebSearchTool(body) ? addHostedWebSearch(body) : replaceRemoteCompactions(body)),
529
+ headers
530
+ });
531
+ try {
532
+ const hostedResponse = await original(hostedRequest);
533
+ if (!hostedResponse.ok) return original(fallbackRequest);
534
+ return hostedResponse;
535
+ } catch {
536
+ return original(fallbackRequest);
537
+ }
538
+ };
539
+ globalThis.fetch = patched;
540
+ return () => {
541
+ if (globalThis.fetch === patched) globalThis.fetch = original;
542
+ };
543
+ }
544
+ /** Enable the transport patch for this Codex plugin scope only. */
545
+ function installHostedWebSearch() {
546
+ hostedPatchUsers += 1;
547
+ hostedPatchRestore ??= installGlobalHostedWebSearchPatch();
548
+ return () => {
549
+ hostedPatchUsers = Math.max(0, hostedPatchUsers - 1);
550
+ if (hostedPatchUsers === 0) {
551
+ hostedPatchRestore?.();
552
+ hostedPatchRestore = void 0;
553
+ }
554
+ };
555
+ }
556
+ /** Iterate an existing DSH stream with the hosted-request context installed. */
557
+ function hostedWebSearchStream(next) {
558
+ return (async function* () {
559
+ const iterator = next()[Symbol.asyncIterator]();
560
+ let completed = false;
561
+ try {
562
+ while (true) {
563
+ const item = await HOSTED_REQUESTS.run(true, () => iterator.next());
564
+ if (item.done) {
565
+ completed = true;
566
+ return;
567
+ }
568
+ yield item.value;
569
+ }
570
+ } finally {
571
+ if (!completed) await iterator.return?.();
572
+ }
573
+ })();
574
+ }
575
+ function textOf(message) {
576
+ return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
577
+ }
578
+ function responsesInput(messages) {
579
+ const result = [];
580
+ for (const message of messages) {
581
+ const text = textOf(message);
582
+ if (message.role === "assistant") {
583
+ if (text.length > 0) result.push({
584
+ type: "message",
585
+ role: "assistant",
586
+ content: [{
587
+ type: "output_text",
588
+ text
589
+ }]
590
+ });
591
+ for (const block of message.content) if (block.type === "tool-call") result.push({
592
+ type: "function_call",
593
+ call_id: block.id,
594
+ name: block.name,
595
+ arguments: block.arguments
596
+ });
597
+ continue;
598
+ }
599
+ const role = message.role === "system" ? "developer" : "user";
600
+ if (text.length > 0) result.push({
601
+ type: "message",
602
+ role,
603
+ content: [{
604
+ type: "input_text",
605
+ text
606
+ }]
607
+ });
608
+ for (const block of message.content) {
609
+ if (block.type !== "tool-result") continue;
610
+ result.push({
611
+ type: "function_call_output",
612
+ call_id: block.toolCallId,
613
+ output: block.content.filter((item) => item.type === "text").map((item) => item.text).join("")
614
+ });
615
+ }
616
+ }
617
+ return result;
618
+ }
619
+ function responsesTools(tools) {
620
+ if (tools === void 0) return void 0;
621
+ return tools.filter((tool) => tool.name !== "web_search").map((tool) => ({
622
+ type: "function",
623
+ name: tool.name,
624
+ description: tool.description,
625
+ parameters: tool.parameters
626
+ }));
627
+ }
628
+ function compactBody(options) {
629
+ const tools = responsesTools(options.tools);
630
+ const compacted = replaceRemoteCompactions({
631
+ model: options.model,
632
+ input: responsesInput(options.messages.slice(0, -1)),
633
+ ...options.system === void 0 ? {} : { instructions: options.system },
634
+ ...tools === void 0 ? {} : { tools },
635
+ ...options.maxTokens === void 0 ? {} : { max_output_tokens: options.maxTokens },
636
+ ...options.reasoningEffort === void 0 ? {} : { reasoning: { effort: options.reasoningEffort } }
637
+ });
638
+ return options.tools?.some((tool) => tool.name === "web_search") ? addHostedWebSearch(compacted) : compacted;
639
+ }
640
+ function compactText(body) {
641
+ const output = Array.isArray(body.output) ? body.output : [];
642
+ const text = [];
643
+ for (const item of output) {
644
+ if (typeof item !== "object" || item === null) continue;
645
+ const value = item;
646
+ if (typeof value.encrypted_content === "string") text.push(value.encrypted_content);
647
+ }
648
+ const encrypted = text.join("\n\n").trim();
649
+ return encrypted.length === 0 ? "" : `${REMOTE_COMPACTION_OPEN}${encrypted}${REMOTE_COMPACTION_CLOSE}`;
650
+ }
651
+ async function remoteCompact(ctx, options) {
652
+ const profile = profileOf(ctx, options.provider);
653
+ if (!supportsResponses(profile, options.provider) || profile?.baseURL === void 0) throw new Error("Codex remote compaction requires an OpenAI Responses provider with baseURL");
654
+ const headers = await apiHeaders(ctx, profile);
655
+ if (headers === void 0) throw new Error("Codex remote compaction has no configured API key");
656
+ const response = await fetch(responsesEndpoint(profile.baseURL, "responses/compact"), {
657
+ method: "POST",
658
+ headers,
659
+ body: JSON.stringify(compactBody(options)),
660
+ ...options.signal === void 0 ? {} : { signal: options.signal }
661
+ });
662
+ if (!response.ok) throw new Error(`remote compaction returned HTTP ${response.status}`);
663
+ const text = compactText(await response.json());
664
+ if (text.length === 0) throw new Error("remote compaction returned no compaction text");
665
+ return text;
666
+ }
667
+ /** Remote-first compaction waterfall with the existing DSH path as fallback. */
668
+ function remoteCompactStream(ctx, options, next) {
669
+ return (async function* () {
670
+ try {
671
+ const text = await remoteCompact(ctx, options);
672
+ yield {
673
+ type: "block-start",
674
+ index: 0,
675
+ blockType: "text"
676
+ };
677
+ yield {
678
+ type: "text-delta",
679
+ index: 0,
680
+ text
681
+ };
682
+ yield {
683
+ type: "block-end",
684
+ index: 0,
685
+ block: {
686
+ type: "text",
687
+ text
688
+ }
689
+ };
690
+ yield {
691
+ type: "usage",
692
+ usage: {
693
+ inputTokens: 0,
694
+ outputTokens: 0
695
+ }
696
+ };
697
+ yield {
698
+ type: "finish",
699
+ reason: { kind: "stop" }
700
+ };
701
+ } catch (error) {
702
+ if (options.signal?.aborted) throw error;
703
+ ctx.logger.warn("codex: remote compaction failed; using dsh-compaction-basic fallback");
704
+ ctx.logger.warn(error);
705
+ yield* hostedWebSearchStream(next);
706
+ }
707
+ })();
708
+ }
709
+ //#endregion
402
710
  //#region lib/types/index.js
403
711
  /** Codex-compatible prompt overlay and core tools for a dsh agent preset. */
404
712
  const name = "codex";
@@ -406,15 +714,132 @@ const inject = [
406
714
  "tools",
407
715
  "systemPrompt",
408
716
  "shell",
409
- "fs"
717
+ "fs",
718
+ "llm",
719
+ "credentials",
720
+ "settings"
410
721
  ];
411
722
  /** Runtime configuration schema for the Codex tool bridge. */
412
723
  const Config = z.object({
413
724
  defaultYieldTimeMs: z.number().step(1).min(0).default(1e4),
414
725
  pollYieldTimeMs: z.number().step(1).min(0).default(5e3),
415
726
  writeYieldTimeMs: z.number().step(1).min(0).default(250),
416
- maxOutputBytes: z.number().step(1).min(1).default(64e3)
727
+ maxOutputBytes: z.number().step(1).min(1).default(64e3),
728
+ hostedWebSearch: z.boolean().default(true),
729
+ remoteCompact: z.boolean().default(true)
730
+ });
731
+ const LLM_PI_AI_SETTINGS = settingsNamespace("llm-pi-ai");
732
+ /** Live Codex-only request controls shared by the Web controls and agent layer. */
733
+ const CODEX_SETTINGS_NAMESPACE = settingsNamespace("codex");
734
+ const CODEX_SETTINGS_SCHEMA = z.object({
735
+ fast: z.boolean().default(false),
736
+ contextWindow: z.number().step(1).min(1).max(CODEX_CONTEXT_MAX)
417
737
  });
738
+ const CODEX_SETTINGS_ENTRY = { fast: false };
739
+ const GPT_REASONING_EFFORTS = {
740
+ low: "low",
741
+ medium: "medium",
742
+ high: "high",
743
+ xhigh: "xhigh",
744
+ max: "max"
745
+ };
746
+ function codexReasoningEfforts(configured) {
747
+ if (configured === false) return false;
748
+ const filtered = configured === void 0 ? {} : Object.fromEntries(Object.entries(configured).filter(([level]) => level !== "off" && level !== "minimal"));
749
+ return Object.keys(filtered).length === 0 ? GPT_REASONING_EFFORTS : filtered;
750
+ }
751
+ /** GPT model ids are the only models whose relay capabilities Codex fills in. */
752
+ function isGptModel(id) {
753
+ return /(?:^|\/)(?:gpt|chatgpt)(?:[-_.]|\d|$)/i.test(id);
754
+ }
755
+ /** Add Codex defaults without overwriting explicit user capabilities. */
756
+ function enrichCodexModel(model) {
757
+ if (!isGptModel(model.id)) return model;
758
+ const input = model.input === void 0 || model.input.length === 0 ? ["text", "image"] : model.input;
759
+ const efforts = codexReasoningEfforts(model.reasoningEfforts);
760
+ if (input === model.input && efforts === model.reasoningEfforts) return model;
761
+ return {
762
+ ...model,
763
+ input,
764
+ reasoningEfforts: efforts
765
+ };
766
+ }
767
+ /** The settings schema keys modelOverrides by id, so its values must not carry an id field. */
768
+ function enrichCodexOverride(id, model) {
769
+ if (!isGptModel(id)) return model;
770
+ const { id: _id, ...withoutId } = enrichCodexModel({
771
+ ...model,
772
+ id
773
+ });
774
+ const inputMissing = model.input === void 0 || model.input.length === 0;
775
+ const nextEfforts = codexReasoningEfforts(model.reasoningEfforts);
776
+ const reasoningChanged = JSON.stringify(nextEfforts) !== JSON.stringify(model.reasoningEfforts);
777
+ if (!inputMissing && !reasoningChanged) return model;
778
+ return {
779
+ ...withoutId,
780
+ reasoningEfforts: nextEfforts
781
+ };
782
+ }
783
+ /** Persist only missing GPT capabilities into the user's existing pi-ai model config. */
784
+ async function enrichConfiguredGptModels(ctx) {
785
+ const settings = ctx.get("settings");
786
+ if (settings === void 0) return;
787
+ const current = settings.get(LLM_PI_AI_SETTINGS);
788
+ if (current?.providers === void 0) return;
789
+ const providers = {};
790
+ let changed = false;
791
+ for (const [provider, profile] of Object.entries(current.providers)) {
792
+ const models = profile.models;
793
+ const overrides = profile.modelOverrides;
794
+ const nextModels = Array.isArray(models) ? models.map(enrichCodexModel) : void 0;
795
+ const nextOverrides = overrides === void 0 ? void 0 : Object.fromEntries(Object.entries(overrides).map(([id, model]) => [id, enrichCodexOverride(id, model)]));
796
+ const modelsChanged = nextModels !== void 0 && nextModels.some((model, index) => model !== models?.[index]);
797
+ const overridesChanged = nextOverrides !== void 0 && overrides !== void 0 && Object.entries(nextOverrides).some(([id, model]) => model !== overrides[id]);
798
+ if (modelsChanged || overridesChanged) {
799
+ changed = true;
800
+ providers[provider] = {
801
+ ...modelsChanged ? { models: nextModels } : {},
802
+ ...overridesChanged ? { modelOverrides: nextOverrides } : {}
803
+ };
804
+ }
805
+ }
806
+ if (changed) await settings.update(LLM_PI_AI_SETTINGS, { providers });
807
+ }
808
+ /** Keep newly edited GPT model entries enriched while Codex mode is mounted. */
809
+ function watchConfiguredGptModels(ctx) {
810
+ let tail = Promise.resolve();
811
+ const schedule = () => {
812
+ tail = tail.then(() => enrichConfiguredGptModels(ctx)).catch((error) => {
813
+ ctx.logger.warn("codex: could not enrich configured GPT model capabilities");
814
+ ctx.logger.warn(error);
815
+ });
816
+ };
817
+ ctx.on("settings/document-updated", (ns) => {
818
+ if (ns === LLM_PI_AI_SETTINGS) schedule();
819
+ });
820
+ schedule();
821
+ }
822
+ /** Install the optional settings source used by the request waterfall. */
823
+ function installCodexSettings(ctx) {
824
+ let source = () => CODEX_SETTINGS_ENTRY;
825
+ installSettingsSection(ctx, CODEX_SETTINGS_NAMESPACE, CODEX_SETTINGS_SCHEMA, CODEX_SETTINGS_ENTRY, {
826
+ setSource: (current) => {
827
+ source = current;
828
+ },
829
+ onChange: () => {}
830
+ });
831
+ return { current: () => source() };
832
+ }
833
+ /** Apply the live Codex controls to one agent request without leaking them to other routes. */
834
+ function applyCodexRequestSettings(request, settings) {
835
+ const { contextWindow: _inheritedContextWindow, serviceTier: _inheritedServiceTier, ...withoutCodexControls } = request;
836
+ if (!isGptModel(request.model)) return withoutCodexControls;
837
+ return {
838
+ ...withoutCodexControls,
839
+ ...settings.contextWindow === void 0 ? {} : { contextWindow: settings.contextWindow },
840
+ ...settings.fast ? { serviceTier: "priority" } : {}
841
+ };
842
+ }
418
843
  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.
419
844
 
420
845
  ## General
@@ -821,9 +1246,25 @@ function apply(ctx, config = {}) {
821
1246
  defaultYieldTimeMs: config.defaultYieldTimeMs ?? 1e4,
822
1247
  pollYieldTimeMs: config.pollYieldTimeMs ?? 5e3,
823
1248
  writeYieldTimeMs: config.writeYieldTimeMs ?? 250,
824
- maxOutputBytes: config.maxOutputBytes ?? 64e3
1249
+ maxOutputBytes: config.maxOutputBytes ?? 64e3,
1250
+ hostedWebSearch: config.hostedWebSearch ?? true,
1251
+ remoteCompact: config.remoteCompact ?? true
825
1252
  };
826
1253
  if (ctx.fs.sandboxMode !== void 0 && ctx.get("sandboxPolicy") === void 0) throw new Error("codex: a sandboxing filesystem requires ctx.sandboxPolicy");
1254
+ watchConfiguredGptModels(ctx);
1255
+ const codexSettings = installCodexSettings(ctx);
1256
+ ctx.on("agent/request", async (_payload, next) => {
1257
+ return applyCodexRequestSettings(await next(), codexSettings.current());
1258
+ });
1259
+ if ((resolved.hostedWebSearch || resolved.remoteCompact) && typeof ctx.effect === "function") {
1260
+ const disposeHostedWebSearch = installHostedWebSearch();
1261
+ ctx.effect(() => disposeHostedWebSearch, "codex: Responses transport wrapper");
1262
+ }
1263
+ if (resolved.hostedWebSearch || resolved.remoteCompact) ctx.on("llm/stream", ((options, next) => {
1264
+ if (resolved.remoteCompact && options.purpose === "compaction" && isGptModel(options.model)) return remoteCompactStream(ctx, options, next);
1265
+ if ((resolved.hostedWebSearch || resolved.remoteCompact) && options.purpose === void 0 && isGptModel(options.model)) return hostedWebSearchStream(next);
1266
+ return next();
1267
+ }));
827
1268
  ctx.systemPrompt.section({
828
1269
  name: "codex:base",
829
1270
  order: 10,
@@ -840,4 +1281,4 @@ var types_default = {
840
1281
  apply
841
1282
  };
842
1283
  //#endregion
843
- export { Config, apply, types_default as default, inject, name };
1284
+ export { CODEX_SETTINGS_NAMESPACE, CODEX_SETTINGS_SCHEMA, Config, apply, applyCodexRequestSettings, types_default as default, enrichCodexModel, inject, name };
@@ -0,0 +1,40 @@
1
+ /** Browser controls for the Codex request settings mounted by the Host plugin. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ declare const en: {
4
+ readonly fast: "Fast mode";
5
+ readonly fastOn: "Fast mode on (priority tier)";
6
+ readonly fastOff: "Fast mode off";
7
+ readonly fastStateOn: "On";
8
+ readonly fastStateOff: "Off";
9
+ readonly contextSize: "Context size";
10
+ readonly contextSizeDescription: "Next request capacity in K tokens. The meter above is current.";
11
+ readonly contextRestore: "Restore model default";
12
+ };
13
+ type CodexKey = keyof typeof en;
14
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
15
+ interface LocaleNamespaceMap {
16
+ codex: CodexKey;
17
+ }
18
+ interface SlotMap {
19
+ 'conversation.input.model.settings': {
20
+ kind: 'list';
21
+ scope: 'session';
22
+ };
23
+ 'conversation.input.context.settings': {
24
+ kind: 'list';
25
+ scope: 'session';
26
+ owner: {
27
+ contextWindow: number;
28
+ };
29
+ };
30
+ }
31
+ }
32
+ export declare const inject: string[];
33
+ /** Mount Fast inside the model selector and context size inside the meter panel. */
34
+ export declare function apply(ctx: Context): void;
35
+ declare const _default: {
36
+ inject: string[];
37
+ apply: typeof apply;
38
+ };
39
+ export default _default;
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CODEX_CONTEXT_MAX, CODEX_CONTEXT_UNIT, CODEX_PRESET_ID } from "../context.js";
3
+ const NS = 'codex';
4
+ const SETTINGS_NAMESPACE = 'codex';
5
+ const en = {
6
+ fast: 'Fast mode',
7
+ fastOn: 'Fast mode on (priority tier)',
8
+ fastOff: 'Fast mode off',
9
+ fastStateOn: 'On',
10
+ fastStateOff: 'Off',
11
+ contextSize: 'Context size',
12
+ contextSizeDescription: 'Next request capacity in K tokens. The meter above is current.',
13
+ contextRestore: 'Restore model default',
14
+ };
15
+ const zh = {
16
+ fast: 'Fast',
17
+ fastOn: 'Fast mode on (priority tier)',
18
+ fastOff: 'Fast mode off',
19
+ fastStateOn: '开',
20
+ fastStateOff: '关',
21
+ contextSize: '上下文大小',
22
+ contextSizeDescription: '设置下次请求的上下文容量,单位为 K tokens;上方是当前请求。',
23
+ contextRestore: '恢复模型默认值',
24
+ };
25
+ function FastModeButton({ sessionId, useSessions, useSettings, setSetting, t }) {
26
+ const agentPreset = useSessions(state => state.byId[sessionId]?.agentPreset);
27
+ const snapshot = useSettings(state => state);
28
+ const fast = snapshot.value?.fast ?? false;
29
+ if (agentPreset !== CODEX_PRESET_ID)
30
+ return null;
31
+ 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: {
32
+ boxSizing: 'border-box',
33
+ display: 'flex',
34
+ alignItems: 'center',
35
+ justifyContent: 'space-between',
36
+ width: '100%',
37
+ height: 40,
38
+ border: 0,
39
+ borderRadius: 10,
40
+ padding: '0 10px',
41
+ color: fast ? 'var(--dsw-static-blue-500)' : 'var(--dsw-alias-label-secondary)',
42
+ background: fast ? 'var(--dsw-alias-interactive-bg-selected)' : 'transparent',
43
+ cursor: snapshot.writable === false ? 'default' : 'pointer',
44
+ font: 'inherit',
45
+ fontSize: 14,
46
+ lineHeight: '22px',
47
+ fontWeight: fast ? 600 : 400,
48
+ textAlign: 'left',
49
+ }, children: [_jsx("span", { children: t('fast') }), _jsx("span", { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 }, children: fast ? t('fastStateOn') : t('fastStateOff') })] }));
50
+ }
51
+ function ContextSizeControl({ contextWindow, useSettings, setSetting, unsetSetting, t }) {
52
+ const snapshot = useSettings(state => state);
53
+ const configured = snapshot.value?.contextWindow;
54
+ const maxK = CODEX_CONTEXT_MAX / CODEX_CONTEXT_UNIT;
55
+ const valueK = Math.min(maxK, Math.max(1, Math.round((configured ?? contextWindow) / CODEX_CONTEXT_UNIT)));
56
+ const setValueK = (nextK) => {
57
+ if (!Number.isFinite(nextK))
58
+ return;
59
+ const normalizedK = Math.min(maxK, Math.max(1, Math.trunc(nextK)));
60
+ const normalized = normalizedK * CODEX_CONTEXT_UNIT;
61
+ if (normalized === contextWindow)
62
+ void unsetSetting('contextWindow');
63
+ else
64
+ void setSetting('contextWindow', normalized);
65
+ };
66
+ return (_jsxs("div", { style: { marginTop: 10, display: 'grid', gap: 5 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }, children: [_jsx("strong", { style: { color: 'var(--dsw-alias-label-primary)', fontSize: 12 }, children: t('contextSize') }), _jsxs("span", { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 11 }, children: [valueK, "K"] })] }), _jsx("input", { type: "range", min: 1, max: maxK, step: 1, value: valueK, disabled: snapshot.writable === false, "aria-label": t('contextSize'), onChange: event => { setValueK(Number(event.target.value)); } }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 7 }, children: [_jsx("input", { type: "number", min: 1, max: maxK, step: 1, value: valueK, disabled: snapshot.writable === false, "aria-label": t('contextSize'), onChange: event => { setValueK(Number(event.target.value)); }, style: {
67
+ width: 76,
68
+ border: '1px solid var(--dsw-alias-border-primary)',
69
+ borderRadius: 5,
70
+ padding: '3px 5px',
71
+ color: 'var(--dsw-alias-label-primary)',
72
+ background: 'var(--dsw-alias-fill-primary)',
73
+ font: 'inherit',
74
+ fontSize: 11,
75
+ } }), _jsx("span", { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 11 }, children: "K" }), _jsx("span", { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 11 }, children: t('contextSizeDescription') })] }), configured !== undefined && (_jsx("button", { type: "button", onClick: () => { void unsetSetting('contextWindow'); }, style: {
76
+ justifySelf: 'start',
77
+ border: 0,
78
+ padding: 0,
79
+ color: 'var(--dsw-static-blue-500)',
80
+ background: 'transparent',
81
+ cursor: 'pointer',
82
+ font: 'inherit',
83
+ fontSize: 11,
84
+ }, children: t('contextRestore') }))] }));
85
+ }
86
+ export const inject = ['slots', 'locale', 'settingsScope'];
87
+ /** Mount Fast inside the model selector and context size inside the meter panel. */
88
+ export function apply(ctx) {
89
+ ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'codex client: dictionaries');
90
+ const settings = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
91
+ const injected = () => ({
92
+ hooks: { settings },
93
+ setSetting: (field, value) => settings.set(field, value),
94
+ unsetSetting: (field) => settings.unset(field),
95
+ });
96
+ ctx.slots.inject('conversation.input.model.settings', () => ctx.slots.register({
97
+ name: 'conversation.input.model.settings',
98
+ id: 'codex-fast',
99
+ order: 0,
100
+ locale: NS,
101
+ inject: injected,
102
+ }, FastModeButton));
103
+ ctx.slots.inject('conversation.input.context.settings', () => ctx.slots.register({
104
+ name: 'conversation.input.context.settings',
105
+ id: 'codex-context-size',
106
+ order: 0,
107
+ locale: NS,
108
+ inject: injected,
109
+ }, ContextSizeControl));
110
+ }
111
+ export default { inject, apply };
112
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,5 @@
1
+ /** User-selectable Codex context controls use whole K-token units. */
2
+ export declare const CODEX_CONTEXT_UNIT = 1000;
3
+ export declare const CODEX_CONTEXT_MAX = 1000000;
4
+ export declare const CODEX_PRESET_ID = "codex";
5
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,5 @@
1
+ /** User-selectable Codex context controls use whole K-token units. */
2
+ export const CODEX_CONTEXT_UNIT = 1_000;
3
+ export const CODEX_CONTEXT_MAX = 1_000_000;
4
+ export const CODEX_PRESET_ID = 'codex';
5
+ //# sourceMappingURL=context.js.map