triflux 7.0.1 → 7.0.3
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.
|
@@ -38,7 +38,7 @@ function renderTmuxInstallHelp() {
|
|
|
38
38
|
export { parseTeamArgs };
|
|
39
39
|
|
|
40
40
|
export async function teamStart(args = []) {
|
|
41
|
-
const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose, mcpProfile } = parseTeamArgs(args);
|
|
41
|
+
const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile } = parseTeamArgs(args);
|
|
42
42
|
// --assign 사용 시 task를 자동 생성
|
|
43
43
|
const task = rawTask || (assigns.length > 0 ? assigns.map(a => a.prompt).join(" + ") : "");
|
|
44
44
|
if (!task) return printStartUsage();
|
|
@@ -72,7 +72,7 @@ export async function teamStart(args = []) {
|
|
|
72
72
|
const state = effectiveMode === "in-process"
|
|
73
73
|
? await startInProcessTeam({ sessionId, task, lead, agents, subtasks, hubUrl })
|
|
74
74
|
: effectiveMode === "headless"
|
|
75
|
-
? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, mcpProfile })
|
|
75
|
+
? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile })
|
|
76
76
|
: effectiveMode === "wt"
|
|
77
77
|
? await startWtTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl })
|
|
78
78
|
: await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { BOLD, DIM, GREEN, RESET, AMBER } from "../../../shared.mjs";
|
|
2
2
|
import { runHeadlessInteractive, resolveCliType } from "../../../headless.mjs";
|
|
3
|
-
import { createTui } from "../../../tui.mjs";
|
|
4
3
|
import { ok, warn } from "../../render.mjs";
|
|
5
4
|
import { buildTasks } from "../../services/task-model.mjs";
|
|
6
5
|
import { clearTeamState } from "../../services/state-store.mjs";
|
|
@@ -14,48 +13,28 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
|
|
|
14
13
|
const startedAt = Date.now();
|
|
15
14
|
ok(`headless ${assignments.length}워커 시작`);
|
|
16
15
|
|
|
17
|
-
// TUI 대시보드 (--dashboard 플래그)
|
|
18
|
-
const tui = dashboard ? createTui({ refreshMs: 1000 }) : null;
|
|
19
|
-
|
|
20
16
|
const handle = await runHeadlessInteractive(sessionId, assignments, {
|
|
21
17
|
timeoutSec: timeoutSec || 300,
|
|
22
18
|
layout,
|
|
23
19
|
autoAttach: !!autoAttach,
|
|
20
|
+
dashboard: !!dashboard,
|
|
24
21
|
progressive: progressive !== false,
|
|
25
|
-
progressIntervalSec:
|
|
26
|
-
onProgress: function onProgress(event) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// verbose 텍스트 출력 (TUI 비활성 시)
|
|
43
|
-
if (verbose && !tui) {
|
|
44
|
-
if (event.type === "session_created") {
|
|
45
|
-
console.log(` ${DIM}세션: ${event.sessionName}${RESET}`);
|
|
46
|
-
} else if (event.type === "worker_added") {
|
|
47
|
-
console.log(` ${DIM}[+] ${event.paneTitle}${RESET}`);
|
|
48
|
-
} else if (event.type === "dispatched") {
|
|
49
|
-
console.log(` ${DIM}[${event.paneName}] ${event.cli} dispatch${RESET}`);
|
|
50
|
-
} else if (event.type === "progress") {
|
|
51
|
-
const last = (event.snapshot || "").split("\n").filter(l => l.trim()).pop() || "";
|
|
52
|
-
if (last) console.log(` ${DIM}[${event.paneName}] ${last.slice(0, 60)}${RESET}`);
|
|
53
|
-
} else if (event.type === "completed") {
|
|
54
|
-
const icon = event.matched && event.exitCode === 0 ? `${GREEN}✓${RESET}` : `${AMBER}✗${RESET}`;
|
|
55
|
-
console.log(` ${icon} [${event.paneName}] ${event.cli} exit=${event.exitCode}${event.sessionDead ? " (dead)" : ""}`);
|
|
56
|
-
}
|
|
22
|
+
progressIntervalSec: verbose ? 10 : 0,
|
|
23
|
+
onProgress: verbose ? function onProgress(event) {
|
|
24
|
+
if (event.type === "session_created") {
|
|
25
|
+
console.log(` ${DIM}세션: ${event.sessionName}${RESET}`);
|
|
26
|
+
} else if (event.type === "worker_added") {
|
|
27
|
+
console.log(` ${DIM}[+] ${event.paneTitle}${RESET}`);
|
|
28
|
+
} else if (event.type === "dispatched") {
|
|
29
|
+
console.log(` ${DIM}[${event.paneName}] ${event.cli} dispatch${RESET}`);
|
|
30
|
+
} else if (event.type === "progress") {
|
|
31
|
+
const last = (event.snapshot || "").split("\n").filter(l => l.trim()).pop() || "";
|
|
32
|
+
if (last) console.log(` ${DIM}[${event.paneName}] ${last.slice(0, 60)}${RESET}`);
|
|
33
|
+
} else if (event.type === "completed") {
|
|
34
|
+
const icon = event.matched && event.exitCode === 0 ? `${GREEN}✓${RESET}` : `${AMBER}✗${RESET}`;
|
|
35
|
+
console.log(` ${icon} [${event.paneName}] ${event.cli} exit=${event.exitCode}${event.sessionDead ? " (dead)" : ""}`);
|
|
57
36
|
}
|
|
58
|
-
},
|
|
37
|
+
} : undefined,
|
|
59
38
|
});
|
|
60
39
|
|
|
61
40
|
// 최소 결과 요약
|
|
@@ -94,21 +73,6 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
|
|
|
94
73
|
}
|
|
95
74
|
}
|
|
96
75
|
|
|
97
|
-
// TUI에 handoff 최종 반영
|
|
98
|
-
if (tui) {
|
|
99
|
-
for (const r of results) {
|
|
100
|
-
if (r.handoff) {
|
|
101
|
-
tui.updateWorker(r.paneName, {
|
|
102
|
-
status: r.matched && r.exitCode === 0 ? "completed" : "failed",
|
|
103
|
-
handoff: r.handoff,
|
|
104
|
-
elapsed: Math.round((Date.now() - startedAt) / 1000),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
tui.render(); // 최종 프레임
|
|
109
|
-
tui.close();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
76
|
// 세션 정리
|
|
113
77
|
handle.kill();
|
|
114
78
|
|
package/hub/team/headless.mjs
CHANGED
|
@@ -131,6 +131,7 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
131
131
|
onProgress,
|
|
132
132
|
progressIntervalSec = 0,
|
|
133
133
|
progressive = true,
|
|
134
|
+
dashboard = false,
|
|
134
135
|
} = opts;
|
|
135
136
|
|
|
136
137
|
mkdirSync(RESULT_DIR, { recursive: true });
|
|
@@ -148,6 +149,16 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
148
149
|
applyTrifluxTheme(sessionName);
|
|
149
150
|
if (safeProgress) safeProgress({ type: "session_created", sessionName, panes: session.panes });
|
|
150
151
|
|
|
152
|
+
// v7.0: dashboard pane — pane 0에 TUI 뷰어를 실행
|
|
153
|
+
if (dashboard) {
|
|
154
|
+
const viewerPath = join(import.meta.dirname || __dirname, "tui-viewer.mjs").replace(/\\/g, "/");
|
|
155
|
+
const viewerCmd = `node "${viewerPath}" --session ${sessionName} --result-dir ${RESULT_DIR.replace(/\\/g, "/")}`;
|
|
156
|
+
try {
|
|
157
|
+
dispatchCommand(sessionName, `${sessionName}:0.0`, viewerCmd);
|
|
158
|
+
psmuxExec(["select-pane", "-t", `${sessionName}:0.0`, "-T", "▲ dashboard"]);
|
|
159
|
+
} catch { /* 무시 */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
dispatches = assignments.map((assignment, i) => {
|
|
152
163
|
const paneName = `worker-${i + 1}`;
|
|
153
164
|
const resolvedCli = resolveCliType(assignment.cli);
|
|
@@ -157,8 +168,8 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
157
168
|
: `${brand.emoji} ${resolvedCli}-${i + 1}`;
|
|
158
169
|
|
|
159
170
|
let newPaneId;
|
|
160
|
-
if (i === 0) {
|
|
161
|
-
// 첫 번째
|
|
171
|
+
if (i === 0 && !dashboard) {
|
|
172
|
+
// 대시보드 없으면: 첫 번째 워커가 빈 lead pane 사용
|
|
162
173
|
newPaneId = `${sessionName}:0.0`;
|
|
163
174
|
} else {
|
|
164
175
|
// 2번째+: split-window로 추가
|
|
@@ -502,6 +513,40 @@ export function autoAttachTerminal(sessionName, opts = {}, workerCount = 2) {
|
|
|
502
513
|
return true;
|
|
503
514
|
}
|
|
504
515
|
|
|
516
|
+
/**
|
|
517
|
+
* v7.0: psmux 세션을 WT 탭에 attach (대시보드 + 워커 전체 뷰)
|
|
518
|
+
* @param {string} sessionName
|
|
519
|
+
* @param {number} workerCount
|
|
520
|
+
* @returns {boolean}
|
|
521
|
+
*/
|
|
522
|
+
export function attachDashboardTab(sessionName, workerCount = 2) {
|
|
523
|
+
try { execSync("where wt.exe", { stdio: "ignore" }); } catch { return false; }
|
|
524
|
+
ensureWtProfile(workerCount);
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
// psmux attach로 전체 세션을 WT 탭에 표시
|
|
528
|
+
const child = spawn("wt.exe", [
|
|
529
|
+
"-w", "0", "nt",
|
|
530
|
+
"--profile", "triflux",
|
|
531
|
+
"--title", `▲ ${sessionName}`,
|
|
532
|
+
"--", "psmux", "attach", "-t", sessionName,
|
|
533
|
+
], { detached: true, stdio: "ignore" });
|
|
534
|
+
child.unref();
|
|
535
|
+
} catch { return false; }
|
|
536
|
+
|
|
537
|
+
// 150ms 후 이전 탭으로 복귀
|
|
538
|
+
const prevTabScript = "Start-Sleep -Milliseconds 150; Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('^+{TAB}')";
|
|
539
|
+
for (const shell of ["pwsh.exe", "powershell.exe"]) {
|
|
540
|
+
try {
|
|
541
|
+
spawn(shell, ["-NoProfile", "-NonInteractive", "-Command", prevTabScript], {
|
|
542
|
+
detached: true, stdio: "ignore",
|
|
543
|
+
}).unref();
|
|
544
|
+
break;
|
|
545
|
+
} catch { /* 다음 shell */ }
|
|
546
|
+
}
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
|
|
505
550
|
/**
|
|
506
551
|
* 모든 워커 pane의 현재 스냅샷을 수집한다.
|
|
507
552
|
*
|
|
@@ -552,19 +597,27 @@ export function getProgressSnapshots(sessionName, dispatches, lines = 15) {
|
|
|
552
597
|
export async function runHeadlessInteractive(sessionName, assignments, opts = {}) {
|
|
553
598
|
const {
|
|
554
599
|
autoAttach = false,
|
|
600
|
+
dashboard = false,
|
|
555
601
|
signal,
|
|
556
602
|
maxIdleSec = 0,
|
|
557
603
|
...runOpts
|
|
558
604
|
} = opts;
|
|
559
605
|
|
|
606
|
+
// dashboard 옵션을 runHeadless에 전달
|
|
607
|
+
if (dashboard) runOpts.dashboard = true;
|
|
608
|
+
|
|
560
609
|
// autoAttach를 session_created 시점에 트리거 (CLI 실행 전에 터미널 열림)
|
|
561
610
|
const userOnProgress = runOpts.onProgress;
|
|
562
611
|
let terminalAttached = false;
|
|
563
612
|
runOpts.onProgress = (event) => {
|
|
564
613
|
if (autoAttach && event.type === "session_created" && !terminalAttached) {
|
|
565
614
|
terminalAttached = true;
|
|
566
|
-
|
|
567
|
-
|
|
615
|
+
if (dashboard) {
|
|
616
|
+
// v7.0: psmux attach로 대시보드+워커 전체 세션을 WT 탭에 표시
|
|
617
|
+
attachDashboardTab(sessionName, assignments.length);
|
|
618
|
+
} else {
|
|
619
|
+
autoAttachTerminal(sessionName, { mode: "window" }, assignments.length);
|
|
620
|
+
}
|
|
568
621
|
}
|
|
569
622
|
if (userOnProgress) userOnProgress(event);
|
|
570
623
|
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hub/team/tui-viewer.mjs — psmux pane용 TUI 대시보드 뷰어
|
|
3
|
+
// 독립 프로세스로 실행되어 headless 워커 상태를 실시간 렌더링한다.
|
|
4
|
+
//
|
|
5
|
+
// 실행: node hub/team/tui-viewer.mjs --session <name> [--result-dir <dir>]
|
|
6
|
+
//
|
|
7
|
+
// 데이터 소스: result 파일 (RESULT_DIR/{session}-worker-N.txt) polling
|
|
8
|
+
// + psmux capture-pane (워커 진행 상황)
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { createTui } from "./tui.mjs";
|
|
14
|
+
import { processHandoff } from "./handoff.mjs";
|
|
15
|
+
|
|
16
|
+
// ── 인자 파싱 ──
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const sessionIdx = args.indexOf("--session");
|
|
19
|
+
const resultDirIdx = args.indexOf("--result-dir");
|
|
20
|
+
const SESSION = sessionIdx >= 0 ? args[sessionIdx + 1] : null;
|
|
21
|
+
const RESULT_DIR = resultDirIdx >= 0 ? args[resultDirIdx + 1] : join(tmpdir(), "tfx-headless");
|
|
22
|
+
|
|
23
|
+
if (!SESSION) {
|
|
24
|
+
process.stderr.write("Usage: node tui-viewer.mjs --session <name>\n");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── TUI 초기화 ──
|
|
29
|
+
const tui = createTui({ refreshMs: 0 }); // 수동 렌더 (폴링 주기에 맞춤)
|
|
30
|
+
|
|
31
|
+
// ── 워커 상태 폴링 ──
|
|
32
|
+
const POLL_MS = 800;
|
|
33
|
+
const knownWorkers = new Map(); // paneName → { resultFile, lastSize, done }
|
|
34
|
+
|
|
35
|
+
function discoverWorkers() {
|
|
36
|
+
if (!existsSync(RESULT_DIR)) return;
|
|
37
|
+
const prefix = `${SESSION}-worker-`;
|
|
38
|
+
for (const f of readdirSync(RESULT_DIR)) {
|
|
39
|
+
if (!f.startsWith(prefix) || !f.endsWith(".txt")) continue;
|
|
40
|
+
const paneName = f.replace(`${SESSION}-`, "").replace(".txt", "");
|
|
41
|
+
if (!knownWorkers.has(paneName)) {
|
|
42
|
+
knownWorkers.set(paneName, {
|
|
43
|
+
resultFile: join(RESULT_DIR, f),
|
|
44
|
+
lastSize: 0,
|
|
45
|
+
done: false,
|
|
46
|
+
});
|
|
47
|
+
tui.updateWorker(paneName, { cli: "codex", status: "running" });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pollWorkers() {
|
|
53
|
+
discoverWorkers();
|
|
54
|
+
|
|
55
|
+
for (const [paneName, state] of knownWorkers) {
|
|
56
|
+
if (state.done) continue;
|
|
57
|
+
|
|
58
|
+
let size = 0;
|
|
59
|
+
try { size = statSync(state.resultFile).size; } catch { continue; }
|
|
60
|
+
|
|
61
|
+
if (size === state.lastSize) continue;
|
|
62
|
+
state.lastSize = size;
|
|
63
|
+
|
|
64
|
+
// 결과 파일 읽기
|
|
65
|
+
let content = "";
|
|
66
|
+
try { content = readFileSync(state.resultFile, "utf8"); } catch { continue; }
|
|
67
|
+
|
|
68
|
+
if (content.trim().length === 0) continue;
|
|
69
|
+
|
|
70
|
+
// handoff 파싱 시도
|
|
71
|
+
const result = processHandoff(content, { exitCode: 0, resultFile: state.resultFile });
|
|
72
|
+
|
|
73
|
+
if (result.handoff && !result.fallback) {
|
|
74
|
+
// handoff 블록 발견 → 완료
|
|
75
|
+
state.done = true;
|
|
76
|
+
tui.updateWorker(paneName, {
|
|
77
|
+
status: result.handoff.status === "failed" ? "failed" : "completed",
|
|
78
|
+
handoff: result.handoff,
|
|
79
|
+
elapsed: Math.round((Date.now() - startTime) / 1000),
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
// 아직 진행 중 — 마지막 줄을 스냅샷으로
|
|
83
|
+
const lastLine = content.trim().split("\n").filter(l => l.trim()).pop() || "";
|
|
84
|
+
tui.updateWorker(paneName, {
|
|
85
|
+
status: "running",
|
|
86
|
+
snapshot: lastLine.slice(0, 60),
|
|
87
|
+
elapsed: Math.round((Date.now() - startTime) / 1000),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
tui.render();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── 메인 루프 ──
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
tui.setStartTime(startTime);
|
|
98
|
+
|
|
99
|
+
// 초기 렌더
|
|
100
|
+
tui.render();
|
|
101
|
+
|
|
102
|
+
const timer = setInterval(pollWorkers, POLL_MS);
|
|
103
|
+
|
|
104
|
+
// 모든 워커 완료 감지 → 10초 후 자동 종료
|
|
105
|
+
let doneCheckTimer = setInterval(() => {
|
|
106
|
+
if (knownWorkers.size > 0 && [...knownWorkers.values()].every(w => w.done)) {
|
|
107
|
+
tui.render(); // 최종 렌더
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
tui.close();
|
|
110
|
+
clearInterval(timer);
|
|
111
|
+
clearInterval(doneCheckTimer);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}, 10000); // 10초 유지 후 종료
|
|
114
|
+
clearInterval(doneCheckTimer);
|
|
115
|
+
}
|
|
116
|
+
}, 2000);
|
|
117
|
+
|
|
118
|
+
// Ctrl+C 정리
|
|
119
|
+
process.on("SIGINT", () => {
|
|
120
|
+
tui.close();
|
|
121
|
+
clearInterval(timer);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 5분 안전 타임아웃
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
tui.close();
|
|
128
|
+
clearInterval(timer);
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}, 5 * 60 * 1000);
|