arona-agent 1.2.2 → 1.2.3

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.
@@ -169,6 +169,81 @@ button { font: inherit; color: inherit; }
169
169
  .session-item .s-time { flex: none; font-size: 12.5px; color: var(--text-3); }
170
170
  .session-empty { color: var(--text-3); font-size: 12.5px; padding: 8px; white-space: nowrap; }
171
171
 
172
+ /* ── 折叠展开过渡(可复用工具类,与 .glass 同级)────────
173
+ 用法:外层 <div class="collapse-body [collapsed]"><div class="collapse-inner">内容</div></div>,
174
+ 切换 collapsed 类即有 高度收展 + 透明度 过渡;内容高度无需预知(grid 0fr/1fr 技巧),
175
+ 初次插入即带 collapsed 不触发动画(过渡只在类切换时发生)。 */
176
+ .collapse-body {
177
+ display: grid;
178
+ grid-template-rows: 1fr;
179
+ opacity: 1;
180
+ transition: grid-template-rows 260ms var(--ease), opacity 200ms var(--ease);
181
+ }
182
+ .collapse-body > .collapse-inner {
183
+ min-height: 0;
184
+ overflow: hidden;
185
+ visibility: visible;
186
+ transition: visibility 0s;
187
+ }
188
+ .collapse-body.collapsed { grid-template-rows: 0fr; opacity: 0; }
189
+ /* 折叠动画播完再隐藏内容(Tab 不可达);展开时立即恢复可见 */
190
+ .collapse-body.collapsed > .collapse-inner { visibility: hidden; transition: visibility 0s 260ms; }
191
+
192
+ /* ── 侧栏工作区分组(工作区嵌套会话)──────── */
193
+ .ws-group { display: flex; flex-direction: column; gap: 2px; }
194
+ .ws-body { flex: none; display: flex; flex-direction: column; gap: 2px; }
195
+ .ws-head {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 7px;
199
+ height: 32px;
200
+ padding: 0 10px;
201
+ border: none;
202
+ border-radius: 9px;
203
+ background: transparent;
204
+ cursor: pointer;
205
+ width: 100%;
206
+ text-align: left;
207
+ color: var(--text-2);
208
+ font-size: 12.5px;
209
+ font-weight: 600;
210
+ }
211
+ .ws-head:hover { background: var(--hover); color: var(--text); }
212
+ .ws-head .ws-folder { display: inline-flex; flex: none; color: var(--text-3); }
213
+ .ws-head .ws-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
214
+ .ws-head .ws-count {
215
+ flex: none;
216
+ font-size: 11.5px;
217
+ font-weight: 500;
218
+ color: var(--text-3);
219
+ background: var(--hover);
220
+ border-radius: 8px;
221
+ padding: 1px 7px;
222
+ }
223
+ .ws-head .ws-chev { display: inline-flex; flex: none; color: var(--text-3); transition: transform 180ms var(--ease); }
224
+ .ws-group.collapsed .ws-chev { transform: rotate(-90deg); }
225
+ .ws-group .session-item { padding-left: 31px; } /* 组内嵌套缩进 */
226
+ .ws-more {
227
+ flex: none;
228
+ border: none;
229
+ background: transparent;
230
+ cursor: pointer;
231
+ color: var(--text-3);
232
+ font-size: 12px;
233
+ text-align: left;
234
+ padding: 6px 31px;
235
+ border-radius: 9px;
236
+ width: 100%;
237
+ }
238
+ .ws-more:hover { background: var(--hover); color: var(--text-2); }
239
+
240
+ /* 折叠窄栏:工作区组头只留文件夹图标 */
241
+ body.sb-collapsed .ws-name,
242
+ body.sb-collapsed .ws-count,
243
+ body.sb-collapsed .ws-chev,
244
+ body.sb-collapsed .ws-more { display: none; }
245
+ body.sb-collapsed .ws-head { justify-content: center; padding: 0; }
246
+
172
247
  .sb-foot { flex: none; padding-top: 18px; margin-top: 12px; }
173
248
 
174
249
  /* ── 会话多选模式 ─────────────────────────── */
@@ -627,6 +702,29 @@ body:not(.has-msg) #composer-wrap {
627
702
  #composer-bar { display: flex; align-items: center; gap: 6px; }
628
703
  .cb-spacer { flex: 1; }
629
704
 
705
+ /* ── 欢迎页工作区选择器(初始页输入框左下角,麦克风左侧;会话开始后隐藏)── */
706
+ .ws-picker {
707
+ display: none;
708
+ align-items: center;
709
+ gap: 7px;
710
+ height: 32px;
711
+ padding: 0 10px 0 9px;
712
+ border: 1px solid var(--border);
713
+ border-radius: 10px;
714
+ background: transparent;
715
+ color: var(--text-2);
716
+ font-size: 12.5px;
717
+ cursor: pointer;
718
+ max-width: 240px;
719
+ }
720
+ body:not(.has-msg) .ws-picker { display: inline-flex; }
721
+ .ws-picker:hover { background: var(--hover); color: var(--text); }
722
+ .ws-picker svg { flex: none; }
723
+ .ws-picker .ws-folder { display: inline-flex; color: var(--text-3); }
724
+ .ws-picker .ws-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
725
+ .ws-picker .ws-chev { display: inline-flex; color: var(--text-3); transition: transform 200ms var(--ease); }
726
+ .ws-picker.open .ws-chev svg { transform: rotate(180deg); }
727
+
630
728
  .icon-btn {
631
729
  border: none;
632
730
  background: none;
@@ -683,13 +781,56 @@ body:not(.has-msg) #slash-menu {
683
781
  .slash-item .desc { color: var(--text-3); font-size: 12px; }
684
782
  .slash-item.highlight { background: var(--hover); }
685
783
 
784
+ /* ── 工作区下拉浮层(欢迎页 composer 上方向弹出)── */
785
+ #ws-menu {
786
+ position: absolute;
787
+ bottom: calc(100% + 8px);
788
+ left: 32px;
789
+ width: min(420px, calc(100% - 64px));
790
+ max-height: 380px;
791
+ overflow-y: auto;
792
+ border-radius: 14px;
793
+ z-index: 30;
794
+ padding: 6px;
795
+ display: flex;
796
+ flex-direction: column;
797
+ gap: 2px;
798
+ }
799
+ body:not(.has-msg) #ws-menu { left: 96px; }
800
+ #ws-menu .ws-all {
801
+ display: flex;
802
+ align-items: center;
803
+ gap: 8px;
804
+ height: 34px;
805
+ padding: 0 10px;
806
+ border: none;
807
+ border-radius: 9px;
808
+ background: transparent;
809
+ cursor: pointer;
810
+ text-align: left;
811
+ width: 100%;
812
+ font-size: 13px;
813
+ font-weight: 600;
814
+ color: var(--text);
815
+ }
816
+ #ws-menu .ws-all:hover { background: var(--hover); }
817
+ #ws-menu .ws-all.current { background: var(--accent-soft); color: var(--accent); }
818
+ #ws-menu .ws-all.current .ws-folder,
819
+ #ws-menu .ws-all.current .ws-count { color: var(--accent); }
820
+ #ws-menu .ws-pick { color: var(--text-2); font-weight: 500; }
821
+ #ws-menu .ws-folder { display: inline-flex; flex: none; color: var(--text-2); }
822
+ #ws-menu .ws-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
823
+ #ws-menu .ws-count { flex: none; font-size: 11.5px; font-weight: 500; color: var(--text-3); }
824
+ #ws-menu .ws-empty { color: var(--text-3); font-size: 12.5px; padding: 10px; }
825
+
686
826
  /* ── macOS 液态玻璃 / 亚克力材质:渐变半透明白面板 + 模糊 + 高光描边;
687
827
  新浮层加 .glass 类即可复用(入场动画 glass-in 自带)── */
688
828
  .glass,
689
829
  #modal,
690
830
  #confirm-box,
691
831
  #slash-menu,
692
- #context-menu {
832
+ #context-menu,
833
+ #ws-menu {
693
834
  background: var(--glass-gradient);
694
835
  -webkit-backdrop-filter: var(--glass-blur);
695
836
  backdrop-filter: var(--glass-blur);
@@ -950,6 +1091,7 @@ body:not(.has-msg) #slash-menu {
950
1091
  #sidebar { transition: none; }
951
1092
  .icon-btn.recording { animation: none; }
952
1093
  .status-line { animation: none; }
953
- .glass, #modal, #confirm-box, #slash-menu, #context-menu { animation: none; transition: none; }
1094
+ .glass, #modal, #confirm-box, #slash-menu, #context-menu, #ws-menu { animation: none; transition: none; }
954
1095
  .glass-out { animation: none !important; }
1096
+ .collapse-body { transition: none; }
955
1097
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arona-agent",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Terminal AI Agent with desktop pet Arona — eye-tracking pupils, voice cloning, Computer Use, TTS/STT, MCP.",
5
5
  "keywords": [
6
6
  "voice-clone",
package/pet/agents.cjs CHANGED
@@ -166,8 +166,10 @@ const HOSHINO = {
166
166
  // 重新生成顺序:skel_to_json → meshify_region → gen_sway(任何前一步都会覆盖后一步产物)
167
167
  skelFile: "hoshino_spr.json",
168
168
  atlasFile: "hoshino_spr.atlas.txt",
169
- // bounds 1161×2074(高 84.8%)→ 1.1 使身高接近 Arona
170
- extraScale: 1.1,
169
+ // bounds 1161×2074:480×780 窗口下比例偏窄、高度先顶格 1.0 恰好满窗高。
170
+ // 1.1 是按旧 320 宽窗口调的:当时宽度先顶格、放大只裁两侧;现窗口加宽后纵向
171
+ // 也被放大 → 脚底/头顶溢出裁切,且调多高都按比例溢出,加高窗口无效)
172
+ extraScale: 1,
171
173
  anims: {
172
174
  idle: "Idle_01",
173
175
  blink: "Eye_Close_01",
@@ -250,8 +252,11 @@ const KOHARU = {
250
252
  // .json = skel_to_json 导出 + gen_sway 衣摆形变;重新生成顺序:skel_to_json → gen_sway
251
253
  skelFile: "koharu_spr.json",
252
254
  atlasFile: "koharu_spr.atlas.txt",
253
- // bounds 1395×2069:高≈Arona,但宽(含光环球)超宽 取宽为最小边,身高只占 70% → extraScale 放大到 1.3
254
- extraScale: 1.3,
255
+ // bounds 1395×2069:宽(含光环球)超宽,480×780 下宽度先顶格,1.0 身高仅 ~91%。
256
+ // 纵向不裁的上限 extra = 780/(2069×480/1395) ≈ 1.095,取 1.08 留 idle 摆动余量
257
+ // (身高 ≈98% 满窗;光环球两侧各裁 ~19px,与旧版 1.3 按比例裁两侧一致)。
258
+ // (1.3 是按旧 320 宽窗口调的,现窗口加宽后纵向溢出裁切脚底/头顶)
259
+ extraScale: 1.08,
255
260
  anims: {
256
261
  idle: "Idle_01",
257
262
  blink: "Eye_Close_01",
package/pet/main.cjs CHANGED
@@ -3,6 +3,8 @@
3
3
  // 支持多角色同屏:ARONA_AGENT = 主 Agent,ARONA_SUB_AGENTS = 逗号分隔的子 Agent 列表;
4
4
  // 每个角色一个 BrowserWindow,共享一个全屏特效窗。
5
5
  const { app, BrowserWindow, ipcMain, screen } = require("electron");
6
+ const crypto = require("crypto");
7
+ const http = require("http");
6
8
  const path = require("path");
7
9
  const fs = require("fs");
8
10
  const os = require("os");
@@ -36,7 +38,7 @@ const POS_FILE = path.join(os.homedir(), ".arona", "pet.json");
36
38
  const WIN_W = 480; // 窗口 = Spine 角色渲染区本体;480 使宽骨架角色(kei 长发/aris 拖地发)满高显示不裁切。
37
39
  // 透明区拦点击的副作用由「动态穿透」兜底:初始 setIgnoreMouseEvents(true,{forward}),
38
40
  // renderer 按光标处 alpha 经 pet:clickable 切换(见 pet:clickable IPC)
39
- const WIN_H = 674;
41
+ const WIN_H = 780;
40
42
  const SUB_OFFSET_X = 500; // 子窗口默认横向错开(略大于 WIN_W)
41
43
  const SUB_OFFSET_Y = 40;
42
44
 
@@ -431,7 +433,7 @@ ipcMain.handle("pet:get-agent-config", (e) => {
431
433
  return { id: agentId, isMain: agentId === MAIN_AGENT_ID, ...AGENTS[agentId] };
432
434
  });
433
435
 
434
- // ---- stdin 协议 ----
436
+ // ---- stdin 协议(非 Windows 平台的备用通道)----
435
437
  let buffer = "";
436
438
 
437
439
  function sendToAgent(agentId, channel, payload) {
@@ -517,6 +519,43 @@ process.stdin.on("end", () => {
517
519
  if (process.platform !== "win32") app.quit();
518
520
  });
519
521
 
522
+ // ---- 本地 HTTP 通道(backend → pet 主方向)----
523
+ // Windows 下 Electron GUI 子系统子进程的 stdin 数据不可达(GUI 白屏坑③同族问题,
524
+ // 症状隐蔽:桌宠静态显示正常,但 set_emotion/tts_level/spawn_agent/quit 全部失效)。
525
+ // 正常路径 = 127.0.0.1 随机端口 + 随机 token 的 HTTP 服务,端口/token 随 hello 行告知
526
+ // backend(防本机其他进程伪造);HTTP 起不来(httpPort=0)时 backend 退回上方 stdin 通道。
527
+ const HTTP_TOKEN = crypto.randomBytes(16).toString("hex");
528
+ let helloSent = false;
529
+ function sendHello(httpPort) {
530
+ if (helloSent) return;
531
+ helloSent = true;
532
+ send({ type: "hello", httpPort: httpPort || 0, token: httpPort ? HTTP_TOKEN : "" });
533
+ }
534
+ const petHttpServer = http.createServer((req, res) => {
535
+ if (req.method !== "POST" || req.headers["x-arona-token"] !== HTTP_TOKEN) {
536
+ res.writeHead(403);
537
+ res.end();
538
+ return;
539
+ }
540
+ let body = "";
541
+ req.on("data", (c) => (body += c));
542
+ req.on("end", () => {
543
+ res.writeHead(204);
544
+ res.end();
545
+ try {
546
+ handleMessage(JSON.parse(body));
547
+ } catch {
548
+ // 非 JSON,忽略
549
+ }
550
+ });
551
+ });
552
+ petHttpServer.on("error", () => {
553
+ sendHello(0); // HTTP 起不来也要发 hello(不带端口,backend 退回 stdin 写入)
554
+ });
555
+ petHttpServer.listen(0, "127.0.0.1", () => {
556
+ sendHello(petHttpServer.address().port);
557
+ });
558
+
520
559
  app.whenReady().then(() => {
521
560
  if (VERBOSE) {
522
561
  // GPU 功能状态:确认 webgl/webgl2 是 hardware/software/disabled(Windows 白屏排查关键)
@@ -115,7 +115,7 @@ function waitLoad() {
115
115
  function resize() {
116
116
  const dpr = window.devicePixelRatio || 1;
117
117
  const w = canvas.clientWidth || 320;
118
- const h = canvas.clientHeight || 674;
118
+ const h = canvas.clientHeight || 780;
119
119
  canvas.width = Math.round(w * dpr);
120
120
  canvas.height = Math.round(h * dpr);
121
121
  gl.viewport(0, 0, canvas.width, canvas.height);
@@ -128,7 +128,7 @@ function fitCamera() {
128
128
  skeleton.getBounds(offset, size, []);
129
129
  const pad = 8;
130
130
  const cw = canvas.clientWidth || 320;
131
- const ch = canvas.clientHeight || 674;
131
+ const ch = canvas.clientHeight || 780;
132
132
  const scale = Math.min(cw / (size.x + pad * 2), ch / (size.y + pad * 2));
133
133
  renderer.camera.setViewport(cw / scale, ch / scale);
134
134
  renderer.camera.position.set(offset.x + size.x / 2, offset.y + size.y / 2, 0);
@@ -139,7 +139,7 @@ function fitCamera() {
139
139
  // 窗口 CSS px → 骨架世界坐标(逆相机变换)
140
140
  function windowToSkeleton(x, y) {
141
141
  const cw = canvas.clientWidth || 320;
142
- const ch = canvas.clientHeight || 674;
142
+ const ch = canvas.clientHeight || 780;
143
143
  const p = new spine.webgl.Vector3(x, y, 0);
144
144
  renderer.camera.screenToWorld(p, cw, ch);
145
145
  return { x: p.x, y: p.y };
@@ -170,7 +170,7 @@ function zoom(cap, src) {
170
170
  app.whenReady().then(() => {
171
171
  fs.mkdirSync(OUT, { recursive: true });
172
172
  win = new BrowserWindow({
173
- width: 480, height: 674, transparent: true, frame: false, show: true,
173
+ width: 480, height: 780, transparent: true, frame: false, show: true,
174
174
  // 窗口被遮挡/后台时 rAF 与 DOM 合成会被节流 → capturePage 拿到过期纹理;关闭节流
175
175
  webPreferences: { preload: path.join(PET, "preload.cjs"), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false },
176
176
  });
@@ -78,7 +78,7 @@ function zoom(cap, src) {
78
78
  app.whenReady().then(() => {
79
79
  fs.mkdirSync(OUT, { recursive: true });
80
80
  win = new BrowserWindow({
81
- width: 480, height: 674, transparent: true, frame: false, show: true,
81
+ width: 480, height: 780, transparent: true, frame: false, show: true,
82
82
  webPreferences: { preload: path.join(PET, "preload.cjs"), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false },
83
83
  });
84
84
  win.webContents.on("console-message", (_e, _a, b) => console.log("[R]", b));
@@ -146,7 +146,7 @@ async function synthPat(downX, downY, shakeAmp) {
146
146
  app.whenReady().then(() => {
147
147
  win = new BrowserWindow({
148
148
  width: 480,
149
- height: 674,
149
+ height: 780,
150
150
  transparent: true,
151
151
  frame: false,
152
152
  alwaysOnTop: true,
package/python/stt.py CHANGED
@@ -18,6 +18,7 @@ sentence_end=true 为最终结果,聚合后输出到 stdout。
18
18
  import json
19
19
  import os
20
20
  import sys
21
+ import threading
21
22
  import uuid
22
23
  import asyncio
23
24
  import signal
@@ -31,8 +32,8 @@ MAX_RECORDING_SECONDS = 15
31
32
  SILENCE_THRESHOLD = 500 # RMS 阈值
32
33
  SILENCE_DURATION_MS = 1500 # 静音超过此时长则结束
33
34
 
34
- # 优雅停止标志:SIGUSR1/SIGINT(GUI 麦克风再点一次)置位后,录音循环提前跳出,
35
- # 走正常 finish-task 流程把已说内容识别出来(区别于 SIGTERM 直接终止=丢弃)。
35
+ # 优雅停止标志:SIGUSR1/SIGINT(POSIX)或 stdin "stop" 行(Windows,见 main)置位后,
36
+ # 录音循环提前跳出,走正常 finish-task 流程把已说内容识别出来(区别于 SIGTERM 直接终止=丢弃)。
36
37
  _stop_recording = False
37
38
 
38
39
 
@@ -41,6 +42,24 @@ def _request_stop(*_args):
41
42
  _stop_recording = True
42
43
 
43
44
 
45
+ def _watch_stdin_stop():
46
+ """监听 stdin 的 "stop" 行置位停止标志。
47
+
48
+ Windows 上进程信号不可捕获(Node proc.kill 全部等效硬杀),Node 侧优雅停止改发
49
+ stdin "stop" 行。EOF 不触发停止——Node 侧可能在识别结束前就关闭了 stdin 管道。
50
+ """
51
+ try:
52
+ while True:
53
+ line = sys.stdin.readline()
54
+ if not line:
55
+ return
56
+ if line.strip().lower() == "stop":
57
+ _request_stop()
58
+ return
59
+ except Exception:
60
+ return
61
+
62
+
44
63
  def build_uri(workspace_id):
45
64
  if workspace_id:
46
65
  return f"wss://{workspace_id}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference"
@@ -242,7 +261,8 @@ def main():
242
261
  print(t("STT: 未设置 QWEN_STT_API_KEY", "STT: QWEN_STT_API_KEY not set"), file=sys.stderr)
243
262
  sys.exit(1)
244
263
 
245
- # 优雅停止:提前结束录音但仍识别已上传内容(Windows 无 SIGUSR1,仅注册存在的信号)
264
+ # 优雅停止:提前结束录音但仍识别已上传内容。
265
+ # POSIX:SIGUSR1/SIGINT 信号;Windows:信号不可捕获,Node 侧发 stdin "stop" 行(监视线程)。
246
266
  for sig_name in ("SIGUSR1", "SIGINT"):
247
267
  sig = getattr(signal, sig_name, None)
248
268
  if sig is not None:
@@ -250,6 +270,7 @@ def main():
250
270
  signal.signal(sig, _request_stop)
251
271
  except (ValueError, OSError):
252
272
  pass
273
+ threading.Thread(target=_watch_stdin_stop, daemon=True).start()
253
274
 
254
275
  text = asyncio.run(stt_recognize(api_key, workspace_id, model, audio_format, sample_rate))
255
276
  print(text)
package/src/agent.ts CHANGED
@@ -25,6 +25,7 @@ import { getMainAgent, getAgentLabel, type SubAgentId, type AgentId } from "./ag
25
25
  import { speakerContextExtension } from "./speaker_context.ts";
26
26
  import { gestureContextExtension } from "./gesture_context.ts";
27
27
  import { t, getLang } from "./locale.ts";
28
+ import { currentWorkspace } from "./workspace.ts";
28
29
  import { nowStr, reserveTokensFor } from "./prompt_utils.ts";
29
30
 
30
31
  function buildSystemPrompt(memoryContent: string): string {
@@ -497,7 +498,7 @@ export async function initAgent(): Promise<{
497
498
  modelRuntime,
498
499
  });
499
500
  if (cliModel.error) {
500
- console.warn(`Model resolution warning: ${cliModel.error}`);
501
+ console.warn(t(`模型解析警告:${cliModel.error}`, `Model resolution warning: ${cliModel.error}`));
501
502
  }
502
503
 
503
504
  // 3. Load memory
@@ -507,7 +508,7 @@ export async function initAgent(): Promise<{
507
508
 
508
509
  // 4. Create ResourceLoader with custom system prompt
509
510
  const loader = new DefaultResourceLoader({
510
- cwd: process.cwd(),
511
+ cwd: currentWorkspace(),
511
512
  agentDir: ARONA_DIR,
512
513
  systemPromptOverride: () => buildSystemPrompt(memoryContent),
513
514
  appendSystemPromptOverride: () => [],
@@ -546,7 +547,7 @@ export async function initAgent(): Promise<{
546
547
  // 压缩阈值:按上下文窗口动态推导 reserveTokens(约 85% 水位触发压缩),
547
548
  // 避免硬编码 150000 导致小窗口模型(如 DeepSeek 64K/128K)每轮误触发压缩
548
549
  // (多一次总结调用 + 前缀每轮变化 → 缓存永不命中)。
549
- const settingsManager = SettingsManager.create(process.cwd(), ARONA_DIR);
550
+ const settingsManager = SettingsManager.create(currentWorkspace(), ARONA_DIR);
550
551
  settingsManager.applyOverrides({
551
552
  compaction: {
552
553
  enabled: true,
@@ -555,7 +556,7 @@ export async function initAgent(): Promise<{
555
556
  },
556
557
  });
557
558
  const { session } = await createAgentSession({
558
- cwd: process.cwd(),
559
+ cwd: currentWorkspace(),
559
560
  agentDir: ARONA_DIR,
560
561
  model: cliModel.model,
561
562
  thinkingLevel: config.thinkingLevel as any,
@@ -682,7 +683,7 @@ function buildSubSystemPrompt(id: SubAgentId, memoryContent: string): string {
682
683
  - You are ${getAgentLabel(id)}. Always speak as yourself — do not play, mimic, or mix in the identity or tone of any other character (Arona, Plana, Shiroko, Hoshino, Hanako, Koharu, Kei, Aris).
683
684
  - After the main agent finishes replying, each enabled sub-agent takes a turn. Keep your reply SHORT (one or two sentences), natural, in-character, and add nothing but your own spoken line.
684
685
  - Do not repeat or summarize the main agent's reply.
685
- - In the conversation history, assistant messages carry a \`Name:\` prefix showing who said them (e.g. "Arona:", "Shiroko:", etc.); user inputs are Sensei speaking. When you reply, do NOT add any name prefix.
686
+ - In the conversation history, assistant messages carry a \`Name:\` prefix showing who said them (e.g. "Arona:", "Shiroko:", etc.); user inputs are Sensei speaking. When you reply, do NOT add any name prefix — never start your reply with your own name and a colon (e.g. "${getAgentLabel(id)}:"), not even in a script/subtitle style. Output only the words you actually say.
686
687
  - Your reply may be read aloud by TTS; keep each sentence under 50 characters/words when possible.
687
688
  - If you have nothing to add, call keep_silent instead of writing filler text.
688
689
 
@@ -696,7 +697,7 @@ Available tools: change_emotion (set the emotion before speaking), keep_silent (
696
697
  - 你是${getAgentLabel(id)}。始终以自己的身份发言,禁止扮演或模仿或混用其他角色(阿洛娜、普拉娜、白子、星野、花子、小春、凯伊、爱丽丝)的身份与语气。
697
698
  - 主 Agent 回复完毕后,每个启用的子 Agent 依次发言。回复保持简短(一两句),贴角色,只说自己的台词。
698
699
  - 不要复读或总结主 Agent 的话。
699
- - 对话历史中,assistant 消息带「角色名:」前缀标明发言者(如「阿洛娜:」「砂狼白子:」等);用户输入是 Sensei 说的。你发言时不要加名字前缀。
700
+ - 对话历史中,assistant 消息带「角色名:」前缀标明发言者(如「阿洛娜:」「砂狼白子:」等);用户输入是 Sensei 说的。你发言时不要加名字前缀——绝不要以「自己的名字+冒号」(如「${getAgentLabel(id)}:」)开头,也不要写成剧本/字幕格式,直接说台词本身。
700
701
  - 你的回复可能被 TTS 朗读,尽量每句 <50 字。
701
702
  - 无话可说就调用 keep_silent,不要写废话。
702
703
 
@@ -734,7 +735,7 @@ export async function initSubAgent(
734
735
  ): Promise<{ session: AgentSession; loader: DefaultResourceLoader; customTools: ToolDefinition[] }> {
735
736
  const cliModel = resolveCliModel({ cliModel: config.model, modelRuntime });
736
737
  if (cliModel.error) {
737
- console.warn(`Model resolution warning (${agentId}): ${cliModel.error}`);
738
+ console.warn(t(`模型解析警告(${agentId}):${cliModel.error}`, `Model resolution warning (${agentId}): ${cliModel.error}`));
738
739
  }
739
740
 
740
741
  const memoryContent = loadMemory();
@@ -742,7 +743,7 @@ export async function initSubAgent(
742
743
  // 子 Agent 若再次快照会把基线推进到当前内容,导致主 Agent 首轮 getMemoryDelta() 漏报
743
744
  // 启动后、首次输入前发生的 MEMORY.md 变更。
744
745
  const loader = new DefaultResourceLoader({
745
- cwd: process.cwd(),
746
+ cwd: currentWorkspace(),
746
747
  agentDir: ARONA_DIR,
747
748
  // 子 Agent 默认不注入项目文档(CLAUDE.md/AGENTS.md)——SDK 会按 cwd 自动把上下文文件
748
749
  // 塞进 system prompt,子 Agent 是纯聊天角色、不需要工作目录约定。需要时由 Agent 主动
@@ -766,7 +767,7 @@ export async function initSubAgent(
766
767
  ...(config.tavilyApiKey ? premiumTavilyTools : []),
767
768
  ];
768
769
 
769
- const settingsManager = SettingsManager.create(process.cwd(), ARONA_DIR);
770
+ const settingsManager = SettingsManager.create(currentWorkspace(), ARONA_DIR);
770
771
  settingsManager.applyOverrides({
771
772
  compaction: {
772
773
  enabled: true,
@@ -776,7 +777,7 @@ export async function initSubAgent(
776
777
  });
777
778
 
778
779
  const { session } = await createAgentSession({
779
- cwd: process.cwd(),
780
+ cwd: currentWorkspace(),
780
781
  agentDir: ARONA_DIR,
781
782
  model: cliModel.model,
782
783
  thinkingLevel: config.thinkingLevel as any,
@@ -52,6 +52,36 @@ export function getAgentLabel(id: AgentId): string {
52
52
  }
53
53
  }
54
54
 
55
+ /**
56
+ * 角色名的全部写法(中英文全名/常用短名),供输出侧剥离「名字:」前缀用。
57
+ * 模型偶发模仿历史消息的「角色名:」前缀,且可能用短名(如「星野:」)而非
58
+ * getAgentLabel 的全名,两种都要覆盖。与 locale 无关:中英文都列出。
59
+ */
60
+ export function getAgentNameVariants(id: AgentId): string[] {
61
+ switch (id) {
62
+ case "arona":
63
+ return ["阿洛娜", "Arona"];
64
+ case "plana":
65
+ return ["普拉娜", "Plana"];
66
+ case "shiroko":
67
+ return ["砂狼白子", "白子", "Shiroko"];
68
+ case "hoshino":
69
+ return ["小鸟游星野", "星野", "Hoshino"];
70
+ case "hanako":
71
+ return ["浦和花子", "花子", "Hanako"];
72
+ case "koharu":
73
+ return ["下江小春", "小春", "Koharu"];
74
+ case "kei":
75
+ return ["天童凯伊", "凯伊", "Kei"];
76
+ case "aris":
77
+ return ["天童爱丽丝", "爱丽丝", "Aris"];
78
+ case "millennium":
79
+ return ["千禧年学员", "Millennium Student"];
80
+ case "justice":
81
+ return ["正义实现部成员", "Justice Task Force Member"];
82
+ }
83
+ }
84
+
55
85
  /** 校验 agent id(含类型收窄) */
56
86
  export function isValidAgentId(id: string): id is AgentId {
57
87
  return (AGENT_IDS as readonly string[]).includes(id);
@@ -19,7 +19,8 @@ import { computerUseTools } from "./tools/computer_use.ts";
19
19
  import { webSearchTool, webExtractTool, premiumTavilyTools } from "./tools/tavily_tools.ts";
20
20
  import { type CodingAgentId } from "./agent_registry.ts";
21
21
  import { nowStr, reserveTokensFor } from "./prompt_utils.ts";
22
- import { getLang } from "./locale.ts";
22
+ import { getLang, t } from "./locale.ts";
23
+ import { currentWorkspace } from "./workspace.ts";
23
24
 
24
25
  const CODING_PERSONA_ZH: Record<CodingAgentId, string> = {
25
26
  millennium: `你是千年科技学园工程部所属的学生,研究员气质,严谨精确,接受来自什亭之箱方面的委托。
@@ -140,11 +141,11 @@ export async function initCodingAgent(
140
141
  ): Promise<{ session: AgentSession; loader: DefaultResourceLoader; customTools: ToolDefinition[] }> {
141
142
  const cliModel = resolveCliModel({ cliModel: config.model, modelRuntime });
142
143
  if (cliModel.error) {
143
- console.warn(`Model resolution warning (${agentId}): ${cliModel.error}`);
144
+ console.warn(t(`模型解析警告(${agentId}):${cliModel.error}`, `Model resolution warning (${agentId}): ${cliModel.error}`));
144
145
  }
145
146
 
146
147
  const loader = new DefaultResourceLoader({
147
- cwd: process.cwd(),
148
+ cwd: currentWorkspace(),
148
149
  agentDir: ARONA_DIR,
149
150
  // 不设 noContextFiles:与主 Agent 一致走 SDK 默认注入链路(CLAUDE.md/AGENTS.md)
150
151
  systemPromptOverride: () => buildCodingSystemPrompt(agentId),
@@ -162,7 +163,7 @@ export async function initCodingAgent(
162
163
  ...mcpTools,
163
164
  ];
164
165
 
165
- const settingsManager = SettingsManager.create(process.cwd(), ARONA_DIR);
166
+ const settingsManager = SettingsManager.create(currentWorkspace(), ARONA_DIR);
166
167
  settingsManager.applyOverrides({
167
168
  compaction: {
168
169
  enabled: true,
@@ -172,7 +173,7 @@ export async function initCodingAgent(
172
173
  });
173
174
 
174
175
  const { session } = await createAgentSession({
175
- cwd: process.cwd(),
176
+ cwd: currentWorkspace(),
176
177
  agentDir: ARONA_DIR,
177
178
  model: cliModel.model,
178
179
  thinkingLevel: config.thinkingLevel as any,