triflux 10.3.4 → 10.7.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/LICENSE +21 -21
- package/bin/tfx-doctor-tui.mjs +1 -1
- package/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-profile.mjs +1 -1
- package/bin/tfx-setup-tui.mjs +1 -1
- package/bin/tfx-setup.mjs +6 -1
- package/bin/triflux.mjs +2396 -1140
- package/hooks/agent-route-guard.mjs +12 -8
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +19 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +108 -24
- package/hooks/hook-registry.json +11 -0
- package/hooks/keyword-rules.json +6 -10
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/pipeline-stop.mjs +3 -6
- package/hooks/safety-guard.mjs +99 -28
- package/hooks/session-start-fast.mjs +143 -0
- package/hooks/subagent-verifier.mjs +5 -4
- package/hub/account-broker.mjs +256 -60
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +95 -57
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +67 -30
- package/hub/bridge.mjs +0 -1
- package/hub/cli-adapter-base.mjs +200 -48
- package/hub/codex-adapter.mjs +76 -96
- package/hub/codex-compat.mjs +3 -3
- package/hub/codex-preflight.mjs +63 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +3 -3
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +5 -5
- package/hub/fullcycle.mjs +33 -17
- package/hub/gemini-adapter.mjs +69 -94
- package/hub/hitl.mjs +89 -30
- package/hub/intent.mjs +161 -38
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/mcp-response-cache.mjs +66 -32
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +35 -37
- package/hub/lib/process-utils.mjs +106 -37
- package/hub/lib/spawn-trace.mjs +527 -0
- package/hub/lib/ssh-command.mjs +34 -4
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +57 -48
- package/hub/promote-penalties.mjs +25 -7
- package/hub/quality/deslop.mjs +70 -49
- package/hub/research.mjs +32 -25
- package/hub/router.mjs +240 -107
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/server.mjs +4 -4
- package/hub/session-fingerprint.mjs +126 -60
- package/hub/state.mjs +84 -43
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +22 -7
- package/hub/team/ansi.mjs +186 -122
- package/hub/team/backend.mjs +28 -10
- package/hub/team/cli/commands/attach.mjs +29 -9
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +32 -11
- package/hub/team/cli/commands/focus.mjs +38 -11
- 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 +11 -4
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -31
- package/hub/team/cli/commands/start/parse-args.mjs +38 -11
- package/hub/team/cli/commands/start/start-headless.mjs +112 -36
- package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
- package/hub/team/cli/commands/start/start-mux.mjs +70 -21
- package/hub/team/cli/commands/start/start-wt.mjs +29 -12
- 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 +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +30 -8
- package/hub/team/cli/services/attach-fallback.mjs +31 -11
- package/hub/team/cli/services/hub-client.mjs +42 -14
- package/hub/team/cli/services/member-selector.mjs +11 -4
- package/hub/team/cli/services/native-control.mjs +48 -21
- package/hub/team/cli/services/runtime-mode.mjs +2 -1
- package/hub/team/cli/services/state-store.mjs +25 -8
- package/hub/team/cli/services/task-model.mjs +16 -6
- package/hub/team/conductor-mesh-bridge.mjs +24 -23
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +41 -21
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/handoff.mjs +31 -15
- package/hub/team/headless.mjs +2 -1
- package/hub/team/health-probe.mjs +69 -54
- package/hub/team/launcher-template.mjs +16 -13
- package/hub/team/native-supervisor.mjs +65 -21
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +119 -76
- package/hub/team/orchestrator.mjs +9 -4
- package/hub/team/pane.mjs +12 -7
- package/hub/team/process-cleanup.mjs +25 -16
- package/hub/team/psmux.mjs +491 -201
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +117 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +3 -1
- package/hub/team/session.mjs +98 -34
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-locks.mjs +15 -13
- package/hub/team/swarm-planner.mjs +32 -21
- package/hub/team/swarm-reconciler.mjs +48 -23
- package/hub/team/tui-lite.mjs +266 -68
- package/hub/team/tui-remote-adapter.mjs +14 -10
- package/hub/team/tui-viewer.mjs +99 -43
- package/hub/team/tui.mjs +708 -271
- package/hub/team/worktree-lifecycle.mjs +152 -58
- package/hub/team/wt-manager.mjs +24 -14
- package/hub/token-mode.mjs +71 -71
- package/hub/tray.mjs +66 -23
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-mcp.mjs +192 -141
- package/hub/workers/delegator-mcp.mjs +507 -333
- package/hub/workers/factory.mjs +8 -8
- package/hub/workers/gemini-worker.mjs +115 -84
- package/hub/workers/interface.mjs +6 -1
- package/hub/workers/worker-utils.mjs +21 -14
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +162 -26
- package/hud/context-monitor.mjs +82 -41
- package/hud/hud-qos-status.mjs +129 -49
- package/hud/mission-board.mjs +6 -3
- package/hud/providers/claude.mjs +226 -115
- package/hud/providers/codex.mjs +62 -22
- package/hud/providers/gemini.mjs +168 -56
- package/hud/renderers.mjs +384 -119
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +78 -38
- package/mesh/index.mjs +11 -5
- package/mesh/mesh-budget.mjs +18 -9
- package/mesh/mesh-heartbeat.mjs +1 -1
- package/mesh/mesh-queue.mjs +3 -5
- package/mesh/mesh-router.mjs +5 -4
- package/package.json +2 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
- package/scripts/__tests__/keyword-detector.test.mjs +77 -28
- package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
- package/scripts/__tests__/remote-spawn.test.mjs +10 -4
- package/scripts/__tests__/session-start-fast.test.mjs +36 -0
- package/scripts/__tests__/skill-template.test.mjs +98 -50
- package/scripts/__tests__/smoke.test.mjs +1 -1
- package/scripts/__tests__/spawn-trace.test.mjs +102 -0
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
- package/scripts/cache-doctor.mjs +11 -4
- package/scripts/cache-warmup.mjs +96 -37
- package/scripts/claudemd-sync.mjs +27 -17
- package/scripts/codex-gateway-preflight.mjs +52 -37
- package/scripts/codex-mcp-gateway-sync.mjs +59 -39
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/config-audit.mjs +232 -0
- package/scripts/convert-to-tmpl.mjs +54 -0
- package/scripts/cross-review-gate.mjs +35 -12
- package/scripts/cross-review-tracker.mjs +21 -8
- package/scripts/demo.mjs +35 -17
- package/scripts/doctor-diagnose.mjs +284 -0
- package/scripts/gen-skill-docs.mjs +7 -2
- package/scripts/gen-skill-manifest.mjs +2 -1
- package/scripts/headless-guard.mjs +86 -48
- package/scripts/hub-ensure.mjs +45 -26
- package/scripts/keyword-detector.mjs +41 -20
- package/scripts/keyword-rules-expander.mjs +47 -30
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +6 -3
- package/scripts/lib/env-probe.mjs +47 -28
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/keyword-rules.mjs +43 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +377 -239
- package/scripts/lib/mcp-guard-engine.mjs +194 -79
- package/scripts/lib/mcp-manifest.mjs +23 -13
- package/scripts/lib/mcp-server-catalog.mjs +300 -63
- package/scripts/lib/psmux-info.mjs +11 -6
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-template.mjs +30 -7
- package/scripts/mcp-check.mjs +58 -39
- package/scripts/mcp-gateway-config.mjs +83 -39
- package/scripts/mcp-gateway-ensure.mjs +43 -35
- package/scripts/mcp-gateway-integration-test.mjs +70 -58
- package/scripts/mcp-gateway-start.mjs +126 -60
- package/scripts/mcp-gateway-verify.mjs +24 -22
- package/scripts/mcp-safety-guard.mjs +44 -11
- package/scripts/notion-read.mjs +199 -84
- package/scripts/pack.mjs +94 -89
- package/scripts/preflight-cache.mjs +27 -10
- package/scripts/preinstall.mjs +42 -13
- package/scripts/remote-spawn.mjs +309 -94
- package/scripts/run.cjs +8 -5
- package/scripts/session-spawn-helper.mjs +130 -39
- package/scripts/session-stale-cleanup.mjs +123 -0
- package/scripts/setup.mjs +941 -492
- package/scripts/test-lock.mjs +20 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
- package/scripts/tfx-batch-stats.mjs +32 -11
- package/scripts/tfx-gate-activate.mjs +11 -4
- package/scripts/tfx-route-post.mjs +87 -20
- package/scripts/tfx-route-worker.mjs +57 -51
- package/scripts/tfx-route.sh +41 -124
- package/scripts/tmp-cleanup.mjs +21 -7
- package/scripts/token-snapshot.mjs +204 -85
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/_templates/base.md +1 -6
- package/skills/merge-worktree/SKILL.md.tmpl +144 -0
- package/skills/shared/telemetry-segment.md +6 -0
- package/skills/star-prompt/SKILL.md.tmpl +222 -0
- package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
- package/skills/tfx-analysis/skill.json +1 -6
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
- package/skills/tfx-auto-codex/skill.json +1 -3
- package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
- package/skills/tfx-autopilot/skill.json +1 -5
- package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
- package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
- package/skills/tfx-autoroute/skill.json +1 -7
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +1 -3
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
- package/skills/tfx-codex-swarm/evals/evals.json +1 -1
- package/skills/tfx-codex-swarm/skill.json +1 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
- package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
- package/skills/tfx-debate/SKILL.md.tmpl +192 -0
- package/skills/tfx-debate/skill.json +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
- package/skills/tfx-deep-analysis/skill.json +1 -5
- package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
- package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
- package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
- package/skills/tfx-deep-qa/skill.json +1 -6
- package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
- package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
- package/skills/tfx-doctor/SKILL.md +21 -0
- package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
- package/skills/tfx-doctor/skill.json +1 -3
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-forge/SKILL.md.tmpl +187 -0
- package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
- package/skills/tfx-fullcycle/skill.json +1 -6
- package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
- package/skills/tfx-gemini/skill.json +1 -3
- package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
- package/skills/tfx-hooks/skill.json +1 -3
- package/skills/tfx-hub/SKILL.md.tmpl +212 -0
- package/skills/tfx-hub/skill.json +1 -3
- package/skills/tfx-index/SKILL.md +1 -0
- package/skills/tfx-index/skill.json +1 -6
- package/skills/tfx-interview/SKILL.md.tmpl +285 -0
- package/skills/tfx-multi/SKILL.md.tmpl +183 -0
- package/skills/tfx-multi/skill.json +1 -3
- package/skills/tfx-panel/SKILL.md.tmpl +189 -0
- package/skills/tfx-panel/skill.json +1 -7
- package/skills/tfx-persist/SKILL.md.tmpl +270 -0
- package/skills/tfx-persist/skill.json +1 -7
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +1 -6
- package/skills/tfx-profile/SKILL.md.tmpl +239 -0
- package/skills/tfx-profile/skill.json +1 -3
- package/skills/tfx-prune/SKILL.md.tmpl +200 -0
- package/skills/tfx-prune/skill.json +1 -7
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
- package/skills/tfx-psmux-rules/skill.json +1 -4
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +1 -6
- package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
- package/skills/tfx-ralph/skill.json +1 -4
- package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
- package/skills/tfx-remote-setup/skill.json +1 -3
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
- package/skills/tfx-remote-spawn/skill.json +1 -4
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +1 -6
- package/skills/tfx-setup/SKILL.md.tmpl +504 -0
- package/skills/tfx-setup/skill.json +1 -3
- package/skills/tfx-swarm/SKILL.md +22 -0
- package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
- package/tui/codex-profile.mjs +88 -33
- package/tui/core.mjs +45 -15
- package/tui/doctor.mjs +75 -28
- package/tui/gemini-profile.mjs +74 -29
- package/tui/monitor-data.mjs +8 -4
- package/tui/monitor.mjs +71 -27
- package/tui/setup.mjs +133 -42
package/hub/team/tui.mjs
CHANGED
|
@@ -2,40 +2,41 @@
|
|
|
2
2
|
// virtual row buffer 기반. dirty-row만 갱신. isTTY 아닐 때 append-only fallback.
|
|
3
3
|
// Tier1(상단 고정) / Tier2(worker rail) / Tier3(focus pane) 3단 계층.
|
|
4
4
|
|
|
5
|
+
import { execFile as _execFile } from "../lib/spawn-trace.mjs";
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
MOCHA,
|
|
9
|
-
color,
|
|
10
|
-
dim,
|
|
7
|
+
altScreenOff,
|
|
8
|
+
altScreenOn,
|
|
11
9
|
bold,
|
|
12
10
|
box,
|
|
13
|
-
|
|
14
|
-
truncate,
|
|
15
|
-
clip,
|
|
16
|
-
stripAnsi,
|
|
17
|
-
wcswidth,
|
|
18
|
-
progressBar,
|
|
19
|
-
statusBadge,
|
|
20
|
-
altScreenOn,
|
|
21
|
-
altScreenOff,
|
|
11
|
+
clearLine,
|
|
22
12
|
clearScreen,
|
|
23
|
-
|
|
13
|
+
clip,
|
|
14
|
+
color,
|
|
24
15
|
cursorHide,
|
|
16
|
+
cursorHome,
|
|
25
17
|
cursorShow,
|
|
18
|
+
dim,
|
|
19
|
+
FG,
|
|
20
|
+
MOCHA,
|
|
26
21
|
moveTo,
|
|
27
|
-
|
|
22
|
+
padRight,
|
|
23
|
+
progressBar,
|
|
24
|
+
RESET,
|
|
25
|
+
statusBadge,
|
|
26
|
+
stripAnsi,
|
|
27
|
+
truncate,
|
|
28
|
+
wcswidth,
|
|
28
29
|
} from "./ansi.mjs";
|
|
29
30
|
|
|
30
|
-
import { execFile as _execFile } from "node:child_process";
|
|
31
|
-
|
|
32
31
|
// package.json에서 동적 로드 (실패 시 fallback)
|
|
33
32
|
let VERSION = "7.x";
|
|
34
33
|
try {
|
|
35
34
|
const { createRequire } = await import("node:module");
|
|
36
35
|
const require = createRequire(import.meta.url);
|
|
37
36
|
VERSION = require("../../package.json").version;
|
|
38
|
-
} catch {
|
|
37
|
+
} catch {
|
|
38
|
+
/* fallback */
|
|
39
|
+
}
|
|
39
40
|
|
|
40
41
|
const FALLBACK_COLUMNS = 100;
|
|
41
42
|
const FALLBACK_ROWS = 30;
|
|
@@ -45,10 +46,13 @@ const MIN_CARD_WIDTH = 28;
|
|
|
45
46
|
// 프레임: ["·","✢","✳","✶","✻","✽"] + 역재생 = 12프레임 왕복
|
|
46
47
|
// 타이밍: 2000ms/cycle, RGB truecolor 보간
|
|
47
48
|
const SPINNER_FRAMES_RAW = ["·", "✢", "✳", "✶", "✻", "✽"];
|
|
48
|
-
const SPINNER_FRAMES = [
|
|
49
|
+
const SPINNER_FRAMES = [
|
|
50
|
+
...SPINNER_FRAMES_RAW,
|
|
51
|
+
...[...SPINNER_FRAMES_RAW].reverse(),
|
|
52
|
+
];
|
|
49
53
|
const SPINNER_CYCLE_MS = 2000;
|
|
50
54
|
const SPINNER_BASE_COLOR = { r: 203, g: 166, b: 247 }; // Catppuccin Mocha mauve
|
|
51
|
-
const SPINNER_SHIMMER = { r: 171, g: 43, b: 63 };
|
|
55
|
+
const SPINNER_SHIMMER = { r: 171, g: 43, b: 63 }; // Claude shimmer #ab2b3f
|
|
52
56
|
const spinnerStart = Date.now();
|
|
53
57
|
let spinnerTick = 0;
|
|
54
58
|
|
|
@@ -68,18 +72,28 @@ function pseudoRandomFrame(step, seed) {
|
|
|
68
72
|
return Math.abs(Math.imul(step + seed, 2654435761)) % SPINNER_FRAMES.length;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
function heartbeat(
|
|
72
|
-
|
|
75
|
+
function heartbeat(
|
|
76
|
+
status,
|
|
77
|
+
shimmerIntensity = 0,
|
|
78
|
+
statusChangedAt = 0,
|
|
79
|
+
time = Date.now(),
|
|
80
|
+
) {
|
|
81
|
+
const transitionElapsed = statusChangedAt
|
|
82
|
+
? Math.max(0, time - statusChangedAt)
|
|
83
|
+
: Number.POSITIVE_INFINITY;
|
|
73
84
|
if (transitionElapsed < 500) {
|
|
74
85
|
const step = Math.floor(transitionElapsed / 50);
|
|
75
86
|
const idx = pseudoRandomFrame(step, statusChangedAt % 997);
|
|
76
|
-
const targetColor =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
const targetColor =
|
|
88
|
+
status === "failed" || status === "error"
|
|
89
|
+
? MOCHA.fail
|
|
90
|
+
: status === "done" || status === "completed"
|
|
91
|
+
? MOCHA.ok
|
|
92
|
+
: shimmerIntensity > 0
|
|
93
|
+
? rgbSeq(
|
|
94
|
+
lerpRgb(SPINNER_BASE_COLOR, SPINNER_SHIMMER, shimmerIntensity),
|
|
95
|
+
)
|
|
96
|
+
: MOCHA.executing;
|
|
83
97
|
return `${targetColor}${SPINNER_FRAMES[idx]}${RESET}`;
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -87,10 +101,13 @@ function heartbeat(status, shimmerIntensity = 0, statusChangedAt = 0, time = Dat
|
|
|
87
101
|
if (status === "failed" || status === "error") return color("✗", MOCHA.fail);
|
|
88
102
|
if (status !== "running") return dim("○");
|
|
89
103
|
const elapsed = time - spinnerStart;
|
|
90
|
-
const idx =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
const idx =
|
|
105
|
+
Math.floor((elapsed / SPINNER_CYCLE_MS) * SPINNER_FRAMES.length) %
|
|
106
|
+
SPINNER_FRAMES.length;
|
|
107
|
+
const c =
|
|
108
|
+
shimmerIntensity > 0
|
|
109
|
+
? lerpRgb(SPINNER_BASE_COLOR, SPINNER_SHIMMER, shimmerIntensity)
|
|
110
|
+
: SPINNER_BASE_COLOR;
|
|
94
111
|
return `${rgbSeq(c)}${SPINNER_FRAMES[idx]}${RESET}`;
|
|
95
112
|
}
|
|
96
113
|
|
|
@@ -107,7 +124,9 @@ function activityWave(tick, count = 4) {
|
|
|
107
124
|
let wave = "";
|
|
108
125
|
for (let i = 0; i < count; i++) {
|
|
109
126
|
const phase = tick * 0.3 + i * 1.5;
|
|
110
|
-
const idx = Math.floor(
|
|
127
|
+
const idx = Math.floor(
|
|
128
|
+
(Math.sin(phase) * 0.5 + 0.5) * (WAVE_CHARS.length - 1),
|
|
129
|
+
);
|
|
111
130
|
wave += WAVE_CHARS[idx];
|
|
112
131
|
}
|
|
113
132
|
return `${MOCHA.executing}${wave}${RESET}`;
|
|
@@ -119,17 +138,33 @@ const DEFAULT_DETAIL_LINES = 10;
|
|
|
119
138
|
const _TIER1_ROWS = 2;
|
|
120
139
|
|
|
121
140
|
const SUMMARY_KEYS = [
|
|
122
|
-
"status",
|
|
123
|
-
"
|
|
141
|
+
"status",
|
|
142
|
+
"lead_action",
|
|
143
|
+
"verdict",
|
|
144
|
+
"files_changed",
|
|
145
|
+
"confidence",
|
|
146
|
+
"risk",
|
|
147
|
+
"detail",
|
|
148
|
+
"error_stage",
|
|
149
|
+
"retryable",
|
|
150
|
+
"partial_output",
|
|
124
151
|
];
|
|
125
152
|
|
|
126
153
|
// ── 레이아웃 브레이크포인트 ──────────────────────────────────────────────
|
|
127
154
|
// 80-119: 28col rail, 120-159: 36col rail, 160+: 균등
|
|
128
155
|
function _resolveRailWidth(totalCols, columnCount) {
|
|
129
156
|
if (columnCount <= 1) return totalCols;
|
|
130
|
-
if (totalCols >= 160)
|
|
131
|
-
|
|
132
|
-
|
|
157
|
+
if (totalCols >= 160)
|
|
158
|
+
return Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount);
|
|
159
|
+
if (totalCols >= 120)
|
|
160
|
+
return Math.min(
|
|
161
|
+
36,
|
|
162
|
+
Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount),
|
|
163
|
+
);
|
|
164
|
+
return Math.min(
|
|
165
|
+
28,
|
|
166
|
+
Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount),
|
|
167
|
+
);
|
|
133
168
|
}
|
|
134
169
|
|
|
135
170
|
function _autoColumnCount(totalCols, workerCount) {
|
|
@@ -145,20 +180,24 @@ function clamp(value, min, max) {
|
|
|
145
180
|
}
|
|
146
181
|
|
|
147
182
|
function stripCodeBlocks(text) {
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
183
|
+
return (
|
|
184
|
+
String(text || "")
|
|
185
|
+
.replace(/\r/g, "")
|
|
186
|
+
// fenced code blocks
|
|
187
|
+
.replace(/```[\s\S]*?(?:```|$)/g, "\n")
|
|
188
|
+
.replace(/^\s*```.*$/gm, "")
|
|
189
|
+
// indented code blocks (4+ spaces or tab at line start)
|
|
190
|
+
.replace(/^(?: {4}|\t).+$/gm, "")
|
|
191
|
+
// shell prompts: PS C:\...>, >, $
|
|
192
|
+
.replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "")
|
|
193
|
+
.trim()
|
|
194
|
+
);
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
function sanitizeTextBlock(text, rawMode = false) {
|
|
161
|
-
const normalized = rawMode
|
|
198
|
+
const normalized = rawMode
|
|
199
|
+
? String(text || "").replace(/\r/g, "")
|
|
200
|
+
: stripCodeBlocks(text);
|
|
162
201
|
return normalized
|
|
163
202
|
.split("\n")
|
|
164
203
|
.map((line) => line.trim())
|
|
@@ -228,26 +267,27 @@ function statusColor(status) {
|
|
|
228
267
|
|
|
229
268
|
// ── MOCHA RGB (gradual fade 보간용) ──
|
|
230
269
|
const MOCHA_RGB = {
|
|
231
|
-
ok:
|
|
232
|
-
partial:
|
|
233
|
-
fail:
|
|
270
|
+
ok: { r: 166, g: 227, b: 161 },
|
|
271
|
+
partial: { r: 250, g: 179, b: 135 },
|
|
272
|
+
fail: { r: 243, g: 139, b: 168 },
|
|
234
273
|
executing: { r: 116, g: 199, b: 236 },
|
|
235
|
-
muted:
|
|
236
|
-
border:
|
|
237
|
-
blue:
|
|
238
|
-
sky:
|
|
239
|
-
yellow:
|
|
240
|
-
peach:
|
|
241
|
-
maroon:
|
|
242
|
-
surface0:
|
|
243
|
-
thinking:
|
|
274
|
+
muted: { r: 147, g: 153, b: 178 },
|
|
275
|
+
border: { r: 69, g: 71, b: 90 },
|
|
276
|
+
blue: { r: 137, g: 180, b: 250 },
|
|
277
|
+
sky: { r: 116, g: 199, b: 236 },
|
|
278
|
+
yellow: { r: 249, g: 226, b: 175 },
|
|
279
|
+
peach: { r: 250, g: 179, b: 135 },
|
|
280
|
+
maroon: { r: 235, g: 160, b: 172 },
|
|
281
|
+
surface0: { r: 49, g: 50, b: 68 },
|
|
282
|
+
thinking: { r: 203, g: 166, b: 247 },
|
|
244
283
|
};
|
|
245
284
|
|
|
246
285
|
function statusToRgb(status) {
|
|
247
286
|
if (status === "ok" || status === "completed") return MOCHA_RGB.ok;
|
|
248
287
|
if (status === "partial") return MOCHA_RGB.partial;
|
|
249
288
|
if (status === "failed") return MOCHA_RGB.fail;
|
|
250
|
-
if (status === "running" || status === "in_progress")
|
|
289
|
+
if (status === "running" || status === "in_progress")
|
|
290
|
+
return MOCHA_RGB.executing;
|
|
251
291
|
return MOCHA_RGB.muted;
|
|
252
292
|
}
|
|
253
293
|
|
|
@@ -288,7 +328,7 @@ function _flashFadeBorderColor(currentStatus, prevStatus, changedAt) {
|
|
|
288
328
|
}
|
|
289
329
|
|
|
290
330
|
function easeOutCubic(t) {
|
|
291
|
-
return 1 - (
|
|
331
|
+
return 1 - (1 - t) ** 3;
|
|
292
332
|
}
|
|
293
333
|
|
|
294
334
|
function borderHighlightPosition(width, bodyLines, time = Date.now()) {
|
|
@@ -299,13 +339,20 @@ function borderHighlightPosition(width, bodyLines, time = Date.now()) {
|
|
|
299
339
|
}
|
|
300
340
|
|
|
301
341
|
function titleFlash(status, changeElapsed) {
|
|
302
|
-
const isCompleted =
|
|
303
|
-
|
|
342
|
+
const isCompleted =
|
|
343
|
+
status === "completed" || status === "done" || status === "ok";
|
|
344
|
+
const isFailed =
|
|
345
|
+
status === "failed" || status === "error" || status === "fail";
|
|
304
346
|
if ((!isCompleted && !isFailed) || changeElapsed > 800) return null;
|
|
305
347
|
const flashRgb = isCompleted ? MOCHA_RGB.ok : MOCHA_RGB.fail;
|
|
306
|
-
const bgRgb =
|
|
307
|
-
|
|
308
|
-
|
|
348
|
+
const bgRgb =
|
|
349
|
+
changeElapsed <= 300
|
|
350
|
+
? flashRgb
|
|
351
|
+
: lerpRgb(
|
|
352
|
+
flashRgb,
|
|
353
|
+
MOCHA_RGB.surface0,
|
|
354
|
+
clamp((changeElapsed - 300) / 500, 0, 1),
|
|
355
|
+
);
|
|
309
356
|
return rgbSeq(bgRgb, 48);
|
|
310
357
|
}
|
|
311
358
|
|
|
@@ -334,9 +381,18 @@ function wrapLine(text, width) {
|
|
|
334
381
|
let current = "";
|
|
335
382
|
for (const word of words) {
|
|
336
383
|
const candidate = current ? `${current} ${word}` : word;
|
|
337
|
-
if (wcswidth(candidate) <= limit) {
|
|
338
|
-
|
|
339
|
-
|
|
384
|
+
if (wcswidth(candidate) <= limit) {
|
|
385
|
+
current = candidate;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (current) {
|
|
389
|
+
lines.push(current);
|
|
390
|
+
current = "";
|
|
391
|
+
}
|
|
392
|
+
if (wcswidth(word) <= limit) {
|
|
393
|
+
current = word;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
340
396
|
let offset = 0;
|
|
341
397
|
while (offset < word.length) {
|
|
342
398
|
lines.push(word.slice(offset, offset + limit));
|
|
@@ -347,20 +403,34 @@ function wrapLine(text, width) {
|
|
|
347
403
|
return lines.length > 0 ? lines : [source.slice(0, limit)];
|
|
348
404
|
}
|
|
349
405
|
|
|
350
|
-
function _wrapText(
|
|
406
|
+
function _wrapText(
|
|
407
|
+
text,
|
|
408
|
+
width,
|
|
409
|
+
maxLines = DEFAULT_DETAIL_LINES,
|
|
410
|
+
rawMode = false,
|
|
411
|
+
) {
|
|
351
412
|
if (maxLines <= 0) return [];
|
|
352
413
|
const input = sanitizeTextBlock(text, rawMode);
|
|
353
414
|
if (!input) return [];
|
|
354
|
-
const wrapped = input
|
|
415
|
+
const wrapped = input
|
|
416
|
+
.split("\n")
|
|
417
|
+
.flatMap((line) => wrapLine(line, width))
|
|
418
|
+
.filter(Boolean);
|
|
355
419
|
if (wrapped.length <= maxLines) return wrapped;
|
|
356
|
-
return [
|
|
420
|
+
return [
|
|
421
|
+
...wrapped.slice(0, maxLines - 1),
|
|
422
|
+
truncate(wrapped[wrapped.length - 1], width),
|
|
423
|
+
];
|
|
357
424
|
}
|
|
358
425
|
|
|
359
426
|
// 스크롤 없이 전체 줄 반환 (focus pane용)
|
|
360
427
|
function wrapTextAll(text, width, rawMode = false) {
|
|
361
428
|
const input = sanitizeTextBlock(text, rawMode);
|
|
362
429
|
if (!input) return [];
|
|
363
|
-
return input
|
|
430
|
+
return input
|
|
431
|
+
.split("\n")
|
|
432
|
+
.flatMap((line) => wrapLine(line, width))
|
|
433
|
+
.filter(Boolean);
|
|
364
434
|
}
|
|
365
435
|
|
|
366
436
|
// ── virtual row buffer ────────────────────────────────────────────────────
|
|
@@ -388,13 +458,20 @@ class RowBuffer {
|
|
|
388
458
|
this._prev = [...this._rows];
|
|
389
459
|
}
|
|
390
460
|
|
|
391
|
-
get rows() {
|
|
392
|
-
|
|
461
|
+
get rows() {
|
|
462
|
+
return this._rows;
|
|
463
|
+
}
|
|
464
|
+
get prevLen() {
|
|
465
|
+
return this._prev.length;
|
|
466
|
+
}
|
|
393
467
|
}
|
|
394
468
|
|
|
395
469
|
// ── 상태 집계 ─────────────────────────────────────────────────────────────
|
|
396
470
|
function countStatuses(names, workers) {
|
|
397
|
-
let ok = 0,
|
|
471
|
+
let ok = 0,
|
|
472
|
+
partial = 0,
|
|
473
|
+
failed = 0,
|
|
474
|
+
running = 0;
|
|
398
475
|
for (const name of names) {
|
|
399
476
|
const st = workers.get(name);
|
|
400
477
|
const s = runtimeStatus(st);
|
|
@@ -409,25 +486,40 @@ function countStatuses(names, workers) {
|
|
|
409
486
|
// ── Tier1: 상단 고정 1행 ─────────────────────────────────────────────────
|
|
410
487
|
function phaseColor(phase, time = Date.now()) {
|
|
411
488
|
const shimmer = currentShimmer(time);
|
|
412
|
-
if (phase === "exec" || phase === "executing")
|
|
413
|
-
|
|
414
|
-
if (phase === "
|
|
489
|
+
if (phase === "exec" || phase === "executing")
|
|
490
|
+
return rgbSeq(lerpRgb(MOCHA_RGB.blue, MOCHA_RGB.sky, shimmer));
|
|
491
|
+
if (phase === "verify" || phase === "verifying")
|
|
492
|
+
return rgbSeq(lerpRgb(MOCHA_RGB.yellow, MOCHA_RGB.peach, shimmer));
|
|
493
|
+
if (phase === "fix" || phase === "fixing")
|
|
494
|
+
return rgbSeq(lerpRgb(MOCHA_RGB.fail, MOCHA_RGB.maroon, shimmer));
|
|
415
495
|
return FG.accent;
|
|
416
496
|
}
|
|
417
497
|
|
|
418
|
-
function buildTier1(
|
|
498
|
+
function buildTier1(
|
|
499
|
+
names,
|
|
500
|
+
workers,
|
|
501
|
+
pipeline,
|
|
502
|
+
elapsed,
|
|
503
|
+
width,
|
|
504
|
+
version,
|
|
505
|
+
time = Date.now(),
|
|
506
|
+
) {
|
|
419
507
|
const { ok, partial, failed, running } = countStatuses(names, workers);
|
|
420
508
|
const phase = pipeline.phase || "exec";
|
|
421
509
|
const row1 = truncate(
|
|
422
510
|
`${color("▲", FG.triflux)} v${version} ${dim("│")} ${color(phase, phaseColor(phase, time))} ${dim("│")} ${elapsed}s ${dim("│")} ` +
|
|
423
|
-
|
|
511
|
+
`${color(`✓${ok}`, MOCHA.ok)} ${color(`◑${partial}`, MOCHA.partial)} ${color(`✗${failed}`, MOCHA.fail)} ${dim(`▶${running}`)}${running > 0 ? ` ${activityWave(spinnerTick)}` : ""}`,
|
|
424
512
|
width,
|
|
425
513
|
);
|
|
426
|
-
const keysHint = color(
|
|
514
|
+
const keysHint = color(
|
|
515
|
+
"Tab:focus • j/k/↑↓:nav • f:follow • r:raw • l:tab • n:recent • 1-9:jump",
|
|
516
|
+
MOCHA.subtext,
|
|
517
|
+
);
|
|
427
518
|
const hintWidth = wcswidth(stripAnsi(keysHint));
|
|
428
|
-
const row2 =
|
|
429
|
-
|
|
430
|
-
|
|
519
|
+
const row2 =
|
|
520
|
+
hintWidth >= width
|
|
521
|
+
? truncate(keysHint, width)
|
|
522
|
+
: padRight(`${" ".repeat(width - hintWidth)}${keysHint}`, width);
|
|
431
523
|
return [row1, row2];
|
|
432
524
|
}
|
|
433
525
|
|
|
@@ -437,7 +529,8 @@ function detailText(st) {
|
|
|
437
529
|
const lines = [];
|
|
438
530
|
for (const key of SUMMARY_KEYS) {
|
|
439
531
|
const value = st.handoff?.[key];
|
|
440
|
-
if (Array.isArray(value) && value.length > 0)
|
|
532
|
+
if (Array.isArray(value) && value.length > 0)
|
|
533
|
+
lines.push(`${key}: ${value.join(", ")}`);
|
|
441
534
|
else if (value) lines.push(`${key}: ${value}`);
|
|
442
535
|
}
|
|
443
536
|
if (st.snapshot) lines.unshift(st.snapshot);
|
|
@@ -452,7 +545,10 @@ function detailHighlights(st) {
|
|
|
452
545
|
.map((line) => line.replace(/^verdict\s*:\s*/i, "").trim())
|
|
453
546
|
.filter(Boolean)
|
|
454
547
|
.filter((line) => line !== verdict)
|
|
455
|
-
.filter(
|
|
548
|
+
.filter(
|
|
549
|
+
(line) =>
|
|
550
|
+
!SUMMARY_KEYS.some((key) => line.toLowerCase().startsWith(`${key}:`)),
|
|
551
|
+
)
|
|
456
552
|
.slice(0, 2);
|
|
457
553
|
}
|
|
458
554
|
|
|
@@ -460,7 +556,7 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
460
556
|
const {
|
|
461
557
|
width,
|
|
462
558
|
selected = false,
|
|
463
|
-
focused = false,
|
|
559
|
+
focused = false, // rail 포커스 여부
|
|
464
560
|
previousSelected = false,
|
|
465
561
|
_rawMode = false,
|
|
466
562
|
compact = false,
|
|
@@ -471,19 +567,29 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
471
567
|
const role = sanitizeOneLine(st.role);
|
|
472
568
|
const status = runtimeStatus(st);
|
|
473
569
|
const sec = Number.isFinite(st._logSec) ? st._logSec : 0;
|
|
474
|
-
const changeElapsed = st._statusChangedAt
|
|
570
|
+
const changeElapsed = st._statusChangedAt
|
|
571
|
+
? Math.max(0, time - st._statusChangedAt)
|
|
572
|
+
: Number.POSITIVE_INFINITY;
|
|
475
573
|
|
|
476
574
|
// Tier2 행 1: 이름 + CLI + role
|
|
477
575
|
const selMark = selected
|
|
478
|
-
?
|
|
576
|
+
? focused
|
|
577
|
+
? color("▶", MOCHA.blue)
|
|
578
|
+
: color(">", FG.triflux)
|
|
479
579
|
: previousSelected
|
|
480
580
|
? dim("~")
|
|
481
581
|
: " ";
|
|
482
|
-
const hb = heartbeat(
|
|
582
|
+
const hb = heartbeat(
|
|
583
|
+
status,
|
|
584
|
+
status === "running" ? currentShimmer(time) : 0,
|
|
585
|
+
st._statusChangedAt,
|
|
586
|
+
time,
|
|
587
|
+
);
|
|
483
588
|
// host 배지 (원격 워커용)
|
|
484
|
-
const hostBadge =
|
|
485
|
-
|
|
486
|
-
|
|
589
|
+
const hostBadge =
|
|
590
|
+
st.host && st.host !== "local"
|
|
591
|
+
? color(`[${st.host}]`, MOCHA.mauve) + " "
|
|
592
|
+
: "";
|
|
487
593
|
const displayRole = dedupeRole(role, name, cli);
|
|
488
594
|
const title = truncate(
|
|
489
595
|
`${selMark} ${hb} ${hostBadge}${color(name, FG.triflux)} ${color("•", MOCHA.overlay)} ${color(cli, cliColor(cli))}${displayRole ? ` ${color(`(${displayRole})`, MOCHA.overlay)}` : ""}`,
|
|
@@ -491,7 +597,9 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
491
597
|
);
|
|
492
598
|
|
|
493
599
|
const cardWidth = Math.max(MIN_CARD_WIDTH, width);
|
|
494
|
-
const borderHighlight = focused
|
|
600
|
+
const borderHighlight = focused
|
|
601
|
+
? borderHighlightPosition(cardWidth, compact ? 2 : 6, time)
|
|
602
|
+
: undefined;
|
|
495
603
|
const titleFlashBg = titleFlash(status, changeElapsed);
|
|
496
604
|
|
|
497
605
|
// status-specific border: focused→mauve, selected→bright, non-selected→glow decay
|
|
@@ -502,37 +610,58 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
502
610
|
}
|
|
503
611
|
if (selected) return statusColor(status);
|
|
504
612
|
const from = statusToRgb(status);
|
|
505
|
-
const decayBase = st._statusChangedAt
|
|
613
|
+
const decayBase = st._statusChangedAt
|
|
614
|
+
? clamp(changeElapsed / CARD_GLOW_MS, 0, 1)
|
|
615
|
+
: 1;
|
|
506
616
|
const decayT = easeOutCubic(decayBase);
|
|
507
|
-
return rgbSeq(lerpRgb(from, MOCHA_RGB.border, 0.5 +
|
|
617
|
+
return rgbSeq(lerpRgb(from, MOCHA_RGB.border, 0.5 + 0.5 * decayT));
|
|
508
618
|
})();
|
|
509
619
|
|
|
510
620
|
if (compact) {
|
|
511
621
|
// compact 2-line 카드
|
|
512
|
-
const progress = Number.isFinite(st.progress)
|
|
622
|
+
const progress = Number.isFinite(st.progress)
|
|
623
|
+
? clamp(st.progress, 0, 1)
|
|
624
|
+
: status === "running"
|
|
625
|
+
? 0.3
|
|
626
|
+
: 1;
|
|
513
627
|
const percent = Math.round(progress * 100);
|
|
514
628
|
const compactLine1 = truncate(
|
|
515
629
|
`${selMark} ${hb} ${hostBadge}${color(name, FG.triflux)} ${dim("•")} ${color(cli, cliColor(cli))} ${statusBadge(status)} ${String(percent).padStart(3)}%`,
|
|
516
630
|
innerWidth,
|
|
517
631
|
);
|
|
518
|
-
const verdict = sanitizeOneLine(
|
|
632
|
+
const verdict = sanitizeOneLine(
|
|
633
|
+
st.handoff?.verdict || st.summary || st.snapshot,
|
|
634
|
+
status,
|
|
635
|
+
);
|
|
519
636
|
const compactLine2 = truncate(color(verdict, MOCHA.text), innerWidth);
|
|
520
|
-
const framed = box(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
637
|
+
const framed = box(
|
|
638
|
+
[compactLine1, compactLine2],
|
|
639
|
+
cardWidth,
|
|
640
|
+
statusBorderColor,
|
|
641
|
+
{
|
|
642
|
+
highlightPos: borderHighlight,
|
|
643
|
+
titleFlashBg,
|
|
644
|
+
},
|
|
645
|
+
);
|
|
524
646
|
return [framed.top, ...framed.body, framed.bot];
|
|
525
647
|
}
|
|
526
648
|
|
|
527
649
|
// Tier2 행 2: 상태 배지 + elapsed + tokens + conf
|
|
528
|
-
const confidence = sanitizeOneLine(
|
|
650
|
+
const confidence = sanitizeOneLine(
|
|
651
|
+
st.handoff?.confidence || st.confidence,
|
|
652
|
+
"n/a",
|
|
653
|
+
);
|
|
529
654
|
const statusLine = truncate(
|
|
530
655
|
`${statusBadge(status)} ${color("•", MOCHA.overlay)} ${color(`${sec}s`, MOCHA.subtext)} ${color("•", MOCHA.overlay)} ${color(`tok ${formatTokens(st.tokens)}`, MOCHA.subtext)} ${color("•", MOCHA.overlay)} ${color(`conf ${confidence}`, MOCHA.subtext)}`,
|
|
531
656
|
innerWidth,
|
|
532
657
|
);
|
|
533
658
|
|
|
534
659
|
// Tier2 행 3: progress bar
|
|
535
|
-
const progress = Number.isFinite(st.progress)
|
|
660
|
+
const progress = Number.isFinite(st.progress)
|
|
661
|
+
? clamp(st.progress, 0, 1)
|
|
662
|
+
: status === "running"
|
|
663
|
+
? 0.3
|
|
664
|
+
: 1;
|
|
536
665
|
const percent = Math.round(progress * 100);
|
|
537
666
|
const barWidth = clamp(Math.floor(innerWidth * 0.3), 8, 16);
|
|
538
667
|
const bar = progressBar(percent, barWidth, time);
|
|
@@ -542,18 +671,33 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
542
671
|
);
|
|
543
672
|
|
|
544
673
|
// Tier2 행 4-6: verdict / findings / files
|
|
545
|
-
const verdict = sanitizeOneLine(
|
|
546
|
-
|
|
547
|
-
|
|
674
|
+
const verdict = sanitizeOneLine(
|
|
675
|
+
st.handoff?.verdict || st.summary || st.snapshot,
|
|
676
|
+
status,
|
|
677
|
+
);
|
|
678
|
+
const findings =
|
|
679
|
+
detailHighlights(st).join(" / ") || "no notable findings yet";
|
|
680
|
+
const files =
|
|
681
|
+
sanitizeFiles(st.handoff?.files_changed || st.files_changed).join(", ") ||
|
|
682
|
+
"none";
|
|
548
683
|
|
|
549
684
|
const verdictClr = statusColor(status);
|
|
550
685
|
const lines = [
|
|
551
686
|
title,
|
|
552
687
|
statusLine,
|
|
553
688
|
progressLine,
|
|
554
|
-
truncate(
|
|
555
|
-
|
|
556
|
-
|
|
689
|
+
truncate(
|
|
690
|
+
`${color("verdict", MOCHA.overlay)} ${color(verdict, verdictClr)}`,
|
|
691
|
+
innerWidth,
|
|
692
|
+
),
|
|
693
|
+
truncate(
|
|
694
|
+
`${color("findings", MOCHA.overlay)} ${color(findings, MOCHA.subtext)}`,
|
|
695
|
+
innerWidth,
|
|
696
|
+
),
|
|
697
|
+
truncate(
|
|
698
|
+
`${color("files", MOCHA.overlay)} ${color(files, MOCHA.subtext)}`,
|
|
699
|
+
innerWidth,
|
|
700
|
+
),
|
|
557
701
|
];
|
|
558
702
|
|
|
559
703
|
const framed = box(lines, cardWidth, statusBorderColor, {
|
|
@@ -577,23 +721,47 @@ function buildFocusPane(name, st, opts = {}) {
|
|
|
577
721
|
const innerWidth = Math.max(12, width - 4);
|
|
578
722
|
|
|
579
723
|
// verdict sticky 4행
|
|
580
|
-
const verdict = sanitizeOneLine(
|
|
581
|
-
|
|
724
|
+
const verdict = sanitizeOneLine(
|
|
725
|
+
st.handoff?.verdict || st.summary || st.snapshot,
|
|
726
|
+
"—",
|
|
727
|
+
);
|
|
728
|
+
const confidence = sanitizeOneLine(
|
|
729
|
+
st.handoff?.confidence || st.confidence,
|
|
730
|
+
"n/a",
|
|
731
|
+
);
|
|
582
732
|
const files = sanitizeFiles(st.handoff?.files_changed || st.files_changed);
|
|
583
733
|
const status = runtimeStatus(st);
|
|
584
734
|
|
|
585
735
|
// Tab bar: 활성 탭은 MOCHA.blue + bold, 비활성은 MOCHA.overlay
|
|
586
736
|
const activeTab = opts.activeTab || "log";
|
|
587
|
-
const tabLog =
|
|
588
|
-
|
|
589
|
-
|
|
737
|
+
const tabLog =
|
|
738
|
+
activeTab === "log"
|
|
739
|
+
? `${MOCHA.blue}${bold("[Log]")}`
|
|
740
|
+
: color("[Log]", MOCHA.overlay);
|
|
741
|
+
const tabDetail =
|
|
742
|
+
activeTab === "detail"
|
|
743
|
+
? `${MOCHA.blue}${bold("[Detail]")}`
|
|
744
|
+
: color("[Detail]", MOCHA.overlay);
|
|
745
|
+
const tabFiles =
|
|
746
|
+
activeTab === "files"
|
|
747
|
+
? `${MOCHA.blue}${bold(`[Files ${files.length}]`)}`
|
|
748
|
+
: color(`[Files ${files.length}]`, MOCHA.overlay);
|
|
590
749
|
const tabBar = truncate(`${tabLog} ${tabDetail} ${tabFiles}`, innerWidth);
|
|
591
750
|
|
|
592
751
|
const stickyLines = [
|
|
593
|
-
truncate(
|
|
752
|
+
truncate(
|
|
753
|
+
`${color(name, FG.triflux)} ${color("•", MOCHA.overlay)} ${statusBadge(status)}`,
|
|
754
|
+
innerWidth,
|
|
755
|
+
),
|
|
594
756
|
tabBar,
|
|
595
|
-
truncate(
|
|
596
|
-
|
|
757
|
+
truncate(
|
|
758
|
+
`${color("verdict", MOCHA.overlay)} ${color(verdict, statusColor(status))}`,
|
|
759
|
+
innerWidth,
|
|
760
|
+
),
|
|
761
|
+
truncate(
|
|
762
|
+
`${color("conf", MOCHA.overlay)} ${color(confidence, MOCHA.text)}`,
|
|
763
|
+
innerWidth,
|
|
764
|
+
),
|
|
597
765
|
color("─", MOCHA.surface0).repeat(Math.max(4, innerWidth)),
|
|
598
766
|
];
|
|
599
767
|
|
|
@@ -605,17 +773,22 @@ function buildFocusPane(name, st, opts = {}) {
|
|
|
605
773
|
const summaryLines = [];
|
|
606
774
|
for (const key of SUMMARY_KEYS) {
|
|
607
775
|
const value = st.handoff?.[key];
|
|
608
|
-
if (Array.isArray(value) && value.length > 0)
|
|
776
|
+
if (Array.isArray(value) && value.length > 0)
|
|
777
|
+
summaryLines.push(`${key}: ${value.join(", ")}`);
|
|
609
778
|
else if (value) summaryLines.push(`${key}: ${value}`);
|
|
610
779
|
}
|
|
611
|
-
allBodyLines =
|
|
612
|
-
|
|
613
|
-
|
|
780
|
+
allBodyLines =
|
|
781
|
+
summaryLines.length > 0
|
|
782
|
+
? summaryLines.flatMap((l) => wrapLine(l, innerWidth))
|
|
783
|
+
: [dim("no structured data")];
|
|
614
784
|
} else if (activeTab === "files") {
|
|
615
|
-
const filesList = sanitizeFiles(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
785
|
+
const filesList = sanitizeFiles(
|
|
786
|
+
st.handoff?.files_changed || st.files_changed,
|
|
787
|
+
);
|
|
788
|
+
allBodyLines =
|
|
789
|
+
filesList.length > 0
|
|
790
|
+
? filesList.map((f, i) => `${i + 1}. ${f}`)
|
|
791
|
+
: [dim("no files changed")];
|
|
619
792
|
} else {
|
|
620
793
|
allBodyLines = wrapTextAll(detailText(st), innerWidth, rawMode);
|
|
621
794
|
}
|
|
@@ -624,16 +797,24 @@ function buildFocusPane(name, st, opts = {}) {
|
|
|
624
797
|
if (followTail) {
|
|
625
798
|
startIdx = Math.max(0, allBodyLines.length - bodyAvail);
|
|
626
799
|
} else {
|
|
627
|
-
startIdx = clamp(
|
|
800
|
+
startIdx = clamp(
|
|
801
|
+
scrollOffset,
|
|
802
|
+
0,
|
|
803
|
+
Math.max(0, allBodyLines.length - bodyAvail),
|
|
804
|
+
);
|
|
628
805
|
}
|
|
629
806
|
|
|
630
807
|
const bodySlice = allBodyLines.slice(startIdx, startIdx + bodyAvail);
|
|
631
808
|
if (bodySlice.length === 0) bodySlice.push(dim("no detail available"));
|
|
632
809
|
|
|
633
810
|
// scroll indicator — MOCHA.overlay for position
|
|
634
|
-
const scrollInfo =
|
|
635
|
-
|
|
636
|
-
|
|
811
|
+
const scrollInfo =
|
|
812
|
+
allBodyLines.length > bodyAvail
|
|
813
|
+
? color(
|
|
814
|
+
`${startIdx + 1}-${Math.min(startIdx + bodyAvail, allBodyLines.length)}/${allBodyLines.length}`,
|
|
815
|
+
MOCHA.overlay,
|
|
816
|
+
)
|
|
817
|
+
: color(`${allBodyLines.length} lines`, MOCHA.overlay);
|
|
637
818
|
|
|
638
819
|
const contentLines = [
|
|
639
820
|
...stickyLines,
|
|
@@ -647,23 +828,49 @@ function buildFocusPane(name, st, opts = {}) {
|
|
|
647
828
|
: MOCHA.border;
|
|
648
829
|
const paneWidth = Math.max(MIN_CARD_WIDTH, width);
|
|
649
830
|
const framed = box(contentLines, paneWidth, borderColor, {
|
|
650
|
-
highlightPos: focused
|
|
831
|
+
highlightPos: focused
|
|
832
|
+
? borderHighlightPosition(paneWidth, contentLines.length, time)
|
|
833
|
+
: undefined,
|
|
651
834
|
});
|
|
652
835
|
return [framed.top, ...framed.body, framed.bot];
|
|
653
836
|
}
|
|
654
837
|
|
|
655
838
|
// ── summary bar (≥4 workers) ──────────────────────────────────────────────
|
|
656
|
-
function buildSummaryBar(
|
|
657
|
-
|
|
839
|
+
function buildSummaryBar(
|
|
840
|
+
names,
|
|
841
|
+
workers,
|
|
842
|
+
selectedWorker,
|
|
843
|
+
pipeline,
|
|
844
|
+
width,
|
|
845
|
+
version,
|
|
846
|
+
) {
|
|
847
|
+
const maxChipWidth = clamp(
|
|
848
|
+
Math.floor((width - 6) / Math.min(names.length, 4)),
|
|
849
|
+
16,
|
|
850
|
+
26,
|
|
851
|
+
);
|
|
658
852
|
const chips = names.map((name, idx) => {
|
|
659
853
|
const st = workers.get(name);
|
|
660
854
|
const status = runtimeStatus(st);
|
|
661
|
-
const progress = Number.isFinite(st.progress)
|
|
855
|
+
const progress = Number.isFinite(st.progress)
|
|
856
|
+
? clamp(st.progress, 0, 1)
|
|
857
|
+
: status === "running"
|
|
858
|
+
? 0.3
|
|
859
|
+
: 1;
|
|
662
860
|
const label = `${selectedWorker === name ? ">" : " "} ${idx + 1}.${name} ${status} ${Math.round(progress * 100)}%`;
|
|
663
861
|
return padRight(truncate(label, maxChipWidth), maxChipWidth);
|
|
664
862
|
});
|
|
665
|
-
const chipsLine = truncate(
|
|
666
|
-
|
|
863
|
+
const chipsLine = truncate(
|
|
864
|
+
chips.join(color(" │ ", MOCHA.overlay)),
|
|
865
|
+
width - 4,
|
|
866
|
+
);
|
|
867
|
+
const keysLine = truncate(
|
|
868
|
+
color(
|
|
869
|
+
"Tab:focus • j/k/↑↓:nav • f:follow • r:raw • l:tab • n:recent • 1-9:jump",
|
|
870
|
+
MOCHA.subtext,
|
|
871
|
+
),
|
|
872
|
+
width - 4,
|
|
873
|
+
);
|
|
667
874
|
const framed = box([chipsLine, keysLine], width);
|
|
668
875
|
return [framed.top, ...framed.body, framed.bot];
|
|
669
876
|
}
|
|
@@ -696,7 +903,9 @@ function buildHelpOverlay(width, height) {
|
|
|
696
903
|
const framed = box(helpLines, innerWidth + 4, MOCHA.blue);
|
|
697
904
|
const framedRows = [framed.top, ...framed.body, framed.bot];
|
|
698
905
|
const topPad = Math.max(0, Math.floor((height - framedRows.length) / 2));
|
|
699
|
-
const leftPad = " ".repeat(
|
|
906
|
+
const leftPad = " ".repeat(
|
|
907
|
+
Math.max(0, Math.floor((width - innerWidth - 4) / 2)),
|
|
908
|
+
);
|
|
700
909
|
const result = [];
|
|
701
910
|
for (let i = 0; i < height; i++) {
|
|
702
911
|
const fi = i - topPad;
|
|
@@ -714,51 +923,96 @@ function _joinColumns(blocks, gap = GRID_GAP) {
|
|
|
714
923
|
const maxHeight = Math.max(...blocks.map((b) => b.length));
|
|
715
924
|
return Array.from({ length: maxHeight }, (_, rowIdx) =>
|
|
716
925
|
blocks
|
|
717
|
-
.map(
|
|
926
|
+
.map(
|
|
927
|
+
(block) =>
|
|
928
|
+
block[rowIdx] || " ".repeat(wcswidth(stripAnsi(block[0] || ""))),
|
|
929
|
+
)
|
|
718
930
|
.join(" ".repeat(gap)),
|
|
719
931
|
);
|
|
720
932
|
}
|
|
721
933
|
|
|
722
934
|
// ── normalizeWorkerState ──────────────────────────────────────────────────
|
|
723
935
|
function normalizeWorkerState(existing, state) {
|
|
724
|
-
const nextHandoff =
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
:
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
936
|
+
const nextHandoff =
|
|
937
|
+
state.handoff === undefined
|
|
938
|
+
? existing.handoff
|
|
939
|
+
: {
|
|
940
|
+
...(existing.handoff || {}),
|
|
941
|
+
...(state.handoff || {}),
|
|
942
|
+
verdict:
|
|
943
|
+
state.handoff?.verdict !== undefined
|
|
944
|
+
? sanitizeOneLine(state.handoff.verdict)
|
|
945
|
+
: existing.handoff?.verdict,
|
|
946
|
+
files_changed:
|
|
947
|
+
state.handoff?.files_changed !== undefined
|
|
948
|
+
? sanitizeFiles(state.handoff.files_changed)
|
|
949
|
+
: existing.handoff?.files_changed,
|
|
950
|
+
confidence:
|
|
951
|
+
state.handoff?.confidence !== undefined
|
|
952
|
+
? sanitizeOneLine(state.handoff.confidence)
|
|
953
|
+
: existing.handoff?.confidence,
|
|
954
|
+
status:
|
|
955
|
+
state.handoff?.status !== undefined
|
|
956
|
+
? sanitizeOneLine(state.handoff.status)
|
|
957
|
+
: existing.handoff?.status,
|
|
958
|
+
};
|
|
742
959
|
|
|
743
960
|
return {
|
|
744
961
|
...existing,
|
|
745
962
|
...state,
|
|
746
|
-
cli:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
963
|
+
cli:
|
|
964
|
+
state.cli !== undefined
|
|
965
|
+
? sanitizeOneLine(state.cli, existing.cli || "codex")
|
|
966
|
+
: existing.cli || "codex",
|
|
967
|
+
role:
|
|
968
|
+
state.role !== undefined ? sanitizeOneLine(state.role) : existing.role,
|
|
969
|
+
status:
|
|
970
|
+
state.status !== undefined
|
|
971
|
+
? sanitizeOneLine(state.status, existing.status || "pending")
|
|
972
|
+
: existing.status || "pending",
|
|
973
|
+
snapshot:
|
|
974
|
+
state.snapshot !== undefined
|
|
975
|
+
? sanitizeTextBlock(state.snapshot)
|
|
976
|
+
: existing.snapshot,
|
|
977
|
+
summary:
|
|
978
|
+
state.summary !== undefined
|
|
979
|
+
? sanitizeTextBlock(state.summary)
|
|
980
|
+
: existing.summary,
|
|
981
|
+
detail:
|
|
982
|
+
state.detail !== undefined
|
|
983
|
+
? sanitizeTextBlock(state.detail)
|
|
984
|
+
: existing.detail,
|
|
985
|
+
findings:
|
|
986
|
+
state.findings !== undefined
|
|
987
|
+
? sanitizeFindings(state.findings)
|
|
988
|
+
: existing.findings,
|
|
989
|
+
files_changed:
|
|
990
|
+
state.files_changed !== undefined
|
|
991
|
+
? sanitizeFiles(state.files_changed)
|
|
992
|
+
: existing.files_changed,
|
|
993
|
+
confidence:
|
|
994
|
+
state.confidence !== undefined
|
|
995
|
+
? sanitizeOneLine(state.confidence)
|
|
996
|
+
: existing.confidence,
|
|
997
|
+
tokens:
|
|
998
|
+
state.tokens !== undefined
|
|
999
|
+
? normalizeTokens(state.tokens)
|
|
1000
|
+
: existing.tokens,
|
|
1001
|
+
progress:
|
|
1002
|
+
state.progress !== undefined
|
|
1003
|
+
? clamp(Number(state.progress) || 0, 0, 1)
|
|
1004
|
+
: existing.progress,
|
|
757
1005
|
handoff: nextHandoff,
|
|
758
|
-
_prevStatus:
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1006
|
+
_prevStatus:
|
|
1007
|
+
state.status !== undefined &&
|
|
1008
|
+
sanitizeOneLine(state.status) !== existing.status
|
|
1009
|
+
? existing.status
|
|
1010
|
+
: existing._prevStatus,
|
|
1011
|
+
_statusChangedAt:
|
|
1012
|
+
state.status !== undefined &&
|
|
1013
|
+
sanitizeOneLine(state.status) !== existing.status
|
|
1014
|
+
? Date.now()
|
|
1015
|
+
: existing._statusChangedAt || 0,
|
|
762
1016
|
};
|
|
763
1017
|
}
|
|
764
1018
|
|
|
@@ -822,16 +1076,20 @@ export function createLogDashboard(opts = {}) {
|
|
|
822
1076
|
function getViewportColumns() {
|
|
823
1077
|
const v = Number.isFinite(columns)
|
|
824
1078
|
? columns
|
|
825
|
-
:
|
|
826
|
-
|
|
827
|
-
|
|
1079
|
+
: Number.isFinite(stream?.columns)
|
|
1080
|
+
? stream.columns
|
|
1081
|
+
: Number.isFinite(process.stdout?.columns)
|
|
1082
|
+
? process.stdout.columns
|
|
1083
|
+
: FALLBACK_COLUMNS;
|
|
828
1084
|
return Math.max(48, v || FALLBACK_COLUMNS);
|
|
829
1085
|
}
|
|
830
1086
|
|
|
831
1087
|
function getViewportRows() {
|
|
832
1088
|
const v = Number.isFinite(stream?.rows)
|
|
833
1089
|
? stream.rows
|
|
834
|
-
:
|
|
1090
|
+
: Number.isFinite(process.stdout?.rows)
|
|
1091
|
+
? process.stdout.rows
|
|
1092
|
+
: FALLBACK_ROWS;
|
|
835
1093
|
return Math.max(10, v || FALLBACK_ROWS);
|
|
836
1094
|
}
|
|
837
1095
|
|
|
@@ -840,8 +1098,12 @@ export function createLogDashboard(opts = {}) {
|
|
|
840
1098
|
}
|
|
841
1099
|
|
|
842
1100
|
function ensureSelectedWorker(names) {
|
|
843
|
-
if (names.length === 0) {
|
|
844
|
-
|
|
1101
|
+
if (names.length === 0) {
|
|
1102
|
+
selectedWorker = null;
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (!selectedWorker || !workers.has(selectedWorker))
|
|
1106
|
+
selectedWorker = names[0];
|
|
845
1107
|
}
|
|
846
1108
|
|
|
847
1109
|
function setSelectedWorker(nextWorker, { preserveTrail = true } = {}) {
|
|
@@ -885,8 +1147,10 @@ export function createLogDashboard(opts = {}) {
|
|
|
885
1147
|
function doClose() {
|
|
886
1148
|
if (closed) return;
|
|
887
1149
|
if (timer) clearInterval(timer);
|
|
888
|
-
if (inputAttached && typeof input?.off === "function")
|
|
889
|
-
|
|
1150
|
+
if (inputAttached && typeof input?.off === "function")
|
|
1151
|
+
input.off("data", handleInput);
|
|
1152
|
+
if (rawModeEnabled && typeof input?.setRawMode === "function")
|
|
1153
|
+
input.setRawMode(false);
|
|
890
1154
|
if (inputAttached && typeof input?.pause === "function") input.pause();
|
|
891
1155
|
exitAltScreen();
|
|
892
1156
|
closed = true;
|
|
@@ -923,33 +1187,84 @@ export function createLogDashboard(opts = {}) {
|
|
|
923
1187
|
}
|
|
924
1188
|
|
|
925
1189
|
// Shift+Arrow: 포커스 이동 + 워커 선택
|
|
926
|
-
if (key === "\x1b[1;2A") {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1190
|
+
if (key === "\x1b[1;2A") {
|
|
1191
|
+
selectRelative(-1);
|
|
1192
|
+
return;
|
|
1193
|
+
} // Shift+Up → 워커 위
|
|
1194
|
+
if (key === "\x1b[1;2B") {
|
|
1195
|
+
selectRelative(1);
|
|
1196
|
+
return;
|
|
1197
|
+
} // Shift+Down → 워커 아래
|
|
1198
|
+
if (key === "\x1b[1;2D") {
|
|
1199
|
+
focus = "rail";
|
|
1200
|
+
render();
|
|
1201
|
+
return;
|
|
1202
|
+
} // Shift+Left → rail
|
|
1203
|
+
if (key === "\x1b[1;2C") {
|
|
1204
|
+
focus = "detail";
|
|
1205
|
+
render();
|
|
1206
|
+
return;
|
|
1207
|
+
} // Shift+Right → detail
|
|
930
1208
|
|
|
931
1209
|
if (focus === "detail") {
|
|
932
1210
|
// detail 포커스: j/k/ArrowDown/Up = 스크롤
|
|
933
|
-
if (key === "j" || key === "\u001b[B") {
|
|
934
|
-
|
|
1211
|
+
if (key === "j" || key === "\u001b[B") {
|
|
1212
|
+
scrollDetail(1);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (key === "k" || key === "\u001b[A") {
|
|
1216
|
+
scrollDetail(-1);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
935
1219
|
} else {
|
|
936
1220
|
// rail 포커스: j/k = 워커 선택
|
|
937
|
-
if (key === "j" || key === "\u001b[B") {
|
|
938
|
-
|
|
1221
|
+
if (key === "j" || key === "\u001b[B") {
|
|
1222
|
+
selectRelative(1);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (key === "k" || key === "\u001b[A") {
|
|
1226
|
+
selectRelative(-1);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
939
1229
|
}
|
|
940
1230
|
|
|
941
1231
|
// g: focus pane 상단 점프
|
|
942
|
-
if (key === "g") {
|
|
1232
|
+
if (key === "g") {
|
|
1233
|
+
followTail = false;
|
|
1234
|
+
detailScrollOffset = 0;
|
|
1235
|
+
render();
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
943
1238
|
// G: focus pane 하단 점프
|
|
944
|
-
if (key === "G") {
|
|
1239
|
+
if (key === "G") {
|
|
1240
|
+
followTail = true;
|
|
1241
|
+
detailScrollOffset = 0;
|
|
1242
|
+
render();
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
945
1245
|
// PgUp/PgDn: 페이지 단위 스크롤
|
|
946
1246
|
const pageSize = Math.max(1, Math.floor(getViewportRows() / 2));
|
|
947
|
-
if (key === "\x1b[5~") {
|
|
948
|
-
|
|
1247
|
+
if (key === "\x1b[5~") {
|
|
1248
|
+
scrollDetail(-pageSize);
|
|
1249
|
+
return;
|
|
1250
|
+
} // PgUp
|
|
1251
|
+
if (key === "\x1b[6~") {
|
|
1252
|
+
scrollDetail(pageSize);
|
|
1253
|
+
return;
|
|
1254
|
+
} // PgDn
|
|
949
1255
|
// f: follow-tail 토글
|
|
950
|
-
if (key === "f") {
|
|
1256
|
+
if (key === "f") {
|
|
1257
|
+
followTail = !followTail;
|
|
1258
|
+
if (followTail) detailScrollOffset = 0;
|
|
1259
|
+
render();
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
951
1262
|
// r: raw mode 토글
|
|
952
|
-
if (key === "r") {
|
|
1263
|
+
if (key === "r") {
|
|
1264
|
+
rawMode = !rawMode;
|
|
1265
|
+
render();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
953
1268
|
// l: 탭 전환 (Log → Detail → Files)
|
|
954
1269
|
if (key === "l") {
|
|
955
1270
|
const tabs = ["log", "detail", "files"];
|
|
@@ -959,16 +1274,29 @@ export function createLogDashboard(opts = {}) {
|
|
|
959
1274
|
return;
|
|
960
1275
|
}
|
|
961
1276
|
// n: 가장 최근 상태 변경 워커로 이동
|
|
962
|
-
if (key === "n") {
|
|
1277
|
+
if (key === "n") {
|
|
1278
|
+
selectMostRecentChangedWorker();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
963
1281
|
// h/?: 도움말 오버레이 토글
|
|
964
|
-
if (key === "h" || key === "?") {
|
|
1282
|
+
if (key === "h" || key === "?") {
|
|
1283
|
+
helpOverlay = true;
|
|
1284
|
+
render();
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
965
1287
|
// q: 대시보드 종료
|
|
966
|
-
if (key === "q") {
|
|
1288
|
+
if (key === "q") {
|
|
1289
|
+
doClose();
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
967
1292
|
// 1-9: 워커 직접 선택
|
|
968
1293
|
if (/^[1-9]$/.test(key)) {
|
|
969
1294
|
const names = visibleWorkerNames();
|
|
970
1295
|
const target = names[Number.parseInt(key, 10) - 1];
|
|
971
|
-
if (target) {
|
|
1296
|
+
if (target) {
|
|
1297
|
+
setSelectedWorker(target);
|
|
1298
|
+
render();
|
|
1299
|
+
}
|
|
972
1300
|
return;
|
|
973
1301
|
}
|
|
974
1302
|
}
|
|
@@ -977,7 +1305,8 @@ export function createLogDashboard(opts = {}) {
|
|
|
977
1305
|
function attachToSession(worker) {
|
|
978
1306
|
const execFileFn = opts.deps?.execFile || _execFile;
|
|
979
1307
|
// 1. rawMode 해제 + input 일시정지 (키 이벤트 차단)
|
|
980
|
-
if (rawModeEnabled && typeof input?.setRawMode === "function")
|
|
1308
|
+
if (rawModeEnabled && typeof input?.setRawMode === "function")
|
|
1309
|
+
input.setRawMode(false);
|
|
981
1310
|
if (typeof input?.pause === "function") input.pause();
|
|
982
1311
|
// 2. altScreen 퇴장
|
|
983
1312
|
exitAltScreen();
|
|
@@ -988,21 +1317,52 @@ export function createLogDashboard(opts = {}) {
|
|
|
988
1317
|
const host = worker.host || "unknown";
|
|
989
1318
|
const ip = worker._sshIp || host;
|
|
990
1319
|
const title = `${host}:${worker.role || sessionName}`;
|
|
991
|
-
execFileFn(
|
|
992
|
-
"
|
|
993
|
-
|
|
1320
|
+
execFileFn(
|
|
1321
|
+
"wt.exe",
|
|
1322
|
+
[
|
|
1323
|
+
"-w",
|
|
1324
|
+
"0",
|
|
1325
|
+
"nt",
|
|
1326
|
+
"--title",
|
|
1327
|
+
title,
|
|
1328
|
+
"--",
|
|
1329
|
+
"ssh",
|
|
1330
|
+
`${worker.sshUser}@${ip}`,
|
|
1331
|
+
"-t",
|
|
1332
|
+
`psmux attach -t ${sessionName}`,
|
|
1333
|
+
],
|
|
1334
|
+
{ detached: true, stdio: "ignore", windowsHide: false },
|
|
1335
|
+
() => {},
|
|
1336
|
+
);
|
|
994
1337
|
} else {
|
|
995
1338
|
// 로컬: psmux attach in new WT tab
|
|
996
1339
|
const title = worker.role || sessionName;
|
|
997
|
-
execFileFn(
|
|
998
|
-
"
|
|
999
|
-
|
|
1340
|
+
execFileFn(
|
|
1341
|
+
"wt.exe",
|
|
1342
|
+
[
|
|
1343
|
+
"-w",
|
|
1344
|
+
"0",
|
|
1345
|
+
"nt",
|
|
1346
|
+
"--title",
|
|
1347
|
+
title,
|
|
1348
|
+
"--",
|
|
1349
|
+
"psmux",
|
|
1350
|
+
"attach",
|
|
1351
|
+
"-t",
|
|
1352
|
+
sessionName,
|
|
1353
|
+
],
|
|
1354
|
+
{ detached: true, stdio: "ignore", windowsHide: false },
|
|
1355
|
+
() => {},
|
|
1356
|
+
);
|
|
1000
1357
|
}
|
|
1001
1358
|
|
|
1002
1359
|
// 3. 200ms 후 altScreen 복귀 + rawMode 재활성화
|
|
1003
1360
|
setTimeout(() => {
|
|
1004
1361
|
enterAltScreen();
|
|
1005
|
-
if (typeof input?.setRawMode === "function") {
|
|
1362
|
+
if (typeof input?.setRawMode === "function") {
|
|
1363
|
+
input.setRawMode(true);
|
|
1364
|
+
rawModeEnabled = true;
|
|
1365
|
+
}
|
|
1006
1366
|
if (typeof input?.resume === "function") input.resume();
|
|
1007
1367
|
render();
|
|
1008
1368
|
}, 200);
|
|
@@ -1014,15 +1374,26 @@ export function createLogDashboard(opts = {}) {
|
|
|
1014
1374
|
function showFlash(msg, durationMs = 5000) {
|
|
1015
1375
|
flashMessage = msg;
|
|
1016
1376
|
if (flashTimer) clearTimeout(flashTimer);
|
|
1017
|
-
flashTimer = setTimeout(() => {
|
|
1377
|
+
flashTimer = setTimeout(() => {
|
|
1378
|
+
flashMessage = "";
|
|
1379
|
+
render();
|
|
1380
|
+
}, durationMs);
|
|
1018
1381
|
render();
|
|
1019
1382
|
}
|
|
1020
1383
|
|
|
1021
1384
|
function attachInput() {
|
|
1022
1385
|
if (inputAttached) return;
|
|
1023
|
-
if (
|
|
1386
|
+
if (
|
|
1387
|
+
!isTTY ||
|
|
1388
|
+
(!forceTTY && !input?.isTTY) ||
|
|
1389
|
+
typeof input?.on !== "function"
|
|
1390
|
+
)
|
|
1391
|
+
return;
|
|
1024
1392
|
inputAttached = true;
|
|
1025
|
-
if (typeof input.setRawMode === "function") {
|
|
1393
|
+
if (typeof input.setRawMode === "function") {
|
|
1394
|
+
input.setRawMode(true);
|
|
1395
|
+
rawModeEnabled = true;
|
|
1396
|
+
}
|
|
1026
1397
|
if (typeof input.resume === "function") input.resume();
|
|
1027
1398
|
input.on("data", handleInput);
|
|
1028
1399
|
}
|
|
@@ -1058,10 +1429,20 @@ export function createLogDashboard(opts = {}) {
|
|
|
1058
1429
|
const renderTime = Date.now();
|
|
1059
1430
|
|
|
1060
1431
|
// Tier1: 상단 고정 2행
|
|
1061
|
-
const tier1 = buildTier1(
|
|
1432
|
+
const tier1 = buildTier1(
|
|
1433
|
+
names,
|
|
1434
|
+
workers,
|
|
1435
|
+
pipeline,
|
|
1436
|
+
elapsed,
|
|
1437
|
+
totalCols,
|
|
1438
|
+
VERSION,
|
|
1439
|
+
renderTime,
|
|
1440
|
+
);
|
|
1062
1441
|
// flash 메시지 (완료/실패 알림)
|
|
1063
1442
|
if (flashMessage) {
|
|
1064
|
-
tier1.push(
|
|
1443
|
+
tier1.push(
|
|
1444
|
+
truncate(` ${color("▸", MOCHA.green)} ${flashMessage}`, totalCols),
|
|
1445
|
+
);
|
|
1065
1446
|
}
|
|
1066
1447
|
|
|
1067
1448
|
// 레이아웃 결정
|
|
@@ -1075,9 +1456,19 @@ export function createLogDashboard(opts = {}) {
|
|
|
1075
1456
|
|
|
1076
1457
|
// summary+detail: summaryBar + focus pane
|
|
1077
1458
|
if (effectiveLayout === "summary+detail") {
|
|
1078
|
-
const summaryBar = buildSummaryBar(
|
|
1459
|
+
const summaryBar = buildSummaryBar(
|
|
1460
|
+
names,
|
|
1461
|
+
workers,
|
|
1462
|
+
selectedWorker,
|
|
1463
|
+
pipeline,
|
|
1464
|
+
totalCols,
|
|
1465
|
+
VERSION,
|
|
1466
|
+
);
|
|
1079
1467
|
const selectedState = workers.get(selectedWorker);
|
|
1080
|
-
const focusPaneHeight = Math.max(
|
|
1468
|
+
const focusPaneHeight = Math.max(
|
|
1469
|
+
8,
|
|
1470
|
+
totalRows - tier1.length - summaryBar.length,
|
|
1471
|
+
);
|
|
1081
1472
|
const focusPane = buildFocusPane(selectedWorker, selectedState, {
|
|
1082
1473
|
width: totalCols,
|
|
1083
1474
|
height: focusPaneHeight,
|
|
@@ -1094,8 +1485,11 @@ export function createLogDashboard(opts = {}) {
|
|
|
1094
1485
|
// 좌우 분할: Left Rail (30%) | Right Focus (70%)
|
|
1095
1486
|
// 목업: Tier2 Left Rail + Tier3 Focus 나란히 렌더링
|
|
1096
1487
|
const GAP = 1; // rail과 focus 사이 구분선
|
|
1097
|
-
const railRatio = focus === "detail" ? 0.
|
|
1098
|
-
const railWidth = Math.max(
|
|
1488
|
+
const railRatio = focus === "detail" ? 0.2 : 0.3;
|
|
1489
|
+
const railWidth = Math.max(
|
|
1490
|
+
MIN_CARD_WIDTH,
|
|
1491
|
+
Math.floor(totalCols * railRatio),
|
|
1492
|
+
);
|
|
1099
1493
|
const focusWidth = totalCols - railWidth - GAP;
|
|
1100
1494
|
const bodyHeight = Math.max(6, totalRows - tier1.length - 1); // -1 for status bar
|
|
1101
1495
|
|
|
@@ -1118,7 +1512,8 @@ export function createLogDashboard(opts = {}) {
|
|
|
1118
1512
|
railLines.push(...card);
|
|
1119
1513
|
}
|
|
1120
1514
|
// rail 높이를 bodyHeight에 맞춤 (부족하면 빈 줄, 넘치면 자름)
|
|
1121
|
-
while (railLines.length < bodyHeight)
|
|
1515
|
+
while (railLines.length < bodyHeight)
|
|
1516
|
+
railLines.push(padRight("", railWidth));
|
|
1122
1517
|
if (railLines.length > bodyHeight) railLines.length = bodyHeight;
|
|
1123
1518
|
|
|
1124
1519
|
// Right Focus: 선택된 워커 상세
|
|
@@ -1135,7 +1530,8 @@ export function createLogDashboard(opts = {}) {
|
|
|
1135
1530
|
time: renderTime,
|
|
1136
1531
|
});
|
|
1137
1532
|
}
|
|
1138
|
-
while (focusLines.length < bodyHeight)
|
|
1533
|
+
while (focusLines.length < bodyHeight)
|
|
1534
|
+
focusLines.push(padRight("", focusWidth));
|
|
1139
1535
|
if (focusLines.length > bodyHeight) focusLines.length = bodyHeight;
|
|
1140
1536
|
|
|
1141
1537
|
// 좌우 합성: rail[i] + separator + focus[i]
|
|
@@ -1219,21 +1615,35 @@ export function createLogDashboard(opts = {}) {
|
|
|
1219
1615
|
// ── 공개 API ─────────────────────────────────────────────────────────
|
|
1220
1616
|
return {
|
|
1221
1617
|
updateWorker(paneName, state) {
|
|
1222
|
-
const existing = workers.get(paneName) || {
|
|
1618
|
+
const existing = workers.get(paneName) || {
|
|
1619
|
+
cli: "codex",
|
|
1620
|
+
status: "pending",
|
|
1621
|
+
};
|
|
1223
1622
|
const merged = normalizeWorkerState(existing, state);
|
|
1224
1623
|
const nextSig = JSON.stringify({
|
|
1225
|
-
cli: merged.cli,
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1624
|
+
cli: merged.cli,
|
|
1625
|
+
status: merged.status,
|
|
1626
|
+
role: merged.role,
|
|
1627
|
+
snapshot: merged.snapshot,
|
|
1628
|
+
summary: merged.summary,
|
|
1629
|
+
detail: merged.detail,
|
|
1630
|
+
findings: merged.findings,
|
|
1631
|
+
files_changed: merged.files_changed,
|
|
1632
|
+
confidence: merged.confidence,
|
|
1633
|
+
tokens: merged.tokens,
|
|
1634
|
+
progress: merged.progress,
|
|
1635
|
+
handoff: merged.handoff,
|
|
1230
1636
|
});
|
|
1231
1637
|
const sigChanged = nextSig !== existing._sig;
|
|
1232
|
-
const explicitElapsed = Number.isFinite(state.elapsed)
|
|
1638
|
+
const explicitElapsed = Number.isFinite(state.elapsed)
|
|
1639
|
+
? Math.max(0, Math.round(state.elapsed))
|
|
1640
|
+
: null;
|
|
1233
1641
|
merged._sig = nextSig;
|
|
1234
1642
|
merged._logSec = sigChanged
|
|
1235
1643
|
? (explicitElapsed ?? nowElapsedSec())
|
|
1236
|
-
:
|
|
1644
|
+
: Number.isFinite(existing._logSec)
|
|
1645
|
+
? existing._logSec
|
|
1646
|
+
: (explicitElapsed ?? nowElapsedSec());
|
|
1237
1647
|
workers.set(paneName, merged);
|
|
1238
1648
|
ensureSelectedWorker(visibleWorkerNames());
|
|
1239
1649
|
// follow-tail: 새 데이터 → 자동 scroll 재계산
|
|
@@ -1287,7 +1697,10 @@ export function createLogDashboard(opts = {}) {
|
|
|
1287
1697
|
|
|
1288
1698
|
setFocusTab(tab) {
|
|
1289
1699
|
const valid = ["log", "detail", "files"];
|
|
1290
|
-
if (valid.includes(tab)) {
|
|
1700
|
+
if (valid.includes(tab)) {
|
|
1701
|
+
focusTab = tab;
|
|
1702
|
+
detailScrollOffset = 0;
|
|
1703
|
+
}
|
|
1291
1704
|
},
|
|
1292
1705
|
|
|
1293
1706
|
getLayout() {
|
|
@@ -1331,27 +1744,30 @@ export function createLogDashboard(opts = {}) {
|
|
|
1331
1744
|
// failed=red, dead/init/starting=dim
|
|
1332
1745
|
|
|
1333
1746
|
const CONDUCTOR_STATE_LABEL = {
|
|
1334
|
-
init:
|
|
1335
|
-
starting:
|
|
1336
|
-
healthy:
|
|
1337
|
-
stalled:
|
|
1338
|
-
input_wait:
|
|
1339
|
-
failed:
|
|
1340
|
-
restarting:
|
|
1341
|
-
dead:
|
|
1342
|
-
completed:
|
|
1747
|
+
init: { label: "INIT", seq: MOCHA.subtext },
|
|
1748
|
+
starting: { label: "START", seq: MOCHA.executing },
|
|
1749
|
+
healthy: { label: "OK", seq: MOCHA.ok },
|
|
1750
|
+
stalled: { label: "STALL", seq: MOCHA.yellow },
|
|
1751
|
+
input_wait: { label: "INPUT_WAIT", seq: FG.cyan },
|
|
1752
|
+
failed: { label: "FAIL", seq: MOCHA.fail },
|
|
1753
|
+
restarting: { label: "RESTART", seq: MOCHA.partial },
|
|
1754
|
+
dead: { label: "DEAD", seq: FG.gray },
|
|
1755
|
+
completed: { label: "DONE", seq: MOCHA.ok },
|
|
1343
1756
|
};
|
|
1344
1757
|
|
|
1345
1758
|
function conductorHealthCell(state) {
|
|
1346
|
-
const entry = CONDUCTOR_STATE_LABEL[state] || {
|
|
1759
|
+
const entry = CONDUCTOR_STATE_LABEL[state] || {
|
|
1760
|
+
label: state.toUpperCase(),
|
|
1761
|
+
seq: FG.gray,
|
|
1762
|
+
};
|
|
1347
1763
|
return `${entry.seq}■ ${entry.label}${RESET}`;
|
|
1348
1764
|
}
|
|
1349
1765
|
|
|
1350
1766
|
function conductorRelTime(ms) {
|
|
1351
|
-
if (!ms) return
|
|
1767
|
+
if (!ms) return "—";
|
|
1352
1768
|
const sec = Math.round((Date.now() - ms) / 1000);
|
|
1353
|
-
if (sec < 0)
|
|
1354
|
-
if (sec < 60)
|
|
1769
|
+
if (sec < 0) return "—";
|
|
1770
|
+
if (sec < 60) return `${sec}s ago`;
|
|
1355
1771
|
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
|
|
1356
1772
|
return `${Math.floor(sec / 3600)}h ago`;
|
|
1357
1773
|
}
|
|
@@ -1369,31 +1785,32 @@ export function renderConductorTier(snapshot, cols = 100) {
|
|
|
1369
1785
|
|
|
1370
1786
|
// ── 열 너비 계산 ────────────────────────────────────────
|
|
1371
1787
|
// ID(8) Agent(7) Host(6) Health(dyn) LastOut(dyn) Restarts(8) Why(rest)
|
|
1372
|
-
const COL_ID
|
|
1373
|
-
const COL_AGENT
|
|
1374
|
-
const COL_HOST
|
|
1788
|
+
const COL_ID = 8;
|
|
1789
|
+
const COL_AGENT = 7;
|
|
1790
|
+
const COL_HOST = 6;
|
|
1375
1791
|
const COL_RESTARTS = 4;
|
|
1376
|
-
const COL_HEALTH
|
|
1377
|
-
const COL_LASTOUT
|
|
1792
|
+
const COL_HEALTH = 12; // '■ INPUT_WAIT' = 12 chars
|
|
1793
|
+
const COL_LASTOUT = 9; // '999m ago' = 8 + space
|
|
1378
1794
|
// Why gets the remainder
|
|
1379
|
-
const fixedCols =
|
|
1795
|
+
const fixedCols =
|
|
1796
|
+
COL_ID + COL_AGENT + COL_HOST + COL_HEALTH + COL_LASTOUT + COL_RESTARTS + 6; // 6 spaces between cols
|
|
1380
1797
|
const COL_WHY = Math.max(4, inner - fixedCols);
|
|
1381
1798
|
|
|
1382
1799
|
function cell(text, width_) {
|
|
1383
|
-
return clip(String(text ??
|
|
1800
|
+
return clip(String(text ?? ""), width_);
|
|
1384
1801
|
}
|
|
1385
1802
|
|
|
1386
1803
|
function buildRow(id, agent, host, healthCell, lastOut, restarts, why) {
|
|
1387
|
-
const idC
|
|
1388
|
-
const agentC
|
|
1389
|
-
const hostC
|
|
1804
|
+
const idC = cell(id, COL_ID);
|
|
1805
|
+
const agentC = cell(agent, COL_AGENT);
|
|
1806
|
+
const hostC = cell(host, COL_HOST);
|
|
1390
1807
|
const restartsC = cell(String(restarts ?? 0), COL_RESTARTS);
|
|
1391
|
-
const lastOutC
|
|
1392
|
-
const whyC
|
|
1808
|
+
const lastOutC = clip(lastOut, COL_LASTOUT);
|
|
1809
|
+
const whyC = cell(why, COL_WHY);
|
|
1393
1810
|
// healthCell already has ANSI codes; pad its visible width manually
|
|
1394
1811
|
const healthVis = wcswidth(stripAnsi(healthCell));
|
|
1395
1812
|
const healthPad = Math.max(0, COL_HEALTH - healthVis);
|
|
1396
|
-
const healthC
|
|
1813
|
+
const healthC = healthCell + " ".repeat(healthPad);
|
|
1397
1814
|
|
|
1398
1815
|
return `${idC} ${agentC} ${hostC} ${healthC} ${lastOutC} ${restartsC} ${whyC}`;
|
|
1399
1816
|
}
|
|
@@ -1408,40 +1825,60 @@ export function renderConductorTier(snapshot, cols = 100) {
|
|
|
1408
1825
|
const dashLeft = 1;
|
|
1409
1826
|
const dashRight = Math.max(0, dashLen - dashLeft);
|
|
1410
1827
|
const borderSeq = MOCHA.border;
|
|
1411
|
-
const topBorder =
|
|
1412
|
-
`${borderSeq}┌${'─'.repeat(dashLeft)}${RESET}${titleColored}${borderSeq}${'─'.repeat(dashRight)}┐${RESET}`;
|
|
1828
|
+
const topBorder = `${borderSeq}┌${"─".repeat(dashLeft)}${RESET}${titleColored}${borderSeq}${"─".repeat(dashRight)}┐${RESET}`;
|
|
1413
1829
|
|
|
1414
1830
|
// ── ヘッダー行 ───────────────────────────────────────────
|
|
1415
|
-
const headerRow = buildRow(
|
|
1831
|
+
const headerRow = buildRow(
|
|
1832
|
+
"ID",
|
|
1833
|
+
"Agent",
|
|
1834
|
+
"Host",
|
|
1835
|
+
clip("Health", COL_HEALTH),
|
|
1836
|
+
"Last Out",
|
|
1837
|
+
"Rst",
|
|
1838
|
+
"Why",
|
|
1839
|
+
);
|
|
1416
1840
|
const headerLine = `${borderSeq}│${RESET} ${dim(headerRow)} ${borderSeq}│${RESET}`;
|
|
1417
1841
|
|
|
1418
1842
|
// ── データ行 ────────────────────────────────────────────
|
|
1419
1843
|
const dataLines = [];
|
|
1420
1844
|
if (!snapshot || snapshot.length === 0) {
|
|
1421
|
-
const emptyMsg = color(
|
|
1422
|
-
const _emptyPad = clip(
|
|
1423
|
-
|
|
1845
|
+
const emptyMsg = color("(no sessions)", FG.muted);
|
|
1846
|
+
const _emptyPad = clip(
|
|
1847
|
+
stripAnsi(emptyMsg) === "(no sessions)" ? emptyMsg : emptyMsg,
|
|
1848
|
+
inner,
|
|
1849
|
+
);
|
|
1850
|
+
dataLines.push(
|
|
1851
|
+
`${borderSeq}│${RESET} ${padRight(emptyMsg, inner - 2)} ${borderSeq}│${RESET}`,
|
|
1852
|
+
);
|
|
1424
1853
|
} else {
|
|
1425
1854
|
for (const s of snapshot) {
|
|
1426
|
-
const id
|
|
1427
|
-
const agent
|
|
1428
|
-
const host
|
|
1429
|
-
const state
|
|
1855
|
+
const id = String(s.id ?? "").slice(0, COL_ID);
|
|
1856
|
+
const agent = String(s.agent ?? "unknown").slice(0, COL_AGENT);
|
|
1857
|
+
const host = "local";
|
|
1858
|
+
const state = s.state ?? "init";
|
|
1430
1859
|
const healthCell = conductorHealthCell(state);
|
|
1431
|
-
const lastOut
|
|
1860
|
+
const lastOut = conductorRelTime(s.health?.lastProbeAt ?? null);
|
|
1432
1861
|
const restarts = s.restarts ?? 0;
|
|
1433
1862
|
// derive "why" from last state transition context
|
|
1434
1863
|
const why = s.health?.inputWaitPattern
|
|
1435
1864
|
? String(s.health.inputWaitPattern).slice(0, COL_WHY)
|
|
1436
|
-
:
|
|
1437
|
-
|
|
1438
|
-
const rowText = buildRow(
|
|
1865
|
+
: "";
|
|
1866
|
+
|
|
1867
|
+
const rowText = buildRow(
|
|
1868
|
+
id,
|
|
1869
|
+
agent,
|
|
1870
|
+
host,
|
|
1871
|
+
healthCell,
|
|
1872
|
+
lastOut,
|
|
1873
|
+
restarts,
|
|
1874
|
+
why,
|
|
1875
|
+
);
|
|
1439
1876
|
dataLines.push(`${borderSeq}│${RESET} ${rowText} ${borderSeq}│${RESET}`);
|
|
1440
1877
|
}
|
|
1441
1878
|
}
|
|
1442
1879
|
|
|
1443
1880
|
// ── Bottom border ─────────────────────────────────────
|
|
1444
|
-
const botBorder = `${borderSeq}└${
|
|
1881
|
+
const botBorder = `${borderSeq}└${"─".repeat(boxWidth)}┘${RESET}`;
|
|
1445
1882
|
|
|
1446
1883
|
return [topBorder, headerLine, ...dataLines, botBorder];
|
|
1447
1884
|
}
|