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: (verbose || dashboard) ? 10 : 0,
26
- onProgress: function onProgress(event) {
27
- // TUI 대시보드 업데이트
28
- if (tui) {
29
- if (event.type === "dispatched") {
30
- tui.updateWorker(event.paneName, { cli: event.cli, status: "running" });
31
- } else if (event.type === "progress") {
32
- const snap = (event.snapshot || "").split("\n").filter(l => l.trim()).pop() || "";
33
- tui.updateWorker(event.paneName, { snapshot: snap });
34
- } else if (event.type === "completed") {
35
- tui.updateWorker(event.paneName, {
36
- status: event.matched && event.exitCode === 0 ? "completed" : "failed",
37
- elapsed: Math.round((Date.now() - (tui._start || Date.now())) / 1000),
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
 
@@ -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
- // 첫 번째 워커: 빈 lead pane 직접 사용 (빈 pane 제거)
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
- // v6.0.20: 항상 별도 창 (포커스 문제 회피)
567
- autoAttachTerminal(sessionName, { mode: "window" }, assignments.length);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "7.0.1",
3
+ "version": "7.0.3",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {