@triflux/remote 10.1.0 → 10.26.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.
- package/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +61 -0
- package/hub/server.mjs +2179 -709
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +78 -17
- package/hub/team/build-worker-prompt.mjs +73 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/claude-native-bridge.mjs +1011 -0
- package/hub/team/claude-session-projection.mjs +74 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +151 -33
- package/hub/team/cli/commands/start/parse-args.mjs +97 -13
- package/hub/team/cli/commands/start/start-headless.mjs +155 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/help.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +44 -16
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +61 -4
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +20 -118
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +256 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2213 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/interactive-native-launcher.mjs +106 -0
- package/hub/team/interactive-native-worker.mjs +80 -0
- package/hub/team/interactive-tui-transport.mjs +465 -0
- package/hub/team/launcher-template.mjs +49 -17
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +120 -42
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -20
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1022 -281
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +574 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1544 -193
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +319 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +292 -0
- package/hub/team/tui-lite.mjs +237 -148
- package/hub/team/tui-remote-adapter.mjs +18 -34
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +108 -48
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +931 -439
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +97 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +517 -63
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +477 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/antigravity-route-worker.mjs +169 -0
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +570 -499
- package/hub/workers/factory.mjs +103 -21
- package/hub/workers/gemini-worker.mjs +126 -284
- package/hub/workers/interface.mjs +24 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +23 -17
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +224 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
package/hub/team/tui-lite.mjs
CHANGED
|
@@ -1,114 +1,56 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
altScreenOff,
|
|
3
|
+
altScreenOn,
|
|
4
|
+
BG,
|
|
5
|
+
bold,
|
|
6
|
+
box,
|
|
7
|
+
clearScreen,
|
|
8
|
+
clearToEnd,
|
|
9
|
+
color,
|
|
10
|
+
cursorHide,
|
|
11
|
+
cursorHome,
|
|
12
|
+
cursorShow,
|
|
13
|
+
dim,
|
|
14
|
+
eraseBelow,
|
|
15
|
+
FG,
|
|
16
|
+
MOCHA,
|
|
17
|
+
moveTo,
|
|
18
|
+
padRight,
|
|
19
|
+
progressBar,
|
|
20
|
+
statusBadge,
|
|
21
|
+
stripAnsi,
|
|
22
|
+
truncate,
|
|
23
|
+
} from "./ansi.mjs";
|
|
24
|
+
import {
|
|
25
|
+
normalizeWorkerState as coreNormalizeWorkerState,
|
|
26
|
+
formatTokens,
|
|
27
|
+
loadVersion,
|
|
28
|
+
resolveViewportColumns,
|
|
29
|
+
resolveViewportRows,
|
|
30
|
+
runtimeStatus,
|
|
31
|
+
sanitizeFiles,
|
|
32
|
+
VALID_TABS as VALID_TABS_ARRAY,
|
|
33
|
+
wrapText as wrapTextFull,
|
|
34
|
+
} from "./tui-core.mjs";
|
|
2
35
|
|
|
3
|
-
const
|
|
4
|
-
const VALID_TABS = new Set(
|
|
5
|
-
|
|
6
|
-
let VERSION = "lite";
|
|
7
|
-
try { const { createRequire } = await import("node:module"); VERSION = createRequire(import.meta.url)("../../package.json").version; } catch {}
|
|
8
|
-
|
|
9
|
-
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
10
|
-
|
|
11
|
-
function sanitizeBlock(text, rawMode = false) {
|
|
12
|
-
const value = String(text || "").replace(/\r/g, "");
|
|
13
|
-
const cleaned = rawMode
|
|
14
|
-
? value
|
|
15
|
-
: value
|
|
16
|
-
.replace(/```[\s\S]*?(?:```|$)/g, "\n")
|
|
17
|
-
.replace(/^\s*```.*$/gm, "")
|
|
18
|
-
.replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "");
|
|
19
|
-
return cleaned
|
|
20
|
-
.split("\n")
|
|
21
|
-
.map((line) => line.trim())
|
|
22
|
-
.filter(Boolean)
|
|
23
|
-
.filter((line) => line !== "--- HANDOFF ---")
|
|
24
|
-
.join("\n")
|
|
25
|
-
.trim();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function sanitizeOneLine(text, fallback = "") {
|
|
29
|
-
return sanitizeBlock(text).replace(/\s+/g, " ").trim() || fallback;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function sanitizeFiles(files) {
|
|
33
|
-
const list = Array.isArray(files) ? files : String(files || "").split(",");
|
|
34
|
-
return list.map((entry) => sanitizeOneLine(entry)).filter(Boolean);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function normalizeTokens(tokens) {
|
|
38
|
-
if (tokens === null || tokens === undefined || tokens === "") return "";
|
|
39
|
-
if (typeof tokens === "number" && Number.isFinite(tokens)) return tokens;
|
|
40
|
-
const raw = sanitizeOneLine(tokens);
|
|
41
|
-
const match = raw.match(/(\d+(?:[.,]\d+)?\s*[kKmM]?)/);
|
|
42
|
-
return match ? match[1].replace(/\s+/g, "").toLowerCase() : raw;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function formatTokens(tokens) {
|
|
46
|
-
if (!tokens && tokens !== 0) return "n/a";
|
|
47
|
-
if (typeof tokens === "number") {
|
|
48
|
-
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}m`;
|
|
49
|
-
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
50
|
-
}
|
|
51
|
-
return String(tokens);
|
|
52
|
-
}
|
|
36
|
+
const VERSION = await loadVersion("lite");
|
|
37
|
+
const VALID_TABS = new Set(VALID_TABS_ARRAY);
|
|
53
38
|
|
|
54
39
|
function wrap(text, width) {
|
|
55
|
-
|
|
56
|
-
const lines = [];
|
|
57
|
-
for (const rawLine of sanitizeBlock(text).split("\n")) {
|
|
58
|
-
const words = rawLine.split(/\s+/).filter(Boolean);
|
|
59
|
-
if (words.length === 0) continue;
|
|
60
|
-
let current = "";
|
|
61
|
-
for (const word of words) {
|
|
62
|
-
const next = current ? `${current} ${word}` : word;
|
|
63
|
-
if (wcswidth(next) <= limit) {
|
|
64
|
-
current = next;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (current) lines.push(current);
|
|
68
|
-
current = word;
|
|
69
|
-
while (wcswidth(current) > limit) {
|
|
70
|
-
lines.push(current.slice(0, limit));
|
|
71
|
-
current = current.slice(limit);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (current) lines.push(current);
|
|
75
|
-
}
|
|
76
|
-
return lines;
|
|
40
|
+
return wrapTextFull(text, width);
|
|
77
41
|
}
|
|
78
42
|
|
|
79
|
-
const runtimeStatus = (worker) => worker?.handoff?.status || worker?.status || "pending";
|
|
80
|
-
|
|
81
43
|
function normalizeWorkerState(existing = {}, state = {}) {
|
|
82
|
-
|
|
83
|
-
? existing.handoff
|
|
84
|
-
: {
|
|
85
|
-
...(existing.handoff || {}),
|
|
86
|
-
...(state.handoff || {}),
|
|
87
|
-
verdict: state.handoff?.verdict !== undefined ? sanitizeOneLine(state.handoff.verdict) : existing.handoff?.verdict,
|
|
88
|
-
confidence: state.handoff?.confidence !== undefined ? sanitizeOneLine(state.handoff.confidence) : existing.handoff?.confidence,
|
|
89
|
-
status: state.handoff?.status !== undefined ? sanitizeOneLine(state.handoff.status) : existing.handoff?.status,
|
|
90
|
-
files_changed: state.handoff?.files_changed !== undefined ? sanitizeFiles(state.handoff.files_changed) : existing.handoff?.files_changed,
|
|
91
|
-
};
|
|
92
|
-
return {
|
|
93
|
-
...existing,
|
|
94
|
-
...state,
|
|
95
|
-
cli: state.cli !== undefined ? sanitizeOneLine(state.cli, existing.cli || "codex") : (existing.cli || "codex"),
|
|
96
|
-
status: state.status !== undefined ? sanitizeOneLine(state.status, existing.status || "pending") : (existing.status || "pending"),
|
|
97
|
-
snapshot: state.snapshot !== undefined ? sanitizeBlock(state.snapshot) : existing.snapshot,
|
|
98
|
-
summary: state.summary !== undefined ? sanitizeBlock(state.summary) : existing.summary,
|
|
99
|
-
detail: state.detail !== undefined ? sanitizeBlock(state.detail) : existing.detail,
|
|
100
|
-
findings: state.findings !== undefined ? sanitizeFiles(state.findings) : existing.findings,
|
|
101
|
-
files_changed: state.files_changed !== undefined ? sanitizeFiles(state.files_changed) : existing.files_changed,
|
|
102
|
-
confidence: state.confidence !== undefined ? sanitizeOneLine(state.confidence) : existing.confidence,
|
|
103
|
-
tokens: state.tokens !== undefined ? normalizeTokens(state.tokens) : existing.tokens,
|
|
104
|
-
progress: state.progress !== undefined ? clamp(Number(state.progress) || 0, 0, 1) : existing.progress,
|
|
105
|
-
handoff,
|
|
106
|
-
};
|
|
44
|
+
return coreNormalizeWorkerState(existing, state);
|
|
107
45
|
}
|
|
108
46
|
|
|
109
47
|
function frame(lines, width, border = MOCHA.border) {
|
|
110
48
|
const body = lines.length ? lines : [dim("내용 없음")];
|
|
111
|
-
const rendered = box(
|
|
49
|
+
const rendered = box(
|
|
50
|
+
body.map((line) => padRight(truncate(line, width - 4), width - 4)),
|
|
51
|
+
width,
|
|
52
|
+
border,
|
|
53
|
+
);
|
|
112
54
|
return [rendered.top, ...rendered.body, rendered.bot];
|
|
113
55
|
}
|
|
114
56
|
|
|
@@ -128,9 +70,10 @@ function buildHeader(width, names, workers, pipeline, startedAt) {
|
|
|
128
70
|
else if (status === "running" || status === "in_progress") counts.running++;
|
|
129
71
|
}
|
|
130
72
|
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
131
|
-
const line1 =
|
|
132
|
-
|
|
133
|
-
|
|
73
|
+
const line1 =
|
|
74
|
+
color(` triflux ${VERSION} `, FG.white, BG.header) +
|
|
75
|
+
` ${bold(`phase ${pipeline.phase || "exec"}`)}` +
|
|
76
|
+
` ${dim(`+${elapsed}s`)} ${names.length} workers`;
|
|
134
77
|
const line2 = `${color(`ok ${counts.ok}`, MOCHA.ok)} ${color(`partial ${counts.partial}`, MOCHA.partial)} ${color(`failed ${counts.failed}`, MOCHA.fail)} ${color(`running ${counts.running}`, MOCHA.executing)}`;
|
|
135
78
|
return [padRight(line1, width), padRight(line2, width)];
|
|
136
79
|
}
|
|
@@ -139,11 +82,17 @@ function buildWorkerRail(names, workers, selectedWorker, width) {
|
|
|
139
82
|
const lines = names.length
|
|
140
83
|
? names.map((name, index) => {
|
|
141
84
|
const worker = workers.get(name);
|
|
85
|
+
const displayName = worker?.displayName || name;
|
|
142
86
|
const status = runtimeStatus(worker);
|
|
143
|
-
const pct = Math.round(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
87
|
+
const pct = Math.round(
|
|
88
|
+
((worker?.progress ?? (status === "completed" ? 1 : 0)) || 0) * 100,
|
|
89
|
+
);
|
|
90
|
+
const token = worker?.tokens
|
|
91
|
+
? ` tok ${formatTokens(worker.tokens)}`
|
|
92
|
+
: "";
|
|
93
|
+
const prefix =
|
|
94
|
+
name === selectedWorker ? color("▶", MOCHA.blue) : dim("·");
|
|
95
|
+
return `${prefix} ${index + 1}.${displayName} ${stripAnsi(statusBadge(status))} ${pct}%${token}`;
|
|
147
96
|
})
|
|
148
97
|
: [dim("workers 없음")];
|
|
149
98
|
return frame(lines, width);
|
|
@@ -167,7 +116,7 @@ function buildDetail(workerName, worker, width, tab, helpVisible) {
|
|
|
167
116
|
}
|
|
168
117
|
if (!workerName || !worker) return frame([dim("선택된 워커 없음")], width);
|
|
169
118
|
const detailLines = [
|
|
170
|
-
bold(workerName),
|
|
119
|
+
bold(worker.displayName || workerName),
|
|
171
120
|
`status ${runtimeStatus(worker)}`,
|
|
172
121
|
`progress ${Math.round((worker.progress || 0) * 100)}% ${progressBar(Math.round((worker.progress || 0) * 100), 12)}`,
|
|
173
122
|
`tokens ${formatTokens(worker.tokens)}`,
|
|
@@ -175,12 +124,27 @@ function buildDetail(workerName, worker, width, tab, helpVisible) {
|
|
|
175
124
|
`verdict ${worker.handoff?.verdict || worker.summary || worker.snapshot || "-"}`,
|
|
176
125
|
];
|
|
177
126
|
if (tab === "files") {
|
|
178
|
-
const files = [
|
|
179
|
-
|
|
127
|
+
const files = [
|
|
128
|
+
...sanitizeFiles(worker.handoff?.files_changed),
|
|
129
|
+
...sanitizeFiles(worker.files_changed),
|
|
130
|
+
];
|
|
131
|
+
detailLines.push(
|
|
132
|
+
...(files.length ? files.map((file) => `files ${file}`) : ["files 없음"]),
|
|
133
|
+
);
|
|
180
134
|
} else if (tab === "detail") {
|
|
181
|
-
detailLines.push(
|
|
135
|
+
detailLines.push(
|
|
136
|
+
...wrap(
|
|
137
|
+
worker.detail || worker.summary || worker.snapshot || "",
|
|
138
|
+
width - 4,
|
|
139
|
+
),
|
|
140
|
+
);
|
|
182
141
|
} else {
|
|
183
|
-
detailLines.push(
|
|
142
|
+
detailLines.push(
|
|
143
|
+
...wrap(
|
|
144
|
+
worker.summary || worker.snapshot || worker.detail || "",
|
|
145
|
+
width - 4,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
184
148
|
}
|
|
185
149
|
return frame(detailLines, width, MOCHA.thinking);
|
|
186
150
|
}
|
|
@@ -209,14 +173,21 @@ export function createLiteDashboard(opts = {}) {
|
|
|
209
173
|
let detailExpanded = true;
|
|
210
174
|
let focusTab = "log";
|
|
211
175
|
let helpVisible = false;
|
|
176
|
+
let prevFrame = [];
|
|
177
|
+
let prevWidth = 0;
|
|
212
178
|
let inputAttached = false;
|
|
213
179
|
let rawModeEnabled = false;
|
|
214
180
|
|
|
215
|
-
const write = (text) => {
|
|
181
|
+
const write = (text) => {
|
|
182
|
+
if (!closed) stream.write(text);
|
|
183
|
+
};
|
|
216
184
|
const workerNames = () => [...workers.keys()].sort();
|
|
217
|
-
const viewportColumns = () =>
|
|
218
|
-
const viewportRows = () =>
|
|
219
|
-
const ensureSelection = (names) => {
|
|
185
|
+
const viewportColumns = () => resolveViewportColumns({ columns, stream });
|
|
186
|
+
const viewportRows = () => resolveViewportRows({ rows, stream });
|
|
187
|
+
const ensureSelection = (names) => {
|
|
188
|
+
if (names.length && (!selectedWorker || !workers.has(selectedWorker)))
|
|
189
|
+
selectedWorker = names[0];
|
|
190
|
+
};
|
|
220
191
|
|
|
221
192
|
function selectRelative(offset) {
|
|
222
193
|
const names = workerNames();
|
|
@@ -227,9 +198,18 @@ export function createLiteDashboard(opts = {}) {
|
|
|
227
198
|
}
|
|
228
199
|
|
|
229
200
|
function triggerOpenSelected() {
|
|
230
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
typeof onOpenSelectedWorker !== "function" ||
|
|
203
|
+
!selectedWorker ||
|
|
204
|
+
!workers.has(selectedWorker)
|
|
205
|
+
)
|
|
206
|
+
return;
|
|
231
207
|
try {
|
|
232
|
-
const result = onOpenSelectedWorker(
|
|
208
|
+
const result = onOpenSelectedWorker(
|
|
209
|
+
selectedWorker,
|
|
210
|
+
workers.get(selectedWorker),
|
|
211
|
+
new Map(workers),
|
|
212
|
+
);
|
|
233
213
|
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
234
214
|
} catch {}
|
|
235
215
|
}
|
|
@@ -237,7 +217,11 @@ export function createLiteDashboard(opts = {}) {
|
|
|
237
217
|
function triggerOpenAll() {
|
|
238
218
|
if (typeof onOpenAllWorkers !== "function") return;
|
|
239
219
|
try {
|
|
240
|
-
const result = onOpenAllWorkers(
|
|
220
|
+
const result = onOpenAllWorkers(
|
|
221
|
+
selectedWorker,
|
|
222
|
+
workers.get(selectedWorker),
|
|
223
|
+
new Map(workers),
|
|
224
|
+
);
|
|
241
225
|
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
242
226
|
} catch {}
|
|
243
227
|
}
|
|
@@ -263,10 +247,26 @@ export function createLiteDashboard(opts = {}) {
|
|
|
263
247
|
return;
|
|
264
248
|
}
|
|
265
249
|
if (key === "\r" || key === "\n") {
|
|
266
|
-
|
|
250
|
+
if (
|
|
251
|
+
typeof onOpenSelectedWorker === "function" &&
|
|
252
|
+
selectedWorker &&
|
|
253
|
+
workers.has(selectedWorker)
|
|
254
|
+
) {
|
|
255
|
+
triggerOpenSelected();
|
|
256
|
+
} else {
|
|
257
|
+
// 콜백 없거나 선택 워커 없으면 탭 순환
|
|
258
|
+
const tabs = ["log", "detail", "files"];
|
|
259
|
+
focusTab = tabs[(tabs.indexOf(focusTab) + 1) % tabs.length];
|
|
260
|
+
}
|
|
261
|
+
render();
|
|
267
262
|
return;
|
|
268
263
|
}
|
|
269
|
-
if (
|
|
264
|
+
if (
|
|
265
|
+
key === "\x1b[13;2u" ||
|
|
266
|
+
key === "\x1b[27;13;2~" ||
|
|
267
|
+
key === "\x1b\r" ||
|
|
268
|
+
key === "\x1b\n"
|
|
269
|
+
) {
|
|
270
270
|
triggerOpenAll();
|
|
271
271
|
return;
|
|
272
272
|
}
|
|
@@ -297,7 +297,7 @@ export function createLiteDashboard(opts = {}) {
|
|
|
297
297
|
|
|
298
298
|
function attachInput() {
|
|
299
299
|
if (inputAttached) return;
|
|
300
|
-
if (!
|
|
300
|
+
if (!input?.isTTY || typeof input?.on !== "function") return;
|
|
301
301
|
inputAttached = true;
|
|
302
302
|
if (typeof input.setRawMode === "function") {
|
|
303
303
|
input.setRawMode(true);
|
|
@@ -313,20 +313,53 @@ export function createLiteDashboard(opts = {}) {
|
|
|
313
313
|
const width = viewportColumns();
|
|
314
314
|
const height = viewportRows();
|
|
315
315
|
const header = buildHeader(width, names, workers, pipeline, startedAt);
|
|
316
|
-
const railOnly =
|
|
316
|
+
const railOnly =
|
|
317
|
+
!detailExpanded ||
|
|
318
|
+
names.length <= 1 ||
|
|
319
|
+
width < 100 ||
|
|
320
|
+
layout === "single";
|
|
317
321
|
if (railOnly) {
|
|
318
|
-
const sections = [
|
|
319
|
-
|
|
322
|
+
const sections = [
|
|
323
|
+
header,
|
|
324
|
+
...buildWorkerRail(names, workers, selectedWorker, width),
|
|
325
|
+
];
|
|
326
|
+
if (detailExpanded)
|
|
327
|
+
sections.push(
|
|
328
|
+
...buildDetail(
|
|
329
|
+
selectedWorker,
|
|
330
|
+
workers.get(selectedWorker),
|
|
331
|
+
width,
|
|
332
|
+
focusTab,
|
|
333
|
+
helpVisible,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
320
336
|
return fitHeight(sections, width, height);
|
|
321
337
|
}
|
|
322
338
|
const railWidth = Math.max(28, Math.floor(width * 0.32));
|
|
323
339
|
const detailWidth = width - railWidth - 1;
|
|
324
340
|
const bodyHeight = Math.max(6, height - header.length);
|
|
325
|
-
const rail = fitHeight(
|
|
326
|
-
|
|
341
|
+
const rail = fitHeight(
|
|
342
|
+
buildWorkerRail(names, workers, selectedWorker, railWidth),
|
|
343
|
+
railWidth,
|
|
344
|
+
bodyHeight,
|
|
345
|
+
);
|
|
346
|
+
const detail = fitHeight(
|
|
347
|
+
buildDetail(
|
|
348
|
+
selectedWorker,
|
|
349
|
+
workers.get(selectedWorker),
|
|
350
|
+
detailWidth,
|
|
351
|
+
focusTab,
|
|
352
|
+
helpVisible,
|
|
353
|
+
),
|
|
354
|
+
detailWidth,
|
|
355
|
+
bodyHeight,
|
|
356
|
+
);
|
|
327
357
|
return [
|
|
328
358
|
...header,
|
|
329
|
-
...Array.from(
|
|
359
|
+
...Array.from(
|
|
360
|
+
{ length: bodyHeight },
|
|
361
|
+
(_, index) => `${rail[index]}${dim("│")}${detail[index]}`,
|
|
362
|
+
),
|
|
330
363
|
];
|
|
331
364
|
}
|
|
332
365
|
|
|
@@ -337,18 +370,43 @@ export function createLiteDashboard(opts = {}) {
|
|
|
337
370
|
const rowsOut = buildRows();
|
|
338
371
|
if (isTTY) {
|
|
339
372
|
const width = viewportColumns();
|
|
340
|
-
const padded = rowsOut.map((line) => padRight(String(line ?? ""), width)
|
|
341
|
-
|
|
373
|
+
const padded = rowsOut.map((line) => padRight(String(line ?? ""), width));
|
|
374
|
+
// Full redraw on first frame or terminal resize to avoid artifacts
|
|
375
|
+
if (prevFrame.length === 0 || width !== prevWidth) {
|
|
376
|
+
prevWidth = width;
|
|
377
|
+
write(
|
|
378
|
+
cursorHome +
|
|
379
|
+
padded.map((l) => l + clearToEnd).join("\n") +
|
|
380
|
+
eraseBelow,
|
|
381
|
+
);
|
|
382
|
+
prevFrame = padded;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// Diff-based rendering: only rewrite lines that actually changed
|
|
386
|
+
let buf = "";
|
|
387
|
+
for (let i = 0; i < padded.length; i++) {
|
|
388
|
+
if (padded[i] !== prevFrame[i]) {
|
|
389
|
+
buf += moveTo(i + 1, 1) + padded[i] + clearToEnd;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (prevFrame.length > padded.length) {
|
|
393
|
+
buf += moveTo(padded.length + 1, 1) + eraseBelow;
|
|
394
|
+
}
|
|
395
|
+
if (buf) write(buf);
|
|
396
|
+
prevFrame = padded;
|
|
342
397
|
} else write(`${rowsOut.join("\n")}\n`);
|
|
343
398
|
}
|
|
344
399
|
|
|
345
400
|
function close() {
|
|
346
401
|
if (closed) return;
|
|
347
402
|
if (timer) clearInterval(timer);
|
|
348
|
-
if (inputAttached && typeof input?.off === "function")
|
|
349
|
-
|
|
403
|
+
if (inputAttached && typeof input?.off === "function")
|
|
404
|
+
input.off("data", handleInput);
|
|
405
|
+
if (rawModeEnabled && typeof input?.setRawMode === "function")
|
|
406
|
+
input.setRawMode(false);
|
|
350
407
|
if (inputAttached && typeof input?.pause === "function") input.pause();
|
|
351
408
|
if (isTTY) write(cursorShow + altScreenOff);
|
|
409
|
+
prevFrame = [];
|
|
352
410
|
closed = true;
|
|
353
411
|
}
|
|
354
412
|
|
|
@@ -359,22 +417,53 @@ export function createLiteDashboard(opts = {}) {
|
|
|
359
417
|
}
|
|
360
418
|
|
|
361
419
|
return {
|
|
362
|
-
updateWorker(name, state) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
420
|
+
updateWorker(name, state) {
|
|
421
|
+
workers.set(name, normalizeWorkerState(workers.get(name), state));
|
|
422
|
+
ensureSelection(workerNames());
|
|
423
|
+
},
|
|
424
|
+
updatePipeline(state) {
|
|
425
|
+
pipeline = { ...pipeline, ...state };
|
|
426
|
+
},
|
|
427
|
+
setStartTime(ms) {
|
|
428
|
+
startedAt = ms;
|
|
429
|
+
},
|
|
430
|
+
selectWorker(name) {
|
|
431
|
+
if (workers.has(name)) selectedWorker = name;
|
|
432
|
+
},
|
|
433
|
+
toggleDetail(force) {
|
|
434
|
+
detailExpanded = typeof force === "boolean" ? force : !detailExpanded;
|
|
435
|
+
},
|
|
367
436
|
render,
|
|
368
|
-
getWorkers() {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
437
|
+
getWorkers() {
|
|
438
|
+
return new Map(workers);
|
|
439
|
+
},
|
|
440
|
+
getFrameCount() {
|
|
441
|
+
return frameCount;
|
|
442
|
+
},
|
|
443
|
+
getPipelineState() {
|
|
444
|
+
return { ...pipeline };
|
|
445
|
+
},
|
|
446
|
+
getSelectedWorker() {
|
|
447
|
+
return selectedWorker;
|
|
448
|
+
},
|
|
449
|
+
isDetailExpanded() {
|
|
450
|
+
return detailExpanded;
|
|
451
|
+
},
|
|
452
|
+
getFocusTab() {
|
|
453
|
+
return focusTab;
|
|
454
|
+
},
|
|
455
|
+
setFocusTab(tab) {
|
|
456
|
+
if (VALID_TABS.has(tab)) focusTab = tab;
|
|
457
|
+
},
|
|
458
|
+
getLayout() {
|
|
459
|
+
return layout;
|
|
460
|
+
},
|
|
461
|
+
toggleHelp(force) {
|
|
462
|
+
helpVisible = typeof force === "boolean" ? force : !helpVisible;
|
|
463
|
+
},
|
|
464
|
+
isHelpVisible() {
|
|
465
|
+
return helpVisible;
|
|
466
|
+
},
|
|
378
467
|
close,
|
|
379
468
|
};
|
|
380
469
|
}
|
|
@@ -4,17 +4,13 @@
|
|
|
4
4
|
// tui.mjs updateWorker() 호환 형식으로 변환한다.
|
|
5
5
|
// 완료/실패 시 notify.mjs 자동 호출.
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
7
|
import { EventEmitter } from "node:events";
|
|
11
8
|
|
|
9
|
+
import { readHosts } from "@triflux/core/hub/lib/hosts-compat.mjs";
|
|
12
10
|
import { STATES } from "./conductor.mjs";
|
|
13
11
|
|
|
14
12
|
// ── 상수 ─────────────────────────────────────────────────────────────────────
|
|
15
13
|
|
|
16
|
-
const HOSTS_JSON_REL = "../../references/hosts.json";
|
|
17
|
-
|
|
18
14
|
const CONDUCTOR_STATE_TO_TUI_STATUS = Object.freeze({
|
|
19
15
|
[STATES.INIT]: "pending",
|
|
20
16
|
[STATES.STARTING]: "pending",
|
|
@@ -31,21 +27,6 @@ const SESSION_PREFIX = "tfx-spawn-";
|
|
|
31
27
|
|
|
32
28
|
// ── 유틸 ─────────────────────────────────────────────────────────────────────
|
|
33
29
|
|
|
34
|
-
function loadHostsJson(hostsJsonPath) {
|
|
35
|
-
try {
|
|
36
|
-
const raw = readFileSync(hostsJsonPath, "utf8");
|
|
37
|
-
return JSON.parse(raw);
|
|
38
|
-
} catch {
|
|
39
|
-
return { hosts: {} };
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function resolveHostsJsonPath(overridePath) {
|
|
44
|
-
if (overridePath) return overridePath;
|
|
45
|
-
const thisDir = fileURLToPath(new URL(".", import.meta.url));
|
|
46
|
-
return join(thisDir, HOSTS_JSON_REL);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
30
|
function resolveSshUser(hostsData, host) {
|
|
50
31
|
if (!host || !hostsData?.hosts) return null;
|
|
51
32
|
return hostsData.hosts[host]?.ssh_user || null;
|
|
@@ -90,9 +71,8 @@ function mapConductorStateToStatus(conductorState) {
|
|
|
90
71
|
function buildWorkerData(entry, watcherSession, hostsData) {
|
|
91
72
|
const host = entry.host || resolveHostFromSessionName(entry.id);
|
|
92
73
|
const snapshot = watcherSession?.lastOutput || "";
|
|
93
|
-
const probeLevel =
|
|
94
|
-
|| watcherSession?.lastProbeLevel
|
|
95
|
-
|| null;
|
|
74
|
+
const probeLevel =
|
|
75
|
+
entry.health?.level || watcherSession?.lastProbeLevel || null;
|
|
96
76
|
|
|
97
77
|
return Object.freeze({
|
|
98
78
|
cli: entry.agent || "claude",
|
|
@@ -115,15 +95,18 @@ function buildWorkerData(entry, watcherSession, hostsData) {
|
|
|
115
95
|
* remote-watcher 전용 엔트리 → TUI 워커 데이터 (conductor 미등록 세션).
|
|
116
96
|
*/
|
|
117
97
|
function buildWatcherOnlyWorkerData(watcherRecord, hostsData) {
|
|
118
|
-
const host =
|
|
119
|
-
|| resolveHostFromSessionName(watcherRecord.sessionName);
|
|
98
|
+
const host =
|
|
99
|
+
watcherRecord.host || resolveHostFromSessionName(watcherRecord.sessionName);
|
|
120
100
|
|
|
121
101
|
return Object.freeze({
|
|
122
102
|
cli: "claude",
|
|
123
103
|
role: resolveRole(null, watcherRecord.sessionName),
|
|
124
|
-
status:
|
|
125
|
-
|
|
126
|
-
|
|
104
|
+
status:
|
|
105
|
+
watcherRecord.state === "completed"
|
|
106
|
+
? "completed"
|
|
107
|
+
: watcherRecord.state === "failed"
|
|
108
|
+
? "failed"
|
|
109
|
+
: "running",
|
|
127
110
|
host: host || "unknown",
|
|
128
111
|
remote: true,
|
|
129
112
|
sshUser: resolveSshUser(hostsData, host),
|
|
@@ -142,7 +125,7 @@ function buildWatcherOnlyWorkerData(watcherRecord, hostsData) {
|
|
|
142
125
|
* @param {object} opts.conductor — createConductor() 인스턴스
|
|
143
126
|
* @param {object} [opts.watcher] — createRemoteWatcher() 인스턴스 (nullable)
|
|
144
127
|
* @param {object} [opts.notifier] — createNotifier() 인스턴스 (nullable)
|
|
145
|
-
* @param {string} [opts.
|
|
128
|
+
* @param {string} [opts.repoRoot] — hosts fallback 탐색용 repo root override
|
|
146
129
|
* @param {number} [opts.pollMs=10000] — conductor snapshot 폴링 간격
|
|
147
130
|
* @param {object} [opts.deps] — 테스트용 의존성 주입
|
|
148
131
|
* @returns {{ start, stop, getWorkers, on, off }}
|
|
@@ -158,14 +141,13 @@ export function createRemoteAdapter(opts = {}) {
|
|
|
158
141
|
|
|
159
142
|
if (!conductor) throw new Error("conductor is required");
|
|
160
143
|
|
|
161
|
-
const
|
|
162
|
-
const loadHosts = deps.loadHostsJson || loadHostsJson;
|
|
144
|
+
const loadHosts = deps.readHosts || readHosts;
|
|
163
145
|
const setIntervalFn = deps.setInterval || setInterval;
|
|
164
146
|
const clearIntervalFn = deps.clearInterval || clearInterval;
|
|
165
147
|
const _nowFn = deps.now || Date.now;
|
|
166
148
|
|
|
167
149
|
const emitter = new EventEmitter();
|
|
168
|
-
let hostsData = loadHosts(
|
|
150
|
+
let hostsData = loadHosts(opts.repoRoot);
|
|
169
151
|
let workers = new Map();
|
|
170
152
|
let pollHandle = null;
|
|
171
153
|
let running = false;
|
|
@@ -321,7 +303,9 @@ export function createRemoteAdapter(opts = {}) {
|
|
|
321
303
|
// watcher-only 세션 (conductor 미등록)
|
|
322
304
|
if (watcher) {
|
|
323
305
|
const watcherStatus = watcher.getStatus();
|
|
324
|
-
for (const [sessionName, record] of Object.entries(
|
|
306
|
+
for (const [sessionName, record] of Object.entries(
|
|
307
|
+
watcherStatus.sessions || {},
|
|
308
|
+
)) {
|
|
325
309
|
if (conductorSessionIds.has(sessionName)) continue;
|
|
326
310
|
const paneName = buildPaneName(sessionName);
|
|
327
311
|
const workerData = buildWatcherOnlyWorkerData(record, hostsData);
|
|
@@ -339,7 +323,7 @@ export function createRemoteAdapter(opts = {}) {
|
|
|
339
323
|
running = true;
|
|
340
324
|
|
|
341
325
|
// hosts.json 리로드
|
|
342
|
-
hostsData = loadHosts(
|
|
326
|
+
hostsData = loadHosts(opts.repoRoot);
|
|
343
327
|
|
|
344
328
|
// conductor stateChange 구독
|
|
345
329
|
conductor.on("stateChange", handleStateChange);
|