@triflux/remote 10.0.1 → 10.25.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 +239 -121
- package/hub/public/dashboard.html +68 -7
- package/hub/server.mjs +2181 -701
- package/hub/store-adapter.mjs +101 -37
- 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 +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -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 +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +127 -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/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 +45 -17
- 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 +55 -1
- 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 +21 -119
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -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 +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- 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 +74 -35
- 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 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1067 -264
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +69 -53
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +62 -34
- package/hub/team/retry-state-machine.mjs +267 -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 +605 -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 +1526 -198
- 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 +317 -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 +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +21 -37
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -45
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +936 -449
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -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 +518 -64
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- 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 +663 -337
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- 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 +67 -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-servers.mjs +70 -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 +179 -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
|
}
|
|
@@ -140,9 +83,14 @@ function buildWorkerRail(names, workers, selectedWorker, width) {
|
|
|
140
83
|
? names.map((name, index) => {
|
|
141
84
|
const worker = workers.get(name);
|
|
142
85
|
const status = runtimeStatus(worker);
|
|
143
|
-
const pct = Math.round(
|
|
144
|
-
|
|
145
|
-
|
|
86
|
+
const pct = Math.round(
|
|
87
|
+
((worker?.progress ?? (status === "completed" ? 1 : 0)) || 0) * 100,
|
|
88
|
+
);
|
|
89
|
+
const token = worker?.tokens
|
|
90
|
+
? ` tok ${formatTokens(worker.tokens)}`
|
|
91
|
+
: "";
|
|
92
|
+
const prefix =
|
|
93
|
+
name === selectedWorker ? color("▶", MOCHA.blue) : dim("·");
|
|
146
94
|
return `${prefix} ${index + 1}.${name} ${stripAnsi(statusBadge(status))} ${pct}%${token}`;
|
|
147
95
|
})
|
|
148
96
|
: [dim("workers 없음")];
|
|
@@ -175,12 +123,27 @@ function buildDetail(workerName, worker, width, tab, helpVisible) {
|
|
|
175
123
|
`verdict ${worker.handoff?.verdict || worker.summary || worker.snapshot || "-"}`,
|
|
176
124
|
];
|
|
177
125
|
if (tab === "files") {
|
|
178
|
-
const files = [
|
|
179
|
-
|
|
126
|
+
const files = [
|
|
127
|
+
...sanitizeFiles(worker.handoff?.files_changed),
|
|
128
|
+
...sanitizeFiles(worker.files_changed),
|
|
129
|
+
];
|
|
130
|
+
detailLines.push(
|
|
131
|
+
...(files.length ? files.map((file) => `files ${file}`) : ["files 없음"]),
|
|
132
|
+
);
|
|
180
133
|
} else if (tab === "detail") {
|
|
181
|
-
detailLines.push(
|
|
134
|
+
detailLines.push(
|
|
135
|
+
...wrap(
|
|
136
|
+
worker.detail || worker.summary || worker.snapshot || "",
|
|
137
|
+
width - 4,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
182
140
|
} else {
|
|
183
|
-
detailLines.push(
|
|
141
|
+
detailLines.push(
|
|
142
|
+
...wrap(
|
|
143
|
+
worker.summary || worker.snapshot || worker.detail || "",
|
|
144
|
+
width - 4,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
184
147
|
}
|
|
185
148
|
return frame(detailLines, width, MOCHA.thinking);
|
|
186
149
|
}
|
|
@@ -209,14 +172,21 @@ export function createLiteDashboard(opts = {}) {
|
|
|
209
172
|
let detailExpanded = true;
|
|
210
173
|
let focusTab = "log";
|
|
211
174
|
let helpVisible = false;
|
|
175
|
+
let prevFrame = [];
|
|
176
|
+
let prevWidth = 0;
|
|
212
177
|
let inputAttached = false;
|
|
213
178
|
let rawModeEnabled = false;
|
|
214
179
|
|
|
215
|
-
const write = (text) => {
|
|
180
|
+
const write = (text) => {
|
|
181
|
+
if (!closed) stream.write(text);
|
|
182
|
+
};
|
|
216
183
|
const workerNames = () => [...workers.keys()].sort();
|
|
217
|
-
const viewportColumns = () =>
|
|
218
|
-
const viewportRows = () =>
|
|
219
|
-
const ensureSelection = (names) => {
|
|
184
|
+
const viewportColumns = () => resolveViewportColumns({ columns, stream });
|
|
185
|
+
const viewportRows = () => resolveViewportRows({ rows, stream });
|
|
186
|
+
const ensureSelection = (names) => {
|
|
187
|
+
if (names.length && (!selectedWorker || !workers.has(selectedWorker)))
|
|
188
|
+
selectedWorker = names[0];
|
|
189
|
+
};
|
|
220
190
|
|
|
221
191
|
function selectRelative(offset) {
|
|
222
192
|
const names = workerNames();
|
|
@@ -227,9 +197,18 @@ export function createLiteDashboard(opts = {}) {
|
|
|
227
197
|
}
|
|
228
198
|
|
|
229
199
|
function triggerOpenSelected() {
|
|
230
|
-
if (
|
|
200
|
+
if (
|
|
201
|
+
typeof onOpenSelectedWorker !== "function" ||
|
|
202
|
+
!selectedWorker ||
|
|
203
|
+
!workers.has(selectedWorker)
|
|
204
|
+
)
|
|
205
|
+
return;
|
|
231
206
|
try {
|
|
232
|
-
const result = onOpenSelectedWorker(
|
|
207
|
+
const result = onOpenSelectedWorker(
|
|
208
|
+
selectedWorker,
|
|
209
|
+
workers.get(selectedWorker),
|
|
210
|
+
new Map(workers),
|
|
211
|
+
);
|
|
233
212
|
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
234
213
|
} catch {}
|
|
235
214
|
}
|
|
@@ -237,7 +216,11 @@ export function createLiteDashboard(opts = {}) {
|
|
|
237
216
|
function triggerOpenAll() {
|
|
238
217
|
if (typeof onOpenAllWorkers !== "function") return;
|
|
239
218
|
try {
|
|
240
|
-
const result = onOpenAllWorkers(
|
|
219
|
+
const result = onOpenAllWorkers(
|
|
220
|
+
selectedWorker,
|
|
221
|
+
workers.get(selectedWorker),
|
|
222
|
+
new Map(workers),
|
|
223
|
+
);
|
|
241
224
|
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
242
225
|
} catch {}
|
|
243
226
|
}
|
|
@@ -263,10 +246,26 @@ export function createLiteDashboard(opts = {}) {
|
|
|
263
246
|
return;
|
|
264
247
|
}
|
|
265
248
|
if (key === "\r" || key === "\n") {
|
|
266
|
-
|
|
249
|
+
if (
|
|
250
|
+
typeof onOpenSelectedWorker === "function" &&
|
|
251
|
+
selectedWorker &&
|
|
252
|
+
workers.has(selectedWorker)
|
|
253
|
+
) {
|
|
254
|
+
triggerOpenSelected();
|
|
255
|
+
} else {
|
|
256
|
+
// 콜백 없거나 선택 워커 없으면 탭 순환
|
|
257
|
+
const tabs = ["log", "detail", "files"];
|
|
258
|
+
focusTab = tabs[(tabs.indexOf(focusTab) + 1) % tabs.length];
|
|
259
|
+
}
|
|
260
|
+
render();
|
|
267
261
|
return;
|
|
268
262
|
}
|
|
269
|
-
if (
|
|
263
|
+
if (
|
|
264
|
+
key === "\x1b[13;2u" ||
|
|
265
|
+
key === "\x1b[27;13;2~" ||
|
|
266
|
+
key === "\x1b\r" ||
|
|
267
|
+
key === "\x1b\n"
|
|
268
|
+
) {
|
|
270
269
|
triggerOpenAll();
|
|
271
270
|
return;
|
|
272
271
|
}
|
|
@@ -297,7 +296,7 @@ export function createLiteDashboard(opts = {}) {
|
|
|
297
296
|
|
|
298
297
|
function attachInput() {
|
|
299
298
|
if (inputAttached) return;
|
|
300
|
-
if (!
|
|
299
|
+
if (!input?.isTTY || typeof input?.on !== "function") return;
|
|
301
300
|
inputAttached = true;
|
|
302
301
|
if (typeof input.setRawMode === "function") {
|
|
303
302
|
input.setRawMode(true);
|
|
@@ -313,20 +312,53 @@ export function createLiteDashboard(opts = {}) {
|
|
|
313
312
|
const width = viewportColumns();
|
|
314
313
|
const height = viewportRows();
|
|
315
314
|
const header = buildHeader(width, names, workers, pipeline, startedAt);
|
|
316
|
-
const railOnly =
|
|
315
|
+
const railOnly =
|
|
316
|
+
!detailExpanded ||
|
|
317
|
+
names.length <= 1 ||
|
|
318
|
+
width < 100 ||
|
|
319
|
+
layout === "single";
|
|
317
320
|
if (railOnly) {
|
|
318
|
-
const sections = [
|
|
319
|
-
|
|
321
|
+
const sections = [
|
|
322
|
+
header,
|
|
323
|
+
...buildWorkerRail(names, workers, selectedWorker, width),
|
|
324
|
+
];
|
|
325
|
+
if (detailExpanded)
|
|
326
|
+
sections.push(
|
|
327
|
+
...buildDetail(
|
|
328
|
+
selectedWorker,
|
|
329
|
+
workers.get(selectedWorker),
|
|
330
|
+
width,
|
|
331
|
+
focusTab,
|
|
332
|
+
helpVisible,
|
|
333
|
+
),
|
|
334
|
+
);
|
|
320
335
|
return fitHeight(sections, width, height);
|
|
321
336
|
}
|
|
322
337
|
const railWidth = Math.max(28, Math.floor(width * 0.32));
|
|
323
338
|
const detailWidth = width - railWidth - 1;
|
|
324
339
|
const bodyHeight = Math.max(6, height - header.length);
|
|
325
|
-
const rail = fitHeight(
|
|
326
|
-
|
|
340
|
+
const rail = fitHeight(
|
|
341
|
+
buildWorkerRail(names, workers, selectedWorker, railWidth),
|
|
342
|
+
railWidth,
|
|
343
|
+
bodyHeight,
|
|
344
|
+
);
|
|
345
|
+
const detail = fitHeight(
|
|
346
|
+
buildDetail(
|
|
347
|
+
selectedWorker,
|
|
348
|
+
workers.get(selectedWorker),
|
|
349
|
+
detailWidth,
|
|
350
|
+
focusTab,
|
|
351
|
+
helpVisible,
|
|
352
|
+
),
|
|
353
|
+
detailWidth,
|
|
354
|
+
bodyHeight,
|
|
355
|
+
);
|
|
327
356
|
return [
|
|
328
357
|
...header,
|
|
329
|
-
...Array.from(
|
|
358
|
+
...Array.from(
|
|
359
|
+
{ length: bodyHeight },
|
|
360
|
+
(_, index) => `${rail[index]}${dim("│")}${detail[index]}`,
|
|
361
|
+
),
|
|
330
362
|
];
|
|
331
363
|
}
|
|
332
364
|
|
|
@@ -337,18 +369,43 @@ export function createLiteDashboard(opts = {}) {
|
|
|
337
369
|
const rowsOut = buildRows();
|
|
338
370
|
if (isTTY) {
|
|
339
371
|
const width = viewportColumns();
|
|
340
|
-
const padded = rowsOut.map((line) => padRight(String(line ?? ""), width)
|
|
341
|
-
|
|
372
|
+
const padded = rowsOut.map((line) => padRight(String(line ?? ""), width));
|
|
373
|
+
// Full redraw on first frame or terminal resize to avoid artifacts
|
|
374
|
+
if (prevFrame.length === 0 || width !== prevWidth) {
|
|
375
|
+
prevWidth = width;
|
|
376
|
+
write(
|
|
377
|
+
cursorHome +
|
|
378
|
+
padded.map((l) => l + clearToEnd).join("\n") +
|
|
379
|
+
eraseBelow,
|
|
380
|
+
);
|
|
381
|
+
prevFrame = padded;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Diff-based rendering: only rewrite lines that actually changed
|
|
385
|
+
let buf = "";
|
|
386
|
+
for (let i = 0; i < padded.length; i++) {
|
|
387
|
+
if (padded[i] !== prevFrame[i]) {
|
|
388
|
+
buf += moveTo(i + 1, 1) + padded[i] + clearToEnd;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (prevFrame.length > padded.length) {
|
|
392
|
+
buf += moveTo(padded.length + 1, 1) + eraseBelow;
|
|
393
|
+
}
|
|
394
|
+
if (buf) write(buf);
|
|
395
|
+
prevFrame = padded;
|
|
342
396
|
} else write(`${rowsOut.join("\n")}\n`);
|
|
343
397
|
}
|
|
344
398
|
|
|
345
399
|
function close() {
|
|
346
400
|
if (closed) return;
|
|
347
401
|
if (timer) clearInterval(timer);
|
|
348
|
-
if (inputAttached && typeof input?.off === "function")
|
|
349
|
-
|
|
402
|
+
if (inputAttached && typeof input?.off === "function")
|
|
403
|
+
input.off("data", handleInput);
|
|
404
|
+
if (rawModeEnabled && typeof input?.setRawMode === "function")
|
|
405
|
+
input.setRawMode(false);
|
|
350
406
|
if (inputAttached && typeof input?.pause === "function") input.pause();
|
|
351
407
|
if (isTTY) write(cursorShow + altScreenOff);
|
|
408
|
+
prevFrame = [];
|
|
352
409
|
closed = true;
|
|
353
410
|
}
|
|
354
411
|
|
|
@@ -359,22 +416,53 @@ export function createLiteDashboard(opts = {}) {
|
|
|
359
416
|
}
|
|
360
417
|
|
|
361
418
|
return {
|
|
362
|
-
updateWorker(name, state) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
419
|
+
updateWorker(name, state) {
|
|
420
|
+
workers.set(name, normalizeWorkerState(workers.get(name), state));
|
|
421
|
+
ensureSelection(workerNames());
|
|
422
|
+
},
|
|
423
|
+
updatePipeline(state) {
|
|
424
|
+
pipeline = { ...pipeline, ...state };
|
|
425
|
+
},
|
|
426
|
+
setStartTime(ms) {
|
|
427
|
+
startedAt = ms;
|
|
428
|
+
},
|
|
429
|
+
selectWorker(name) {
|
|
430
|
+
if (workers.has(name)) selectedWorker = name;
|
|
431
|
+
},
|
|
432
|
+
toggleDetail(force) {
|
|
433
|
+
detailExpanded = typeof force === "boolean" ? force : !detailExpanded;
|
|
434
|
+
},
|
|
367
435
|
render,
|
|
368
|
-
getWorkers() {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
436
|
+
getWorkers() {
|
|
437
|
+
return new Map(workers);
|
|
438
|
+
},
|
|
439
|
+
getFrameCount() {
|
|
440
|
+
return frameCount;
|
|
441
|
+
},
|
|
442
|
+
getPipelineState() {
|
|
443
|
+
return { ...pipeline };
|
|
444
|
+
},
|
|
445
|
+
getSelectedWorker() {
|
|
446
|
+
return selectedWorker;
|
|
447
|
+
},
|
|
448
|
+
isDetailExpanded() {
|
|
449
|
+
return detailExpanded;
|
|
450
|
+
},
|
|
451
|
+
getFocusTab() {
|
|
452
|
+
return focusTab;
|
|
453
|
+
},
|
|
454
|
+
setFocusTab(tab) {
|
|
455
|
+
if (VALID_TABS.has(tab)) focusTab = tab;
|
|
456
|
+
},
|
|
457
|
+
getLayout() {
|
|
458
|
+
return layout;
|
|
459
|
+
},
|
|
460
|
+
toggleHelp(force) {
|
|
461
|
+
helpVisible = typeof force === "boolean" ? force : !helpVisible;
|
|
462
|
+
},
|
|
463
|
+
isHelpVisible() {
|
|
464
|
+
return helpVisible;
|
|
465
|
+
},
|
|
378
466
|
close,
|
|
379
467
|
};
|
|
380
468
|
}
|
|
@@ -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;
|
|
@@ -57,7 +38,7 @@ function resolveSshUser(hostsData, host) {
|
|
|
57
38
|
* @returns {string|null}
|
|
58
39
|
*/
|
|
59
40
|
function resolveHostFromSessionName(sessionName) {
|
|
60
|
-
if (!sessionName
|
|
41
|
+
if (!sessionName?.startsWith(SESSION_PREFIX)) return null;
|
|
61
42
|
const rest = sessionName.slice(SESSION_PREFIX.length);
|
|
62
43
|
const dashIdx = rest.indexOf("-");
|
|
63
44
|
if (dashIdx === -1) return rest || 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
|
-
const
|
|
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;
|
|
@@ -175,7 +157,7 @@ export function createRemoteAdapter(opts = {}) {
|
|
|
175
157
|
function handleStateChange({ sessionId, from, to, reason }) {
|
|
176
158
|
const snapshots = conductor.getSnapshot();
|
|
177
159
|
const entry = snapshots.find((s) => s.id === sessionId);
|
|
178
|
-
if (!entry
|
|
160
|
+
if (!entry?.remote) return;
|
|
179
161
|
|
|
180
162
|
const watcherStatus = getWatcherSession(sessionId);
|
|
181
163
|
const workerData = buildWorkerData(entry, watcherStatus, hostsData);
|
|
@@ -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);
|