@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.
Files changed (162) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2179 -709
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +78 -17
  9. package/hub/team/build-worker-prompt.mjs +73 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +1130 -0
  12. package/hub/team/claude-native-bridge.mjs +1011 -0
  13. package/hub/team/claude-session-projection.mjs +74 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +151 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +97 -13
  24. package/hub/team/cli/commands/start/start-headless.mjs +155 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/help.mjs +1 -1
  33. package/hub/team/cli/index.mjs +2 -2
  34. package/hub/team/cli/manifest.mjs +38 -8
  35. package/hub/team/cli/render.mjs +52 -30
  36. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  37. package/hub/team/cli/services/hub-client.mjs +44 -16
  38. package/hub/team/cli/services/member-selector.mjs +37 -30
  39. package/hub/team/cli/services/native-control.mjs +144 -116
  40. package/hub/team/cli/services/runtime-mode.mjs +61 -4
  41. package/hub/team/cli/services/state-store.mjs +66 -9
  42. package/hub/team/cli/services/task-model.mjs +40 -30
  43. package/hub/team/codex-review.mjs +521 -0
  44. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  45. package/hub/team/conductor-registry.mjs +121 -0
  46. package/hub/team/conductor.mjs +1379 -671
  47. package/hub/team/consensus-meta.mjs +176 -0
  48. package/hub/team/dashboard-anchor.mjs +4 -5
  49. package/hub/team/dashboard-layout.mjs +3 -1
  50. package/hub/team/dashboard-open.mjs +20 -118
  51. package/hub/team/dashboard.mjs +76 -28
  52. package/hub/team/event-log.mjs +18 -10
  53. package/hub/team/execution-mode.mjs +256 -0
  54. package/hub/team/extract-completion-payload.mjs +110 -0
  55. package/hub/team/git-preflight.mjs +286 -0
  56. package/hub/team/handoff.mjs +49 -18
  57. package/hub/team/headless-bridge-session.mjs +17 -0
  58. package/hub/team/headless.mjs +2213 -1156
  59. package/hub/team/health-probe.mjs +232 -69
  60. package/hub/team/interactive-native-launcher.mjs +106 -0
  61. package/hub/team/interactive-native-worker.mjs +80 -0
  62. package/hub/team/interactive-tui-transport.mjs +465 -0
  63. package/hub/team/launcher-template.mjs +49 -17
  64. package/hub/team/lead-control.mjs +91 -4
  65. package/hub/team/mcp-selector.mjs +145 -0
  66. package/hub/team/native-supervisor.mjs +90 -26
  67. package/hub/team/native.mjs +120 -42
  68. package/hub/team/nativeProxy.mjs +184 -113
  69. package/hub/team/notify.mjs +147 -78
  70. package/hub/team/orchestrator.mjs +173 -161
  71. package/hub/team/pane.mjs +43 -20
  72. package/hub/team/process-cleanup.mjs +69 -25
  73. package/hub/team/psmux.mjs +1022 -281
  74. package/hub/team/recovery-store.mjs +149 -0
  75. package/hub/team/remote-probe.mjs +68 -52
  76. package/hub/team/remote-session.mjs +131 -59
  77. package/hub/team/remote-watcher.mjs +61 -33
  78. package/hub/team/retry-state-machine.mjs +336 -0
  79. package/hub/team/routing.mjs +51 -25
  80. package/hub/team/runtime-strategy.mjs +200 -0
  81. package/hub/team/schemas/worker-completion.json +39 -0
  82. package/hub/team/sentinel-capture.mjs +127 -0
  83. package/hub/team/session-sync.mjs +153 -6
  84. package/hub/team/session.mjs +574 -611
  85. package/hub/team/shared.mjs +13 -13
  86. package/hub/team/staleState.mjs +72 -30
  87. package/hub/team/swarm-cli.mjs +390 -0
  88. package/hub/team/swarm-hypervisor.mjs +1544 -193
  89. package/hub/team/swarm-intent.mjs +216 -0
  90. package/hub/team/swarm-locks.mjs +140 -25
  91. package/hub/team/swarm-planner.mjs +121 -21
  92. package/hub/team/swarm-preflight.mjs +319 -0
  93. package/hub/team/swarm-reconciler.mjs +128 -23
  94. package/hub/team/synapse-cli.mjs +458 -0
  95. package/hub/team/synapse-http.mjs +60 -0
  96. package/hub/team/synapse-registry.mjs +329 -0
  97. package/hub/team/terminal-opener.mjs +216 -0
  98. package/hub/team/tui-core.mjs +292 -0
  99. package/hub/team/tui-lite.mjs +237 -148
  100. package/hub/team/tui-remote-adapter.mjs +18 -34
  101. package/hub/team/tui-synapse.mjs +223 -0
  102. package/hub/team/tui-viewer.mjs +108 -48
  103. package/hub/team/tui-widgets.mjs +262 -0
  104. package/hub/team/tui.mjs +931 -439
  105. package/hub/team/worker-completion-validator.mjs +84 -0
  106. package/hub/team/worker-sandbox.mjs +97 -0
  107. package/hub/team/worker-signal.mjs +263 -0
  108. package/hub/team/worker-signal.types.d.ts +52 -0
  109. package/hub/team/worktree-lifecycle.mjs +517 -63
  110. package/hub/team/wt-manager.mjs +297 -26
  111. package/hub/tools.mjs +477 -252
  112. package/hub/tray.mjs +411 -368
  113. package/hub/workers/antigravity-route-worker.mjs +169 -0
  114. package/hub/workers/claude-worker.mjs +162 -118
  115. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  116. package/hub/workers/codex-mcp.mjs +724 -507
  117. package/hub/workers/delegator-mcp.mjs +570 -499
  118. package/hub/workers/factory.mjs +103 -21
  119. package/hub/workers/gemini-worker.mjs +126 -284
  120. package/hub/workers/interface.mjs +24 -2
  121. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  122. package/hub/workers/worker-utils.mjs +23 -17
  123. package/package.json +4 -3
  124. package/scripts/lib/agent-json.mjs +27 -0
  125. package/scripts/lib/async.mjs +174 -0
  126. package/scripts/lib/claudemd-scanner.mjs +223 -0
  127. package/scripts/lib/cli-agy.mjs +62 -0
  128. package/scripts/lib/cli-claude.mjs +78 -0
  129. package/scripts/lib/cli-codex.mjs +199 -0
  130. package/scripts/lib/cli-gemini.mjs +37 -0
  131. package/scripts/lib/codex-recovery.sh +50 -0
  132. package/scripts/lib/context.mjs +67 -0
  133. package/scripts/lib/cross-review-utils.mjs +59 -0
  134. package/scripts/lib/doctor-env-checks.mjs +121 -0
  135. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  136. package/scripts/lib/env-probe.mjs +269 -0
  137. package/scripts/lib/env.mjs +14 -0
  138. package/scripts/lib/gemini-profiles.mjs +119 -0
  139. package/scripts/lib/handoff.mjs +187 -0
  140. package/scripts/lib/hook-utils.mjs +16 -0
  141. package/scripts/lib/hub.mjs +48 -0
  142. package/scripts/lib/keyword-rules.mjs +195 -0
  143. package/scripts/lib/logger.mjs +105 -0
  144. package/scripts/lib/mcp-filter.mjs +883 -0
  145. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  146. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  147. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  148. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  149. package/scripts/lib/mcp-health.mjs +773 -0
  150. package/scripts/lib/mcp-manifest.mjs +89 -0
  151. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  152. package/scripts/lib/pid.mjs +63 -0
  153. package/scripts/lib/process-utils.mjs +26 -0
  154. package/scripts/lib/psmux-info.mjs +224 -0
  155. package/scripts/lib/quota.mjs +47 -0
  156. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  157. package/scripts/lib/skill-state.mjs +220 -0
  158. package/scripts/lib/skill-template.mjs +375 -0
  159. package/scripts/lib/team.mjs +75 -0
  160. package/scripts/lib/timeout.mjs +66 -0
  161. package/scripts/lib/tmp.mjs +26 -0
  162. package/scripts/lib/toml.mjs +113 -0
@@ -1,114 +1,56 @@
1
- import { altScreenOff, altScreenOn, BG, bold, box, clearScreen, clearToEnd, color, cursorHide, cursorHome, cursorShow, dim, eraseBelow, FG, MOCHA, padRight, progressBar, statusBadge, stripAnsi, truncate, wcswidth } from "./ansi.mjs";
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 FALLBACK_COLUMNS = 100, FALLBACK_ROWS = 24;
4
- const VALID_TABS = new Set(["log", "detail", "files"]);
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
- const limit = Math.max(8, width);
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
- const handoff = state.handoff === undefined
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(body.map((line) => padRight(truncate(line, width - 4), width - 4)), width, border);
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 = color(` triflux ${VERSION} `, FG.black, BG.header)
132
- + ` ${bold(`phase ${pipeline.phase || "exec"}`)}`
133
- + ` ${dim(`+${elapsed}s`)} ${names.length} workers`;
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(((worker?.progress ?? (status === "completed" ? 1 : 0)) || 0) * 100);
144
- const token = worker?.tokens ? ` tok ${formatTokens(worker.tokens)}` : "";
145
- const prefix = name === selectedWorker ? color("▶", MOCHA.blue) : dim("·");
146
- return `${prefix} ${index + 1}.${name} ${stripAnsi(statusBadge(status))} ${pct}%${token}`;
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 = [...sanitizeFiles(worker.handoff?.files_changed), ...sanitizeFiles(worker.files_changed)];
179
- detailLines.push(...(files.length ? files.map((file) => `files ${file}`) : ["files 없음"]));
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(...wrap(worker.detail || worker.summary || worker.snapshot || "", width - 4));
135
+ detailLines.push(
136
+ ...wrap(
137
+ worker.detail || worker.summary || worker.snapshot || "",
138
+ width - 4,
139
+ ),
140
+ );
182
141
  } else {
183
- detailLines.push(...wrap(worker.summary || worker.snapshot || worker.detail || "", width - 4));
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) => { if (!closed) stream.write(text); };
181
+ const write = (text) => {
182
+ if (!closed) stream.write(text);
183
+ };
216
184
  const workerNames = () => [...workers.keys()].sort();
217
- const viewportColumns = () => Math.max(48, columns || stream?.columns || process.stdout?.columns || FALLBACK_COLUMNS);
218
- const viewportRows = () => Math.max(10, rows || stream?.rows || process.stdout?.rows || FALLBACK_ROWS);
219
- const ensureSelection = (names) => { if (names.length && (!selectedWorker || !workers.has(selectedWorker))) selectedWorker = names[0]; };
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 (typeof onOpenSelectedWorker !== "function" || !selectedWorker || !workers.has(selectedWorker)) return;
201
+ if (
202
+ typeof onOpenSelectedWorker !== "function" ||
203
+ !selectedWorker ||
204
+ !workers.has(selectedWorker)
205
+ )
206
+ return;
231
207
  try {
232
- const result = onOpenSelectedWorker(selectedWorker, workers.get(selectedWorker), new Map(workers));
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(selectedWorker, workers.get(selectedWorker), new Map(workers));
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
- triggerOpenSelected();
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 (key === "\x1b[13;2u" || key === "\x1b[27;13;2~" || key === "\x1b\r" || key === "\x1b\n") {
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 (!isTTY || !input?.isTTY || typeof input?.on !== "function") return;
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 = !detailExpanded || names.length <= 1 || width < 100 || layout === "single";
316
+ const railOnly =
317
+ !detailExpanded ||
318
+ names.length <= 1 ||
319
+ width < 100 ||
320
+ layout === "single";
317
321
  if (railOnly) {
318
- const sections = [header, ...buildWorkerRail(names, workers, selectedWorker, width)];
319
- if (detailExpanded) sections.push(...buildDetail(selectedWorker, workers.get(selectedWorker), width, focusTab, helpVisible));
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(buildWorkerRail(names, workers, selectedWorker, railWidth), railWidth, bodyHeight);
326
- const detail = fitHeight(buildDetail(selectedWorker, workers.get(selectedWorker), detailWidth, focusTab, helpVisible), detailWidth, bodyHeight);
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({ length: bodyHeight }, (_, index) => `${rail[index]}${dim("│")}${detail[index]}`),
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) + clearToEnd);
341
- write(cursorHome + padded.join("\n") + eraseBelow);
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") input.off("data", handleInput);
349
- if (rawModeEnabled && typeof input?.setRawMode === "function") input.setRawMode(false);
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) { workers.set(name, normalizeWorkerState(workers.get(name), state)); ensureSelection(workerNames()); },
363
- updatePipeline(state) { pipeline = { ...pipeline, ...state }; },
364
- setStartTime(ms) { startedAt = ms; },
365
- selectWorker(name) { if (workers.has(name)) selectedWorker = name; },
366
- toggleDetail(force) { detailExpanded = typeof force === "boolean" ? force : !detailExpanded; },
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() { return new Map(workers); },
369
- getFrameCount() { return frameCount; },
370
- getPipelineState() { return { ...pipeline }; },
371
- getSelectedWorker() { return selectedWorker; },
372
- isDetailExpanded() { return detailExpanded; },
373
- getFocusTab() { return focusTab; },
374
- setFocusTab(tab) { if (VALID_TABS.has(tab)) focusTab = tab; },
375
- getLayout() { return layout; },
376
- toggleHelp(force) { helpVisible = typeof force === "boolean" ? force : !helpVisible; },
377
- isHelpVisible() { return helpVisible; },
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 = entry.health?.level
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 = watcherRecord.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: watcherRecord.state === "completed" ? "completed"
125
- : watcherRecord.state === "failed" ? "failed"
126
- : "running",
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.hostsJsonPath] — hosts.json 경로 override
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 hostsJsonPath = resolveHostsJsonPath(opts.hostsJsonPath);
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(hostsJsonPath);
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(watcherStatus.sessions || {})) {
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(hostsJsonPath);
326
+ hostsData = loadHosts(opts.repoRoot);
343
327
 
344
328
  // conductor stateChange 구독
345
329
  conductor.on("stateChange", handleStateChange);