triflux 7.0.6 → 7.1.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/team/headless.mjs +18 -2
- package/hub/team/tui-viewer.mjs +26 -13
- package/hub/team/tui.mjs +31 -25
- package/package.json +1 -1
package/hub/team/headless.mjs
CHANGED
|
@@ -522,8 +522,25 @@ export function attachDashboardTab(sessionName, workerCount = 2) {
|
|
|
522
522
|
try { execSync("where wt.exe", { stdio: "ignore" }); } catch { return false; }
|
|
523
523
|
ensureWtProfile(workerCount);
|
|
524
524
|
|
|
525
|
+
// v7.1: 같은 WT 창에서 가로 스플릿 (새 탭 대신)
|
|
526
|
+
// 포커스 탈취가 최소화되고 Claude Code와 나란히 보임
|
|
527
|
+
if (process.env.WT_SESSION) {
|
|
528
|
+
try {
|
|
529
|
+
const child = spawn("wt.exe", [
|
|
530
|
+
"-w", "0", "sp", "-H", "-s", "0.35",
|
|
531
|
+
"--profile", "triflux",
|
|
532
|
+
"--title", `▲ ${sessionName}`,
|
|
533
|
+
"--", "psmux", "attach", "-t", sessionName,
|
|
534
|
+
], { detached: true, stdio: "ignore" });
|
|
535
|
+
child.unref();
|
|
536
|
+
// 스플릿 후 원래 pane으로 포커스 복귀
|
|
537
|
+
try { spawn("wt.exe", ["-w", "0", "mf", "up"], { detached: true, stdio: "ignore" }).unref(); } catch {}
|
|
538
|
+
return true;
|
|
539
|
+
} catch { /* fallthrough to tab */ }
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// WT 세션 외부 → 새 탭 fallback
|
|
525
543
|
try {
|
|
526
|
-
// psmux attach로 전체 세션을 WT 탭에 표시
|
|
527
544
|
const child = spawn("wt.exe", [
|
|
528
545
|
"-w", "0", "nt",
|
|
529
546
|
"--profile", "triflux",
|
|
@@ -533,7 +550,6 @@ export function attachDashboardTab(sessionName, workerCount = 2) {
|
|
|
533
550
|
child.unref();
|
|
534
551
|
} catch { return false; }
|
|
535
552
|
|
|
536
|
-
// 150ms 후 이전 탭으로 복귀
|
|
537
553
|
const prevTabScript = "Start-Sleep -Milliseconds 150; Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('^+{TAB}')";
|
|
538
554
|
for (const shell of ["pwsh.exe", "powershell.exe"]) {
|
|
539
555
|
try {
|
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
|
-
import { existsSync, readFileSync,
|
|
5
|
+
import { existsSync, readFileSync, 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
|
// ── 메인 폴링 ──
|
|
@@ -110,16 +119,20 @@ function poll() {
|
|
|
110
119
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
111
120
|
if (resultSize > 10 || shellReturned) {
|
|
112
121
|
workerState.set(paneName, { paneIdx: pane.index, done: true });
|
|
113
|
-
const meaningful = lines.filter(l => !(/^(PS\s|>|\$)/.test(l)) && !(/tokens?\s+used/i.test(l)));
|
|
122
|
+
const meaningful = lines.filter((l) => !(/^(PS\s|>|\$)/.test(l)) && !(/tokens?\s+used/i.test(l)));
|
|
114
123
|
const verdict = meaningful.pop()?.slice(0, 80) || "completed";
|
|
115
|
-
tui.updateWorker(paneName, {
|
|
124
|
+
tui.updateWorker(paneName, {
|
|
125
|
+
cli,
|
|
126
|
+
role: pane.title,
|
|
127
|
+
status: "completed",
|
|
128
|
+
handoff: { verdict, confidence: tokensLine ? "high" : "low" },
|
|
129
|
+
elapsed,
|
|
130
|
+
});
|
|
116
131
|
} else {
|
|
117
132
|
const meaningful = lines.filter(l => !(/^(PS\s|>|\$)/.test(l)));
|
|
118
133
|
const snap = meaningful.pop()?.slice(0, 60) || lastLine.slice(0, 60);
|
|
119
134
|
tui.updateWorker(paneName, { cli, role: pane.title, status: "running", snapshot: snap, elapsed });
|
|
120
135
|
}
|
|
121
|
-
// debug: stderr에 상태 출력 (캡처 영향 없음)
|
|
122
|
-
process.stderr.write(`[dbg] ${paneName} cli=${cli} status=${shellReturned?"done":"run"} lines=${lines.length}\n`);
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
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
|
+
].join("|");
|
|
117
|
+
const sigChanged = nextSig !== existing._sig;
|
|
118
|
+
const explicitElapsed = Number.isFinite(state.elapsed) ? Math.max(0, Math.round(state.elapsed)) : null;
|
|
119
|
+
merged._sig = nextSig;
|
|
120
|
+
merged._logSec = sigChanged
|
|
121
|
+
? (explicitElapsed ?? nowElapsedSec())
|
|
122
|
+
: (Number.isFinite(existing._logSec) ? existing._logSec : (explicitElapsed ?? nowElapsedSec()));
|
|
123
|
+
workers.set(paneName, merged);
|
|
124
|
+
},
|
|
125
|
+
updatePipeline(state) {
|
|
126
|
+
pipeline = { ...pipeline, ...state };
|
|
127
|
+
},
|
|
128
|
+
setStartTime(ms) {
|
|
129
|
+
startedAt = ms;
|
|
124
130
|
},
|
|
125
|
-
updatePipeline(state) { pipeline = { ...pipeline, ...state }; },
|
|
126
|
-
setStartTime(ms) { startedAt = ms; },
|
|
127
131
|
render,
|
|
128
132
|
getWorkers() { return new Map(workers); },
|
|
129
133
|
getFrameCount() { return frameCount; },
|
|
130
134
|
close() {
|
|
131
135
|
if (closed) return;
|
|
132
|
-
closed = true;
|
|
133
136
|
if (timer) clearInterval(timer);
|
|
134
|
-
|
|
137
|
+
closed = true;
|
|
135
138
|
},
|
|
136
139
|
};
|
|
137
140
|
}
|
|
141
|
+
|
|
142
|
+
// 하위 호환
|
|
143
|
+
export { createLogDashboard as createTui };
|