farming-code 2.2.8 → 2.2.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.
Files changed (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1592 -741
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +54 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -0,0 +1,500 @@
1
+ // One deadline covers the complete picker transaction, including best-effort
2
+ // Escape cleanup. Individual menu steps must not each restart this budget.
3
+ const DEFAULT_TIMEOUT_MS = 30_000;
4
+ const DEFAULT_POLL_INTERVAL_MS = 100;
5
+ const MAX_CLEANUP_RESERVE_MS = 1_000;
6
+
7
+ function sleep(ms) {
8
+ return new Promise(resolve => setTimeout(resolve, ms));
9
+ }
10
+
11
+ function abortError(signal, fallbackMessage) {
12
+ const reason = signal?.reason;
13
+ if (reason instanceof Error) return reason;
14
+ const error = new Error(typeof reason === 'string' && reason ? reason : fallbackMessage);
15
+ error.name = 'AbortError';
16
+ return error;
17
+ }
18
+
19
+ function throwIfAborted(signal, fallbackMessage = 'Codex Terminal profile update was canceled') {
20
+ if (signal?.aborted) throw abortError(signal, fallbackMessage);
21
+ }
22
+
23
+ function withDeadline(value, options = {}) {
24
+ const deadline = Number(options.deadline);
25
+ const signal = options.signal;
26
+ const timeoutMessage = options.timeoutMessage || 'Timed out applying the Codex Terminal profile';
27
+ throwIfAborted(signal);
28
+ if (!Number.isFinite(deadline)) return Promise.resolve(value);
29
+ const remainingMs = deadline - Date.now();
30
+ if (remainingMs <= 0) return Promise.reject(new Error(timeoutMessage));
31
+
32
+ return new Promise((resolve, reject) => {
33
+ let settled = false;
34
+ const finish = (callback, result) => {
35
+ if (settled) return;
36
+ settled = true;
37
+ clearTimeout(timer);
38
+ signal?.removeEventListener('abort', onAbort);
39
+ callback(result);
40
+ };
41
+ const onAbort = () => finish(reject, abortError(signal, 'Codex Terminal profile update was canceled'));
42
+ const timer = setTimeout(() => finish(reject, new Error(timeoutMessage)), remainingMs);
43
+ signal?.addEventListener('abort', onAbort, { once: true });
44
+ Promise.resolve(value).then(
45
+ result => finish(resolve, result),
46
+ error => finish(reject, error),
47
+ );
48
+ });
49
+ }
50
+
51
+ function callWithDeadline(operation, options = {}) {
52
+ throwIfAborted(options.signal);
53
+ return withDeadline(Promise.resolve().then(operation), options);
54
+ }
55
+
56
+ function normalizedValue(value) {
57
+ return String(value || '').trim().toLowerCase();
58
+ }
59
+
60
+ function normalizedReasoning(value) {
61
+ const normalized = normalizedValue(value).replace(/[\s_-]+/g, '');
62
+ if (normalized === 'extrahigh') return 'xhigh';
63
+ return normalized;
64
+ }
65
+
66
+ function stripAnsi(value) {
67
+ return String(value || '').replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
68
+ }
69
+
70
+ function codexServiceTierConfirmations(outputText) {
71
+ const text = stripAnsi(outputText).replace(/\r/g, '\n');
72
+ return Array.from(text.matchAll(
73
+ /(?:^|\n)\s*[•●]\s+(?:Service tier set to\s+(priority|default)\b|Fast mode is\s+(on|off)\b)/gi
74
+ )).map(match => {
75
+ const fast = match[1]
76
+ ? normalizedValue(match[1]) === 'priority'
77
+ : normalizedValue(match[2]) === 'on';
78
+ return {
79
+ serviceTier: fast ? 'priority' : 'default',
80
+ fast,
81
+ };
82
+ });
83
+ }
84
+
85
+ function newCodexServiceTierConfirmation(previousOutput, currentOutput) {
86
+ const previous = stripAnsi(previousOutput);
87
+ const current = stripAnsi(currentOutput);
88
+ const previousConfirmations = codexServiceTierConfirmations(previous);
89
+ const currentConfirmations = codexServiceTierConfirmations(current);
90
+ if (current.startsWith(previous)) {
91
+ return codexServiceTierConfirmations(current.slice(previous.length)).at(-1) || null;
92
+ }
93
+ if (currentConfirmations.length > previousConfirmations.length) {
94
+ return currentConfirmations.at(-1) || null;
95
+ }
96
+ const previousLast = previousConfirmations.at(-1) || null;
97
+ const currentLast = currentConfirmations.at(-1) || null;
98
+ if (!currentLast) return null;
99
+ if (!previousLast || currentLast.serviceTier !== previousLast.serviceTier) return currentLast;
100
+ return null;
101
+ }
102
+
103
+ function terminalCommand(command) {
104
+ return [{ type: 'paste', text: command }, '\r'];
105
+ }
106
+
107
+ function numberedOptionsAfter(previewText, headingPattern) {
108
+ const text = String(previewText || '');
109
+ const matches = Array.from(text.matchAll(headingPattern));
110
+ const heading = matches[matches.length - 1];
111
+ if (!heading || typeof heading.index !== 'number') return null;
112
+
113
+ const options = [];
114
+ const body = text.slice(heading.index + heading[0].length);
115
+ for (const line of body.split(/\r?\n/)) {
116
+ const match = line.match(/^\s*(?:[>›❯]\s*)?(\d{1,2})[.)]?\s+(.+?)\s*$/u);
117
+ if (!match) continue;
118
+ options.push({
119
+ input: match[1],
120
+ label: match[2].replace(/\s{2,}.*$/, '').trim(),
121
+ line: match[2].trim(),
122
+ });
123
+ }
124
+ return options;
125
+ }
126
+
127
+ function codexModelMenuOptions(previewText) {
128
+ return numberedOptionsAfter(previewText, /Select Model and Effort/gi);
129
+ }
130
+
131
+ function codexReasoningMenuOptions(previewText) {
132
+ return numberedOptionsAfter(previewText, /Select Reasoning Level for\s+[^\r\n]+/gi);
133
+ }
134
+
135
+ function codexAdvancedReasoningMenuOptions(previewText) {
136
+ return numberedOptionsAfter(previewText, /Advanced Reasoning/gi);
137
+ }
138
+
139
+ function modelSelectionInput(previewText, model) {
140
+ const target = normalizedValue(model);
141
+ const options = codexModelMenuOptions(previewText);
142
+ if (!options) return null;
143
+ const option = options.find(item => {
144
+ const firstToken = item.line.match(/^([A-Za-z0-9][A-Za-z0-9._:/-]*)\b/)?.[1];
145
+ return normalizedValue(firstToken) === target;
146
+ });
147
+ return option?.input || '';
148
+ }
149
+
150
+ function reasoningSelectionInput(previewText, effort) {
151
+ const target = normalizedReasoning(effort);
152
+ const options = codexAdvancedReasoningMenuOptions(previewText)
153
+ || codexReasoningMenuOptions(previewText);
154
+ if (!options) return null;
155
+ const option = options.find(item => {
156
+ const label = normalizedReasoning(item.label);
157
+ const line = normalizedReasoning(item.line);
158
+ return label === target || line === target || line.startsWith(target);
159
+ });
160
+ return option?.input || '';
161
+ }
162
+
163
+ function moreReasoningSelectionInput(previewText) {
164
+ const options = codexReasoningMenuOptions(previewText);
165
+ if (!options) return null;
166
+ const option = options.find(item => normalizedReasoning(item.label).startsWith('morereasoning'));
167
+ return option?.input || '';
168
+ }
169
+
170
+ function codexTerminalProfileFromPreview(previewText) {
171
+ const text = String(previewText || '');
172
+ const matches = Array.from(text.matchAll(
173
+ /\b([A-Za-z0-9][A-Za-z0-9._:/-]*-[A-Za-z0-9._-]+)\s+(minimal|low|medium|high|xhigh|extra\s+high|max|ultra)\b(\s+fast\b)?/gi
174
+ ));
175
+ const match = matches[matches.length - 1];
176
+ if (!match) return null;
177
+ const confirmedTier = codexServiceTierConfirmations(text).at(-1) || null;
178
+ return {
179
+ model: normalizedValue(match[1]),
180
+ effort: normalizedReasoning(match[2]),
181
+ fast: match[3] ? true : (confirmedTier ? confirmedTier.fast : null),
182
+ };
183
+ }
184
+
185
+ function codexTerminalProfileFromOutput(outputText) {
186
+ const text = stripAnsi(outputText).replace(/\r/g, '\n');
187
+ const matches = Array.from(text.matchAll(
188
+ /(?:^|\n)\s*[•●]\s+Model changed to\s+([A-Za-z0-9][A-Za-z0-9._:/-]*-[A-Za-z0-9._-]+)\s+(minimal|low|medium|high|xhigh|extra\s+high|max|ultra)\b/gi
189
+ ));
190
+ const match = matches.at(-1);
191
+ if (!match) return null;
192
+ const confirmedTier = codexServiceTierConfirmations(text).at(-1) || null;
193
+ return {
194
+ model: normalizedValue(match[1]),
195
+ effort: normalizedReasoning(match[2]),
196
+ fast: confirmedTier ? confirmedTier.fast : null,
197
+ };
198
+ }
199
+
200
+ function profileMatches(current, target, options = {}) {
201
+ if (!current) return false;
202
+ if (normalizedValue(current.model) !== normalizedValue(target.model)) return false;
203
+ if (normalizedReasoning(current.effort) !== normalizedReasoning(target.effort)) return false;
204
+ if (options.includeFast === true) {
205
+ return current.fast === (target.serviceTier === 'priority');
206
+ }
207
+ return true;
208
+ }
209
+
210
+ async function waitForPreview(readPreview, predicate, options = {}) {
211
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
212
+ const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
213
+ ? options.pollIntervalMs
214
+ : DEFAULT_POLL_INTERVAL_MS;
215
+ const sleepFn = typeof options.sleep === 'function' ? options.sleep : sleep;
216
+ const deadline = Number.isFinite(options.deadline) ? options.deadline : Date.now() + timeoutMs;
217
+
218
+ for (;;) {
219
+ const preview = String(await callWithDeadline(readPreview, {
220
+ deadline,
221
+ signal: options.signal,
222
+ timeoutMessage: options.timeoutMessage || 'Timed out waiting for Codex Terminal',
223
+ }) || '');
224
+ const result = predicate(preview);
225
+ if (result) return { preview, result };
226
+ if (Date.now() >= deadline) throw new Error(options.timeoutMessage || 'Timed out waiting for Codex Terminal');
227
+ await callWithDeadline(
228
+ () => sleepFn(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()))),
229
+ {
230
+ deadline,
231
+ signal: options.signal,
232
+ timeoutMessage: options.timeoutMessage || 'Timed out waiting for Codex Terminal',
233
+ },
234
+ );
235
+ }
236
+ }
237
+
238
+ function validateTargetProfile(profile) {
239
+ const model = String(profile?.model || '').trim();
240
+ const effort = String(profile?.effort || '').trim();
241
+ const serviceTier = profile?.serviceTier === 'priority' ? 'priority' : 'default';
242
+ if (!model || model.length > 120 || /[\u0000-\u001f\u007f\s]/.test(model)) {
243
+ throw new Error('A valid Codex model is required');
244
+ }
245
+ if (!effort || effort.length > 40 || /[\u0000-\u001f\u007f]/.test(effort)) {
246
+ throw new Error('A valid Codex reasoning effort is required');
247
+ }
248
+ return { model, effort, serviceTier };
249
+ }
250
+
251
+ async function applyCodexTerminalProfile({
252
+ profile,
253
+ readPreview,
254
+ readOutput,
255
+ sendInput,
256
+ onInputSafe,
257
+ timeoutMs = DEFAULT_TIMEOUT_MS,
258
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
259
+ sleep: sleepFn,
260
+ signal,
261
+ }) {
262
+ const target = validateTargetProfile(profile);
263
+ const totalTimeoutMs = Math.max(1, Number.isFinite(timeoutMs) ? timeoutMs : DEFAULT_TIMEOUT_MS);
264
+ const totalDeadline = Date.now() + totalTimeoutMs;
265
+ const cleanupReserveMs = Math.min(
266
+ MAX_CLEANUP_RESERVE_MS,
267
+ Math.max(1, Math.floor(totalTimeoutMs / 10)),
268
+ );
269
+ const operationDeadline = Math.max(Date.now(), totalDeadline - cleanupReserveMs);
270
+ const waitOptions = {
271
+ deadline: operationDeadline,
272
+ pollIntervalMs,
273
+ sleep: sleepFn,
274
+ signal,
275
+ };
276
+ const runStep = (operation, timeoutMessage) => callWithDeadline(operation, {
277
+ deadline: operationDeadline,
278
+ signal,
279
+ timeoutMessage,
280
+ });
281
+ let inputSafe = false;
282
+ const markInputSafe = () => {
283
+ if (inputSafe) return;
284
+ inputSafe = true;
285
+ if (typeof onInputSafe === 'function') onInputSafe();
286
+ };
287
+ let preview = String(await runStep(readPreview, 'Timed out reading the Codex Terminal') || '');
288
+ let current = codexTerminalProfileFromPreview(preview);
289
+ if (!current) {
290
+ throw new Error('Codex Terminal is not idle; wait for its composer before changing the model');
291
+ }
292
+ if (
293
+ codexModelMenuOptions(preview)
294
+ || codexReasoningMenuOptions(preview)
295
+ || codexAdvancedReasoningMenuOptions(preview)
296
+ ) {
297
+ throw new Error('Close the active Codex Terminal menu before changing the model');
298
+ }
299
+
300
+ let pickerDepth = 0;
301
+ try {
302
+ if (!profileMatches(current, target)) {
303
+ // Sending into a TUI is an uncertain commit: the PTY may receive the
304
+ // command even if the transport reply is lost. Anticipate the menu
305
+ // depth before the write so bounded cleanup always sends enough Escape.
306
+ pickerDepth = 1;
307
+ await runStep(
308
+ () => sendInput(terminalCommand('/model')),
309
+ 'Timed out opening the Codex model menu',
310
+ );
311
+ const modelMenu = await waitForPreview(
312
+ readPreview,
313
+ text => {
314
+ const options = codexModelMenuOptions(text);
315
+ return options && options.length > 0 ? options : null;
316
+ },
317
+ {
318
+ ...waitOptions,
319
+ timeoutMessage: 'Codex did not open its model menu',
320
+ }
321
+ );
322
+ const modelInput = modelSelectionInput(modelMenu.preview, target.model);
323
+ if (!modelInput) {
324
+ throw new Error(`Model ${target.model} is not available in this Codex CLI`);
325
+ }
326
+ await runStep(
327
+ () => sendInput(modelInput),
328
+ `Timed out selecting model ${target.model}`,
329
+ );
330
+
331
+ const reasoningStep = await waitForPreview(
332
+ readPreview,
333
+ text => {
334
+ const nextProfile = codexTerminalProfileFromPreview(text);
335
+ if (profileMatches(nextProfile, target)) return { complete: true };
336
+ const options = codexReasoningMenuOptions(text);
337
+ return options && options.length > 0 ? { options } : null;
338
+ },
339
+ {
340
+ ...waitOptions,
341
+ timeoutMessage: `Codex did not open the reasoning menu for ${target.model}`,
342
+ }
343
+ );
344
+
345
+ if (!reasoningStep.result.complete) {
346
+ let reasoningInput = reasoningSelectionInput(reasoningStep.preview, target.effort);
347
+ if (!reasoningInput) {
348
+ const moreInput = moreReasoningSelectionInput(reasoningStep.preview);
349
+ if (moreInput) {
350
+ pickerDepth = 2;
351
+ await runStep(
352
+ () => sendInput(moreInput),
353
+ `Timed out opening advanced reasoning for ${target.model}`,
354
+ );
355
+ const advancedStep = await waitForPreview(
356
+ readPreview,
357
+ text => {
358
+ const options = codexAdvancedReasoningMenuOptions(text);
359
+ return options && options.length > 0 ? options : null;
360
+ },
361
+ {
362
+ ...waitOptions,
363
+ timeoutMessage: `Codex did not open advanced reasoning for ${target.model}`,
364
+ }
365
+ );
366
+ reasoningInput = reasoningSelectionInput(advancedStep.preview, target.effort);
367
+ }
368
+ }
369
+ if (!reasoningInput) {
370
+ throw new Error(`Reasoning effort ${target.effort} is not available for ${target.model}`);
371
+ }
372
+ await runStep(
373
+ () => sendInput(reasoningInput),
374
+ `Timed out selecting ${target.effort} reasoning for ${target.model}`,
375
+ );
376
+ const applied = await waitForPreview(
377
+ readPreview,
378
+ text => profileMatches(codexTerminalProfileFromPreview(text), target),
379
+ {
380
+ ...waitOptions,
381
+ timeoutMessage: `Codex did not confirm ${target.model} ${target.effort}`,
382
+ }
383
+ );
384
+ pickerDepth = 0;
385
+ preview = applied.preview;
386
+ current = codexTerminalProfileFromPreview(preview);
387
+ } else {
388
+ pickerDepth = 0;
389
+ preview = reasoningStep.preview;
390
+ current = codexTerminalProfileFromPreview(preview);
391
+ }
392
+ }
393
+
394
+ const wantsFast = target.serviceTier === 'priority';
395
+ if (!current) {
396
+ current = codexTerminalProfileFromPreview(String(await runStep(
397
+ readPreview,
398
+ 'Timed out reading the active Codex model',
399
+ ) || ''));
400
+ }
401
+ if (!current) throw new Error('Codex Terminal stopped reporting its active model');
402
+ if (current.fast !== wantsFast) {
403
+ const fastCommand = `/fast ${wantsFast ? 'on' : 'off'}`;
404
+ if (typeof readOutput === 'function') {
405
+ const previousOutput = String(await runStep(
406
+ readOutput,
407
+ 'Timed out reading Codex Terminal output',
408
+ ) || '');
409
+ await runStep(
410
+ () => sendInput(terminalCommand(fastCommand)),
411
+ `Timed out sending ${fastCommand}`,
412
+ );
413
+ // `/fast on|off` is one non-interactive slash command. Once its full
414
+ // input has been accepted, later Terminal input cannot leak into a
415
+ // picker and may proceed while confirmation is observed separately.
416
+ markInputSafe();
417
+ const confirmation = await waitForPreview(
418
+ readOutput,
419
+ output => {
420
+ const explicit = newCodexServiceTierConfirmation(previousOutput, output);
421
+ if (explicit) return explicit;
422
+ const renderedProfile = codexTerminalProfileFromPreview(stripAnsi(output));
423
+ if (profileMatches(renderedProfile, target, { includeFast: true })) {
424
+ return { serviceTier: wantsFast ? 'priority' : 'default', fast: wantsFast };
425
+ }
426
+ return null;
427
+ },
428
+ {
429
+ ...waitOptions,
430
+ timeoutMessage: `Codex did not confirm its Fast mode service tier`,
431
+ }
432
+ );
433
+ const confirmed = confirmation.result;
434
+ if (confirmed.fast !== wantsFast) {
435
+ throw new Error(`Codex did not ${wantsFast ? 'enable' : 'disable'} Fast mode`);
436
+ }
437
+ current = { ...current, fast: confirmed.fast };
438
+ } else {
439
+ await runStep(
440
+ () => sendInput(terminalCommand(fastCommand)),
441
+ `Timed out sending ${fastCommand}`,
442
+ );
443
+ markInputSafe();
444
+ const fastApplied = await waitForPreview(
445
+ readPreview,
446
+ text => profileMatches(codexTerminalProfileFromPreview(text), target, { includeFast: true }),
447
+ {
448
+ ...waitOptions,
449
+ timeoutMessage: `Codex did not ${wantsFast ? 'enable' : 'disable'} Fast mode`,
450
+ }
451
+ );
452
+ preview = fastApplied.preview;
453
+ current = codexTerminalProfileFromPreview(preview);
454
+ }
455
+ }
456
+
457
+ markInputSafe();
458
+
459
+ return {
460
+ model: current.model,
461
+ effort: current.effort,
462
+ serviceTier: current.fast ? 'priority' : 'default',
463
+ };
464
+ } catch (error) {
465
+ while (pickerDepth > 0) {
466
+ try {
467
+ await callWithDeadline(() => sendInput('\x1b'), {
468
+ deadline: totalDeadline,
469
+ // A client disconnect still cancels the transaction, but once a
470
+ // picker was opened we use the reserved bounded window to close it.
471
+ timeoutMessage: 'Timed out closing the Codex Terminal menu',
472
+ });
473
+ } catch {
474
+ break;
475
+ }
476
+ pickerDepth -= 1;
477
+ }
478
+ throw error;
479
+ }
480
+ }
481
+
482
+ module.exports = {
483
+ DEFAULT_POLL_INTERVAL_MS,
484
+ DEFAULT_TIMEOUT_MS,
485
+ applyCodexTerminalProfile,
486
+ codexServiceTierConfirmations,
487
+ codexTerminalProfileFromOutput,
488
+ codexAdvancedReasoningMenuOptions,
489
+ codexModelMenuOptions,
490
+ codexReasoningMenuOptions,
491
+ codexTerminalProfileFromPreview,
492
+ modelSelectionInput,
493
+ moreReasoningSelectionInput,
494
+ newCodexServiceTierConfirmation,
495
+ normalizedReasoning,
496
+ reasoningSelectionInput,
497
+ terminalCommand,
498
+ validateTargetProfile,
499
+ waitForPreview,
500
+ };
@@ -49,6 +49,16 @@ function heartbeatAssistantMessage(value) {
49
49
  return heartbeat.message || '';
50
50
  }
51
51
 
52
+ function stripCodexAppDirectives(value) {
53
+ // These directives are private transport hints consumed by the Codex app.
54
+ // Farming does not implement their native cards, so rendering them as
55
+ // Markdown leaks protocol syntax and local paths into otherwise clean Chat.
56
+ return String(value || '').replace(
57
+ /::(?:code-comment|created-thread|git-(?:stage|commit|create-branch|push|create-pr))\{[^\r\n]*\}/gi,
58
+ '',
59
+ );
60
+ }
61
+
52
62
  function stripCodexInternalContextBlocks(value) {
53
63
  let text = normalizeCodexTranscriptText(value);
54
64
  if (!text) return '';
@@ -82,6 +92,7 @@ function stripCodexInternalContextBlocks(value) {
82
92
  });
83
93
 
84
94
  text = text.replace(/(^|\n)\s*# AGENTS\.md instructions for[^\n]*\n[\s\S]*?<\/INSTRUCTIONS>\s*(?=\n|$)/gi, '$1');
95
+ text = stripCodexAppDirectives(text);
85
96
 
86
97
  return text
87
98
  .replace(/[ \t]+\n/g, '\n')
@@ -99,5 +110,6 @@ module.exports = {
99
110
  heartbeatUserMessage,
100
111
  isCodexInjectedContextMessage,
101
112
  parseHeartbeatEnvelope,
113
+ stripCodexAppDirectives,
102
114
  stripCodexInternalContextBlocks,
103
115
  };