agentlas 0.5.5 → 0.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/README.md +73 -0
- package/engine/agentlas-capabilities.cjs +34 -3
- package/engine/agentlas-composer.cjs +3 -0
- package/engine/agentlas-experience-exchange.cjs +1401 -0
- package/engine/agentlas-experience-mcp.cjs +1147 -0
- package/engine/agentlas-i18n.cjs +16 -0
- package/engine/agentlas-input.cjs +2 -0
- package/engine/agentlas-repl.cjs +161 -38
- package/engine/agentlas-ui.cjs +5 -1
- package/engine/agentlas.cjs +348 -53
- package/package.json +1 -1
- package/test/cloud-save-publish.cjs +34 -0
- package/test/engine-hardening-regression.cjs +74 -0
- package/test/experience-exchange-contract.cjs +569 -0
- package/test/experience-mcp-contract.cjs +391 -0
- package/test/fixtures/portable-experience-bundle-v1-golden.json +124 -0
- package/test/route-regression.cjs +357 -0
- package/test/runtime-env-protection.cjs +45 -1
- package/test/smoke.sh +4 -0
- package/test/terminal-ui-regression.cjs +26 -3
package/engine/agentlas-i18n.cjs
CHANGED
|
@@ -111,6 +111,14 @@ const STRINGS = {
|
|
|
111
111
|
"team.usage": "usage: /team · /team <agent> <claude-code|codex|gemini|auto>",
|
|
112
112
|
"team.set": "%s → %s",
|
|
113
113
|
"routedImage": "routed to %s for image support",
|
|
114
|
+
"usageBar": "tokens",
|
|
115
|
+
"config.title": "Engine auto engagement — explicit on/off (default: off)",
|
|
116
|
+
"config.storm": "Stormbreaker auto-engage on direct-routed real work",
|
|
117
|
+
"config.network": "hep-network auto-borrow on direct-routed real work",
|
|
118
|
+
"config.usage": "usage: /config storm|network on|off · explicit /storm and /network always work",
|
|
119
|
+
"config.set": "%s → %s",
|
|
120
|
+
"config.autoEngage": "auto-engaging %s (opted in via /config — turn off with /config %s off)",
|
|
121
|
+
"help.config": "explicit on/off for Stormbreaker/hep-network auto engagement",
|
|
114
122
|
"guard.imageWarn": "this agent works with images, but %s can't generate them — try /runtime gemini (or codex), or /team to pin one",
|
|
115
123
|
"multimodal.title": "Multimodal fallback",
|
|
116
124
|
"multimodal.set": "multimodal %s → %s",
|
|
@@ -317,6 +325,14 @@ const STRINGS = {
|
|
|
317
325
|
"team.usage": "사용법: /team · /team <에이전트> <claude-code|codex|gemini|auto>",
|
|
318
326
|
"team.set": "%s → %s",
|
|
319
327
|
"routedImage": "이미지 지원을 위해 %s로 라우팅",
|
|
328
|
+
"usageBar": "토큰",
|
|
329
|
+
"config.title": "엔진 자동 개입 — 명시적 on/off (기본: off)",
|
|
330
|
+
"config.storm": "직답 라우팅된 실작업에 Stormbreaker 자동 개입",
|
|
331
|
+
"config.network": "직답 라우팅된 실작업에 hep-network 자동 빌림",
|
|
332
|
+
"config.usage": "사용법: /config storm|network on|off · /storm·/network 명시 실행은 항상 가능",
|
|
333
|
+
"config.set": "%s → %s",
|
|
334
|
+
"config.autoEngage": "%s 자동 개입 (/config로 켜져 있음 — 해제: /config %s off)",
|
|
335
|
+
"help.config": "Stormbreaker/hep-network 자동 개입 명시적 on/off",
|
|
320
336
|
"guard.imageWarn": "이 에이전트는 이미지 작업인데 %s는 이미지 생성 불가 — /runtime gemini(또는 codex), 또는 /team으로 고정",
|
|
321
337
|
"multimodal.title": "멀티모달 fallback",
|
|
322
338
|
"multimodal.set": "멀티모달 %s → %s",
|
|
@@ -89,6 +89,7 @@ const SLASH_COMMAND_META = [
|
|
|
89
89
|
{ command: "/permission", description: "Set read/write/full permission", category: "Settings", usage: "/permission full", detail: "No argument shows what read, write, and full mean.", aliases: ["/perm"] },
|
|
90
90
|
{ command: "/permissions", description: "Show or set current permission", category: "Settings", usage: "/permissions", detail: "Codex-style permission screen for Agentlas read/write/full." },
|
|
91
91
|
{ command: "/setup", description: "Run first-time setup again", category: "Settings", usage: "/setup", detail: "Re-run language, runtime, and default permission setup in-place." },
|
|
92
|
+
{ command: "/config", description: "Explicit on/off for Stormbreaker and hep-network auto engagement", category: "Settings", usage: "/config [storm|network] [on|off]", detail: "Both engines default to off — no automatic activation. Turn one on to let direct-routed real-work prompts engage it; /storm and /network stay available explicitly either way.", examples: ["/config", "/config storm on", "/config network off"], aliases: ["/toggles"] },
|
|
92
93
|
{ command: "/cwd", description: "Show or change the working folder", category: "Files", usage: "/cwd <path>", detail: "Change the folder used for tools, file mentions, and local commands." },
|
|
93
94
|
{ command: "/memory", description: "Show the memory injected into this run", category: "Context", usage: "/memory", detail: "Print the project memory that Agentlas adds to agent turns." },
|
|
94
95
|
{ command: "/side", description: "Ask a side question without saving it to chat context", category: "Context", usage: "/side <question>", detail: "Runs a one-off answer using current context, then returns without appending to chat history.", aliases: ["/btw"] },
|
|
@@ -137,6 +138,7 @@ const HELP_KEY_BY_COMMAND = {
|
|
|
137
138
|
"/permission": "help.permission",
|
|
138
139
|
"/permissions": "help.permissions",
|
|
139
140
|
"/setup": "help.setup",
|
|
141
|
+
"/config": "help.config",
|
|
140
142
|
"/cwd": "help.cwd",
|
|
141
143
|
"/memory": "help.memory",
|
|
142
144
|
"/side": "help.side",
|
package/engine/agentlas-repl.cjs
CHANGED
|
@@ -301,6 +301,7 @@ function startRepl(opts) {
|
|
|
301
301
|
try {
|
|
302
302
|
ui.beginTurn({
|
|
303
303
|
...composerMeta(),
|
|
304
|
+
usage: () => usageSummaryLine(), // 턴 중에도 최신 사용량을 footer에 라이브 반영
|
|
304
305
|
onInterrupt: () => {
|
|
305
306
|
if (!currentAbort || currentAbort.signal.aborted) return;
|
|
306
307
|
currentAbort.abort();
|
|
@@ -325,11 +326,16 @@ function startRepl(opts) {
|
|
|
325
326
|
: state.subject.system;
|
|
326
327
|
state.routePreambleOnce = null;
|
|
327
328
|
const sys = H.augmentSystem(db, subjectSystem, ctx, false);
|
|
329
|
+
// claude(--resume)·codex(resume <thread_id>)만 서버측에서 대화를 이어받아 이전 턴의
|
|
330
|
+
// 시스템 프롬프트를 유지한다(native-host.cjs claudeArgs/codexArgs). gemini CLI는 resume
|
|
331
|
+
// 플래그가 없어 매 턴이 새 프로세스 — session.id로 시스템 프롬프트를 비우면 2턴째부터
|
|
332
|
+
// 페르소나가 통째로 사라진다. 그래서 gemini는 매 턴 시스템 프롬프트를 다시 보낸다.
|
|
333
|
+
const resumesServerSide = rt.kind === "claude-code" || rt.kind === "codex";
|
|
328
334
|
const res = await runNativeTurn({
|
|
329
335
|
kind: rt.kind,
|
|
330
336
|
bin,
|
|
331
337
|
prompt,
|
|
332
|
-
systemPrompt: session.id ? "" : sys,
|
|
338
|
+
systemPrompt: session.id && resumesServerSide ? "" : sys,
|
|
333
339
|
cwd: state.cwd,
|
|
334
340
|
permission: state.permission,
|
|
335
341
|
session,
|
|
@@ -466,6 +472,23 @@ function startRepl(opts) {
|
|
|
466
472
|
state.routePreambleOnce = null;
|
|
467
473
|
applyRuntimeFor(state.subject);
|
|
468
474
|
}
|
|
475
|
+
// 직답 모드 — 전문 에이전트 확신이 없을 때. 페르소나 없음 + 능력(이미지) 라우팅 없음:
|
|
476
|
+
// 런타임은 세션 기본(baseRuntime) 그대로라, 일반 질문이 gemini 등으로 끌려가지 않는다.
|
|
477
|
+
function setSubjectDirect() {
|
|
478
|
+
if (state.subject && state.subject.kind === "direct") return; // 연속 직답 — 세션/히스토리 유지
|
|
479
|
+
state.subject = {
|
|
480
|
+
kind: "direct",
|
|
481
|
+
id: null,
|
|
482
|
+
slug: "agentlas-direct",
|
|
483
|
+
label: ui.lang === "ko" ? "Agentlas 직답" : "Agentlas direct",
|
|
484
|
+
system: H.directSystemPrompt ? H.directSystemPrompt(ui.lang) : "You are the Agentlas terminal's default assistant.",
|
|
485
|
+
capAgent: null,
|
|
486
|
+
};
|
|
487
|
+
state.history = [];
|
|
488
|
+
state.routePreambleOnce = null;
|
|
489
|
+
state.runtime = baseRuntime;
|
|
490
|
+
state.native = {};
|
|
491
|
+
}
|
|
469
492
|
function setSubjectFirm(firm) {
|
|
470
493
|
const sys = H.firmSystemPrompt(db, firm);
|
|
471
494
|
state.subject = {
|
|
@@ -474,7 +497,7 @@ function startRepl(opts) {
|
|
|
474
497
|
slug: firm.slug,
|
|
475
498
|
label: displayName(firm) + " CEO",
|
|
476
499
|
system: sys,
|
|
477
|
-
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, system_prompt: sys },
|
|
500
|
+
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, tagline_en: firm.tagline_en, entity_kind: "team", system_prompt: sys },
|
|
478
501
|
};
|
|
479
502
|
state.history = [];
|
|
480
503
|
state.routePreambleOnce = null;
|
|
@@ -721,6 +744,31 @@ function startRepl(opts) {
|
|
|
721
744
|
case "runtime":
|
|
722
745
|
setRuntime(arg);
|
|
723
746
|
return true;
|
|
747
|
+
case "config":
|
|
748
|
+
case "toggles": {
|
|
749
|
+
// 클로드코드 /config 스타일 — 자동 엔진 개입은 전부 명시적 opt-in (기본 off).
|
|
750
|
+
const ENGINE_FLAGS = [
|
|
751
|
+
{ key: "storm", pref: "autoStorm", label: ui.t("config.storm") },
|
|
752
|
+
{ key: "network", pref: "autoNetwork", label: ui.t("config.network") },
|
|
753
|
+
];
|
|
754
|
+
const showConfig = () => {
|
|
755
|
+
ui.line("");
|
|
756
|
+
ui.rule(ui.t("config.title"));
|
|
757
|
+
for (const f of ENGINE_FLAGS) {
|
|
758
|
+
const on = !!prefs[f.pref];
|
|
759
|
+
ui.line(" " + (on ? ui.c.green("● on ") : ui.c.dim("○ off")) + " " + ui.c.emerald(f.key.padEnd(9)) + ui.c.dim(f.label));
|
|
760
|
+
}
|
|
761
|
+
ui.line(" " + ui.c.faint(ui.t("config.usage")));
|
|
762
|
+
};
|
|
763
|
+
const [cfgKey, cfgVal] = arg.trim().split(/\s+/);
|
|
764
|
+
if (!cfgKey) return showConfig(), true;
|
|
765
|
+
const flag = ENGINE_FLAGS.find((f) => f.key === cfgKey.toLowerCase());
|
|
766
|
+
if (!flag || !/^(on|off)$/i.test(cfgVal || "")) return ui.warn(ui.t("config.usage")), true;
|
|
767
|
+
prefs[flag.pref] = /^on$/i.test(cfgVal);
|
|
768
|
+
if (opts.savePrefs) opts.savePrefs(prefs);
|
|
769
|
+
ui.ok(ui.t("config.set", flag.key, prefs[flag.pref] ? "on" : "off"));
|
|
770
|
+
return showConfig(), true;
|
|
771
|
+
}
|
|
724
772
|
case "storm": {
|
|
725
773
|
if (!arg) return ui.warn("usage: /storm <goal> [--research]"), true;
|
|
726
774
|
let goal = arg;
|
|
@@ -735,7 +783,13 @@ function startRepl(opts) {
|
|
|
735
783
|
case "build": {
|
|
736
784
|
if (!arg) return ui.warn("usage: /build <만들고 싶은 에이전트/팀 설명>"), true;
|
|
737
785
|
ui.line("");
|
|
738
|
-
|
|
786
|
+
if (typeof H.terminalBuild !== "function") throw new Error("Terminal MCP build preflight is unavailable");
|
|
787
|
+
await H.terminalBuild(db, arg, {
|
|
788
|
+
cwd: state.cwd,
|
|
789
|
+
input: process.stdin,
|
|
790
|
+
promptOutput: process.stderr,
|
|
791
|
+
out: (line) => ui.line(String(line)),
|
|
792
|
+
});
|
|
739
793
|
return true;
|
|
740
794
|
}
|
|
741
795
|
case "route": {
|
|
@@ -756,14 +810,6 @@ function startRepl(opts) {
|
|
|
756
810
|
else await H.hepRun(["hep-search", arg], { cwd: state.cwd });
|
|
757
811
|
return true;
|
|
758
812
|
}
|
|
759
|
-
case "install": {
|
|
760
|
-
if (!arg) return ui.warn("usage: /install <slug> (/search 로 먼저 찾으세요)"), true;
|
|
761
|
-
if (H.cloudInstall) {
|
|
762
|
-
try { const r = await H.cloudInstall(db, arg.trim()); ui.ok((r && r.name) ? `설치됨: ${r.name}` : `설치됨: ${arg.trim()}`); }
|
|
763
|
-
catch (e) { ui.error((e && e.message) || String(e)); }
|
|
764
|
-
} else ui.warn("설치 기능을 사용할 수 없습니다.");
|
|
765
|
-
return true;
|
|
766
|
-
}
|
|
767
813
|
case "network":
|
|
768
814
|
case "taskforce": {
|
|
769
815
|
if (!arg) return ui.warn("usage: /network <요청> (A2A 태스크포스)"), true;
|
|
@@ -976,13 +1022,23 @@ function startRepl(opts) {
|
|
|
976
1022
|
const s = n >= 1 && n <= list.length ? list[n - 1] : null;
|
|
977
1023
|
if (!s) return ui.warn(ui.t("resume.noNum")), true;
|
|
978
1024
|
const agent = H.resolveAgent(db, s.agentSlug);
|
|
979
|
-
if (
|
|
980
|
-
|
|
981
|
-
if (s.
|
|
1025
|
+
if (agent) {
|
|
1026
|
+
setSubjectAgent(agent);
|
|
1027
|
+
} else if (s.agentSlug === "agentlas-direct") {
|
|
1028
|
+
setSubjectDirect(); // 직답 세션도 이어서 재개
|
|
1029
|
+
} else {
|
|
1030
|
+
return ui.error(ui.t("noAgent", s.agentSlug)), true;
|
|
1031
|
+
}
|
|
1032
|
+
// 저장된 CLI 런타임이 실제로 설치돼 있을 때만 그 세션을 복원한다. s.kind가 없거나
|
|
1033
|
+
// (손상된 resume 레코드) 런타임이 미설치면, 잘못된/"undefined" 키로 native 맵을
|
|
1034
|
+
// 덮어써 현재 세션을 잃는 것을 막고 새 대화로 시작한다.
|
|
1035
|
+
if (s.kind && s.sessionId && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
|
|
982
1036
|
state.runtime = { mode: "cli", kind: s.kind };
|
|
983
1037
|
baseRuntime = state.runtime;
|
|
1038
|
+
state.native = { [s.kind]: { id: s.sessionId } };
|
|
1039
|
+
} else {
|
|
1040
|
+
state.native = {};
|
|
984
1041
|
}
|
|
985
|
-
state.native = { [s.kind]: { id: s.sessionId } };
|
|
986
1042
|
try {
|
|
987
1043
|
const fs2 = require("node:fs");
|
|
988
1044
|
if (s.cwd && fs2.existsSync(s.cwd)) state.cwd = s.cwd;
|
|
@@ -1097,10 +1153,14 @@ function startRepl(opts) {
|
|
|
1097
1153
|
if (H.autoRouteAgent) {
|
|
1098
1154
|
const choice = H.autoRouteAgent(db, t, ui.lang);
|
|
1099
1155
|
if (choice) {
|
|
1100
|
-
|
|
1156
|
+
if (choice.direct) {
|
|
1157
|
+
setSubjectDirect();
|
|
1158
|
+
} else {
|
|
1159
|
+
setSubjectAgent(choice.agent);
|
|
1160
|
+
}
|
|
1101
1161
|
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1102
|
-
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${
|
|
1103
|
-
routingNote(state.subject);
|
|
1162
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${state.subject.label}`);
|
|
1163
|
+
if (!choice.direct) routingNote(state.subject);
|
|
1104
1164
|
await runTurn(t);
|
|
1105
1165
|
return ask();
|
|
1106
1166
|
}
|
|
@@ -1120,37 +1180,97 @@ function startRepl(opts) {
|
|
|
1120
1180
|
await handleSlash(t); // /exit 는 내부에서 process.exit
|
|
1121
1181
|
return;
|
|
1122
1182
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1183
|
+
// 직답 모드는 대상 고정이 아니라 "자동 라우팅 유지" — 매 메시지 재라우팅해,
|
|
1184
|
+
// 나중에 전문 요청이 오면 해당 에이전트로 자연스럽게 넘어간다.
|
|
1185
|
+
const directMode = !!(state.subject && state.subject.kind === "direct");
|
|
1186
|
+
if (!state.subject || directMode) {
|
|
1187
|
+
if (!state.subject) {
|
|
1188
|
+
const ags = H.listAgents(db);
|
|
1189
|
+
const single = !/\s/.test(t);
|
|
1190
|
+
if (/^\d+$/.test(t)) {
|
|
1191
|
+
const n = parseInt(t, 10);
|
|
1192
|
+
if (n >= 1 && n <= ags.length) {
|
|
1193
|
+
setSubjectAgent(ags[n - 1]);
|
|
1194
|
+
ui.ok(ui.t("switched", state.subject.label));
|
|
1195
|
+
routingNote(state.subject);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (single) {
|
|
1200
|
+
const a = H.resolveAgent(db, t);
|
|
1201
|
+
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1202
|
+
const f = H.resolveFirm(db, t);
|
|
1203
|
+
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1133
1204
|
}
|
|
1134
|
-
}
|
|
1135
|
-
if (single) {
|
|
1136
|
-
const a = H.resolveAgent(db, t);
|
|
1137
|
-
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1138
|
-
const f = H.resolveFirm(db, t);
|
|
1139
|
-
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1140
1205
|
}
|
|
1141
1206
|
if (H.autoRouteAgent) {
|
|
1142
1207
|
const choice = H.autoRouteAgent(db, t, ui.lang);
|
|
1143
1208
|
if (choice) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1209
|
+
if (choice.direct) {
|
|
1210
|
+
setSubjectDirect();
|
|
1211
|
+
if (!directMode) {
|
|
1212
|
+
// 첫 직답 진입에만 알림 — 연속 직답 대화에서는 조용히.
|
|
1213
|
+
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1214
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `direct answer (no agent)`);
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
setSubjectAgent(choice.agent);
|
|
1218
|
+
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1219
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
|
|
1220
|
+
routingNote(state.subject);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
// 자동 엔진 개입은 명시적 opt-in 전용 (/config, 기본 off): direct 판정 + 실작업형 프롬프트일 때만.
|
|
1225
|
+
if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
|
|
1226
|
+
if (prefs.autoStorm && H.stormRun) {
|
|
1227
|
+
ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
|
|
1228
|
+
await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (prefs.autoNetwork && H.hepRun) {
|
|
1232
|
+
ui.info(ui.t("config.autoEngage", "hep-network", "network"));
|
|
1233
|
+
ui.line("");
|
|
1234
|
+
await H.hepRun(["hep-network", t, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
|
|
1235
|
+
return;
|
|
1148
1236
|
}
|
|
1149
1237
|
}
|
|
1150
1238
|
}
|
|
1151
1239
|
await runTurn(expandMentions(t));
|
|
1152
1240
|
}
|
|
1153
1241
|
|
|
1242
|
+
// 실작업(goal)형 프롬프트 감지 — 질문/잡담에는 자동 엔진(storm/network)을 절대 걸지 않는다.
|
|
1243
|
+
function goalLikePrompt(t) {
|
|
1244
|
+
const s = String(t || "").trim();
|
|
1245
|
+
if (s.length < 12) return false;
|
|
1246
|
+
if (/[??]\s*$/.test(s)) return false;
|
|
1247
|
+
return /(해줘|해라|만들|구현|수정|배포|정리|작성|분석|리팩터|고쳐|추가|빌드|테스트|돌려|실행|자동화|automate|build|implement|fix|refactor|deploy|create|write|run|ship)/i.test(s);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// ── 연결 LLM 세션 사용량 요약 (챗 입력창 아래 상시 표시) ──
|
|
1251
|
+
// 호스트 이점: 단일 모델 CLI는 못 보여주는 멀티 LLM 합산 사용량을 항상 노출한다.
|
|
1252
|
+
function fmtTok(n) {
|
|
1253
|
+
if (!n) return "0";
|
|
1254
|
+
if (n >= 1e6) return (n >= 1e7 ? Math.round(n / 1e6) : (n / 1e6).toFixed(1)) + "m";
|
|
1255
|
+
if (n >= 1e3) return (n >= 1e5 ? Math.round(n / 1e3) : (n / 1e3).toFixed(1)) + "k";
|
|
1256
|
+
return String(n);
|
|
1257
|
+
}
|
|
1258
|
+
function usageSummaryLine() {
|
|
1259
|
+
const shortLabel = (label) => (label === "claude-code" ? "claude" : label);
|
|
1260
|
+
const labels = [];
|
|
1261
|
+
const push = (label) => { if (label && label !== "(none)" && !labels.includes(label)) labels.push(label); };
|
|
1262
|
+
for (const kind of installedKinds()) push(kind); // 연결(설치)된 CLI 런타임 — 미사용이어도 표시
|
|
1263
|
+
push(runtimeLabel(state.runtime)); // 현재 런타임 (BYOK/Ollama 포함)
|
|
1264
|
+
for (const label of Object.keys(state.cost)) push(label); // 세션 중 사용한 나머지
|
|
1265
|
+
const parts = labels.map((label) => {
|
|
1266
|
+
const e = state.cost[label];
|
|
1267
|
+
if (!e) return `${shortLabel(label)} 0`;
|
|
1268
|
+
if (e.in || e.out) return `${shortLabel(label)} ${fmtTok(e.in)}→${fmtTok(e.out)}`;
|
|
1269
|
+
return `${shortLabel(label)} ${e.turns}${ui.lang === "ko" ? "턴" : "t"}`;
|
|
1270
|
+
});
|
|
1271
|
+
return `${ui.t("usageBar")} ${parts.join(" · ")}`;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1154
1274
|
// ── composer (raw-mode bottom box) main loop ──
|
|
1155
1275
|
function composerMeta() {
|
|
1156
1276
|
const rt = runtimeLabel(state.runtime);
|
|
@@ -1162,6 +1282,7 @@ function startRepl(opts) {
|
|
|
1162
1282
|
permission: state.permission,
|
|
1163
1283
|
permissionLabel,
|
|
1164
1284
|
status: `${rt}${eff} · ${subj} · ${ui.t("permCycleHint")} · ${ui.t("composer.hint")} · ↑↓ history`,
|
|
1285
|
+
usage: usageSummaryLine(), // 챗 입력창 아래 상시 LLM 사용량 표시줄
|
|
1165
1286
|
onCyclePermission: () => {
|
|
1166
1287
|
const cycle = permissionCycle.step(state.permission);
|
|
1167
1288
|
if (cycle.armed) {
|
|
@@ -1228,6 +1349,8 @@ function startRepl(opts) {
|
|
|
1228
1349
|
if (closed) return process.exit(0);
|
|
1229
1350
|
if (slashPalette.setEnabled) slashPalette.setEnabled(true);
|
|
1230
1351
|
const cont = buffer != null;
|
|
1352
|
+
// 클래식 모드에도 사용량 상시 표시 (composer 모드에선 입력박스 아래 줄이 담당)
|
|
1353
|
+
if (!cont && !composer && Object.keys(state.cost).length) ui.line(ui.c.faint(" " + usageSummaryLine()));
|
|
1231
1354
|
rl.question(cont ? ui.c.dim(" … ") : "\n" + ui.promptLabel(), async (line) => {
|
|
1232
1355
|
if (input.isContinuation(line)) {
|
|
1233
1356
|
return ask((cont ? buffer + "\n" : "") + input.stripContinuation(line));
|
package/engine/agentlas-ui.cjs
CHANGED
|
@@ -280,7 +280,11 @@ class Ui {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
-
|
|
283
|
+
// 턴 중에도 사용량 표시줄 유지 (chrome.usage: 문자열 또는 라이브 getter)
|
|
284
|
+
const usageSrc = this._turnChrome.usage;
|
|
285
|
+
const usageText = typeof usageSrc === "function" ? usageSrc() : usageSrc;
|
|
286
|
+
const usageLine = usageText ? this.c.faint(truncateCells(String(usageText), width)) : null;
|
|
287
|
+
return [...taskLines, rule, prompt, rule, statusLine, ...(usageLine ? [usageLine] : [])];
|
|
284
288
|
}
|
|
285
289
|
|
|
286
290
|
_eraseFooter() {
|