triflux 7.0.6 → 7.1.0
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/tui-viewer.mjs +28 -12
- package/hub/team/tui.mjs +31 -25
- package/package.json +1 -1
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// hub/team/tui-viewer.mjs — psmux pane용
|
|
3
|
-
// 같은 psmux 세션의 워커 pane을 capture-pane으로
|
|
2
|
+
// hub/team/tui-viewer.mjs — psmux pane용 append-only 로그 뷰어 v3
|
|
3
|
+
// 같은 psmux 세션의 워커 pane을 capture-pane으로 모니터링한다.
|
|
4
4
|
|
|
5
5
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
|
-
import {
|
|
9
|
+
import { createLogDashboard } from "./tui.mjs";
|
|
10
10
|
import { processHandoff } from "./handoff.mjs";
|
|
11
11
|
|
|
12
12
|
const args = process.argv.slice(2);
|
|
13
13
|
const sessionIdx = args.indexOf("--session");
|
|
14
|
+
const resultDirIdx = args.indexOf("--result-dir");
|
|
14
15
|
const SESSION = sessionIdx >= 0 ? args[sessionIdx + 1] : null;
|
|
15
|
-
const RESULT_DIR =
|
|
16
|
+
const RESULT_DIR = resultDirIdx >= 0
|
|
17
|
+
? args[resultDirIdx + 1]
|
|
18
|
+
: join(tmpdir(), "tfx-headless");
|
|
16
19
|
|
|
17
20
|
if (!SESSION) {
|
|
18
21
|
process.stderr.write("Usage: node tui-viewer.mjs --session <name>\n");
|
|
19
22
|
process.exit(1);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
const tui =
|
|
25
|
+
const tui = createLogDashboard({ refreshMs: 0 });
|
|
23
26
|
const startTime = Date.now();
|
|
24
27
|
tui.setStartTime(startTime);
|
|
25
28
|
|
|
@@ -34,7 +37,9 @@ function listPanes() {
|
|
|
34
37
|
const [index, title, pid] = line.split(":");
|
|
35
38
|
return { index: parseInt(index, 10), title: title || "", pid };
|
|
36
39
|
});
|
|
37
|
-
} catch {
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
// ── pane 캡처 ──
|
|
@@ -43,7 +48,9 @@ function capturePane(paneIdx, lines = 5) {
|
|
|
43
48
|
return execFileSync("psmux", [
|
|
44
49
|
"capture-pane", "-t", `${SESSION}:0.${paneIdx}`, "-p",
|
|
45
50
|
], { encoding: "utf8", timeout: 2000 }).trim().split("\n").slice(-lines).join("\n");
|
|
46
|
-
} catch {
|
|
51
|
+
} catch {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
// ── result 파일에서 handoff 파싱 ──
|
|
@@ -54,7 +61,9 @@ function checkResultFile(paneName) {
|
|
|
54
61
|
const content = readFileSync(resultFile, "utf8");
|
|
55
62
|
if (content.trim().length === 0) return null;
|
|
56
63
|
return processHandoff(content, { exitCode: 0, resultFile });
|
|
57
|
-
} catch {
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
// ── 메인 폴링 ──
|
|
@@ -63,6 +72,9 @@ const workerState = new Map(); // paneName → { paneIdx, done }
|
|
|
63
72
|
|
|
64
73
|
function poll() {
|
|
65
74
|
const panes = listPanes();
|
|
75
|
+
if (panes.length <= 1 && workerState.size === 0) {
|
|
76
|
+
process.stderr.write(`[poll] no workers found. panes=${JSON.stringify(panes)} session=${SESSION}\n`);
|
|
77
|
+
}
|
|
66
78
|
// pane 0 = 대시보드 (자기 자신), pane 1+ = 워커
|
|
67
79
|
for (const pane of panes) {
|
|
68
80
|
if (pane.index === 0) continue; // 자기 자신 건너뜀
|
|
@@ -110,16 +122,20 @@ function poll() {
|
|
|
110
122
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
111
123
|
if (resultSize > 10 || shellReturned) {
|
|
112
124
|
workerState.set(paneName, { paneIdx: pane.index, done: true });
|
|
113
|
-
const meaningful = lines.filter(l => !(/^(PS\s|>|\$)/.test(l)) && !(/tokens?\s+used/i.test(l)));
|
|
125
|
+
const meaningful = lines.filter((l) => !(/^(PS\s|>|\$)/.test(l)) && !(/tokens?\s+used/i.test(l)));
|
|
114
126
|
const verdict = meaningful.pop()?.slice(0, 80) || "completed";
|
|
115
|
-
tui.updateWorker(paneName, {
|
|
127
|
+
tui.updateWorker(paneName, {
|
|
128
|
+
cli,
|
|
129
|
+
role: pane.title,
|
|
130
|
+
status: "completed",
|
|
131
|
+
handoff: { verdict, confidence: tokensLine ? "high" : "low" },
|
|
132
|
+
elapsed,
|
|
133
|
+
});
|
|
116
134
|
} else {
|
|
117
135
|
const meaningful = lines.filter(l => !(/^(PS\s|>|\$)/.test(l)));
|
|
118
136
|
const snap = meaningful.pop()?.slice(0, 60) || lastLine.slice(0, 60);
|
|
119
137
|
tui.updateWorker(paneName, { cli, role: pane.title, status: "running", snapshot: snap, elapsed });
|
|
120
138
|
}
|
|
121
|
-
// debug: stderr에 상태 출력 (캡처 영향 없음)
|
|
122
|
-
process.stderr.write(`[dbg] ${paneName} cli=${cli} status=${shellReturned?"done":"run"} lines=${lines.length}\n`);
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
tui.render();
|
package/hub/team/tui.mjs
CHANGED
|
@@ -31,14 +31,14 @@ export function createLogDashboard(opts = {}) {
|
|
|
31
31
|
let closed = false;
|
|
32
32
|
let frameCount = 0;
|
|
33
33
|
const lastLineByWorker = new Map();
|
|
34
|
-
let lastPipelineLine = "";
|
|
35
34
|
|
|
36
35
|
function out(text) { if (!closed) stream.write(`${text}\n`); }
|
|
37
36
|
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
function nowElapsedSec() {
|
|
38
|
+
return Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function elapsedLabel(sec) {
|
|
42
42
|
return dim(`[${sec}s]`);
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -79,13 +79,8 @@ export function createLogDashboard(opts = {}) {
|
|
|
79
79
|
const workerLabel = color(name, FG.triflux);
|
|
80
80
|
const statusText = statusLabel(status);
|
|
81
81
|
const message = messageLabel(st);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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)}`;
|
|
82
|
+
const sec = Number.isFinite(st._logSec) ? st._logSec : nowElapsedSec();
|
|
83
|
+
return `${elapsedLabel(sec)} ${icon} ${workerLabel} (${cliLabel}) ${statusText} ${dim("—")} ${message}`;
|
|
89
84
|
}
|
|
90
85
|
|
|
91
86
|
// 현재 상태와 마지막으로 출력한 라인을 비교해 변경분만 append
|
|
@@ -102,16 +97,8 @@ export function createLogDashboard(opts = {}) {
|
|
|
102
97
|
out(line);
|
|
103
98
|
}
|
|
104
99
|
}
|
|
105
|
-
|
|
106
|
-
const nextPipelineLine = pipelineLine();
|
|
107
|
-
if (nextPipelineLine !== lastPipelineLine) {
|
|
108
|
-
lastPipelineLine = nextPipelineLine;
|
|
109
|
-
out(nextPipelineLine);
|
|
110
|
-
}
|
|
111
100
|
}
|
|
112
101
|
|
|
113
|
-
out(`${dim("[0s]")} ${color("▲ triflux", FG.triflux)} ${dim(`v${VERSION}`)} ${dim("log-dashboard started")}`);
|
|
114
|
-
|
|
115
102
|
if (refreshMs > 0) {
|
|
116
103
|
timer = setInterval(render, refreshMs);
|
|
117
104
|
if (timer.unref) timer.unref();
|
|
@@ -120,18 +107,37 @@ export function createLogDashboard(opts = {}) {
|
|
|
120
107
|
return {
|
|
121
108
|
updateWorker(paneName, state) {
|
|
122
109
|
const existing = workers.get(paneName) || { cli: "codex", status: "pending" };
|
|
123
|
-
|
|
110
|
+
const merged = { ...existing, ...state };
|
|
111
|
+
const nextSig = [
|
|
112
|
+
merged.cli || "",
|
|
113
|
+
merged.status || "",
|
|
114
|
+
merged.snapshot || "",
|
|
115
|
+
merged.handoff?.verdict || "",
|
|
116
|
+
merged.handoff?.lead_action || "",
|
|
117
|
+
merged.handoff?.confidence || "",
|
|
118
|
+
merged.handoff?.risk || "",
|
|
119
|
+
].join("|");
|
|
120
|
+
const sigChanged = nextSig !== existing._sig;
|
|
121
|
+
const explicitElapsed = Number.isFinite(state.elapsed) ? Math.max(0, Math.round(state.elapsed)) : null;
|
|
122
|
+
merged._sig = nextSig;
|
|
123
|
+
merged._logSec = sigChanged
|
|
124
|
+
? (explicitElapsed ?? nowElapsedSec())
|
|
125
|
+
: (Number.isFinite(existing._logSec) ? existing._logSec : (explicitElapsed ?? nowElapsedSec()));
|
|
126
|
+
workers.set(paneName, merged);
|
|
127
|
+
},
|
|
128
|
+
updatePipeline(state) {
|
|
129
|
+
pipeline = { ...pipeline, ...state };
|
|
130
|
+
},
|
|
131
|
+
setStartTime(ms) {
|
|
132
|
+
startedAt = ms;
|
|
124
133
|
},
|
|
125
|
-
updatePipeline(state) { pipeline = { ...pipeline, ...state }; },
|
|
126
|
-
setStartTime(ms) { startedAt = ms; },
|
|
127
134
|
render,
|
|
128
135
|
getWorkers() { return new Map(workers); },
|
|
129
136
|
getFrameCount() { return frameCount; },
|
|
130
137
|
close() {
|
|
131
138
|
if (closed) return;
|
|
132
|
-
closed = true;
|
|
133
139
|
if (timer) clearInterval(timer);
|
|
134
|
-
|
|
140
|
+
closed = true;
|
|
135
141
|
},
|
|
136
142
|
};
|
|
137
143
|
}
|