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.
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
  // ============================================================