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.
@@ -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 {
@@ -1,25 +1,28 @@
1
1
  #!/usr/bin/env node
2
- // hub/team/tui-viewer.mjs — psmux pane용 TUI 대시보드 뷰어 v2
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, readdirSync, statSync } from "node:fs";
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 { createTui } from "./tui.mjs";
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 = join(tmpdir(), "tfx-headless");
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 = createTui({ refreshMs: 0 });
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 { return []; }
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 { return ""; }
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 { return null; }
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, { cli, role: pane.title, status: "completed", handoff: { verdict, confidence: tokensLine ? "high" : "low" }, elapsed });
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 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));
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
- return `${elapsedLabel(st)} ${icon} ${workerLabel} (${cliLabel}) ${statusText} ${dim("—")} ${message}`;
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
- workers.set(paneName, { ...existing, ...state });
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
- out(`${dim(`[${Math.max(0, Math.round((Date.now() - startedAt) / 1000))}s]`)} ${dim("log-dashboard stopped")}`);
137
+ closed = true;
135
138
  },
136
139
  };
137
140
  }
141
+
142
+ // 하위 호환
143
+ export { createLogDashboard as createTui };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "7.0.6",
3
+ "version": "7.1.1",
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": {