@songsid/agend 2.1.7-beta.5 → 2.1.7-beta.7
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/dist/backend/codex.d.ts +34 -9
- package/dist/backend/codex.js +197 -33
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +51 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +16 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/cli.js +79 -132
- package/dist/cli.js.map +1 -1
- package/dist/completion-install.d.ts +32 -0
- package/dist/completion-install.js +125 -1
- package/dist/completion-install.js.map +1 -1
- package/dist/config-validator.js +45 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +4 -1
- package/dist/daemon.js +23 -6
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +109 -0
- package/dist/delivery-outbox.js +306 -10
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +67 -7
- package/dist/fleet-manager.js +215 -59
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +10 -0
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -2
- package/dist/instance-lifecycle.js +50 -32
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +17 -0
- package/dist/instructions.js +24 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/ls-rows.d.ts +50 -0
- package/dist/ls-rows.js +152 -0
- package/dist/ls-rows.js.map +1 -0
- package/dist/outbound-handlers.d.ts +1 -1
- package/dist/outbound-handlers.js +6 -7
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -1
- package/dist/outbound-schemas.js +3 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/quickstart.js +9 -3
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-credential-check.d.ts +26 -7
- package/dist/remote-credential-check.js +157 -10
- package/dist/remote-credential-check.js.map +1 -1
- package/dist/settings-api.d.ts +17 -0
- package/dist/settings-api.js +62 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/status-emojis.d.ts +118 -0
- package/dist/status-emojis.js +206 -0
- package/dist/status-emojis.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/ui/dashboard.html +7 -5
- package/dist/ui/settings.html +168 -6
- package/dist/ui/view.html +138 -45
- package/dist/usage/usage-api.js +30 -0
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/backend/codex.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare class CodexBackend implements CliBackend {
|
|
|
13
13
|
private configuredStatusLinePattern;
|
|
14
14
|
/** Set by buildCommand when the resume plan fell back; read once by the daemon. */
|
|
15
15
|
private launchWarning;
|
|
16
|
+
/** Set by writeConfig when the status_line could not be made verifiable (#931). */
|
|
17
|
+
private statusLineWarning;
|
|
16
18
|
constructor(instanceDir: string);
|
|
17
19
|
/**
|
|
18
20
|
* Compute and (on first call for an existing instance) migrate to a SHORT
|
|
@@ -60,7 +62,18 @@ export declare class CodexBackend implements CliBackend {
|
|
|
60
62
|
getBottomReadyPattern(): RegExp | null;
|
|
61
63
|
/** Observed on real Codex 0.156.0: the live input row precedes its footer. */
|
|
62
64
|
isDeliveryInputReadyPane(pane: string): boolean;
|
|
63
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Live status chrome that must veto the broad prompt/context ready match.
|
|
67
|
+
*
|
|
68
|
+
* #964: Codex relabels the live status row with the reasoning title
|
|
69
|
+
* (`• Planning the edit (12s • esc to interrupt)`), so "Working" is only one
|
|
70
|
+
* of its titles. Match the row's shape instead, as observed on 0.156/0.157:
|
|
71
|
+
* a column-zero bullet, any title, then the `(… esc to interrupt)`
|
|
72
|
+
* affordance with an optional elapsed time (`12s`, `2m 05s`, `·` or `•`
|
|
73
|
+
* separator), optionally followed by ` · N background terminal(s) …`.
|
|
74
|
+
* Anchoring on the whole row, not on the phrase, keeps an agent reply that
|
|
75
|
+
* merely mentions "esc to interrupt" from pinning the pane as busy.
|
|
76
|
+
*/
|
|
64
77
|
getBusyPattern(): RegExp;
|
|
65
78
|
/**
|
|
66
79
|
* Codex 0.154's Astra theme can keep animating a star field around the input
|
|
@@ -101,16 +114,28 @@ export declare class CodexBackend implements CliBackend {
|
|
|
101
114
|
*/
|
|
102
115
|
private disableStartupUpdateCheck;
|
|
103
116
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
117
|
+
* Suppress Codex's rate-limit model-switch reminder for unattended instances.
|
|
118
|
+
* Codex documents this as `notice.hide_rate_limit_model_nudge` (a boolean in
|
|
119
|
+
* the `[notice]` table). This changes only the per-instance config; the
|
|
120
|
+
* runtime holdOnly dialog remains as defense-in-depth for older or changed
|
|
121
|
+
* CLI behavior. No model option is selected automatically.
|
|
122
|
+
*/
|
|
123
|
+
private hideRateLimitModelNudge;
|
|
124
|
+
/**
|
|
125
|
+
* Ensure Codex's TUI status line shows context usage: the Context item is
|
|
126
|
+
* both what /ctx scrapes and the footer AgEnD's readiness proofs rely on.
|
|
127
|
+
* Rules (never drops or reorders the user's items):
|
|
128
|
+
* 1. the effective `tui.status_line` already has a context item
|
|
129
|
+
* (context-remaining / -usage / -used) → leave the config untouched.
|
|
108
130
|
* 2. no context item:
|
|
109
131
|
* - no status_line at all → write status_line = ["context-remaining"]
|
|
110
|
-
* - status_line exists →
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
132
|
+
* - status_line exists → prepend "context-remaining" to it
|
|
133
|
+
* Best-effort string edit (no TOML writer), but every candidate edit is
|
|
134
|
+
* parsed back and kept only if the effective `tui.status_line` becomes
|
|
135
|
+
* `["context-remaining", ...the user's items]`. A `status_line` key in some
|
|
136
|
+
* other table is not the TUI's and must not be the one edited (#931). If no
|
|
137
|
+
* edit can be verified, the launch warns the operator instead of leaving the
|
|
138
|
+
* pane silently unready.
|
|
114
139
|
*/
|
|
115
140
|
private enableContextStatusLine;
|
|
116
141
|
/** Null when the instance did not ask for a profile — today's behaviour. */
|
package/dist/backend/codex.js
CHANGED
|
@@ -109,6 +109,100 @@ function tomlTable(value) {
|
|
|
109
109
|
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)
|
|
110
110
|
? value : null;
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Set one boolean inside a TOML table while leaving unrelated config text
|
|
114
|
+
* intact. Existing tables keep their layout; when the table isn't declared,
|
|
115
|
+
* use the equivalent dotted key in the root table. Table headers embedded in
|
|
116
|
+
* multiline strings are ignored.
|
|
117
|
+
*/
|
|
118
|
+
function setTomlTableBoolean(content, table, key) {
|
|
119
|
+
const lines = content.split(/(?<=\n)/);
|
|
120
|
+
let multiline = null;
|
|
121
|
+
const headers = [];
|
|
122
|
+
for (let index = 0; index < lines.length; index++) {
|
|
123
|
+
const line = lines[index];
|
|
124
|
+
if (!multiline) {
|
|
125
|
+
const header = line.match(/^\s*(?:\[([^\[\]]+)\]|\[\[([^\[\]]+)\]\])\s*(?:#.*)?(?:\r?\n)?$/);
|
|
126
|
+
if (header)
|
|
127
|
+
headers.push({ index, name: (header[1] ?? header[2]).trim(), isArray: header[2] !== undefined });
|
|
128
|
+
}
|
|
129
|
+
for (const delimiter of ['"""', "'''"]) {
|
|
130
|
+
if (multiline && multiline !== delimiter)
|
|
131
|
+
continue;
|
|
132
|
+
let count = 0;
|
|
133
|
+
let pos = 0;
|
|
134
|
+
while ((pos = line.indexOf(delimiter, pos)) !== -1) {
|
|
135
|
+
if (delimiter === "'''" || pos === 0 || line[pos - 1] !== "\\")
|
|
136
|
+
count++;
|
|
137
|
+
pos += delimiter.length;
|
|
138
|
+
}
|
|
139
|
+
if (count % 2 === 1)
|
|
140
|
+
multiline = multiline === delimiter ? null : delimiter;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const assignment = new RegExp(`^(\\s*${key}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)(\\r?\\n?)$`);
|
|
144
|
+
const tableIndex = headers.findIndex(header => header.name === table && !header.isArray);
|
|
145
|
+
if (tableIndex >= 0) {
|
|
146
|
+
const start = headers[tableIndex].index + 1;
|
|
147
|
+
const end = headers[tableIndex + 1]?.index ?? lines.length;
|
|
148
|
+
for (let index = start; index < end; index++) {
|
|
149
|
+
const existing = lines[index].match(assignment);
|
|
150
|
+
if (existing) {
|
|
151
|
+
lines[index] = `${existing[1]}true${existing[2]}${existing[3]}`;
|
|
152
|
+
return lines.join("");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (end > 0 && !lines[end - 1].endsWith("\n"))
|
|
156
|
+
lines[end - 1] += "\n";
|
|
157
|
+
lines.splice(end, 0, `${key} = true\n`);
|
|
158
|
+
return lines.join("");
|
|
159
|
+
}
|
|
160
|
+
// Inline tables cannot be extended with dotted keys, so update the simple
|
|
161
|
+
// root-level `notice = { ... }` form in place when it is present.
|
|
162
|
+
const rootEnd = headers[0]?.index ?? lines.length;
|
|
163
|
+
const dottedAssignment = new RegExp(`^(\\s*${table}\\s*\\.\\s*${key}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)(\\r?\\n?)$`);
|
|
164
|
+
for (let index = 0; index < rootEnd; index++) {
|
|
165
|
+
const existing = lines[index].match(dottedAssignment);
|
|
166
|
+
if (existing) {
|
|
167
|
+
lines[index] = `${existing[1]}true${existing[2]}${existing[3]}`;
|
|
168
|
+
return lines.join("");
|
|
169
|
+
}
|
|
170
|
+
const inline = lines[index].match(new RegExp(`^(\\s*${table}\\s*=\\s*\\{)(.*)(\\})(\\s*(?:#.*)?)(\\r?\\n?)$`));
|
|
171
|
+
if (inline) {
|
|
172
|
+
const option = new RegExp(`(${key}\\s*=\\s*)(?:true|false)`);
|
|
173
|
+
const body = option.test(inline[2])
|
|
174
|
+
? inline[2].replace(option, "$1true")
|
|
175
|
+
: `${inline[2].trim().length > 0 ? `${inline[2]}, ` : inline[2]}${key} = true`;
|
|
176
|
+
lines[index] = `${inline[1]}${body}${inline[3]}${inline[4]}${inline[5]}`;
|
|
177
|
+
return lines.join("");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// A dotted assignment is valid in the root table and can extend an implicit
|
|
181
|
+
// parent table created by `[notice.child]` without duplicating `[notice]`.
|
|
182
|
+
const line = `${table}.${key} = true\n`;
|
|
183
|
+
if (rootEnd === 0)
|
|
184
|
+
lines.unshift(line);
|
|
185
|
+
else if (lines[rootEnd - 1].endsWith("\n"))
|
|
186
|
+
lines.splice(rootEnd, 0, line);
|
|
187
|
+
else {
|
|
188
|
+
lines[rootEnd - 1] += "\n";
|
|
189
|
+
lines.splice(rootEnd, 0, line);
|
|
190
|
+
}
|
|
191
|
+
return lines.join("");
|
|
192
|
+
}
|
|
193
|
+
const CODEX_CONTEXT_STATUS_ITEM_RE = /^context-(?:remaining|usage|used)$/;
|
|
194
|
+
/** The TUI's effective status_line items: null when unset, "invalid" when unusable. */
|
|
195
|
+
function effectiveTuiStatusLine(content) {
|
|
196
|
+
try {
|
|
197
|
+
const items = tomlTable(tomlTable(parseToml(content))?.tui)?.status_line;
|
|
198
|
+
if (items === undefined)
|
|
199
|
+
return null;
|
|
200
|
+
return Array.isArray(items) && items.every(item => typeof item === "string") ? items : "invalid";
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return "invalid";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
112
206
|
function projectTrustTable(config, root) {
|
|
113
207
|
const projects = tomlTable(tomlTable(config)?.projects);
|
|
114
208
|
return projects ? tomlTable(projects[root]) : null;
|
|
@@ -493,6 +587,8 @@ const CODEX_FALLBACK_MODELS = [
|
|
|
493
587
|
{ id: "gpt-5.6-terra", label: "GPT-5.6 Terra", description: "balanced agentic coding" },
|
|
494
588
|
{ id: "gpt-5.6-luna", label: "GPT-5.6 Luna", description: "fast, efficient agentic coding" },
|
|
495
589
|
];
|
|
590
|
+
/** A live Codex status row, whatever its title (#964); see getBusyPattern. */
|
|
591
|
+
const CODEX_LIVE_STATUS_ROW = /(?:^|\n)•[ \t]+\S[^\r\n]*?\((?:(?:\d+[hms][ \t]+)+[•·][ \t]+)?esc to interrupt\)(?:[ \t]+·[ \t][^\r\n]*)?[ \t]*(?=\r?\n|$)/i;
|
|
496
592
|
const CODEX_RUN_STATE_STATUS_ITEMS = new Set(["run-state", "status"]);
|
|
497
593
|
// Deliberately small allow-list of built-in Codex status_line values. These
|
|
498
594
|
// patterns identify positive rendered chrome; unknown/custom items fail closed.
|
|
@@ -545,6 +641,8 @@ export class CodexBackend {
|
|
|
545
641
|
configuredStatusLinePattern;
|
|
546
642
|
/** Set by buildCommand when the resume plan fell back; read once by the daemon. */
|
|
547
643
|
launchWarning = null;
|
|
644
|
+
/** Set by writeConfig when the status_line could not be made verifiable (#931). */
|
|
645
|
+
statusLineWarning = null;
|
|
548
646
|
constructor(instanceDir) {
|
|
549
647
|
this.instanceDir = instanceDir;
|
|
550
648
|
this.binaryPath = resolveBinary("codex");
|
|
@@ -706,9 +804,20 @@ export class CodexBackend {
|
|
|
706
804
|
}
|
|
707
805
|
return false;
|
|
708
806
|
}
|
|
709
|
-
/**
|
|
807
|
+
/**
|
|
808
|
+
* Live status chrome that must veto the broad prompt/context ready match.
|
|
809
|
+
*
|
|
810
|
+
* #964: Codex relabels the live status row with the reasoning title
|
|
811
|
+
* (`• Planning the edit (12s • esc to interrupt)`), so "Working" is only one
|
|
812
|
+
* of its titles. Match the row's shape instead, as observed on 0.156/0.157:
|
|
813
|
+
* a column-zero bullet, any title, then the `(… esc to interrupt)`
|
|
814
|
+
* affordance with an optional elapsed time (`12s`, `2m 05s`, `·` or `•`
|
|
815
|
+
* separator), optionally followed by ` · N background terminal(s) …`.
|
|
816
|
+
* Anchoring on the whole row, not on the phrase, keeps an agent reply that
|
|
817
|
+
* merely mentions "esc to interrupt" from pinning the pane as busy.
|
|
818
|
+
*/
|
|
710
819
|
getBusyPattern() {
|
|
711
|
-
return
|
|
820
|
+
return CODEX_LIVE_STATUS_ROW;
|
|
712
821
|
}
|
|
713
822
|
/**
|
|
714
823
|
* Codex 0.154's Astra theme can keep animating a star field around the input
|
|
@@ -839,6 +948,7 @@ export class CodexBackend {
|
|
|
839
948
|
atomicWritePrivate(join(this.isolatedCodexHome, "config.toml"), content);
|
|
840
949
|
this.enableContextStatusLine();
|
|
841
950
|
this.disableStartupUpdateCheck();
|
|
951
|
+
this.hideRateLimitModelNudge();
|
|
842
952
|
// Write fleet instructions into AGENTS.md (additive via marker block)
|
|
843
953
|
if (config.instructions) {
|
|
844
954
|
try {
|
|
@@ -903,59 +1013,112 @@ export class CodexBackend {
|
|
|
903
1013
|
catch { /* best effort */ }
|
|
904
1014
|
}
|
|
905
1015
|
/**
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
1016
|
+
* Suppress Codex's rate-limit model-switch reminder for unattended instances.
|
|
1017
|
+
* Codex documents this as `notice.hide_rate_limit_model_nudge` (a boolean in
|
|
1018
|
+
* the `[notice]` table). This changes only the per-instance config; the
|
|
1019
|
+
* runtime holdOnly dialog remains as defense-in-depth for older or changed
|
|
1020
|
+
* CLI behavior. No model option is selected automatically.
|
|
1021
|
+
*/
|
|
1022
|
+
hideRateLimitModelNudge() {
|
|
1023
|
+
const configPath = join(this.isolatedCodexHome, "config.toml");
|
|
1024
|
+
let content = "";
|
|
1025
|
+
try {
|
|
1026
|
+
content = readFileSync(configPath, "utf-8");
|
|
1027
|
+
}
|
|
1028
|
+
catch {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const updated = setTomlTableBoolean(content, "notice", "hide_rate_limit_model_nudge");
|
|
1032
|
+
if (updated !== content) {
|
|
1033
|
+
try {
|
|
1034
|
+
atomicWritePrivate(configPath, updated);
|
|
1035
|
+
}
|
|
1036
|
+
catch { /* best effort */ }
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Ensure Codex's TUI status line shows context usage: the Context item is
|
|
1041
|
+
* both what /ctx scrapes and the footer AgEnD's readiness proofs rely on.
|
|
1042
|
+
* Rules (never drops or reorders the user's items):
|
|
1043
|
+
* 1. the effective `tui.status_line` already has a context item
|
|
1044
|
+
* (context-remaining / -usage / -used) → leave the config untouched.
|
|
910
1045
|
* 2. no context item:
|
|
911
1046
|
* - no status_line at all → write status_line = ["context-remaining"]
|
|
912
|
-
* - status_line exists →
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
1047
|
+
* - status_line exists → prepend "context-remaining" to it
|
|
1048
|
+
* Best-effort string edit (no TOML writer), but every candidate edit is
|
|
1049
|
+
* parsed back and kept only if the effective `tui.status_line` becomes
|
|
1050
|
+
* `["context-remaining", ...the user's items]`. A `status_line` key in some
|
|
1051
|
+
* other table is not the TUI's and must not be the one edited (#931). If no
|
|
1052
|
+
* edit can be verified, the launch warns the operator instead of leaving the
|
|
1053
|
+
* pane silently unready.
|
|
916
1054
|
*/
|
|
917
1055
|
enableContextStatusLine() {
|
|
918
1056
|
this.configuredStatusLineItems = undefined;
|
|
919
1057
|
this.configuredStatusLinePattern = undefined;
|
|
1058
|
+
this.statusLineWarning = null;
|
|
920
1059
|
const configPath = join(this.isolatedCodexHome, "config.toml");
|
|
1060
|
+
// The private copy is rebuilt from the shared one at every launch, so the
|
|
1061
|
+
// operator must be pointed at the file their status_line actually lives in.
|
|
1062
|
+
const sharedConfigPath = join(this.sharedCodexHome, "config.toml");
|
|
921
1063
|
let content = "";
|
|
922
1064
|
try {
|
|
923
1065
|
content = readFileSync(configPath, "utf-8");
|
|
924
1066
|
}
|
|
925
1067
|
catch { /* no file yet */ }
|
|
926
|
-
// TOML allows quoted keys: `"status_line" = [...]` and `['tui']` are both
|
|
927
|
-
// legal. The regexes below accept an optional surrounding quote pair so that
|
|
928
|
-
// users who write their config with quoted keys still get context-remaining
|
|
929
|
-
// injected correctly. Both single and double TOML quotes are accepted.
|
|
930
|
-
// Rule 1: any existing context item → don't touch anything.
|
|
931
|
-
if (/["']?status_line["']?\s*=\s*\[[^\]]*context-(remaining|usage|used)[^\]]*\]/.test(content))
|
|
932
|
-
return;
|
|
933
1068
|
const ITEM = "context-remaining";
|
|
934
|
-
const
|
|
935
|
-
if (
|
|
1069
|
+
const current = effectiveTuiStatusLine(content);
|
|
1070
|
+
if (current === "invalid") {
|
|
1071
|
+
this.statusLineWarning = t("codex.status_line_unverifiable", "tui.status_line is not a list of item names, or config.toml does not parse", sharedConfigPath);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
// Rule 1: the TUI already shows a context item → don't touch anything.
|
|
1075
|
+
if (current?.some(item => CODEX_CONTEXT_STATUS_ITEM_RE.test(item)))
|
|
1076
|
+
return;
|
|
1077
|
+
// TOML allows quoted keys: `"status_line" = [...]`, `"tui"."status_line"`,
|
|
1078
|
+
// `['tui']`. Every textual candidate is tried; parsing decides.
|
|
1079
|
+
const candidates = [];
|
|
1080
|
+
if (current) {
|
|
936
1081
|
// Rule 2b: prepend our item to the user's existing array (don't overwrite).
|
|
937
1082
|
// First position keeps "Context N% left" at the far left of the footer so a
|
|
938
1083
|
// long cwd/other items can't push it past 80 cols and truncate it.
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
1084
|
+
for (const m of content.matchAll(/["']?status_line["']?\s*=\s*\[([^\]]*)\]/g)) {
|
|
1085
|
+
const inner = m[1].trim().replace(/^,\s*/, "").replace(/,\s*$/, "");
|
|
1086
|
+
const newInner = inner.length ? `"${ITEM}", ${inner}` : `"${ITEM}"`;
|
|
1087
|
+
candidates.push(`${content.slice(0, m.index)}status_line = [${newInner}]${content.slice(m.index + m[0].length)}`);
|
|
1088
|
+
}
|
|
942
1089
|
}
|
|
943
1090
|
else {
|
|
944
1091
|
// Rule 2a: no status_line at all → add a minimal one.
|
|
945
|
-
|
|
946
|
-
content += "\n";
|
|
1092
|
+
const base = content.length && !content.endsWith("\n") ? `${content}\n` : content;
|
|
947
1093
|
// Also recognise quoted section headers: `["tui"]` and `['tui']`.
|
|
948
|
-
if (/^\[["']?tui["']?\]/m.test(
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
else {
|
|
952
|
-
content += `\n[tui]\nstatus_line = ["${ITEM}"]\n`;
|
|
1094
|
+
if (/^\[["']?tui["']?\][^\n]*\n/m.test(base)) {
|
|
1095
|
+
candidates.push(base.replace(/^\[["']?tui["']?\][^\n]*\n/m, h => `${h}status_line = ["${ITEM}"]\n`));
|
|
953
1096
|
}
|
|
1097
|
+
candidates.push(`${base}\n[tui]\nstatus_line = ["${ITEM}"]\n`);
|
|
1098
|
+
// A config that defines tui with dotted keys cannot take a [tui] table.
|
|
1099
|
+
candidates.push(`tui.status_line = ["${ITEM}"]\n${base}`);
|
|
1100
|
+
// Nor can an inline `tui = { … }` table: the item goes inside it.
|
|
1101
|
+
candidates.push(base.replace(/^([ \t]*["']?tui["']?[ \t]*=[ \t]*\{)[ \t]*\}/m, `$1 status_line = ["${ITEM}"] }`));
|
|
1102
|
+
candidates.push(base.replace(/^([ \t]*["']?tui["']?[ \t]*=[ \t]*\{)/m, `$1 status_line = ["${ITEM}"],`));
|
|
1103
|
+
}
|
|
1104
|
+
const wanted = [ITEM, ...(current ?? [])];
|
|
1105
|
+
const fixed = candidates.find(candidate => {
|
|
1106
|
+
const items = effectiveTuiStatusLine(candidate);
|
|
1107
|
+
return Array.isArray(items) && items.length === wanted.length && items.every((item, i) => item === wanted[i]);
|
|
1108
|
+
});
|
|
1109
|
+
if (!fixed) {
|
|
1110
|
+
this.statusLineWarning = t("codex.status_line_unverifiable", "no edit of tui.status_line could be verified", sharedConfigPath);
|
|
1111
|
+
return;
|
|
954
1112
|
}
|
|
955
1113
|
try {
|
|
956
|
-
atomicWritePrivate(configPath,
|
|
1114
|
+
atomicWritePrivate(configPath, fixed);
|
|
1115
|
+
}
|
|
1116
|
+
catch (err) {
|
|
1117
|
+
// Never block launch on statusline config — but a verified edit that
|
|
1118
|
+
// could not be written leaves the pane as unready as an unfixable one.
|
|
1119
|
+
const reason = `the verified edit could not be written: ${err.code ?? "write failed"}`;
|
|
1120
|
+
this.statusLineWarning = t("codex.status_line_unverifiable", reason, sharedConfigPath);
|
|
957
1121
|
}
|
|
958
|
-
catch { /* best effort — never block launch on statusline config */ }
|
|
959
1122
|
}
|
|
960
1123
|
/** Null when the instance did not ask for a profile — today's behaviour. */
|
|
961
1124
|
readProfile(config) {
|
|
@@ -1670,9 +1833,10 @@ export class CodexBackend {
|
|
|
1670
1833
|
});
|
|
1671
1834
|
}
|
|
1672
1835
|
consumeLaunchWarning() {
|
|
1673
|
-
const
|
|
1836
|
+
const warnings = [this.statusLineWarning, this.launchWarning].filter((w) => !!w);
|
|
1837
|
+
this.statusLineWarning = null;
|
|
1674
1838
|
this.launchWarning = null;
|
|
1675
|
-
return
|
|
1839
|
+
return warnings.length ? warnings.join("\n") : null;
|
|
1676
1840
|
}
|
|
1677
1841
|
getQuitCommand() { return "/quit"; }
|
|
1678
1842
|
getCompactCommand() { return "/compact"; }
|