@triflux/remote 10.0.0-alpha.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/pipe.mjs +579 -0
- package/hub/public/dashboard.html +355 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/server.mjs +1124 -0
- package/hub/store-adapter.mjs +851 -0
- package/hub/store.mjs +897 -0
- package/hub/team/agent-map.json +11 -0
- package/hub/team/ansi.mjs +379 -0
- package/hub/team/backend.mjs +90 -0
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +106 -0
- package/hub/team/cli/commands/start/parse-args.mjs +130 -0
- package/hub/team/cli/commands/start/start-headless.mjs +109 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/cli/help.mjs +42 -0
- package/hub/team/cli/index.mjs +41 -0
- package/hub/team/cli/manifest.mjs +29 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +208 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +117 -0
- package/hub/team/cli/services/runtime-mode.mjs +62 -0
- package/hub/team/cli/services/state-store.mjs +48 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard-layout.mjs +33 -0
- package/hub/team/dashboard-open.mjs +153 -0
- package/hub/team/dashboard.mjs +274 -0
- package/hub/team/handoff.mjs +303 -0
- package/hub/team/headless.mjs +1149 -0
- package/hub/team/native-supervisor.mjs +392 -0
- package/hub/team/native.mjs +649 -0
- package/hub/team/nativeProxy.mjs +681 -0
- package/hub/team/orchestrator.mjs +161 -0
- package/hub/team/pane.mjs +153 -0
- package/hub/team/psmux.mjs +1354 -0
- package/hub/team/routing.mjs +223 -0
- package/hub/team/session.mjs +611 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +361 -0
- package/hub/team/tui-lite.mjs +380 -0
- package/hub/team/tui-viewer.mjs +463 -0
- package/hub/team/tui.mjs +1245 -0
- package/hub/tools.mjs +554 -0
- package/hub/tray.mjs +376 -0
- package/hub/workers/claude-worker.mjs +475 -0
- package/hub/workers/codex-mcp.mjs +504 -0
- package/hub/workers/delegator-mcp.mjs +1076 -0
- package/hub/workers/factory.mjs +21 -0
- package/hub/workers/gemini-worker.mjs +373 -0
- package/hub/workers/interface.mjs +52 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/package.json +31 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hub/team/tui-viewer.mjs — worker state aggregator v5
|
|
3
|
+
// psmux capture-pane 기반 워커 상태 집계 + TUI 렌더링
|
|
4
|
+
// data ingest: ~2Hz (500ms), render: 8-12FPS (별도 루프)
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { createLogDashboard } from "./tui.mjs";
|
|
11
|
+
import { createLiteDashboard } from "./tui-lite.mjs";
|
|
12
|
+
import { openHeadlessDashboardTarget } from "./dashboard-open.mjs";
|
|
13
|
+
import { processHandoff } from "./handoff.mjs";
|
|
14
|
+
import { statusBadge } from "./ansi.mjs";
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// ── CLI 인자 파싱 ──
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
function argVal(flag) {
|
|
20
|
+
const idx = args.indexOf(flag);
|
|
21
|
+
return idx >= 0 ? args[idx + 1] : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SESSION = argVal("--session");
|
|
25
|
+
const RESULT_DIR = argVal("--result-dir") ?? join(tmpdir(), "tfx-headless");
|
|
26
|
+
const LAYOUT = argVal("--layout") ?? "single";
|
|
27
|
+
|
|
28
|
+
if (!SESSION) {
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
"Usage: node tui-viewer.mjs --session <name> [--result-dir <dir>] [--layout <name>]\n",
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
execFileSync("psmux", ["--version"], { encoding: "utf8", timeout: 2000 });
|
|
37
|
+
} catch {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
"ERROR: psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)\n",
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── 메모리 보호 상수 ──
|
|
45
|
+
const MAX_BODY_BYTES = 10240;
|
|
46
|
+
|
|
47
|
+
// ── TUI 초기화 ──
|
|
48
|
+
// WT pane에서 spawn 시 process.stdout.isTTY=false일 수 있음
|
|
49
|
+
// forceTTY 시 alternate screen이 WT pane에서 렌더링 안 되는 문제 → append-only 유지
|
|
50
|
+
const tuiFactory = LAYOUT === "lite" ? createLiteDashboard : createLogDashboard;
|
|
51
|
+
const tui = tuiFactory({
|
|
52
|
+
refreshMs: 0, // render 루프를 직접 제어
|
|
53
|
+
stream: process.stdout,
|
|
54
|
+
input: process.stdin,
|
|
55
|
+
columns: process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 120,
|
|
56
|
+
layout: LAYOUT,
|
|
57
|
+
onOpenSelectedWorker: (workerName) => openHeadlessDashboardTarget(SESSION, {
|
|
58
|
+
worker: workerName,
|
|
59
|
+
openAll: false,
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
}),
|
|
62
|
+
onOpenAllWorkers: () => openHeadlessDashboardTarget(SESSION, {
|
|
63
|
+
openAll: true,
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
tui.setStartTime(startTime);
|
|
69
|
+
|
|
70
|
+
// ── 내부 raw data 누출 방지 패턴 ──
|
|
71
|
+
const INTERNAL_PATTERNS = [
|
|
72
|
+
/\$trifluxExit/,
|
|
73
|
+
/\.err\b/,
|
|
74
|
+
/completion[-_]token/i,
|
|
75
|
+
/^---\s*HANDOFF\s*---$/i,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function isInternalLine(line) {
|
|
79
|
+
return INTERNAL_PATTERNS.some((re) => re.test(line));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── 코드블록 필터링 → filtered_body 생성 ──
|
|
83
|
+
function filterCodeBlocks(text) {
|
|
84
|
+
return String(text || "")
|
|
85
|
+
.replace(/\r/g, "")
|
|
86
|
+
.replace(/```[\s\S]*?(?:```|$)/gm, "\n")
|
|
87
|
+
.replace(/^\s*```.*$/gm, "")
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toFilteredBody(text) {
|
|
92
|
+
return filterCodeBlocks(text)
|
|
93
|
+
.split("\n")
|
|
94
|
+
.map((l) => l.trim())
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.filter((l) => !isInternalLine(l))
|
|
97
|
+
.filter((l) => !/^(PS\s|>|\$)\s*/.test(l))
|
|
98
|
+
.join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toLines(text) {
|
|
102
|
+
return toFilteredBody(text).split("\n").filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── 토큰 라벨 추출 ──
|
|
106
|
+
function extractTokenLabel(text) {
|
|
107
|
+
const m = String(text || "").match(
|
|
108
|
+
/(\d+(?:[.,]\d+)?\s*[kKmM]?)(?=\s*tokens?\s+used|\s*tokens?\b)/i,
|
|
109
|
+
);
|
|
110
|
+
return m ? m[1].replace(/\s+/g, "").toLowerCase() : "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── findings 추출 ──
|
|
114
|
+
function extractFindings(lines, verdict = "") {
|
|
115
|
+
return lines
|
|
116
|
+
.map((l) => l.replace(/^verdict\s*:\s*/i, "").trim())
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.filter(
|
|
119
|
+
(l) =>
|
|
120
|
+
!/^(status|lead_action|confidence|files_changed|detail|risk|error_stage|retryable|partial_output)\s*:/i.test(
|
|
121
|
+
l,
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
.filter((l) => l !== verdict)
|
|
125
|
+
.slice(-2);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Phase 가중치 진행률 (Plan=10%, Research=30%, Exec=50%, Verify=10%) ──
|
|
129
|
+
const PHASE_WEIGHTS = {
|
|
130
|
+
plan: 0.10,
|
|
131
|
+
research:0.40, // plan + research
|
|
132
|
+
exec: 0.90, // plan + research + exec
|
|
133
|
+
verify: 1.00,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
function estimateProgress(lines, context = {}) {
|
|
137
|
+
if (context.done) return 1;
|
|
138
|
+
|
|
139
|
+
const text = lines.join("\n").toLowerCase();
|
|
140
|
+
let phase = "plan";
|
|
141
|
+
|
|
142
|
+
if (/verify|assert|test|check|confirm/.test(text)) phase = "verify";
|
|
143
|
+
else if (/edit|patch|implement|write|update|fix|refactor/.test(text)) phase = "exec";
|
|
144
|
+
else if (/search|read|inspect|analy|review|research/.test(text)) phase = "research";
|
|
145
|
+
|
|
146
|
+
let ratio = PHASE_WEIGHTS[phase];
|
|
147
|
+
|
|
148
|
+
// 라인 수 기반 보정
|
|
149
|
+
if (lines.length < 2) ratio = Math.min(ratio, 0.12);
|
|
150
|
+
|
|
151
|
+
// 토큰 발생 시 최소 88%
|
|
152
|
+
if (context.tokens) ratio = Math.max(ratio, 0.88);
|
|
153
|
+
|
|
154
|
+
// 결과 파일 존재 or 쉘 복귀 → 완료로 간주
|
|
155
|
+
if (context.resultSize > 10 || context.shellReturned) return 1;
|
|
156
|
+
|
|
157
|
+
return Math.min(0.97, ratio);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── psmux 래퍼 ──
|
|
161
|
+
function listPanes() {
|
|
162
|
+
try {
|
|
163
|
+
const out = execFileSync(
|
|
164
|
+
"psmux",
|
|
165
|
+
["list-panes", "-t", SESSION, "-F", "#{pane_index}:#{pane_title}:#{pane_pid}"],
|
|
166
|
+
{ encoding: "utf8", timeout: 2000 },
|
|
167
|
+
);
|
|
168
|
+
return out
|
|
169
|
+
.trim()
|
|
170
|
+
.split("\n")
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.map((line) => {
|
|
173
|
+
const [index, title, pid] = line.split(":");
|
|
174
|
+
return { index: parseInt(index, 10), title: title || "", pid };
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function capturePane(paneIdx, lines = 20) {
|
|
182
|
+
try {
|
|
183
|
+
return execFileSync(
|
|
184
|
+
"psmux",
|
|
185
|
+
["capture-pane", "-t", `${SESSION}:0.${paneIdx}`, "-p"],
|
|
186
|
+
{ encoding: "utf8", timeout: 2000 },
|
|
187
|
+
)
|
|
188
|
+
.trim()
|
|
189
|
+
.split("\n")
|
|
190
|
+
.slice(-lines)
|
|
191
|
+
.join("\n");
|
|
192
|
+
} catch {
|
|
193
|
+
return "";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function checkResultFile(paneName) {
|
|
198
|
+
const resultFile = join(RESULT_DIR, `${SESSION}-${paneName}.txt`);
|
|
199
|
+
if (!existsSync(resultFile)) return null;
|
|
200
|
+
try {
|
|
201
|
+
const content = readFileSync(resultFile, "utf8");
|
|
202
|
+
if (!content.trim()) return null;
|
|
203
|
+
return {
|
|
204
|
+
resultFile,
|
|
205
|
+
content,
|
|
206
|
+
processed: processHandoff(content, { exitCode: 0, resultFile }),
|
|
207
|
+
};
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── 워커 상태 모델 ──
|
|
214
|
+
// 각 워커는 다음 필드를 가짐:
|
|
215
|
+
// raw_body — capture-pane 원시 텍스트
|
|
216
|
+
// filtered_body — 코드블록 + 내부 패턴 제거된 텍스트
|
|
217
|
+
// verdict — 한 줄 결론
|
|
218
|
+
// findings[] — 주목할 라인 (최대 2)
|
|
219
|
+
// handoff{} — { status, lead_action, verdict, ... }
|
|
220
|
+
// progress — 0~1
|
|
221
|
+
// activityAt — 마지막 변경 타임스탬프
|
|
222
|
+
// done — boolean
|
|
223
|
+
function makeWorkerState(paneIdx) {
|
|
224
|
+
return {
|
|
225
|
+
paneIdx,
|
|
226
|
+
done: false,
|
|
227
|
+
raw_body: "",
|
|
228
|
+
filtered_body: "",
|
|
229
|
+
verdict: "",
|
|
230
|
+
findings: [],
|
|
231
|
+
handoff: null,
|
|
232
|
+
progress: 0,
|
|
233
|
+
activityAt: Date.now(),
|
|
234
|
+
title: "",
|
|
235
|
+
cli: "codex",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// HANDOFF status / lead_action 분리
|
|
240
|
+
function splitHandoff(handoff) {
|
|
241
|
+
if (!handoff) return { status: "pending", lead_action: null };
|
|
242
|
+
return {
|
|
243
|
+
status: handoff.status || "pending",
|
|
244
|
+
lead_action: handoff.lead_action || null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── 상태 집계 저장소 ──
|
|
249
|
+
const workerState = new Map(); // paneName → 내부 상태
|
|
250
|
+
let emptyPollCount = 0;
|
|
251
|
+
|
|
252
|
+
// ── data ingest (4Hz = 250ms) ──
|
|
253
|
+
function ingest() {
|
|
254
|
+
const panes = listPanes();
|
|
255
|
+
|
|
256
|
+
if (!panes.some((p) => p.index !== 0)) {
|
|
257
|
+
emptyPollCount++;
|
|
258
|
+
const threshold = workerState.size === 0 ? 15 : 10;
|
|
259
|
+
if (emptyPollCount >= threshold) {
|
|
260
|
+
// 세션 종료 후에도 최종 결과 유지 — 키 입력 시 종료
|
|
261
|
+
clearInterval(ingestTimer);
|
|
262
|
+
clearInterval(renderTimer);
|
|
263
|
+
tui.render();
|
|
264
|
+
process.stdout.write("\n\x1b[38;5;245m 세션 종료됨 — 아무 키나 누르면 닫힘\x1b[0m");
|
|
265
|
+
if (process.stdin.isTTY) {
|
|
266
|
+
process.stdin.setRawMode(true);
|
|
267
|
+
process.stdin.resume();
|
|
268
|
+
process.stdin.once("data", () => { cleanup(); process.exit(0); });
|
|
269
|
+
} else {
|
|
270
|
+
setTimeout(() => { cleanup(); process.exit(0); }, 30000);
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
emptyPollCount = 0;
|
|
277
|
+
|
|
278
|
+
for (const pane of panes) {
|
|
279
|
+
if (pane.index === 0) continue;
|
|
280
|
+
|
|
281
|
+
const paneName = `worker-${pane.index}`;
|
|
282
|
+
let ws = workerState.get(paneName);
|
|
283
|
+
if (!ws) {
|
|
284
|
+
ws = makeWorkerState(pane.index);
|
|
285
|
+
workerState.set(paneName, ws);
|
|
286
|
+
}
|
|
287
|
+
if (ws.done) continue;
|
|
288
|
+
|
|
289
|
+
// CLI 타입 감지
|
|
290
|
+
let cli = "codex";
|
|
291
|
+
if (pane.title.includes("gemini") || pane.title.includes("🔵")) cli = "gemini";
|
|
292
|
+
else if (pane.title.includes("claude") || pane.title.includes("🟠")) cli = "claude";
|
|
293
|
+
ws.title = pane.title;
|
|
294
|
+
ws.cli = cli;
|
|
295
|
+
|
|
296
|
+
const resultData = checkResultFile(paneName);
|
|
297
|
+
if (resultData?.processed && !resultData.processed.fallback) {
|
|
298
|
+
// 결과 파일 처리 완료
|
|
299
|
+
const raw = resultData.content;
|
|
300
|
+
const filtered = toFilteredBody(raw);
|
|
301
|
+
const lines = filtered.split("\n").filter(Boolean);
|
|
302
|
+
const handoff = resultData.processed.handoff;
|
|
303
|
+
const verdict = handoff.verdict || "completed";
|
|
304
|
+
|
|
305
|
+
ws.done = true;
|
|
306
|
+
ws.raw_body = raw;
|
|
307
|
+
ws.filtered_body = filtered;
|
|
308
|
+
ws.verdict = verdict;
|
|
309
|
+
ws.findings = extractFindings(lines, verdict);
|
|
310
|
+
ws.handoff = handoff;
|
|
311
|
+
ws.progress = 1;
|
|
312
|
+
ws.activityAt = Date.now();
|
|
313
|
+
|
|
314
|
+
const { status, lead_action } = splitHandoff(handoff);
|
|
315
|
+
pushToTui(paneName, cli, pane.title, {
|
|
316
|
+
status: status === "failed" ? "failed" : "completed",
|
|
317
|
+
handoff,
|
|
318
|
+
summary: verdict,
|
|
319
|
+
detail: filtered,
|
|
320
|
+
findings: ws.findings,
|
|
321
|
+
tokens: extractTokenLabel(raw),
|
|
322
|
+
progress: 1,
|
|
323
|
+
elapsed: Math.round((Date.now() - startTime) / 1000),
|
|
324
|
+
_leadAction: lead_action,
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 스냅샷 기반 진행 중 상태
|
|
330
|
+
const snapshot = capturePane(pane.index, 20);
|
|
331
|
+
const raw_body = snapshot;
|
|
332
|
+
const filtered_body = toFilteredBody(snapshot);
|
|
333
|
+
const lines = filtered_body.split("\n").filter(Boolean);
|
|
334
|
+
const lastLine = lines.at(-1) || "";
|
|
335
|
+
|
|
336
|
+
const resultFile = join(RESULT_DIR, `${SESSION}-${paneName}.txt`);
|
|
337
|
+
let resultSize = 0;
|
|
338
|
+
try { resultSize = statSync(resultFile).size; } catch { /* missing */ }
|
|
339
|
+
|
|
340
|
+
const shellReturned = /^(PS\s|>|\$)\s*/.test(lastLine) && lines.length > 2;
|
|
341
|
+
const tokens = extractTokenLabel(snapshot);
|
|
342
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
343
|
+
|
|
344
|
+
if (resultSize > 10 || shellReturned) {
|
|
345
|
+
const resultContent = existsSync(resultFile)
|
|
346
|
+
? readFileSync(resultFile, "utf8")
|
|
347
|
+
: snapshot;
|
|
348
|
+
const rLines = toLines(resultContent);
|
|
349
|
+
const verdict = extractFindings(rLines).at(-1) || lastLine || "completed";
|
|
350
|
+
const handoffStatus = /fail|error|exception/i.test(rLines.join("\n")) ? "failed" : "ok";
|
|
351
|
+
const handoff = {
|
|
352
|
+
status: handoffStatus,
|
|
353
|
+
lead_action: handoffStatus === "failed" ? "retry" : "accept",
|
|
354
|
+
verdict,
|
|
355
|
+
confidence: tokens ? "high" : "medium",
|
|
356
|
+
files_changed: [],
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
ws.done = true;
|
|
360
|
+
ws.raw_body = resultContent;
|
|
361
|
+
ws.filtered_body = toFilteredBody(resultContent);
|
|
362
|
+
ws.verdict = verdict;
|
|
363
|
+
ws.findings = extractFindings(rLines, verdict);
|
|
364
|
+
ws.handoff = handoff;
|
|
365
|
+
ws.progress = 1;
|
|
366
|
+
ws.activityAt = Date.now();
|
|
367
|
+
|
|
368
|
+
pushToTui(paneName, cli, pane.title, {
|
|
369
|
+
status: handoffStatus === "failed" ? "failed" : "completed",
|
|
370
|
+
handoff,
|
|
371
|
+
summary: verdict,
|
|
372
|
+
detail: ws.filtered_body,
|
|
373
|
+
findings: ws.findings,
|
|
374
|
+
tokens,
|
|
375
|
+
progress: 1,
|
|
376
|
+
elapsed,
|
|
377
|
+
});
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 진행 중
|
|
382
|
+
const progress = estimateProgress(lines, { tokens, resultSize, shellReturned, done: false });
|
|
383
|
+
const verdict = lastLine;
|
|
384
|
+
|
|
385
|
+
ws.raw_body = raw_body.length > MAX_BODY_BYTES ? raw_body.slice(-MAX_BODY_BYTES) : raw_body;
|
|
386
|
+
ws.filtered_body = filtered_body.length > MAX_BODY_BYTES ? filtered_body.slice(-MAX_BODY_BYTES) : filtered_body;
|
|
387
|
+
ws.verdict = verdict;
|
|
388
|
+
ws.findings = extractFindings(lines, lastLine);
|
|
389
|
+
ws.progress = progress;
|
|
390
|
+
ws.activityAt = Date.now();
|
|
391
|
+
|
|
392
|
+
pushToTui(paneName, cli, pane.title, {
|
|
393
|
+
status: "running",
|
|
394
|
+
snapshot: lastLine,
|
|
395
|
+
summary: lastLine,
|
|
396
|
+
detail: filtered_body,
|
|
397
|
+
findings: ws.findings,
|
|
398
|
+
confidence: tokens ? "medium" : "low",
|
|
399
|
+
tokens,
|
|
400
|
+
progress,
|
|
401
|
+
elapsed,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── tui.updateWorker 래퍼 — raw internal data 누출 방지 ──
|
|
407
|
+
function pushToTui(paneName, cli, paneTitle, update) {
|
|
408
|
+
// _leadAction은 tui에 노출하지 않음 (내부용)
|
|
409
|
+
const { _leadAction: _ignored, ...safeUpdate } = update;
|
|
410
|
+
// pane title에서 실제 역할만 추출: "⚪ codex (executor)" → "executor"
|
|
411
|
+
const roleMatch = paneTitle.match(/\(([^)]+)\)$/);
|
|
412
|
+
const role = roleMatch ? roleMatch[1] : "";
|
|
413
|
+
tui.updateWorker(paneName, { cli, role, ...safeUpdate });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ── render 루프 (8-12FPS ≈ 100ms) ──
|
|
417
|
+
let renderTimer = null;
|
|
418
|
+
function startRender() {
|
|
419
|
+
renderTimer = setInterval(() => { tui.render(); }, 100);
|
|
420
|
+
if (renderTimer.unref) renderTimer.unref();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── 완료 감지 ──
|
|
424
|
+
const doneCheck = setInterval(() => {
|
|
425
|
+
if (workerState.size > 0 && [...workerState.values()].every((w) => w.done)) {
|
|
426
|
+
tui.render();
|
|
427
|
+
clearInterval(doneCheck);
|
|
428
|
+
clearInterval(ingestTimer);
|
|
429
|
+
clearInterval(renderTimer);
|
|
430
|
+
process.stdout.write("\n\x1b[38;5;245m 전체 완료 — 아무 키나 누르면 닫힘\x1b[0m");
|
|
431
|
+
if (process.stdin.isTTY) {
|
|
432
|
+
process.stdin.setRawMode(true);
|
|
433
|
+
process.stdin.resume();
|
|
434
|
+
process.stdin.once("data", () => { cleanup(); process.exit(0); });
|
|
435
|
+
} else {
|
|
436
|
+
setTimeout(() => { cleanup(); process.exit(0); }, 30000);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}, 2000);
|
|
440
|
+
|
|
441
|
+
// ── resize 대응 ──
|
|
442
|
+
process.stdout.on("resize", () => {
|
|
443
|
+
tui.render();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ── 정리 ──
|
|
447
|
+
function cleanup() {
|
|
448
|
+
clearInterval(ingestTimer);
|
|
449
|
+
clearInterval(renderTimer);
|
|
450
|
+
clearInterval(doneCheck);
|
|
451
|
+
tui.close();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── 진입점 ──
|
|
455
|
+
tui.render();
|
|
456
|
+
const ingestTimer = setInterval(ingest, 500); // 2Hz
|
|
457
|
+
startRender();
|
|
458
|
+
|
|
459
|
+
// 타임아웃 (10분)
|
|
460
|
+
setTimeout(() => { cleanup(); process.exit(0); }, 10 * 60 * 1000);
|
|
461
|
+
|
|
462
|
+
// Ctrl-C
|
|
463
|
+
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|