@triflux/core 10.87.0 → 10.88.1
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/hub/bridge.mjs +411 -5
- package/hub/lib/jev-addon.mjs +1675 -0
- package/hub/team/pane.mjs +99 -0
- package/hub/team/retry-state-machine.mjs +22 -2
- package/hud/hud-qos-status.mjs +45 -16
- package/hud/renderers.mjs +13 -3
- package/hud/session-status.mjs +46 -0
- package/hud/tmux-status.mjs +140 -0
- package/package.json +1 -1
- package/scripts/lib/doctor-env-checks.mjs +377 -0
- package/scripts/lib/mcp-filter.mjs +86 -3
package/hub/team/pane.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { judgeComposerState } from "../lib/jev-addon.mjs";
|
|
7
8
|
import { privateTmpDir } from "../lib/private-tmp.mjs";
|
|
8
9
|
import { psmuxExec } from "./psmux.mjs";
|
|
9
10
|
import { detectMultiplexer, tmuxExec } from "./session.mjs";
|
|
@@ -251,6 +252,104 @@ export function submitPromptWithVerification({
|
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
/**
|
|
256
|
+
* 결정론적 컴포저 상태 판정이 모호할 때 Jev로 컴포저 상태를 판별해 Enter 재전송 여부를 결정한다.
|
|
257
|
+
*/
|
|
258
|
+
export async function submitPromptWithJevVerification({
|
|
259
|
+
cli = "claude",
|
|
260
|
+
prompt = "",
|
|
261
|
+
capture,
|
|
262
|
+
sendEnter,
|
|
263
|
+
sleep = sleepMs,
|
|
264
|
+
settleMs = PASTE_SETTLE_MS,
|
|
265
|
+
retryLimit = SUBMIT_RETRY_LIMIT,
|
|
266
|
+
jevOptions = {},
|
|
267
|
+
} = {}) {
|
|
268
|
+
const strictVerification = isAntigravityCli(cli);
|
|
269
|
+
const promptPrefix = promptComposerNeedle(prompt);
|
|
270
|
+
const isConfirmedSubmitted = (text) => {
|
|
271
|
+
if (!promptPrefix) return true;
|
|
272
|
+
if (!text) return !strictVerification;
|
|
273
|
+
if (hasBusyMarkerOutsideComposer(text)) return true;
|
|
274
|
+
const state = composerPromptState(text, promptPrefix);
|
|
275
|
+
if (state === "absent") return true;
|
|
276
|
+
if (state === "unknown" && !strictVerification) return true;
|
|
277
|
+
return false;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
submitPromptWithVerification({
|
|
282
|
+
cli,
|
|
283
|
+
prompt,
|
|
284
|
+
capture,
|
|
285
|
+
sendEnter,
|
|
286
|
+
sleep,
|
|
287
|
+
settleMs,
|
|
288
|
+
retryLimit,
|
|
289
|
+
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
const capturedText =
|
|
292
|
+
typeof capture === "function" ? String(capture() ?? "") : "";
|
|
293
|
+
const verdict = await judgeComposerState(
|
|
294
|
+
{ capturedText, promptPrefix },
|
|
295
|
+
jevOptions,
|
|
296
|
+
);
|
|
297
|
+
if (verdict?.state === "submitted_running") {
|
|
298
|
+
return { submitted: true, jevState: verdict.state };
|
|
299
|
+
}
|
|
300
|
+
if (
|
|
301
|
+
verdict?.state === "stuck_in_composer" &&
|
|
302
|
+
typeof sendEnter === "function"
|
|
303
|
+
) {
|
|
304
|
+
sendEnter();
|
|
305
|
+
sleep(400);
|
|
306
|
+
const afterText =
|
|
307
|
+
typeof capture === "function" ? String(capture() ?? "") : "";
|
|
308
|
+
if (isConfirmedSubmitted(afterText)) {
|
|
309
|
+
return { submitted: true, jevState: "submitted_running" };
|
|
310
|
+
}
|
|
311
|
+
const postVerdict = await judgeComposerState(
|
|
312
|
+
{ capturedText: afterText, promptPrefix },
|
|
313
|
+
jevOptions,
|
|
314
|
+
);
|
|
315
|
+
if (postVerdict?.state === "submitted_running") {
|
|
316
|
+
return { submitted: true, jevState: postVerdict.state };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
|
321
|
+
const capturedText =
|
|
322
|
+
typeof capture === "function" ? String(capture() ?? "") : "";
|
|
323
|
+
const verdict = await judgeComposerState(
|
|
324
|
+
{ capturedText, promptPrefix },
|
|
325
|
+
jevOptions,
|
|
326
|
+
);
|
|
327
|
+
if (
|
|
328
|
+
verdict?.state === "stuck_in_composer" &&
|
|
329
|
+
typeof sendEnter === "function"
|
|
330
|
+
) {
|
|
331
|
+
sendEnter();
|
|
332
|
+
sleep(400);
|
|
333
|
+
const afterText =
|
|
334
|
+
typeof capture === "function" ? String(capture() ?? "") : "";
|
|
335
|
+
if (isConfirmedSubmitted(afterText)) {
|
|
336
|
+
return { submitted: true, jevState: "submitted_running" };
|
|
337
|
+
}
|
|
338
|
+
const postVerdict = await judgeComposerState(
|
|
339
|
+
{ capturedText: afterText, promptPrefix },
|
|
340
|
+
jevOptions,
|
|
341
|
+
);
|
|
342
|
+
return {
|
|
343
|
+
submitted: postVerdict?.state === "submitted_running",
|
|
344
|
+
jevState: postVerdict?.state || verdict.state,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
submitted: verdict?.state !== "modal_blocking",
|
|
349
|
+
jevState: verdict?.state || null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
254
353
|
function confirmSubmit(target, prompt, cli, sendEnter) {
|
|
255
354
|
submitPromptWithVerification({
|
|
256
355
|
cli,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
resolveCodexModelEffort,
|
|
32
32
|
} from "../../scripts/lib/model-effort.mjs";
|
|
33
33
|
import { AGY_FAMILY_PROFILES } from "../../scripts/lib/model-family-resolver.mjs";
|
|
34
|
+
import { judgeSameFailureCause } from "../lib/jev-addon.mjs";
|
|
34
35
|
|
|
35
36
|
export const STATES = Object.freeze({
|
|
36
37
|
PLANNING: "PLANNING",
|
|
@@ -126,10 +127,14 @@ export function createRetryStateMachine(options = {}) {
|
|
|
126
127
|
return transition(STATES.DONE);
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
function reportVerifyFail(failureReason) {
|
|
130
|
+
function reportVerifyFail(failureReason, failOptions = {}) {
|
|
130
131
|
const reason = String(failureReason || "unknown");
|
|
131
|
-
|
|
132
|
+
const isSameCause =
|
|
133
|
+
reason === state.lastFailureReason ||
|
|
134
|
+
(Boolean(state.lastFailureReason) && failOptions.sameCause === true);
|
|
135
|
+
if (isSameCause) {
|
|
132
136
|
state.stuckCounter += 1;
|
|
137
|
+
state.lastFailureReason = reason;
|
|
133
138
|
} else {
|
|
134
139
|
state.stuckCounter = 1;
|
|
135
140
|
state.lastFailureReason = reason;
|
|
@@ -155,6 +160,20 @@ export function createRetryStateMachine(options = {}) {
|
|
|
155
160
|
return transition(STATES.DIAGNOSING, { reason });
|
|
156
161
|
}
|
|
157
162
|
|
|
163
|
+
async function reportVerifyFailAsync(failureReason, jevOptions = {}) {
|
|
164
|
+
const reason = String(failureReason || "unknown");
|
|
165
|
+
let sameCause = false;
|
|
166
|
+
if (state.lastFailureReason && reason !== state.lastFailureReason) {
|
|
167
|
+
const verdict = await judgeSameFailureCause(
|
|
168
|
+
state.lastFailureReason,
|
|
169
|
+
reason,
|
|
170
|
+
jevOptions,
|
|
171
|
+
);
|
|
172
|
+
sameCause = verdict?.sameCause === true;
|
|
173
|
+
}
|
|
174
|
+
return reportVerifyFail(reason, { sameCause });
|
|
175
|
+
}
|
|
176
|
+
|
|
158
177
|
function escalate() {
|
|
159
178
|
if (state.cliIndex + 1 >= state.cliChain.length) {
|
|
160
179
|
return transition(STATES.BUDGET_EXCEEDED, {
|
|
@@ -271,6 +290,7 @@ export function createRetryStateMachine(options = {}) {
|
|
|
271
290
|
startIteration,
|
|
272
291
|
reportVerifySuccess,
|
|
273
292
|
reportVerifyFail,
|
|
293
|
+
reportVerifyFailAsync,
|
|
274
294
|
escalate,
|
|
275
295
|
on,
|
|
276
296
|
serialize,
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -52,8 +52,14 @@ import {
|
|
|
52
52
|
getProviderRow,
|
|
53
53
|
renderAlignedRows,
|
|
54
54
|
} from "./renderers.mjs";
|
|
55
|
+
import { sessionStatus } from "./session-status.mjs";
|
|
55
56
|
import { selectTier } from "./terminal.mjs";
|
|
56
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
formatTimeCell,
|
|
59
|
+
formatTimeCellDH,
|
|
60
|
+
readStdinJson,
|
|
61
|
+
stripAnsi,
|
|
62
|
+
} from "./utils.mjs";
|
|
57
63
|
|
|
58
64
|
async function main() {
|
|
59
65
|
if (process.argv.includes(CLAUDE_REFRESH_FLAG)) {
|
|
@@ -74,7 +80,12 @@ async function main() {
|
|
|
74
80
|
const stdin = await readStdinJson();
|
|
75
81
|
// agy statusLine 은 같은 HUD 를 호출하되 product 로 자신을 밝히고 터미널 폭을 직접 넘긴다.
|
|
76
82
|
const agySession = stdin?.product === "antigravity";
|
|
77
|
-
|
|
83
|
+
const codexSession = stdin?.product === "codex";
|
|
84
|
+
const cacheOnly = process.argv.includes("--cache-only");
|
|
85
|
+
const status = await sessionStatus(stdin);
|
|
86
|
+
const currentMarker = agySession ? "a" : codexSession ? "x" : "c";
|
|
87
|
+
const jevMarker = codexSession ? "c" : "x";
|
|
88
|
+
if (agySession || codexSession) {
|
|
78
89
|
if (stdin.terminal_width > 0)
|
|
79
90
|
process.env.COLUMNS = String(stdin.terminal_width);
|
|
80
91
|
} else {
|
|
@@ -94,16 +105,23 @@ async function main() {
|
|
|
94
105
|
const antigravitySnapshot = antigravityAllowed
|
|
95
106
|
? readAntigravityQuotaSnapshot()
|
|
96
107
|
: null;
|
|
97
|
-
if (antigravitySnapshot?.shouldRefresh)
|
|
108
|
+
if (!cacheOnly && antigravitySnapshot?.shouldRefresh)
|
|
109
|
+
scheduleAntigravityQuotaRefresh();
|
|
98
110
|
// 설정이 없는 홈에서는 갱신 프로세스를 시작하지 않는다.
|
|
99
111
|
if (
|
|
112
|
+
!cacheOnly &&
|
|
100
113
|
!stdinClaudeUsage &&
|
|
101
114
|
claudeUsageSnapshot.shouldRefresh &&
|
|
102
115
|
existsSync(join(homedir(), ".claude"))
|
|
103
116
|
) {
|
|
104
117
|
scheduleClaudeUsageRefresh();
|
|
105
118
|
}
|
|
106
|
-
if (
|
|
119
|
+
if (
|
|
120
|
+
!cacheOnly &&
|
|
121
|
+
showCodex &&
|
|
122
|
+
codexSnapshot.shouldRefresh &&
|
|
123
|
+
existsSync(getCodexHome())
|
|
124
|
+
) {
|
|
107
125
|
scheduleCodexRateLimitRefresh();
|
|
108
126
|
}
|
|
109
127
|
|
|
@@ -127,6 +145,9 @@ async function main() {
|
|
|
127
145
|
codexLoggedOut,
|
|
128
146
|
showAntigravity: antigravityAllowed,
|
|
129
147
|
antigravityQuota,
|
|
148
|
+
sessionStatus: status,
|
|
149
|
+
currentMarker,
|
|
150
|
+
jevMarker,
|
|
130
151
|
});
|
|
131
152
|
const header = bandMode ? `${BAND_HEADER}\n` : "";
|
|
132
153
|
process.stdout.write(`${header}\x1b[0m${microLine}\n`);
|
|
@@ -136,15 +157,10 @@ async function main() {
|
|
|
136
157
|
const rows =
|
|
137
158
|
bandState === "claude"
|
|
138
159
|
? []
|
|
139
|
-
: getClaudeRows(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{
|
|
144
|
-
accountName: getClaudeEmail(),
|
|
145
|
-
promptCache: stdin?.prompt_cache,
|
|
146
|
-
},
|
|
147
|
-
);
|
|
160
|
+
: getClaudeRows(currentTier, null, claudeUsage, {
|
|
161
|
+
accountName: getClaudeEmail(),
|
|
162
|
+
promptCache: stdin?.prompt_cache,
|
|
163
|
+
});
|
|
148
164
|
let codexRowIndex = -1;
|
|
149
165
|
if (showCodex) {
|
|
150
166
|
codexRowIndex = rows.length;
|
|
@@ -169,11 +185,20 @@ async function main() {
|
|
|
169
185
|
geminiBlue,
|
|
170
186
|
antigravityQuota,
|
|
171
187
|
getAntigravityAccountLabel(),
|
|
172
|
-
{ contextView: agySession ? contextView : null },
|
|
173
188
|
),
|
|
174
189
|
);
|
|
175
190
|
}
|
|
176
191
|
|
|
192
|
+
for (const row of rows) {
|
|
193
|
+
const marker = stripAnsi(row.prefix);
|
|
194
|
+
const extra =
|
|
195
|
+
marker === `${currentMarker}:`
|
|
196
|
+
? status.context
|
|
197
|
+
: marker === `${jevMarker}:`
|
|
198
|
+
? status.jev
|
|
199
|
+
: "";
|
|
200
|
+
if (extra) row.right = [row.right, extra].filter(Boolean).join(" ");
|
|
201
|
+
}
|
|
177
202
|
const outputLines = renderAlignedRows(rows);
|
|
178
203
|
if (outputLines[codexRowIndex] != null && codexLoggedOut) {
|
|
179
204
|
outputLines[codexRowIndex] = `${DIM}${outputLines[codexRowIndex]}${RESET}`;
|
|
@@ -181,7 +206,11 @@ async function main() {
|
|
|
181
206
|
// 알림 배너와 TUI 스타일이 HUD 내용에 겹치지 않도록 한다. band 는 자기 자리에 그리니 띄우지 않는다.
|
|
182
207
|
// agy 는 statusLine.padding 이 같은 역할을 한다.
|
|
183
208
|
const leadingBreaks =
|
|
184
|
-
bandMode || agySession
|
|
209
|
+
bandMode || agySession || codexSession
|
|
210
|
+
? ""
|
|
211
|
+
: contextView.percent >= 85
|
|
212
|
+
? "\n\n"
|
|
213
|
+
: "\n";
|
|
185
214
|
const resetLines = outputLines.map((line) => `\x1b[0m${line}`);
|
|
186
215
|
// band 는 첫 줄 표지로 이 HUD 가 --band 를 아는지 확인한다.
|
|
187
216
|
const header = bandMode ? `${BAND_HEADER}\n` : "";
|
|
@@ -190,7 +219,7 @@ async function main() {
|
|
|
190
219
|
|
|
191
220
|
main().catch(() => {
|
|
192
221
|
process.stdout.write(
|
|
193
|
-
`\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | ctx
|
|
222
|
+
`\x1b[0m${bold(claudeOrange("c"))}: ${dim(`5h:--% ${formatTimeCell("")} 1w:--% ${formatTimeCellDH("")} | ctx:--`)}\n`,
|
|
194
223
|
);
|
|
195
224
|
});
|
|
196
225
|
|
package/hud/renderers.mjs
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
SEVEN_DAY_MS,
|
|
25
25
|
TIME_CELL_INNER_WIDTH,
|
|
26
26
|
} from "./constants.mjs";
|
|
27
|
-
import { buildContextUsageView } from "./context-monitor.mjs";
|
|
28
27
|
import { cacheGap, claudePromptCache } from "./prompt-cache.mjs";
|
|
29
28
|
import { getTerminalColumns, tierBar, tierDimBar } from "./terminal.mjs";
|
|
30
29
|
import {
|
|
@@ -91,7 +90,7 @@ export function getMicroLine(
|
|
|
91
90
|
showAntigravity = true,
|
|
92
91
|
codexLoggedOut = false,
|
|
93
92
|
} = options;
|
|
94
|
-
|
|
93
|
+
|
|
95
94
|
// Claude 5h/1w
|
|
96
95
|
const cF =
|
|
97
96
|
claudeUsage?.fiveHourPercent != null
|
|
@@ -144,7 +143,18 @@ export function getMicroLine(
|
|
|
144
143
|
: agyText;
|
|
145
144
|
segments.push(`${marker}${dim(":")}${agyValue}`);
|
|
146
145
|
}
|
|
147
|
-
|
|
146
|
+
if (options.sessionStatus) {
|
|
147
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
148
|
+
const marker = stripAnsi(segments[index]).split(":")[0];
|
|
149
|
+
const extra =
|
|
150
|
+
marker === options.currentMarker
|
|
151
|
+
? options.sessionStatus.context
|
|
152
|
+
: marker === options.jevMarker
|
|
153
|
+
? options.sessionStatus.jev
|
|
154
|
+
: "";
|
|
155
|
+
if (extra) segments[index] += ` ${extra}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
148
158
|
return truncateAnsi(segments.join(" "), cols);
|
|
149
159
|
}
|
|
150
160
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { buildContextUsageView } from "./context-monitor.mjs";
|
|
2
|
+
import { clampPercent } from "./utils.mjs";
|
|
3
|
+
|
|
4
|
+
export async function sessionStatus(stdin, env = process.env) {
|
|
5
|
+
let jev = "";
|
|
6
|
+
try {
|
|
7
|
+
const { isJevEnabled } = await import("../hub/lib/jev-addon.mjs");
|
|
8
|
+
jev = isJevEnabled({ env }) ? "j" : "";
|
|
9
|
+
} catch {
|
|
10
|
+
// 설정을 판정하지 못한 것은 런타임 호출 실패의 증거가 아니다.
|
|
11
|
+
}
|
|
12
|
+
const window = stdin?.context_window;
|
|
13
|
+
const view = buildContextUsageView(stdin);
|
|
14
|
+
const usageValues = Object.entries(window?.current_usage || {})
|
|
15
|
+
.filter(([key]) =>
|
|
16
|
+
[
|
|
17
|
+
"total_tokens",
|
|
18
|
+
"input_tokens",
|
|
19
|
+
"cache_creation_input_tokens",
|
|
20
|
+
"cache_read_input_tokens",
|
|
21
|
+
].includes(key),
|
|
22
|
+
)
|
|
23
|
+
.map(([, value]) => value);
|
|
24
|
+
const hasUsage =
|
|
25
|
+
usageValues.length > 0 &&
|
|
26
|
+
usageValues.every((value) => Number.isFinite(value) && value >= 0);
|
|
27
|
+
const hasWindow =
|
|
28
|
+
Number.isFinite(window?.context_window_size) &&
|
|
29
|
+
window.context_window_size > 0;
|
|
30
|
+
const knownZero = hasUsage && usageValues.every((value) => value === 0);
|
|
31
|
+
let context = "ctx:--";
|
|
32
|
+
if (hasWindow && hasUsage && (knownZero || view.source === "stdin.tokens")) {
|
|
33
|
+
const used = knownZero ? 0 : view.usedTokens;
|
|
34
|
+
context = `ctx:${clampPercent((used / window.context_window_size) * 100)}%`;
|
|
35
|
+
} else if (
|
|
36
|
+
!(stdin?.product === "antigravity" && window?.context_window_size === 0)
|
|
37
|
+
) {
|
|
38
|
+
const used = Number.isFinite(window?.used_percentage)
|
|
39
|
+
? clampPercent(window.used_percentage)
|
|
40
|
+
: Number.isFinite(window?.remaining_percentage)
|
|
41
|
+
? 100 - clampPercent(window.remaining_percentage)
|
|
42
|
+
: null;
|
|
43
|
+
if (used != null) context = `ctx:${used}%`;
|
|
44
|
+
}
|
|
45
|
+
return { context, jev };
|
|
46
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { readCodexContext } from "../hub/team/session-context.mjs";
|
|
6
|
+
import { stripAnsi } from "./utils.mjs";
|
|
7
|
+
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
const quote = (value) => `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
10
|
+
|
|
11
|
+
export async function configureCodexHud(runTmux, target, env = process.env) {
|
|
12
|
+
// 기존 서버가 다른 홈에서 시작됐어도 신규 세션의 HUD는 실행자의 환경을 따른다.
|
|
13
|
+
for (const key of [
|
|
14
|
+
"HOME",
|
|
15
|
+
"CODEX_HOME",
|
|
16
|
+
"XDG_CONFIG_HOME",
|
|
17
|
+
"XDG_STATE_HOME",
|
|
18
|
+
"PATH",
|
|
19
|
+
"TFX_JEV",
|
|
20
|
+
"TFX_JEV_CLI",
|
|
21
|
+
"JEV_API_KEY",
|
|
22
|
+
"TYPESAFE_API_KEY",
|
|
23
|
+
"TFX_DISABLE_CODEX",
|
|
24
|
+
"TFX_DISABLE_ANTIGRAVITY",
|
|
25
|
+
"TFX_MACHINE_PROFILE_PATH",
|
|
26
|
+
]) {
|
|
27
|
+
try {
|
|
28
|
+
await runTmux(null, [
|
|
29
|
+
"set-environment",
|
|
30
|
+
"-t",
|
|
31
|
+
target,
|
|
32
|
+
...(env[key] == null ? ["-r", key] : [key, env[key]]),
|
|
33
|
+
]);
|
|
34
|
+
} catch {
|
|
35
|
+
// execFile의 오류 문자열에 환경변수 값이 포함되지 않게 한다.
|
|
36
|
+
throw new Error(`Codex HUD environment setup failed: ${key}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const { stdout } = await runTmux(null, [
|
|
40
|
+
"display-message",
|
|
41
|
+
"-p",
|
|
42
|
+
"-t",
|
|
43
|
+
target,
|
|
44
|
+
"#{session_id}|#{pane_id}",
|
|
45
|
+
]);
|
|
46
|
+
const [sessionId, paneId] = stdout.trim().split("|");
|
|
47
|
+
if (!/^\$\d+$/.test(sessionId) || !/^%\d+$/.test(paneId))
|
|
48
|
+
throw new Error("Codex HUD session/pane identity unavailable");
|
|
49
|
+
const command = [
|
|
50
|
+
process.execPath,
|
|
51
|
+
fileURLToPath(import.meta.url),
|
|
52
|
+
sessionId,
|
|
53
|
+
paneId,
|
|
54
|
+
]
|
|
55
|
+
.map(quote)
|
|
56
|
+
.join(" ")
|
|
57
|
+
.replace(/#/g, "##");
|
|
58
|
+
for (const [key, value] of [
|
|
59
|
+
["status", "3"],
|
|
60
|
+
["status-interval", "5"],
|
|
61
|
+
["status-format[0]", `#(${command} #{client_width})`],
|
|
62
|
+
...[1, 2].map((n) => [`status-format[${n}]`, `#{@tfx_hud_${n}}`]),
|
|
63
|
+
])
|
|
64
|
+
await runTmux(null, ["set-option", "-t", target, key, value]);
|
|
65
|
+
for (const key of ["status-format[3]", "@tfx_hud_3"])
|
|
66
|
+
await runTmux(null, ["set-option", "-u", "-t", target, key]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function updateTmuxHud(session, pane, columns) {
|
|
70
|
+
// CLI 모듈은 triflux 설치본에 있다. core의 공유 HUD에서는 이 진입점을 실행하지 않는다.
|
|
71
|
+
const { resolveCodexTmuxThread } = await import(
|
|
72
|
+
new URL("../bin/tfx-live.mjs", import.meta.url)
|
|
73
|
+
);
|
|
74
|
+
const { threadId } = await resolveCodexTmuxThread(pane, {
|
|
75
|
+
requireRegistryAgreement: true,
|
|
76
|
+
runTmux: async (_remote, args) => {
|
|
77
|
+
// tmux 3.8은 format의 탭을 밑줄로 바꾸므로 출력 뒤에 탭으로 복원한다.
|
|
78
|
+
const safeArgs = args.map((arg) => arg.replace(/\t/g, "|"));
|
|
79
|
+
const result = await exec("tmux", safeArgs, { timeout: 2000 });
|
|
80
|
+
return { stdout: result.stdout.replace(/\|/g, "\t") };
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const context = threadId ? await readCodexContext(null, threadId) : null;
|
|
84
|
+
const limit = context?.executionContextLimitTokens;
|
|
85
|
+
const used = context?.estimatedContextTokens;
|
|
86
|
+
const input = {
|
|
87
|
+
product: "codex",
|
|
88
|
+
terminal_width: Number(columns) || 120,
|
|
89
|
+
// 모델별 기본 분모는 쓰지 않고 현재 rollout의 실행 창만 쓴다.
|
|
90
|
+
...(Number.isFinite(limit) &&
|
|
91
|
+
limit > 0 &&
|
|
92
|
+
Number.isFinite(used) &&
|
|
93
|
+
used >= 0
|
|
94
|
+
? {
|
|
95
|
+
context_window: {
|
|
96
|
+
context_window_size: limit,
|
|
97
|
+
current_usage: { total_tokens: used },
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
};
|
|
102
|
+
const rendering = exec(
|
|
103
|
+
process.execPath,
|
|
104
|
+
[
|
|
105
|
+
fileURLToPath(new URL("./hud-qos-status.mjs", import.meta.url)),
|
|
106
|
+
"--cache-only",
|
|
107
|
+
],
|
|
108
|
+
{
|
|
109
|
+
timeout: 10000,
|
|
110
|
+
env: { ...process.env, COLUMNS: String(input.terminal_width) },
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
rendering.child.stdin.end(JSON.stringify(input));
|
|
114
|
+
const { stdout } = await rendering;
|
|
115
|
+
const lines = stripAnsi(stdout).trim().split("\n");
|
|
116
|
+
for (let n = 0; n < 3; n += 1) {
|
|
117
|
+
await exec(
|
|
118
|
+
"tmux",
|
|
119
|
+
[
|
|
120
|
+
"set-option",
|
|
121
|
+
"-t",
|
|
122
|
+
session,
|
|
123
|
+
`@tfx_hud_${n}`,
|
|
124
|
+
(lines[n] || "").replace(/#/g, "##"),
|
|
125
|
+
],
|
|
126
|
+
{ timeout: 2000 },
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return lines[0] || "";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
133
|
+
updateTmuxHud(...process.argv.slice(2))
|
|
134
|
+
.then((status) => {
|
|
135
|
+
process.stdout.write(`${status}\n`);
|
|
136
|
+
})
|
|
137
|
+
.catch(() => {
|
|
138
|
+
process.stdout.write("c: ctx:--\n");
|
|
139
|
+
});
|
|
140
|
+
}
|