arona-agent 1.2.2 → 1.2.4

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.
@@ -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,
package/src/commands.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { execSync } from "child_process";
2
+ import { execFileSync, spawn } from "child_process";
3
3
  import { writeFileSync } from "fs";
4
4
  import { join } from "path";
5
5
  import type { AgentSession, DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
@@ -13,6 +13,7 @@ import { MAIN_AGENT_IDS, SUB_AGENT_IDS, getMainAgent, getSubAgents, getAgentLabe
13
13
  import { pet } from "./pet.ts";
14
14
  import type { UndoManager } from "./undo.ts";
15
15
  import { t } from "./locale.ts";
16
+ import { currentWorkspace, groupByWorkspace } from "./workspace.ts";
16
17
 
17
18
  export interface CommandContext {
18
19
  session: AgentSession;
@@ -245,7 +246,9 @@ function truncateStyled(text: string, maxW: number, style: (s: string) => string
245
246
  /**
246
247
  * Interactive session picker. Uses raw-mode stdin to capture up/down arrows.
247
248
  * Pressing Enter or Escape selects (Enter = current, Escape = cancel).
248
- * Sessions are listed newest first; up arrow moves toward newer, down arrow toward older.
249
+ * Sessions are grouped by workspace (current workspace first, then by most
250
+ * recent use, ungrouped legacy sessions last); group headers are skipped by
251
+ * the cursor. Up arrow moves toward newer, down arrow toward older.
249
252
  */
250
253
  async function handleResume(ctx: CommandContext) {
251
254
  const sessions = memory.listSessions();
@@ -265,7 +268,23 @@ async function handleResume(ctx: CommandContext) {
265
268
  process.stdin.resume();
266
269
  process.stdin.setEncoding("utf8");
267
270
 
268
- let cursor = 0; // index of currently highlighted session
271
+ // 工作区分组 扁平行序列(标题行不可选中,↑/↓ 跳过)
272
+ const current = currentWorkspace();
273
+ const groups = groupByWorkspace(sessions);
274
+ type Row = { kind: "header"; text: string } | { kind: "session"; session: memory.SessionInfo };
275
+ const rows: Row[] = [];
276
+ for (const g of groups) {
277
+ rows.push({
278
+ kind: "header",
279
+ text: g.workspace === current
280
+ ? t(`当前工作区 · ${g.label}`, `Current workspace · ${g.label}`)
281
+ : g.label,
282
+ });
283
+ for (const s of g.sessions) rows.push({ kind: "session", session: s });
284
+ }
285
+ const selectableIdx = rows.flatMap((r, i) => (r.kind === "session" ? [i] : []));
286
+
287
+ let sel = 0; // 当前高亮会话在 selectableIdx 中的下标
269
288
  let drawnScreenLines = 0; // 已画出的屏幕行数(按终端宽度折行后),用于精确上移
270
289
  const cols = process.stdout.columns ?? 80;
271
290
 
@@ -281,13 +300,19 @@ async function handleResume(ctx: CommandContext) {
281
300
  const maxW = cols - 1;
282
301
  const out: string[] = [];
283
302
  out.push(chalk.bold.cyan(t("已保存的会话(↑/↓ 选择,回车确认,Esc 取消):", "Saved sessions (↑/↓ select, Enter confirm, Esc cancel):")));
284
- sessions.forEach((s, i) => {
285
- const date = new Date(s.timestamp).toLocaleString();
286
- const marker = i === cursor ? "" : " ";
287
- const text = `${marker}${i + 1}. ${s.preview} (${date} · ${s.model})`;
288
- const styled = i === cursor
289
- ? truncateStyled(text, maxW, (t) => chalk.bold.cyan(t))
290
- : truncateStyled(text, maxW, (t) => t);
303
+ let sessionNo = 0;
304
+ rows.forEach((row, i) => {
305
+ if (row.kind === "header") {
306
+ out.push(truncateStyled(` ${row.text}`, maxW, (x) => chalk.bold.yellow(x)));
307
+ return;
308
+ }
309
+ const active = selectableIdx[sel] === i;
310
+ const date = new Date(row.session.timestamp).toLocaleString();
311
+ const marker = active ? "▶ " : " ";
312
+ const text = `${marker}${++sessionNo}. ${row.session.preview} (${date} · ${row.session.model})`;
313
+ const styled = active
314
+ ? truncateStyled(text, maxW, (x) => chalk.bold.cyan(x))
315
+ : truncateStyled(text, maxW, (x) => x);
291
316
  out.push(styled);
292
317
  });
293
318
  out.push(chalk.cyan(t(" (按回车恢复当前选中项)", " (press Enter to resume the highlighted item)")));
@@ -307,18 +332,20 @@ async function handleResume(ctx: CommandContext) {
307
332
  // Arrow keys come as escape sequences: ESC [ A/B
308
333
  if (key === "\x1b[A") {
309
334
  // Up arrow: move toward newer (lower index)
310
- cursor = Math.max(0, cursor - 1);
335
+ sel = Math.max(0, sel - 1);
311
336
  render();
312
337
  } else if (key === "\x1b[B") {
313
338
  // Down arrow: move toward older
314
- cursor = Math.min(sessions.length - 1, cursor + 1);
339
+ sel = Math.min(selectableIdx.length - 1, sel + 1);
315
340
  render();
316
341
  } else if (key === "\r" || key === "\n") {
317
342
  // Enter: confirm selection
318
343
  cleanup();
319
- const sel = sessions[cursor];
320
- ctx.resumeSession(sel.path);
321
- console.log(chalk.green(t(`已恢复:${sel.preview}`, `Resumed: ${sel.preview}`)));
344
+ const row = rows[selectableIdx[sel]];
345
+ if (row.kind === "session") {
346
+ ctx.resumeSession(row.session.path);
347
+ console.log(chalk.green(t(`已恢复:${row.session.preview}`, `Resumed: ${row.session.preview}`)));
348
+ }
322
349
  resolveFn();
323
350
  } else if (key === "\x1b" || key === "\x1b\x1b") {
324
351
  // Escape: cancel
@@ -380,11 +407,23 @@ function handleExport(ctx: CommandContext) {
380
407
  const exportPath = join(process.cwd(), `arona-export-${Date.now()}.md`);
381
408
  writeFileSync(exportPath, markdown);
382
409
  console.log(chalk.green(t(`已导出到 ${exportPath}`, `Exported to ${exportPath}`)));
410
+ openFileCrossPlatform(exportPath);
411
+ }
383
412
 
413
+ /** 用系统默认程序打开文件(导出后自动弹出)。无对应命令/失败时静默忽略(文件已导出成功)。 */
414
+ function openFileCrossPlatform(filePath: string): void {
384
415
  try {
385
- execSync(`open "${exportPath}"`, { stdio: "ignore" });
416
+ if (process.platform === "darwin") {
417
+ execFileSync("open", [filePath], { stdio: "ignore" });
418
+ } else if (process.platform === "win32") {
419
+ // start 经 cmd 解析;首个 "" 是窗口标题占位(start 会把第一个带引号参数当标题)
420
+ const child = spawn("cmd", ["/c", "start", "", filePath], { stdio: "ignore", detached: true });
421
+ child.unref();
422
+ } else {
423
+ execFileSync("xdg-open", [filePath], { stdio: "ignore" });
424
+ }
386
425
  } catch {
387
- // Not macOS or open not available
426
+ // 平台无对应打开命令(常见于无桌面的 Linux):忽略
388
427
  }
389
428
  }
390
429
 
package/src/config.ts CHANGED
@@ -140,6 +140,10 @@ interface Settings {
140
140
  autoLoadSkills?: boolean;
141
141
  // CLI 模式开关(用户手写字段;true 时裸 `arona` 启动进命令行,默认 GUI)
142
142
  CLIEnabled?: boolean;
143
+ // GUI 选择过的工作区文件夹(最近选择在前,上限 12 条)
144
+ workspaces?: string[];
145
+ // GUI 上次活动的工作区(启动时恢复)
146
+ lastWorkspace?: string;
143
147
  }
144
148
 
145
149
  /**
@@ -173,6 +177,24 @@ export function updateSettings(patch: Record<string, unknown>): void {
173
177
  }
174
178
  }
175
179
 
180
+ /** GUI 选择过的工作区列表(最近选择在前)。 */
181
+ export function getStoredWorkspaces(): string[] {
182
+ const s = loadSettings();
183
+ return Array.isArray(s.workspaces) ? s.workspaces.filter((w): w is string => typeof w === "string" && w.length > 0) : [];
184
+ }
185
+
186
+ /** GUI 上次活动的工作区(启动恢复用;未记录返回 null)。 */
187
+ export function getLastWorkspace(): string | null {
188
+ const s = loadSettings();
189
+ return typeof s.lastWorkspace === "string" && s.lastWorkspace ? s.lastWorkspace : null;
190
+ }
191
+
192
+ /** 记住一次工作区选择:置顶列表并写入 lastWorkspace。 */
193
+ export function rememberWorkspace(workspace: string): void {
194
+ const rest = getStoredWorkspaces().filter((w) => w !== workspace);
195
+ updateSettings({ workspaces: [workspace, ...rest].slice(0, 12), lastWorkspace: workspace });
196
+ }
197
+
176
198
  // ============================================================
177
199
  // Model prefix auto-detection
178
200
  // ============================================================