@sejuneoh/aitop 0.0.0-stage → 0.21.2
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 -0
- package/README.md +40 -2
- package/bin/aitop.mjs +360 -0
- package/package.json +36 -4
- package/src/attention.mjs +70 -0
- package/src/brand.mjs +30 -0
- package/src/budget.mjs +29 -0
- package/src/cache.mjs +78 -0
- package/src/collect.mjs +51 -0
- package/src/config.mjs +169 -0
- package/src/doctor.mjs +218 -0
- package/src/frames.mjs +24 -0
- package/src/help.mjs +111 -0
- package/src/json.mjs +57 -0
- package/src/keys.mjs +154 -0
- package/src/log.mjs +130 -0
- package/src/notify.mjs +251 -0
- package/src/order.mjs +69 -0
- package/src/pricing.mjs +130 -0
- package/src/procs.mjs +205 -0
- package/src/sources/agy-ledger.mjs +153 -0
- package/src/sources/antigravity.mjs +763 -0
- package/src/sources/claude-ledger.mjs +181 -0
- package/src/sources/claude-tracker.mjs +183 -0
- package/src/sources/claude.mjs +352 -0
- package/src/sources/codex-ledger.mjs +141 -0
- package/src/sources/codex.mjs +374 -0
- package/src/sources/common.mjs +228 -0
- package/src/sources/files.mjs +116 -0
- package/src/sources/gemini.mjs +26 -0
- package/src/sources/patch.mjs +78 -0
- package/src/sqlite.mjs +50 -0
- package/src/tail.mjs +109 -0
- package/src/text.mjs +256 -0
- package/src/turns.mjs +79 -0
- package/src/ui/chrome.mjs +163 -0
- package/src/ui/detail.mjs +360 -0
- package/src/ui/draw.mjs +152 -0
- package/src/ui/glyphs.mjs +58 -0
- package/src/ui/table.mjs +425 -0
- package/src/ui/theme.mjs +137 -0
- package/src/ui.mjs +99 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// 날짜별 사용량 장부: 최근 7일 안에 바뀐 Claude 기록(서브에이전트 포함)에서 사용량만 날짜별로 모은다.
|
|
2
|
+
// Antigravity CLI(agy)·Codex CLI 사용량은 agy-ledger.mjs·codex-ledger.mjs 가 같은 모양으로 모으고, 여기서 함께 더한다.
|
|
3
|
+
// 세션 화면과 상관없이 끝난 세션까지 세야 해서, 세션 tracker 와 따로 가볍게 읽고 캐시도 따로 둔다.
|
|
4
|
+
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { JsonlTail } from '../tail.mjs';
|
|
8
|
+
import { loadCache, markDirty } from '../cache.mjs';
|
|
9
|
+
import { claudeCost } from '../pricing.mjs';
|
|
10
|
+
import { dayKey, lastDays, addSlot, lastHourOf, recentTodayOf } from './common.mjs';
|
|
11
|
+
import { collectAgyLedger } from './agy-ledger.mjs';
|
|
12
|
+
import { collectCodexLedger } from './codex-ledger.mjs';
|
|
13
|
+
import { debugIo } from '../log.mjs';
|
|
14
|
+
|
|
15
|
+
export { dayKey };
|
|
16
|
+
|
|
17
|
+
const HOME = process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude');
|
|
18
|
+
const PROJECTS = join(HOME, 'projects');
|
|
19
|
+
const KIND = 'ledger';
|
|
20
|
+
const DAYS = 7;
|
|
21
|
+
const KEEP_DAYS = 10;
|
|
22
|
+
const LIST_TTL_MS = 30_000;
|
|
23
|
+
const RECENT_IDS = 64;
|
|
24
|
+
const LOADING_MIN_BYTES = 4 * 1024 * 1024;
|
|
25
|
+
|
|
26
|
+
export class UsageTracker {
|
|
27
|
+
constructor(path) {
|
|
28
|
+
this.tail = new JsonlTail(path);
|
|
29
|
+
this.tail.onReset = () => {
|
|
30
|
+
this.recent = new Map();
|
|
31
|
+
this.days = {};
|
|
32
|
+
this.slots = {};
|
|
33
|
+
};
|
|
34
|
+
this.recent = new Map();
|
|
35
|
+
this.days = {};
|
|
36
|
+
// 지출 속도용 5분 칸 (common.mjs addSlot).
|
|
37
|
+
this.slots = {};
|
|
38
|
+
const c = loadCache(path, KIND);
|
|
39
|
+
if (c) {
|
|
40
|
+
this.tail.offset = c.offset;
|
|
41
|
+
this.days = c.days ?? {};
|
|
42
|
+
this.slots = c.slots ?? {};
|
|
43
|
+
this.recent = new Map(c.recent ?? []);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
update(budget) {
|
|
48
|
+
const n = this.tail.read((x) => this.apply(x), budget);
|
|
49
|
+
if (n) markDirty(this.tail.path, () => this.snapshot(), KIND);
|
|
50
|
+
return n;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get pending() {
|
|
54
|
+
return Math.max(0, this.tail.size - this.tail.offset);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
snapshot() {
|
|
58
|
+
const keep = new Set(lastDays(KEEP_DAYS));
|
|
59
|
+
const days = Object.fromEntries(Object.entries(this.days).filter(([k]) => keep.has(k)));
|
|
60
|
+
return { offset: this.tail.lineOffset, days, slots: this.slots, recent: [...this.recent] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 같은 message.id 가 여러 줄로 반복되므로 직전 값과의 차이만, 그 줄의 날짜와 5분 칸에 더한다.
|
|
64
|
+
apply(x) {
|
|
65
|
+
const m = x.message;
|
|
66
|
+
if (x.type !== 'assistant' || !m?.usage || !m.id) return;
|
|
67
|
+
const u = m.usage;
|
|
68
|
+
const n = (o, k) => o?.[k] ?? 0;
|
|
69
|
+
const tokensOf = (o) => n(o, 'input_tokens') + n(o, 'output_tokens') + n(o, 'cache_read_input_tokens') + n(o, 'cache_creation_input_tokens');
|
|
70
|
+
const prev = this.recent.get(m.id);
|
|
71
|
+
const cost = claudeCost(m.model, u);
|
|
72
|
+
const at = Date.parse(x.timestamp) || Date.now();
|
|
73
|
+
const day = (this.days[dayKey(at)] ??= { tokens: 0, cost: 0, costNoCache: 0, unknown: false });
|
|
74
|
+
const dCost = (cost?.total ?? 0) - (prev?.cost ?? 0);
|
|
75
|
+
const dNoCache = (cost?.noCache ?? 0) - (prev?.costNoCache ?? 0);
|
|
76
|
+
const unknown = !cost && tokensOf(u) > 0;
|
|
77
|
+
day.tokens += tokensOf(u) - tokensOf(prev?.u);
|
|
78
|
+
day.cost += dCost;
|
|
79
|
+
day.costNoCache = (day.costNoCache ?? 0) + (cost?.noCache ?? 0) - (prev?.costNoCache ?? 0);
|
|
80
|
+
if (unknown) day.unknown = true;
|
|
81
|
+
addSlot(this.slots, at, dCost, dNoCache, unknown);
|
|
82
|
+
this.recent.delete(m.id);
|
|
83
|
+
this.recent.set(m.id, { u, cost: cost?.total ?? 0, costNoCache: cost?.noCache ?? 0 });
|
|
84
|
+
if (this.recent.size > RECENT_IDS) this.recent.delete(this.recent.keys().next().value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let listed = { at: 0, files: [] };
|
|
89
|
+
|
|
90
|
+
// 최근 7일 안에 바뀐 메인 기록과 서브에이전트 기록.
|
|
91
|
+
function recentFiles() {
|
|
92
|
+
if (Date.now() - listed.at < LIST_TTL_MS) return listed.files;
|
|
93
|
+
const cutoff = Date.now() - DAYS * 86400_000;
|
|
94
|
+
const files = [];
|
|
95
|
+
const add = (p) => {
|
|
96
|
+
try {
|
|
97
|
+
if (statSync(p).mtimeMs >= cutoff) files.push(p);
|
|
98
|
+
} catch {}
|
|
99
|
+
};
|
|
100
|
+
let dirs = [];
|
|
101
|
+
try {
|
|
102
|
+
dirs = readdirSync(PROJECTS);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
debugIo('ledger', 'claude projects read failed', e);
|
|
105
|
+
}
|
|
106
|
+
for (const dir of dirs) {
|
|
107
|
+
let names = [];
|
|
108
|
+
try {
|
|
109
|
+
names = readdirSync(join(PROJECTS, dir));
|
|
110
|
+
} catch (e) {
|
|
111
|
+
debugIo('ledger', 'claude project folder read failed', e, dir);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const n of names) {
|
|
115
|
+
const p = join(PROJECTS, dir, n);
|
|
116
|
+
if (n.endsWith('.jsonl')) add(p);
|
|
117
|
+
else if (existsSync(join(p, 'subagents'))) {
|
|
118
|
+
try {
|
|
119
|
+
for (const s of readdirSync(join(p, 'subagents'))) if (s.endsWith('.jsonl')) add(join(p, 'subagents', s));
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
listed = { at: Date.now(), files };
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const trackers = new Map();
|
|
129
|
+
|
|
130
|
+
// 오늘·최근 7일·최근 1시간 합계 (Claude + agy + Codex). 아직 다 읽지 못했으면 loading 에 진행률(0~1).
|
|
131
|
+
export function collectLedger({ budget, now = Date.now() } = {}) {
|
|
132
|
+
const files = recentFiles();
|
|
133
|
+
const keep = new Set(files);
|
|
134
|
+
for (const p of trackers.keys()) if (!keep.has(p)) trackers.delete(p);
|
|
135
|
+
let pending = 0;
|
|
136
|
+
let size = 0;
|
|
137
|
+
for (const p of files) {
|
|
138
|
+
let t = trackers.get(p);
|
|
139
|
+
if (!t) trackers.set(p, (t = new UsageTracker(p)));
|
|
140
|
+
t.update(budget);
|
|
141
|
+
pending += t.pending;
|
|
142
|
+
size += t.tail.size;
|
|
143
|
+
}
|
|
144
|
+
const others = [collectAgyLedger({ budget, now }), collectCodexLedger({ budget })];
|
|
145
|
+
const books = [...trackers.values()].map((t) => t.days);
|
|
146
|
+
const slots = [...trackers.values()].map((t) => t.slots);
|
|
147
|
+
for (const o of others) {
|
|
148
|
+
pending += o.pending;
|
|
149
|
+
size += o.size;
|
|
150
|
+
books.push(...o.days);
|
|
151
|
+
slots.push(...o.slots);
|
|
152
|
+
}
|
|
153
|
+
const sum = (keys) => {
|
|
154
|
+
const out = { tokens: 0, cost: 0, costNoCache: 0, unknown: false };
|
|
155
|
+
for (const days of books)
|
|
156
|
+
for (const k of keys) {
|
|
157
|
+
const d = days[k];
|
|
158
|
+
if (!d) continue;
|
|
159
|
+
out.tokens += d.tokens;
|
|
160
|
+
out.cost += d.cost;
|
|
161
|
+
out.costNoCache += d.costNoCache ?? 0;
|
|
162
|
+
out.unknown ||= d.unknown;
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
today: sum([dayKey(now)]),
|
|
168
|
+
week: sum(lastDays(DAYS, now)),
|
|
169
|
+
// 7일 막대용 날짜별 비용. 오래된 날부터 오늘까지 (읽을 때 날짜별 합계에서 만든다).
|
|
170
|
+
days7: lastDays(DAYS, now).reverse().map((date) => {
|
|
171
|
+
const d = sum([date]);
|
|
172
|
+
return { date, cost: d.cost, costNoCache: d.costNoCache, unknown: d.unknown };
|
|
173
|
+
}),
|
|
174
|
+
lastHour: lastHourOf(slots, now),
|
|
175
|
+
recentToday: recentTodayOf(slots, now),
|
|
176
|
+
// 살아 있는 세션은 계속 조금씩 늘어나므로, 많이 밀렸을 때만 '집계 중' 으로 본다.
|
|
177
|
+
loading: pending > LOADING_MIN_BYTES && size ? 1 - pending / size : null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const _test = { lastDays };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Claude 대화 기록 한 개(메인 세션 또는 서브에이전트)를 따라 읽으며 토큰·비용·진행 단계를 누적한다.
|
|
2
|
+
import { JsonlTail } from '../tail.mjs';
|
|
3
|
+
import { loadCache, markDirty } from '../cache.mjs';
|
|
4
|
+
import { claudeCost } from '../pricing.mjs';
|
|
5
|
+
import { addActivity, emptyTokens, pushEvent, describeTool, toolDetail, isNoisePrompt, parseTs } from './common.mjs';
|
|
6
|
+
import { addChange } from './files.mjs';
|
|
7
|
+
|
|
8
|
+
const MAX_TURNS = 50;
|
|
9
|
+
|
|
10
|
+
const RECENT_IDS = 64;
|
|
11
|
+
const LOADING_MIN_BYTES = 4 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
export class ClaudeTracker {
|
|
14
|
+
constructor(path) {
|
|
15
|
+
this.tail = new JsonlTail(path);
|
|
16
|
+
this.tail.onReset = () => this.reset();
|
|
17
|
+
this.reset();
|
|
18
|
+
this.title = null;
|
|
19
|
+
this.branch = null;
|
|
20
|
+
|
|
21
|
+
const c = loadCache(path);
|
|
22
|
+
if (c) {
|
|
23
|
+
this.tail.offset = c.offset;
|
|
24
|
+
Object.assign(this, { tokens: c.tokens, cost: c.cost, costNoCache: c.costNoCache, costUnknown: c.costUnknown, context: c.context, model: c.model, title: c.title, branch: c.branch, events: c.events, lastTool: c.lastTool, lastKind: c.lastKind, files: c.files ?? [], turns: c.turns ?? [], turnCount: c.turnCount ?? 0, firstAt: c.firstAt ?? null, activity: c.activity ?? {} });
|
|
25
|
+
this.recent = new Map(c.recent);
|
|
26
|
+
this.pending = new Map(c.pending);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 누적값 초기화. 기록 파일이 줄어 처음부터 다시 읽을 때도 쓴다.
|
|
31
|
+
reset() {
|
|
32
|
+
this.recent = new Map();
|
|
33
|
+
this.tokens = emptyTokens();
|
|
34
|
+
this.cost = 0;
|
|
35
|
+
this.costNoCache = 0;
|
|
36
|
+
// 가격표에 없는 모델의 사용량이 섞였는지. 그 몫은 비용에 넣지 않고 화면에 '?' 로 알린다.
|
|
37
|
+
this.costUnknown = false;
|
|
38
|
+
this.context = 0;
|
|
39
|
+
this.model = null;
|
|
40
|
+
this.events = [];
|
|
41
|
+
this.lastTool = null;
|
|
42
|
+
this.pending = new Map();
|
|
43
|
+
this.lastKind = null;
|
|
44
|
+
// Edit/Write 로 고친 파일 (files.mjs)
|
|
45
|
+
this.files = [];
|
|
46
|
+
// 사용자 지시: { n: 몇 번째 지시, at, text }. 변경 파일을 지시 단위로 나눌 때 쓴다.
|
|
47
|
+
this.turns = [];
|
|
48
|
+
this.turnCount = 0;
|
|
49
|
+
// 기록의 첫 시각. 서브에이전트 편집을 지시에 나눌 때 쓴다.
|
|
50
|
+
this.firstAt = null;
|
|
51
|
+
// 시간별 토큰 (common.mjs addActivity)
|
|
52
|
+
this.activity = {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get path() {
|
|
56
|
+
return this.tail.path;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get mtimeMs() {
|
|
60
|
+
return this.tail.mtimeMs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 아직 읽지 않은 양이 많으면 0~1 사이 진행률, 거의 다 읽었으면 null.
|
|
64
|
+
// 새로 붙은 몇 줄을 다음 갱신으로 미룬 것까지 '읽는 중' 으로 보이면 실제 진행 단계가 가려진다.
|
|
65
|
+
get loading() {
|
|
66
|
+
return this.tail.size - this.tail.offset > LOADING_MIN_BYTES ? this.tail.progress : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
update(budget) {
|
|
70
|
+
const n = this.tail.read((x) => this.apply(x), budget);
|
|
71
|
+
if (n) markDirty(this.path, () => this.snapshot());
|
|
72
|
+
return n;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
snapshot() {
|
|
76
|
+
return {
|
|
77
|
+
offset: this.tail.lineOffset,
|
|
78
|
+
tokens: this.tokens,
|
|
79
|
+
cost: this.cost,
|
|
80
|
+
costNoCache: this.costNoCache,
|
|
81
|
+
costUnknown: this.costUnknown,
|
|
82
|
+
context: this.context,
|
|
83
|
+
model: this.model,
|
|
84
|
+
title: this.title,
|
|
85
|
+
branch: this.branch,
|
|
86
|
+
events: this.events,
|
|
87
|
+
lastTool: this.lastTool,
|
|
88
|
+
lastKind: this.lastKind,
|
|
89
|
+
recent: [...this.recent],
|
|
90
|
+
pending: [...this.pending],
|
|
91
|
+
files: this.files,
|
|
92
|
+
turns: this.turns,
|
|
93
|
+
turnCount: this.turnCount,
|
|
94
|
+
firstAt: this.firstAt,
|
|
95
|
+
activity: this.activity,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 같은 message.id 가 블록마다 여러 줄로 반복되므로, 직전 값과의 차이만 더한다.
|
|
100
|
+
addUsage(id, model, u, at) {
|
|
101
|
+
const prev = this.recent.get(id);
|
|
102
|
+
const n = (x, k) => x?.[k] ?? 0;
|
|
103
|
+
const t = this.tokens;
|
|
104
|
+
const before = t.total;
|
|
105
|
+
t.input += n(u, 'input_tokens') - n(prev?.u, 'input_tokens');
|
|
106
|
+
t.output += n(u, 'output_tokens') - n(prev?.u, 'output_tokens');
|
|
107
|
+
t.cacheRead += n(u, 'cache_read_input_tokens') - n(prev?.u, 'cache_read_input_tokens');
|
|
108
|
+
t.cacheWrite += n(u, 'cache_creation_input_tokens') - n(prev?.u, 'cache_creation_input_tokens');
|
|
109
|
+
t.total = t.input + t.output + t.cacheRead + t.cacheWrite;
|
|
110
|
+
addActivity(this.activity, at, t.total - before);
|
|
111
|
+
const cost = claudeCost(model, u);
|
|
112
|
+
const used = n(u, 'input_tokens') + n(u, 'output_tokens') + n(u, 'cache_read_input_tokens') + n(u, 'cache_creation_input_tokens');
|
|
113
|
+
if (!cost && used > 0) this.costUnknown = true;
|
|
114
|
+
this.cost += (cost?.total ?? 0) - (prev?.cost?.total ?? 0);
|
|
115
|
+
this.costNoCache += (cost?.noCache ?? 0) - (prev?.cost?.noCache ?? 0);
|
|
116
|
+
this.recent.delete(id);
|
|
117
|
+
this.recent.set(id, { u, cost });
|
|
118
|
+
if (this.recent.size > RECENT_IDS) this.recent.delete(this.recent.keys().next().value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
apply(x) {
|
|
122
|
+
if (x.type === 'ai-title' && x.aiTitle) this.title = x.aiTitle;
|
|
123
|
+
if (x.gitBranch) this.branch = x.gitBranch;
|
|
124
|
+
if (this.firstAt == null && x.timestamp) this.firstAt = parseTs(x.timestamp);
|
|
125
|
+
if (x.isMeta) return;
|
|
126
|
+
|
|
127
|
+
if (x.type === 'assistant' && x.message) {
|
|
128
|
+
const m = x.message;
|
|
129
|
+
if (m.model && !m.model.startsWith('<')) this.model = m.model;
|
|
130
|
+
if (m.usage && m.id) {
|
|
131
|
+
this.addUsage(m.id, m.model ?? this.model, m.usage, parseTs(x.timestamp));
|
|
132
|
+
this.context = (m.usage.input_tokens ?? 0) + (m.usage.cache_read_input_tokens ?? 0) + (m.usage.cache_creation_input_tokens ?? 0);
|
|
133
|
+
}
|
|
134
|
+
for (const c of Array.isArray(m.content) ? m.content : []) {
|
|
135
|
+
if (c.type === 'tool_use') {
|
|
136
|
+
this.lastTool = describeTool(c.name, c.input);
|
|
137
|
+
this.pending.set(c.id, { name: c.name, what: this.lastTool, detail: toolDetail(c.name, c.input), agent: c.input?.subagent_type ?? null });
|
|
138
|
+
pushEvent(this, x.timestamp, 'tool', this.lastTool);
|
|
139
|
+
this.lastKind = 'tool';
|
|
140
|
+
} else if (c.type === 'text' && c.text?.trim()) {
|
|
141
|
+
pushEvent(this, x.timestamp, 'reply', c.text);
|
|
142
|
+
this.lastKind = 'reply';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (x.type === 'user' && x.message) {
|
|
148
|
+
if (x.toolUseResult) addChange(this.files, x.toolUseResult, x.timestamp);
|
|
149
|
+
const c = x.message.content;
|
|
150
|
+
if (Array.isArray(c)) for (const p of c) if (p.type === 'tool_result') this.pending.delete(p.tool_use_id);
|
|
151
|
+
const text = typeof c === 'string' ? c : Array.isArray(c) ? c.filter((p) => p.type === 'text').map((p) => p.text).join(' ') : '';
|
|
152
|
+
if (text && !isNoisePrompt(text)) {
|
|
153
|
+
const e = pushEvent(this, x.timestamp, 'prompt', text);
|
|
154
|
+
if (e) {
|
|
155
|
+
this.turns.push({ n: ++this.turnCount, at: e.at, text: e.text.slice(0, 120) });
|
|
156
|
+
if (this.turns.length > MAX_TURNS) this.turns.shift();
|
|
157
|
+
}
|
|
158
|
+
this.lastKind = 'prompt';
|
|
159
|
+
this.pending.clear();
|
|
160
|
+
} else if (Array.isArray(c) && c.some((p) => p.type === 'tool_result')) this.lastKind = 'result';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 허락을 기다리는 동안 보일 요청 내용. 열린 도구 호출 가운데 대기 문구(approve Bash 등)에 이름이 나온 것을 먼저,
|
|
165
|
+
// 없으면 가장 최근 것을 고른다. 팀원(Agent·Task) 호출만 열려 있으면 없음.
|
|
166
|
+
waitDetail(waitingFor = '') {
|
|
167
|
+
const tools = [...this.pending.values()].filter((p) => p.name !== 'Agent' && p.name !== 'Task');
|
|
168
|
+
const words = new Set(String(waitingFor ?? '').split(/[^A-Za-z0-9_]+/).filter(Boolean));
|
|
169
|
+
const named = tools.filter((p) => words.has(p.name));
|
|
170
|
+
return (named.at(-1) ?? tools.at(-1))?.detail ?? null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 지금 무엇을 하는 단계인지: 도구 실행 / 팀원 대기 / 생각 중 / 답변 작성 중.
|
|
174
|
+
phase() {
|
|
175
|
+
const open = [...this.pending.values()];
|
|
176
|
+
const agents = open.filter((p) => p.name === 'Agent' || p.name === 'Task');
|
|
177
|
+
const tools = open.filter((p) => p.name !== 'Agent' && p.name !== 'Task');
|
|
178
|
+
if (tools.length) return { kind: 'tool', text: `도구 실행 · ${tools.at(-1).name}` };
|
|
179
|
+
if (agents.length) return { kind: 'team', text: `팀원 ${agents.length}명 작업 중` };
|
|
180
|
+
if (this.lastKind === 'prompt' || this.lastKind === 'result') return { kind: 'think', text: '생각 중' };
|
|
181
|
+
return { kind: 'write', text: '답변 작성 중' };
|
|
182
|
+
}
|
|
183
|
+
}
|