triflux 7.0.5 → 7.0.6
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/team/headless.mjs +13 -10
- package/hub/team/tui-viewer.mjs +17 -20
- package/hub/team/tui.mjs +68 -92
- package/package.json +1 -1
- package/scripts/headless-guard.mjs +5 -0
package/hub/team/headless.mjs
CHANGED
|
@@ -144,16 +144,8 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
144
144
|
applyTrifluxTheme(sessionName);
|
|
145
145
|
if (safeProgress) safeProgress({ type: "session_created", sessionName, panes: session.panes });
|
|
146
146
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
const viewerPath = join(import.meta.dirname, "tui-viewer.mjs").replace(/\\/g, "/");
|
|
150
|
-
const resultDir = RESULT_DIR.replace(/\\/g, "/");
|
|
151
|
-
const viewerCmd = `node ${viewerPath} --session ${sessionName} --result-dir ${resultDir}`;
|
|
152
|
-
try {
|
|
153
|
-
psmuxExec(["send-keys", "-t", `${sessionName}:0.0`, viewerCmd, "Enter"]);
|
|
154
|
-
psmuxExec(["select-pane", "-t", `${sessionName}:0.0`, "-T", "▲ dashboard"]);
|
|
155
|
-
} catch { /* 무시 */ }
|
|
156
|
-
}
|
|
147
|
+
// dashboard: 워커 pane을 먼저 생성한 후 pane 0에 대시보드를 실행
|
|
148
|
+
// (listPanes로 워커 감지가 가능하려면 워커 pane이 먼저 존재해야 함)
|
|
157
149
|
|
|
158
150
|
dispatches = assignments.map((assignment, i) => {
|
|
159
151
|
const paneName = `worker-${i + 1}`;
|
|
@@ -196,6 +188,17 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
196
188
|
// 모든 split 완료 후 레이아웃 한 번만 정렬 (깜빡임 방지)
|
|
197
189
|
try { psmuxExec(["select-layout", "-t", sessionName, "tiled"]); } catch { /* 무시 */ }
|
|
198
190
|
|
|
191
|
+
// v7.0.6: 워커 pane 생성 후 대시보드 시작 (워커가 먼저 존재해야 listPanes 감지 가능)
|
|
192
|
+
if (dashboard) {
|
|
193
|
+
const viewerPath = join(import.meta.dirname, "tui-viewer.mjs").replace(/\\/g, "/");
|
|
194
|
+
const resultDir = RESULT_DIR.replace(/\\/g, "/");
|
|
195
|
+
const viewerCmd = `node ${viewerPath} --session ${sessionName}`;
|
|
196
|
+
try {
|
|
197
|
+
psmuxExec(["send-keys", "-t", `${sessionName}:0.0`, viewerCmd, "Enter"]);
|
|
198
|
+
psmuxExec(["select-pane", "-t", `${sessionName}:0.0`, "-T", "▲ dashboard"]);
|
|
199
|
+
} catch { /* 무시 */ }
|
|
200
|
+
}
|
|
201
|
+
|
|
199
202
|
} else {
|
|
200
203
|
// ─── 기존 모드: 모든 pane을 한 번에 생성 ───
|
|
201
204
|
const paneCount = assignments.length + 1;
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -91,38 +91,35 @@ function poll() {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// 진행 중 — pane 캡처로 스냅샷
|
|
94
|
-
const snapshot = capturePane(pane.index,
|
|
95
|
-
const
|
|
94
|
+
const snapshot = capturePane(pane.index, 5);
|
|
95
|
+
const lines = snapshot.split("\n").filter(l => l.trim());
|
|
96
|
+
const lastLine = lines.pop() || "";
|
|
96
97
|
|
|
97
98
|
if (!existing) {
|
|
98
99
|
workerState.set(paneName, { paneIdx: pane.index, done: false });
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
// result
|
|
102
|
+
// 완료 감지: (1) result 파일 존재, (2) 셸 프롬프트 복귀, (3) "tokens used" 텍스트
|
|
102
103
|
const resultFile = join(RESULT_DIR, `${SESSION}-${paneName}.txt`);
|
|
103
104
|
let resultSize = 0;
|
|
104
105
|
try { resultSize = statSync(resultFile).size; } catch {}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
const shellReturned = /^(PS\s|>|\$)\s*/.test(lastLine) && lines.length > 2;
|
|
108
|
+
const tokensLine = lines.find(l => /tokens?\s+used/i.test(l));
|
|
109
|
+
|
|
110
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
111
|
+
if (resultSize > 10 || shellReturned) {
|
|
108
112
|
workerState.set(paneName, { paneIdx: pane.index, done: true });
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
role: pane.title,
|
|
113
|
-
status: "completed",
|
|
114
|
-
handoff: fb.handoff,
|
|
115
|
-
elapsed: Math.round((Date.now() - startTime) / 1000),
|
|
116
|
-
});
|
|
113
|
+
const meaningful = lines.filter(l => !(/^(PS\s|>|\$)/.test(l)) && !(/tokens?\s+used/i.test(l)));
|
|
114
|
+
const verdict = meaningful.pop()?.slice(0, 80) || "completed";
|
|
115
|
+
tui.updateWorker(paneName, { cli, role: pane.title, status: "completed", handoff: { verdict, confidence: tokensLine ? "high" : "low" }, elapsed });
|
|
117
116
|
} else {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
status: "running",
|
|
122
|
-
snapshot: lastLine.slice(0, 60),
|
|
123
|
-
elapsed: Math.round((Date.now() - startTime) / 1000),
|
|
124
|
-
});
|
|
117
|
+
const meaningful = lines.filter(l => !(/^(PS\s|>|\$)/.test(l)));
|
|
118
|
+
const snap = meaningful.pop()?.slice(0, 60) || lastLine.slice(0, 60);
|
|
119
|
+
tui.updateWorker(paneName, { cli, role: pane.title, status: "running", snapshot: snap, elapsed });
|
|
125
120
|
}
|
|
121
|
+
// debug: stderr에 상태 출력 (캡처 영향 없음)
|
|
122
|
+
process.stderr.write(`[dbg] ${paneName} cli=${cli} status=${shellReturned?"done":"run"} lines=${lines.length}\n`);
|
|
126
123
|
}
|
|
127
124
|
|
|
128
125
|
tui.render();
|
package/hub/team/tui.mjs
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
// hub/team/tui.mjs —
|
|
2
|
-
//
|
|
3
|
-
// 외부 의존성 0 — Node.js 내장 + ansi.mjs만 사용
|
|
1
|
+
// hub/team/tui.mjs — Append-only 로그 대시보드 (v8)
|
|
2
|
+
// ANSI 색상은 유지하되 커서 이동/화면 덮어쓰기는 사용하지 않는다.
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
cursorHome, clearLine, cursorHide, cursorShow,
|
|
7
|
-
RESET, BOLD, DIM, FG, BG,
|
|
8
|
-
color, dim, truncate, stripAnsi,
|
|
9
|
-
STATUS_ICON, CLI_ICON,
|
|
10
|
-
} from "./ansi.mjs";
|
|
4
|
+
import { RESET, FG, color, dim, STATUS_ICON } from "./ansi.mjs";
|
|
11
5
|
|
|
12
6
|
// package.json에서 동적 로드 (실패 시 fallback)
|
|
13
7
|
let VERSION = "7.x";
|
|
@@ -18,18 +12,16 @@ try {
|
|
|
18
12
|
} catch { /* fallback */ }
|
|
19
13
|
|
|
20
14
|
/**
|
|
21
|
-
*
|
|
15
|
+
* 로그 스트림 대시보드 생성 (append-only)
|
|
22
16
|
* @param {object} [opts]
|
|
23
17
|
* @param {NodeJS.WriteStream} [opts.stream=process.stdout]
|
|
24
|
-
* @param {number} [opts.refreshMs=1000] — 자동
|
|
25
|
-
* @
|
|
26
|
-
* @returns {TuiHandle}
|
|
18
|
+
* @param {number} [opts.refreshMs=1000] — 자동 렌더 주기 (0=수동만)
|
|
19
|
+
* @returns {LogDashboardHandle}
|
|
27
20
|
*/
|
|
28
|
-
export function
|
|
21
|
+
export function createLogDashboard(opts = {}) {
|
|
29
22
|
const {
|
|
30
23
|
stream = process.stdout,
|
|
31
24
|
refreshMs = 1000,
|
|
32
|
-
width: fixedWidth = 0,
|
|
33
25
|
} = opts;
|
|
34
26
|
|
|
35
27
|
const workers = new Map();
|
|
@@ -38,105 +30,88 @@ export function createTui(opts = {}) {
|
|
|
38
30
|
let timer = null;
|
|
39
31
|
let closed = false;
|
|
40
32
|
let frameCount = 0;
|
|
33
|
+
const lastLineByWorker = new Map();
|
|
34
|
+
let lastPipelineLine = "";
|
|
41
35
|
|
|
42
|
-
function
|
|
43
|
-
function out(text) { if (!closed) stream.write(text); }
|
|
36
|
+
function out(text) { if (!closed) stream.write(`${text}\n`); }
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const done = vals.filter((s) => s.status === "completed").length;
|
|
51
|
-
const fail = vals.filter((s) => s.status === "failed").length;
|
|
52
|
-
const run = vals.filter((s) => s.status === "running").length;
|
|
53
|
-
|
|
54
|
-
const left = ` ${color("▲ triflux", FG.triflux)} ${dim(`v${VERSION}`)}`;
|
|
55
|
-
const right = `${color(`${done}✓`, FG.green)} ${color(`${fail}✗`, FG.red)} ${run}⏳ ${dim(`${vals.length}총`)} ${dim(`${elapsed}s`)}`;
|
|
56
|
-
const gap = Math.max(1, w() - stripAnsi(left).length - stripAnsi(right).length - 1);
|
|
57
|
-
return `${BG.header}${left}${" ".repeat(gap)}${right}${RESET}`;
|
|
38
|
+
function elapsedLabel(st) {
|
|
39
|
+
const sec = Number.isFinite(st.elapsed)
|
|
40
|
+
? Math.max(0, Math.round(st.elapsed))
|
|
41
|
+
: Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
42
|
+
return dim(`[${sec}s]`);
|
|
58
43
|
}
|
|
59
44
|
|
|
60
|
-
function
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const conf = st.handoff?.confidence ? dim(` ${st.handoff.confidence}`) : "";
|
|
66
|
-
|
|
67
|
-
const lines = [];
|
|
68
|
-
lines.push(` ${icon} ${st.cli} ${role} ${sIcon} ${el}${conf}`);
|
|
69
|
-
|
|
70
|
-
if (st.handoff?.verdict) {
|
|
71
|
-
lines.push(` ${truncate(st.handoff.verdict, w() - 8)}`);
|
|
72
|
-
}
|
|
45
|
+
function oneLine(text, fallback = "n/a") {
|
|
46
|
+
const normalized = String(text || "").replace(/\s+/g, " ").trim();
|
|
47
|
+
if (!normalized) return fallback;
|
|
48
|
+
return normalized.length > 160 ? `${normalized.slice(0, 157)}...` : normalized;
|
|
49
|
+
}
|
|
73
50
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
51
|
+
function cliColor(cli) {
|
|
52
|
+
if (cli === "gemini") return FG.gemini;
|
|
53
|
+
if (cli === "claude") return FG.claude;
|
|
54
|
+
if (cli === "codex") return FG.codex;
|
|
55
|
+
return FG.white;
|
|
56
|
+
}
|
|
79
57
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
58
|
+
function statusLabel(status) {
|
|
59
|
+
if (status === "completed") return color("completed", FG.green);
|
|
60
|
+
if (status === "failed") return color("failed", FG.red);
|
|
61
|
+
if (status === "running") return color("running", FG.blue);
|
|
62
|
+
return dim(status || "pending");
|
|
63
|
+
}
|
|
86
64
|
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
function messageLabel(st) {
|
|
66
|
+
if (st.handoff?.verdict) return oneLine(st.handoff.verdict, "completed");
|
|
67
|
+
if (st.snapshot) return oneLine(st.snapshot, st.status || "running");
|
|
68
|
+
if (st.status === "failed" && st.handoff?.lead_action) {
|
|
69
|
+
return oneLine(`action=${st.handoff.lead_action}`, "failed");
|
|
89
70
|
}
|
|
90
|
-
|
|
91
|
-
return lines;
|
|
71
|
+
return st.status || "pending";
|
|
92
72
|
}
|
|
93
73
|
|
|
94
|
-
function
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
104
|
-
return ` ${parts.join(dim("→"))}`;
|
|
74
|
+
function workerLine(name, st) {
|
|
75
|
+
const status = st.status || "pending";
|
|
76
|
+
const icon = STATUS_ICON[status] || STATUS_ICON.pending;
|
|
77
|
+
const cli = st.cli || "codex";
|
|
78
|
+
const cliLabel = `${cliColor(cli)}${cli}${RESET}`;
|
|
79
|
+
const workerLabel = color(name, FG.triflux);
|
|
80
|
+
const statusText = statusLabel(status);
|
|
81
|
+
const message = messageLabel(st);
|
|
82
|
+
return `${elapsedLabel(st)} ${icon} ${workerLabel} (${cliLabel}) ${statusText} ${dim("—")} ${message}`;
|
|
105
83
|
}
|
|
106
84
|
|
|
107
|
-
function
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
return `${BG.header} ${done}✓ 완료 │ ~${tokenSaved} 토큰 절감 ${RESET}`;
|
|
85
|
+
function pipelineLine() {
|
|
86
|
+
const phase = pipeline.phase || "exec";
|
|
87
|
+
const fix = Number.isFinite(pipeline.fix_attempt) ? pipeline.fix_attempt : 0;
|
|
88
|
+
return `${dim(`[${Math.max(0, Math.round((Date.now() - startedAt) / 1000))}s]`)} ${color("◇", FG.accent)} ${color("pipeline", FG.accent)} ${dim("(system)")} ${dim("state")} ${dim("—")} ${oneLine(`${phase} (fix=${fix})`, phase)}`;
|
|
112
89
|
}
|
|
113
90
|
|
|
91
|
+
// 현재 상태와 마지막으로 출력한 라인을 비교해 변경분만 append
|
|
114
92
|
function render() {
|
|
115
93
|
if (closed) return;
|
|
116
94
|
frameCount++;
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} else {
|
|
126
|
-
for (const [name, st] of workers) {
|
|
127
|
-
for (const l of workerCard(name, st)) buf.push(l + clearLine + "\n");
|
|
128
|
-
buf.push("\n");
|
|
95
|
+
|
|
96
|
+
const names = [...workers.keys()].sort();
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
const st = workers.get(name);
|
|
99
|
+
const line = workerLine(name, st);
|
|
100
|
+
if (line !== lastLineByWorker.get(name)) {
|
|
101
|
+
lastLineByWorker.set(name, line);
|
|
102
|
+
out(line);
|
|
129
103
|
}
|
|
130
104
|
}
|
|
131
105
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
106
|
+
const nextPipelineLine = pipelineLine();
|
|
107
|
+
if (nextPipelineLine !== lastPipelineLine) {
|
|
108
|
+
lastPipelineLine = nextPipelineLine;
|
|
109
|
+
out(nextPipelineLine);
|
|
110
|
+
}
|
|
137
111
|
}
|
|
138
112
|
|
|
139
|
-
|
|
113
|
+
out(`${dim("[0s]")} ${color("▲ triflux", FG.triflux)} ${dim(`v${VERSION}`)} ${dim("log-dashboard started")}`);
|
|
114
|
+
|
|
140
115
|
if (refreshMs > 0) {
|
|
141
116
|
timer = setInterval(render, refreshMs);
|
|
142
117
|
if (timer.unref) timer.unref();
|
|
@@ -156,6 +131,7 @@ export function createTui(opts = {}) {
|
|
|
156
131
|
if (closed) return;
|
|
157
132
|
closed = true;
|
|
158
133
|
if (timer) clearInterval(timer);
|
|
134
|
+
out(`${dim(`[${Math.max(0, Math.round((Date.now() - startedAt) / 1000))}s]`)} ${dim("log-dashboard stopped")}`);
|
|
159
135
|
},
|
|
160
136
|
};
|
|
161
137
|
}
|
package/package.json
CHANGED
|
@@ -135,6 +135,11 @@ async function main() {
|
|
|
135
135
|
process.exit(0);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// psmux send-keys / capture-pane은 통과 (pane 내 간접 실행)
|
|
139
|
+
if (/psmux\s+(send-keys|capture-pane|list-panes|split-window|select-pane)/.test(cmd)) {
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
138
143
|
// codex/gemini 직접 CLI 호출 → deny
|
|
139
144
|
if (/\bcodex\s+exec\b/.test(cmd) || /\bgemini\s+(-p|--prompt)\b/.test(cmd)) {
|
|
140
145
|
deny(
|