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.
@@ -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
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 { 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
  // ── 메인 폴링 ──
@@ -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, { cli, role: pane.title, status: "completed", handoff: { verdict, confidence: tokensLine ? "high" : "low" }, elapsed });
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 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
+ 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
- out(`${dim(`[${Math.max(0, Math.round((Date.now() - startedAt) / 1000))}s]`)} ${dim("log-dashboard stopped")}`);
140
+ closed = true;
135
141
  },
136
142
  };
137
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "7.0.6",
3
+ "version": "7.1.0",
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": {