triflux 10.9.19 → 10.9.21
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/CLAUDE.md +212 -0
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/team/dashboard-open.mjs +1 -68
- package/hub/team/native-supervisor.mjs +9 -2
- package/hub/team/psmux.mjs +5 -13
- package/hub/team/session.mjs +6 -26
- package/hub/team/swarm-hypervisor.mjs +205 -27
- package/hub/team/synapse-http.mjs +1 -0
- package/hub/team/tui-core.mjs +292 -0
- package/hub/team/tui-lite.mjs +20 -154
- package/hub/team/tui-synapse.mjs +213 -0
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +159 -255
- package/hub/workers/delegator-mcp.mjs +2 -2
- package/package.json +21 -62
- package/references/hosts.json +46 -0
- package/scripts/__tests__/keyword-detector.test.mjs +4 -4
- package/scripts/cross-review-gate.mjs +13 -0
- package/scripts/remote-spawn.mjs +11 -46
- package/scripts/session-spawn-helper.mjs +8 -21
- package/scripts/test-tfx-route-no-claude-native.mjs +4 -2
- package/scripts/tfx-route.sh +13 -0
- package/skills/tfx-deep-interview/SKILL.md +6 -6
- package/skills/tfx-deep-interview/SKILL.md.tmpl +6 -6
- package/skills/tfx-index/SKILL.md +1 -1
- package/skills/tfx-index/SKILL.md.tmpl +1 -1
- package/skills/tfx-interview/SKILL.md +9 -9
- package/skills/tfx-interview/SKILL.md.tmpl +9 -9
- package/skills/tfx-plan/SKILL.md +1 -1
- package/skills/tfx-plan/SKILL.md.tmpl +1 -1
- package/skills/tfx-research/SKILL.md +1 -1
- package/skills/tfx-research/SKILL.md.tmpl +1 -1
- package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
- package/skills/tfx-workspace/evals/evals.json +79 -0
- package/skills/tfx-workspace/iteration-1/benchmark.json +524 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +25 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +20 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +20 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/review.html +1325 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +30 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/benchmark.json +144 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +35 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +35 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
- package/skills/tfx-workspace/iteration-2/review.html +1325 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +77 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +82 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
- package/.claude-plugin/marketplace.json +0 -34
- package/.claude-plugin/plugin.json +0 -22
- package/config/mcp-registry.json +0 -29
- package/scripts/__tests__/release-governance.test.mjs +0 -148
- package/scripts/release/bump-version.mjs +0 -77
- package/scripts/release/check-sync.mjs +0 -51
- package/scripts/release/lib.mjs +0 -303
- package/scripts/release/prepare.mjs +0 -85
- package/scripts/release/publish.mjs +0 -87
- package/scripts/release/verify.mjs +0 -81
- package/scripts/release/version-manifest.json +0 -26
- package/tui/codex-profile.mjs +0 -457
- package/tui/core.mjs +0 -266
- package/tui/doctor.mjs +0 -375
- package/tui/gemini-profile.mjs +0 -299
- package/tui/monitor-data.mjs +0 -152
- package/tui/monitor.mjs +0 -339
- package/tui/setup.mjs +0 -598
package/hub/team/tui-lite.mjs
CHANGED
|
@@ -22,159 +22,30 @@ import {
|
|
|
22
22
|
truncate,
|
|
23
23
|
wcswidth,
|
|
24
24
|
} from "./ansi.mjs";
|
|
25
|
+
import {
|
|
26
|
+
clamp,
|
|
27
|
+
formatTokens,
|
|
28
|
+
loadVersion,
|
|
29
|
+
normalizeWorkerState as coreNormalizeWorkerState,
|
|
30
|
+
resolveViewportColumns,
|
|
31
|
+
resolveViewportRows,
|
|
32
|
+
runtimeStatus,
|
|
33
|
+
sanitizeFiles,
|
|
34
|
+
sanitizeOneLine,
|
|
35
|
+
sanitizeTextBlock,
|
|
36
|
+
wrapText as wrapTextFull,
|
|
37
|
+
VALID_TABS as VALID_TABS_ARRAY,
|
|
38
|
+
} from "./tui-core.mjs";
|
|
25
39
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const VALID_TABS = new Set(["log", "detail", "files"]);
|
|
29
|
-
|
|
30
|
-
let VERSION = "lite";
|
|
31
|
-
try {
|
|
32
|
-
const { createRequire } = await import("node:module");
|
|
33
|
-
VERSION = createRequire(import.meta.url)("../../package.json").version;
|
|
34
|
-
} catch {}
|
|
35
|
-
|
|
36
|
-
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
37
|
-
|
|
38
|
-
function sanitizeBlock(text, rawMode = false) {
|
|
39
|
-
const value = String(text || "").replace(/\r/g, "");
|
|
40
|
-
const cleaned = rawMode
|
|
41
|
-
? value
|
|
42
|
-
: value
|
|
43
|
-
.replace(/```[\s\S]*?(?:```|$)/g, "\n")
|
|
44
|
-
.replace(/^\s*```.*$/gm, "")
|
|
45
|
-
.replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "");
|
|
46
|
-
return cleaned
|
|
47
|
-
.split("\n")
|
|
48
|
-
.map((line) => line.trim())
|
|
49
|
-
.filter(Boolean)
|
|
50
|
-
.filter((line) => line !== "--- HANDOFF ---")
|
|
51
|
-
.join("\n")
|
|
52
|
-
.trim();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function sanitizeOneLine(text, fallback = "") {
|
|
56
|
-
return sanitizeBlock(text).replace(/\s+/g, " ").trim() || fallback;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function sanitizeFiles(files) {
|
|
60
|
-
const list = Array.isArray(files) ? files : String(files || "").split(",");
|
|
61
|
-
return list.map((entry) => sanitizeOneLine(entry)).filter(Boolean);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeTokens(tokens) {
|
|
65
|
-
if (tokens === null || tokens === undefined || tokens === "") return "";
|
|
66
|
-
if (typeof tokens === "number" && Number.isFinite(tokens)) return tokens;
|
|
67
|
-
const raw = sanitizeOneLine(tokens);
|
|
68
|
-
const match = raw.match(/(\d+(?:[.,]\d+)?\s*[kKmM]?)/);
|
|
69
|
-
return match ? match[1].replace(/\s+/g, "").toLowerCase() : raw;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function formatTokens(tokens) {
|
|
73
|
-
if (!tokens && tokens !== 0) return "n/a";
|
|
74
|
-
if (typeof tokens === "number") {
|
|
75
|
-
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}m`;
|
|
76
|
-
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
77
|
-
}
|
|
78
|
-
return String(tokens);
|
|
79
|
-
}
|
|
40
|
+
const VERSION = await loadVersion("lite");
|
|
41
|
+
const VALID_TABS = new Set(VALID_TABS_ARRAY);
|
|
80
42
|
|
|
81
43
|
function wrap(text, width) {
|
|
82
|
-
|
|
83
|
-
const lines = [];
|
|
84
|
-
for (const rawLine of sanitizeBlock(text).split("\n")) {
|
|
85
|
-
const words = rawLine.split(/\s+/).filter(Boolean);
|
|
86
|
-
if (words.length === 0) continue;
|
|
87
|
-
let current = "";
|
|
88
|
-
for (const word of words) {
|
|
89
|
-
const next = current ? `${current} ${word}` : word;
|
|
90
|
-
if (wcswidth(next) <= limit) {
|
|
91
|
-
current = next;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (current) lines.push(current);
|
|
95
|
-
current = word;
|
|
96
|
-
while (wcswidth(current) > limit) {
|
|
97
|
-
lines.push(current.slice(0, limit));
|
|
98
|
-
current = current.slice(limit);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (current) lines.push(current);
|
|
102
|
-
}
|
|
103
|
-
return lines;
|
|
44
|
+
return wrapTextFull(text, width);
|
|
104
45
|
}
|
|
105
46
|
|
|
106
|
-
const runtimeStatus = (worker) =>
|
|
107
|
-
worker?.handoff?.status || worker?.status || "pending";
|
|
108
|
-
|
|
109
47
|
function normalizeWorkerState(existing = {}, state = {}) {
|
|
110
|
-
|
|
111
|
-
state.handoff === undefined
|
|
112
|
-
? existing.handoff
|
|
113
|
-
: {
|
|
114
|
-
...(existing.handoff || {}),
|
|
115
|
-
...(state.handoff || {}),
|
|
116
|
-
verdict:
|
|
117
|
-
state.handoff?.verdict !== undefined
|
|
118
|
-
? sanitizeOneLine(state.handoff.verdict)
|
|
119
|
-
: existing.handoff?.verdict,
|
|
120
|
-
confidence:
|
|
121
|
-
state.handoff?.confidence !== undefined
|
|
122
|
-
? sanitizeOneLine(state.handoff.confidence)
|
|
123
|
-
: existing.handoff?.confidence,
|
|
124
|
-
status:
|
|
125
|
-
state.handoff?.status !== undefined
|
|
126
|
-
? sanitizeOneLine(state.handoff.status)
|
|
127
|
-
: existing.handoff?.status,
|
|
128
|
-
files_changed:
|
|
129
|
-
state.handoff?.files_changed !== undefined
|
|
130
|
-
? sanitizeFiles(state.handoff.files_changed)
|
|
131
|
-
: existing.handoff?.files_changed,
|
|
132
|
-
};
|
|
133
|
-
return {
|
|
134
|
-
...existing,
|
|
135
|
-
...state,
|
|
136
|
-
cli:
|
|
137
|
-
state.cli !== undefined
|
|
138
|
-
? sanitizeOneLine(state.cli, existing.cli || "codex")
|
|
139
|
-
: existing.cli || "codex",
|
|
140
|
-
status:
|
|
141
|
-
state.status !== undefined
|
|
142
|
-
? sanitizeOneLine(state.status, existing.status || "pending")
|
|
143
|
-
: existing.status || "pending",
|
|
144
|
-
snapshot:
|
|
145
|
-
state.snapshot !== undefined
|
|
146
|
-
? sanitizeBlock(state.snapshot)
|
|
147
|
-
: existing.snapshot,
|
|
148
|
-
summary:
|
|
149
|
-
state.summary !== undefined
|
|
150
|
-
? sanitizeBlock(state.summary)
|
|
151
|
-
: existing.summary,
|
|
152
|
-
detail:
|
|
153
|
-
state.detail !== undefined
|
|
154
|
-
? sanitizeBlock(state.detail)
|
|
155
|
-
: existing.detail,
|
|
156
|
-
findings:
|
|
157
|
-
state.findings !== undefined
|
|
158
|
-
? sanitizeFiles(state.findings)
|
|
159
|
-
: existing.findings,
|
|
160
|
-
files_changed:
|
|
161
|
-
state.files_changed !== undefined
|
|
162
|
-
? sanitizeFiles(state.files_changed)
|
|
163
|
-
: existing.files_changed,
|
|
164
|
-
confidence:
|
|
165
|
-
state.confidence !== undefined
|
|
166
|
-
? sanitizeOneLine(state.confidence)
|
|
167
|
-
: existing.confidence,
|
|
168
|
-
tokens:
|
|
169
|
-
state.tokens !== undefined
|
|
170
|
-
? normalizeTokens(state.tokens)
|
|
171
|
-
: existing.tokens,
|
|
172
|
-
progress:
|
|
173
|
-
state.progress !== undefined
|
|
174
|
-
? clamp(Number(state.progress) || 0, 0, 1)
|
|
175
|
-
: existing.progress,
|
|
176
|
-
handoff,
|
|
177
|
-
};
|
|
48
|
+
return coreNormalizeWorkerState(existing, state);
|
|
178
49
|
}
|
|
179
50
|
|
|
180
51
|
function frame(lines, width, border = MOCHA.border) {
|
|
@@ -314,13 +185,8 @@ export function createLiteDashboard(opts = {}) {
|
|
|
314
185
|
if (!closed) stream.write(text);
|
|
315
186
|
};
|
|
316
187
|
const workerNames = () => [...workers.keys()].sort();
|
|
317
|
-
const viewportColumns = () =>
|
|
318
|
-
|
|
319
|
-
48,
|
|
320
|
-
columns || stream?.columns || process.stdout?.columns || FALLBACK_COLUMNS,
|
|
321
|
-
);
|
|
322
|
-
const viewportRows = () =>
|
|
323
|
-
Math.max(10, rows || stream?.rows || process.stdout?.rows || FALLBACK_ROWS);
|
|
188
|
+
const viewportColumns = () => resolveViewportColumns({ columns, stream });
|
|
189
|
+
const viewportRows = () => resolveViewportRows({ rows, stream });
|
|
324
190
|
const ensureSelection = (names) => {
|
|
325
191
|
if (names.length && (!selectedWorker || !workers.has(selectedWorker)))
|
|
326
192
|
selectedWorker = names[0];
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// hub/team/tui-synapse.mjs — Synapse 실시간 관제 (Phase 3)
|
|
2
|
+
// HTTP polling 기반 이벤트 수신 + 메트릭 수집 + 스파크라인 렌더링
|
|
3
|
+
|
|
4
|
+
import { color, dim, MOCHA } from "./ansi.mjs";
|
|
5
|
+
import { sparkline } from "./tui-widgets.mjs";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SYNAPSE_BASE_URL = "http://127.0.0.1:27888";
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
9
|
+
const MAX_METRIC_SAMPLES = 30;
|
|
10
|
+
|
|
11
|
+
// ── SynapseEventStream ────────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Synapse 서버로부터 이벤트를 polling으로 수신
|
|
14
|
+
* @param {object} [opts]
|
|
15
|
+
* @param {string} [opts.baseUrl] - Synapse 서버 URL
|
|
16
|
+
* @param {number} [opts.pollIntervalMs] - polling 간격
|
|
17
|
+
* @param {function} [opts.fetchImpl] - fetch 구현 (테스트용)
|
|
18
|
+
* @param {function} [opts.onEvent] - 이벤트 콜백
|
|
19
|
+
* @param {function} [opts.onError] - 에러 콜백
|
|
20
|
+
*/
|
|
21
|
+
export function createSynapseEventStream(opts = {}) {
|
|
22
|
+
const {
|
|
23
|
+
baseUrl = DEFAULT_SYNAPSE_BASE_URL,
|
|
24
|
+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
25
|
+
onEvent,
|
|
26
|
+
onError,
|
|
27
|
+
} = opts;
|
|
28
|
+
|
|
29
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch?.bind(globalThis);
|
|
30
|
+
let timer = null;
|
|
31
|
+
let lastEventId = 0;
|
|
32
|
+
let running = false;
|
|
33
|
+
|
|
34
|
+
async function poll(force = false) {
|
|
35
|
+
if (!fetchImpl || (!running && !force)) return;
|
|
36
|
+
try {
|
|
37
|
+
const url = new URL("/synapse/events", baseUrl);
|
|
38
|
+
url.searchParams.set("since", String(lastEventId));
|
|
39
|
+
const res = await fetchImpl(url.toString(), {
|
|
40
|
+
method: "GET",
|
|
41
|
+
headers: { accept: "application/json" },
|
|
42
|
+
signal: AbortSignal.timeout?.(5000),
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) return;
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
if (Array.isArray(data.events)) {
|
|
47
|
+
for (const event of data.events) {
|
|
48
|
+
if (event.id > lastEventId) lastEventId = event.id;
|
|
49
|
+
onEvent?.(event);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
onError?.(err);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
start() {
|
|
59
|
+
if (running) return;
|
|
60
|
+
running = true;
|
|
61
|
+
// 즉시 첫 poll + 주기적 반복
|
|
62
|
+
poll();
|
|
63
|
+
timer = setInterval(poll, pollIntervalMs);
|
|
64
|
+
if (timer.unref) timer.unref();
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
stop() {
|
|
68
|
+
running = false;
|
|
69
|
+
if (timer) {
|
|
70
|
+
clearInterval(timer);
|
|
71
|
+
timer = null;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
get isRunning() {
|
|
76
|
+
return running;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** 테스트/수동 용: 즉시 poll 실행 (running 상태 무관) */
|
|
80
|
+
async pollOnce() {
|
|
81
|
+
await poll(true);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
reset() {
|
|
85
|
+
lastEventId = 0;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── MetricsCollector ──────────────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* Synapse 이벤트에서 메트릭(토큰, 지연시간, 성공률)을 수집
|
|
93
|
+
*/
|
|
94
|
+
export function createMetricsCollector(opts = {}) {
|
|
95
|
+
const { maxSamples = MAX_METRIC_SAMPLES } = opts;
|
|
96
|
+
|
|
97
|
+
const metrics = {
|
|
98
|
+
tokenRates: [], // 초당 토큰 소비율
|
|
99
|
+
latencies: [], // 요청 지연시간 (ms)
|
|
100
|
+
successRates: [], // 성공률 (0-1)
|
|
101
|
+
eventCount: 0,
|
|
102
|
+
lastEventAt: 0,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function pushSample(arr, value) {
|
|
106
|
+
arr.push(value);
|
|
107
|
+
if (arr.length > maxSamples) arr.shift();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
/** Synapse 이벤트를 처리하여 메트릭 갱신 */
|
|
112
|
+
ingest(event) {
|
|
113
|
+
if (!event || typeof event !== "object") return;
|
|
114
|
+
metrics.eventCount++;
|
|
115
|
+
metrics.lastEventAt = Date.now();
|
|
116
|
+
|
|
117
|
+
// 토큰 소비율
|
|
118
|
+
if (typeof event.tokens === "number" && event.tokens > 0) {
|
|
119
|
+
const elapsed = event.elapsed || 1;
|
|
120
|
+
pushSample(metrics.tokenRates, event.tokens / elapsed);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 지연시간
|
|
124
|
+
if (typeof event.latencyMs === "number") {
|
|
125
|
+
pushSample(metrics.latencies, event.latencyMs);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 성공률 (이벤트별 ok/fail)
|
|
129
|
+
if (event.status) {
|
|
130
|
+
const ok = event.status === "ok" || event.status === "completed" ? 1 : 0;
|
|
131
|
+
pushSample(metrics.successRates, ok);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/** 현재 메트릭 스냅샷 반환 */
|
|
136
|
+
snapshot() {
|
|
137
|
+
const avgLatency =
|
|
138
|
+
metrics.latencies.length > 0
|
|
139
|
+
? metrics.latencies.reduce((a, b) => a + b, 0) / metrics.latencies.length
|
|
140
|
+
: 0;
|
|
141
|
+
const avgSuccessRate =
|
|
142
|
+
metrics.successRates.length > 0
|
|
143
|
+
? metrics.successRates.reduce((a, b) => a + b, 0) /
|
|
144
|
+
metrics.successRates.length
|
|
145
|
+
: 1;
|
|
146
|
+
const lastTokenRate =
|
|
147
|
+
metrics.tokenRates.length > 0
|
|
148
|
+
? metrics.tokenRates[metrics.tokenRates.length - 1]
|
|
149
|
+
: 0;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
tokenRates: [...metrics.tokenRates],
|
|
153
|
+
latencies: [...metrics.latencies],
|
|
154
|
+
successRates: [...metrics.successRates],
|
|
155
|
+
avgLatency: Math.round(avgLatency),
|
|
156
|
+
avgSuccessRate: Math.round(avgSuccessRate * 100),
|
|
157
|
+
lastTokenRate: Math.round(lastTokenRate),
|
|
158
|
+
eventCount: metrics.eventCount,
|
|
159
|
+
lastEventAt: metrics.lastEventAt,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
reset() {
|
|
164
|
+
metrics.tokenRates.length = 0;
|
|
165
|
+
metrics.latencies.length = 0;
|
|
166
|
+
metrics.successRates.length = 0;
|
|
167
|
+
metrics.eventCount = 0;
|
|
168
|
+
metrics.lastEventAt = 0;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Tier1 메트릭 렌더러 ───────────────────────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* Synapse 메트릭을 Tier1에 표시할 한 줄 문자열로 렌더링
|
|
176
|
+
* @param {object} snapshot - MetricsCollector.snapshot() 결과
|
|
177
|
+
* @param {number} [width=60] - 최대 표시 폭
|
|
178
|
+
* @returns {string} 렌더링된 메트릭 행
|
|
179
|
+
*/
|
|
180
|
+
export function renderMetricsTier1(snapshot, width = 60) {
|
|
181
|
+
if (!snapshot || snapshot.eventCount === 0) {
|
|
182
|
+
return dim("synapse: waiting for events…");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const parts = [];
|
|
186
|
+
|
|
187
|
+
// 토큰 스파크라인
|
|
188
|
+
if (snapshot.tokenRates.length > 0) {
|
|
189
|
+
const spark = sparkline(snapshot.tokenRates, 8, MOCHA.executing);
|
|
190
|
+
parts.push(`tok/s ${spark} ${color(String(snapshot.lastTokenRate), MOCHA.executing)}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 지연시간
|
|
194
|
+
if (snapshot.latencies.length > 0) {
|
|
195
|
+
const latColor =
|
|
196
|
+
snapshot.avgLatency > 1000 ? MOCHA.fail :
|
|
197
|
+
snapshot.avgLatency > 500 ? MOCHA.partial : MOCHA.ok;
|
|
198
|
+
parts.push(`lat ${color(`${snapshot.avgLatency}ms`, latColor)}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 성공률
|
|
202
|
+
if (snapshot.successRates.length > 0) {
|
|
203
|
+
const srColor =
|
|
204
|
+
snapshot.avgSuccessRate < 80 ? MOCHA.fail :
|
|
205
|
+
snapshot.avgSuccessRate < 95 ? MOCHA.partial : MOCHA.ok;
|
|
206
|
+
parts.push(`ok ${color(`${snapshot.avgSuccessRate}%`, srColor)}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 이벤트 카운트
|
|
210
|
+
parts.push(dim(`ev:${snapshot.eventCount}`));
|
|
211
|
+
|
|
212
|
+
return parts.join(dim(" │ "));
|
|
213
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// hub/team/tui-widgets.mjs — UX 리뉴얼 위젯 (ISSUE-14)
|
|
2
|
+
// k9s/lazygit/btop 스타일 위젯: 스파크라인, 검색, 패널 리사이즈
|
|
3
|
+
|
|
4
|
+
import { color, dim, MOCHA } from "./ansi.mjs";
|
|
5
|
+
|
|
6
|
+
// ── 스파크라인 ────────────────────────────────────────────────────────────
|
|
7
|
+
// Unicode block elements for sparkline rendering
|
|
8
|
+
const SPARK_CHARS = "▁▂▃▄▅▆▇█";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 토큰 소비 추이를 미니 스파크라인으로 렌더링
|
|
12
|
+
* @param {number[]} values - 시계열 값 배열
|
|
13
|
+
* @param {number} [width=8] - 표시 폭 (문자 수)
|
|
14
|
+
* @param {string} [fg] - ANSI 색상 시퀀스
|
|
15
|
+
* @returns {string} 스파크라인 문자열
|
|
16
|
+
*/
|
|
17
|
+
export function sparkline(values, width = 8, fg = MOCHA.executing) {
|
|
18
|
+
if (!values || values.length === 0) return dim("─".repeat(width));
|
|
19
|
+
|
|
20
|
+
// 최근 width개만 사용
|
|
21
|
+
const data = values.slice(-width);
|
|
22
|
+
const min = Math.min(...data);
|
|
23
|
+
const max = Math.max(...data);
|
|
24
|
+
const range = max - min || 1;
|
|
25
|
+
|
|
26
|
+
const chars = data.map((v) => {
|
|
27
|
+
const idx = Math.round(((v - min) / range) * (SPARK_CHARS.length - 1));
|
|
28
|
+
return SPARK_CHARS[idx];
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// width보다 짧으면 왼쪽 패딩
|
|
32
|
+
const pad = width - chars.length;
|
|
33
|
+
const padStr = pad > 0 ? dim("─".repeat(pad)) : "";
|
|
34
|
+
return padStr + color(chars.join(""), fg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 워커별 토큰 히스토리 추적기
|
|
39
|
+
* @param {number} [maxSamples=16] - 최대 샘플 수
|
|
40
|
+
*/
|
|
41
|
+
export function createTokenTracker(maxSamples = 16) {
|
|
42
|
+
const histories = new Map();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
/** 워커의 토큰 값을 기록 */
|
|
46
|
+
record(workerName, tokens) {
|
|
47
|
+
if (tokens === null || tokens === undefined || tokens === "") return;
|
|
48
|
+
const numValue =
|
|
49
|
+
typeof tokens === "number" ? tokens : parseFloat(String(tokens));
|
|
50
|
+
if (!Number.isFinite(numValue)) return;
|
|
51
|
+
|
|
52
|
+
let history = histories.get(workerName);
|
|
53
|
+
if (!history) {
|
|
54
|
+
history = [];
|
|
55
|
+
histories.set(workerName, history);
|
|
56
|
+
}
|
|
57
|
+
history.push(numValue);
|
|
58
|
+
if (history.length > maxSamples) history.shift();
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/** 워커의 스파크라인 반환 */
|
|
62
|
+
sparkline(workerName, width = 8) {
|
|
63
|
+
const history = histories.get(workerName);
|
|
64
|
+
return sparkline(history, width);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** 워커의 히스토리 반환 */
|
|
68
|
+
getHistory(workerName) {
|
|
69
|
+
return histories.get(workerName) || [];
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
clear() {
|
|
73
|
+
histories.clear();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 검색 (/ + n/N) ───────────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* 인라인 검색 상태 관리자
|
|
81
|
+
* vim 스타일 / 검색 + n(다음)/N(이전) 탐색
|
|
82
|
+
*/
|
|
83
|
+
export function createSearchState() {
|
|
84
|
+
let query = "";
|
|
85
|
+
let isActive = false;
|
|
86
|
+
let inputBuffer = "";
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
/** 검색 모드 활성화 */
|
|
90
|
+
activate() {
|
|
91
|
+
isActive = true;
|
|
92
|
+
inputBuffer = "";
|
|
93
|
+
query = "";
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** 검색 모드 비활성화 */
|
|
97
|
+
deactivate() {
|
|
98
|
+
isActive = false;
|
|
99
|
+
inputBuffer = "";
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/** 현재 검색 활성 여부 */
|
|
103
|
+
get active() {
|
|
104
|
+
return isActive;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/** 현재 쿼리 */
|
|
108
|
+
get query() {
|
|
109
|
+
return query;
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/** 입력 버퍼 (검색 모드 중 타이핑) */
|
|
113
|
+
get buffer() {
|
|
114
|
+
return inputBuffer;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/** 키 입력 처리. true 반환 시 해당 키를 소비함 */
|
|
118
|
+
handleKey(key) {
|
|
119
|
+
if (!isActive) return false;
|
|
120
|
+
|
|
121
|
+
// Enter: 검색 확정
|
|
122
|
+
if (key === "\r" || key === "\n") {
|
|
123
|
+
query = inputBuffer;
|
|
124
|
+
isActive = false;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Escape: 검색 취소
|
|
129
|
+
if (key === "\x1b") {
|
|
130
|
+
this.deactivate();
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Backspace
|
|
135
|
+
if (key === "\x7f" || key === "\b") {
|
|
136
|
+
inputBuffer = inputBuffer.slice(0, -1);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Ctrl+C: 취소
|
|
141
|
+
if (key === "\u0003") {
|
|
142
|
+
this.deactivate();
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 일반 문자
|
|
147
|
+
if (key.length === 1 && key >= " ") {
|
|
148
|
+
inputBuffer += key;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true; // 검색 모드 중 다른 키는 무시
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 이름 목록에서 쿼리와 매칭되는 인덱스 찾기
|
|
157
|
+
* @param {string[]} names - 워커 이름 배열
|
|
158
|
+
* @param {number} currentIdx - 현재 선택 인덱스
|
|
159
|
+
* @param {number} direction - 1(다음) 또는 -1(이전)
|
|
160
|
+
* @returns {number} 매칭 인덱스 또는 -1
|
|
161
|
+
*/
|
|
162
|
+
findMatch(names, currentIdx, direction = 1) {
|
|
163
|
+
if (!query || names.length === 0) return -1;
|
|
164
|
+
const q = query.toLowerCase();
|
|
165
|
+
const len = names.length;
|
|
166
|
+
for (let i = 1; i <= len; i++) {
|
|
167
|
+
const idx = (currentIdx + i * direction + len) % len;
|
|
168
|
+
if (names[idx].toLowerCase().includes(q)) return idx;
|
|
169
|
+
}
|
|
170
|
+
return -1;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/** 검색 프롬프트 렌더링 */
|
|
174
|
+
renderPrompt(width) {
|
|
175
|
+
if (!isActive) {
|
|
176
|
+
if (query) return dim(` /${query}`);
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
const prompt = `/${inputBuffer}`;
|
|
180
|
+
const cursor = "█";
|
|
181
|
+
return color(prompt + cursor, MOCHA.blue);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── 패널 리사이즈 (H/L) ──────────────────────────────────────────────────
|
|
187
|
+
/**
|
|
188
|
+
* 패널 비율 관리자
|
|
189
|
+
* H: rail 축소, L: rail 확대 (k9s 스타일)
|
|
190
|
+
*/
|
|
191
|
+
export function createPanelResizer(opts = {}) {
|
|
192
|
+
const { minRatio = 0.15, maxRatio = 0.5, step = 0.05 } = opts;
|
|
193
|
+
let railRatio = opts.initialRatio || 0.3;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
get ratio() {
|
|
197
|
+
return railRatio;
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/** rail 비율 축소 (H) — detail 패널 확대 */
|
|
201
|
+
shrinkRail() {
|
|
202
|
+
railRatio = Math.max(minRatio, railRatio - step);
|
|
203
|
+
return railRatio;
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
/** rail 비율 확대 (L) — detail 패널 축소 */
|
|
207
|
+
expandRail() {
|
|
208
|
+
railRatio = Math.min(maxRatio, railRatio + step);
|
|
209
|
+
return railRatio;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/** 비율 리셋 */
|
|
213
|
+
reset() {
|
|
214
|
+
railRatio = opts.initialRatio || 0.3;
|
|
215
|
+
return railRatio;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── vim 모션 헬퍼 ─────────────────────────────────────────────────────────
|
|
221
|
+
/**
|
|
222
|
+
* gg/G 모션을 위한 키 시퀀스 감지
|
|
223
|
+
* 두 번 연속 'g'를 누르면 gg (첫 번째 항목으로 이동)
|
|
224
|
+
*/
|
|
225
|
+
export function createVimMotion() {
|
|
226
|
+
let lastKey = "";
|
|
227
|
+
let lastKeyTime = 0;
|
|
228
|
+
const DOUBLE_TAP_MS = 500;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
/**
|
|
232
|
+
* 키 입력 처리
|
|
233
|
+
* @returns {"gg"|"G"|null} 감지된 모션 또는 null
|
|
234
|
+
*/
|
|
235
|
+
handleKey(key) {
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
|
|
238
|
+
if (key === "g") {
|
|
239
|
+
if (lastKey === "g" && now - lastKeyTime < DOUBLE_TAP_MS) {
|
|
240
|
+
lastKey = "";
|
|
241
|
+
return "gg"; // 첫 번째 항목으로
|
|
242
|
+
}
|
|
243
|
+
lastKey = "g";
|
|
244
|
+
lastKeyTime = now;
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (key === "G") {
|
|
249
|
+
lastKey = "";
|
|
250
|
+
return "G"; // 마지막 항목으로
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
lastKey = "";
|
|
254
|
+
return null;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
reset() {
|
|
258
|
+
lastKey = "";
|
|
259
|
+
lastKeyTime = 0;
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|