arona-agent 1.2.1 → 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/gui/main.cjs +68 -11
- package/gui/renderer/app.js +289 -39
- package/gui/renderer/index.html +6 -0
- package/gui/renderer/style.css +144 -2
- package/package.json +1 -1
- package/pet/agents.cjs +9 -4
- package/pet/main.cjs +41 -2
- package/pet/renderer/spinetest.js +3 -3
- package/pet/tools/gallery_capture.cjs +1 -1
- package/pet/tools/mouth_capture.cjs +1 -1
- package/pet/tools/visual_test.cjs +1 -1
- package/python/__pycache__/stt.cpython-314.pyc +0 -0
- package/python/__pycache__/tts_say.cpython-314.pyc +0 -0
- package/python/stt.py +24 -3
- package/src/agent.ts +11 -10
- package/src/agent_registry.ts +30 -0
- package/src/coding_agent.ts +6 -5
- package/src/commands.ts +56 -17
- package/src/config.ts +22 -0
- package/src/gui/controller.ts +385 -159
- package/src/gui/index.ts +57 -11
- package/src/gui/protocol.ts +11 -2
- package/src/gui/setup_backend.ts +15 -7
- package/src/index.ts +25 -5
- package/src/memory.ts +105 -12
- package/src/pet.ts +60 -1
- package/src/renderer.ts +27 -3
- package/src/repl.ts +24 -6
- package/src/setup.ts +14 -6
- package/src/speaker_context.ts +67 -1
- package/src/utils/python.ts +11 -6
- package/src/voice.ts +3 -2
- package/src/voices.ts +5 -1
- package/src/workspace.ts +163 -0
package/gui/main.cjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// ARONA GUI Electron 主进程:单窗口,与 Node 后端父进程经 stdin/stdout JSON lines 通信
|
|
2
2
|
// (协议行前缀 ###GUI### 过滤 Electron 日志;与桌宠桥同模式)。
|
|
3
|
-
const { app, BrowserWindow, Menu, ipcMain, nativeImage } = require("electron");
|
|
3
|
+
const { app, BrowserWindow, Menu, ipcMain, nativeImage, dialog } = require("electron");
|
|
4
|
+
const crypto = require("crypto");
|
|
4
5
|
const fs = require("fs");
|
|
6
|
+
const http = require("http");
|
|
7
|
+
const os = require("os");
|
|
5
8
|
const path = require("path");
|
|
6
9
|
|
|
7
10
|
// Windows:GUI 是纯 HTML/CSS(无 WebGL),默认硬件加速下页面加载/脚本均正常但不 paint(白屏,
|
|
@@ -16,6 +19,7 @@ if (process.platform === "win32" && process.env.ARONA_GUI_GPU !== "1") {
|
|
|
16
19
|
app.setPath("userData", path.join(app.getPath("appData"), "arona-agent-gui"));
|
|
17
20
|
|
|
18
21
|
const APP_TITLE = "Arona Agent";
|
|
22
|
+
const DEMO_CAPTURE_PATH = path.join(os.tmpdir(), "arona_gui_demo.png");
|
|
19
23
|
const ICON_PATH = path.join(__dirname, "renderer", "assets", "icon.png");
|
|
20
24
|
|
|
21
25
|
const PREFIX = "###GUI###";
|
|
@@ -110,7 +114,7 @@ function smokeProbeUI() {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
// ARONA_GUI_DEMO=1:不调用 LLM,向前端注入一段脚本化 agent_event 序列(思考 / 工具行 /
|
|
113
|
-
// 编码子代理实时过程 / 总结文本),预览渲染效果;结束时 capturePage
|
|
117
|
+
// 编码子代理实时过程 / 总结文本),预览渲染效果;结束时 capturePage 截图到系统临时目录(os.tmpdir(),跨平台)。
|
|
114
118
|
function demoScenario() {
|
|
115
119
|
const send = (m) => { if (win && !win.isDestroyed()) win.webContents.send("gui-event", m); };
|
|
116
120
|
const ev = (agentId, type, extra = {}) => send({ type: "agent_event", agentId, event: { type, ...extra } });
|
|
@@ -165,8 +169,8 @@ function demoScenario() {
|
|
|
165
169
|
).catch(() => {});
|
|
166
170
|
await sleep(300);
|
|
167
171
|
const imgTop = await win.webContents.capturePage();
|
|
168
|
-
fs.writeFileSync(
|
|
169
|
-
console.error("DEMO_CAPTURE
|
|
172
|
+
fs.writeFileSync(DEMO_CAPTURE_PATH, imgTop.toPNG());
|
|
173
|
+
console.error("DEMO_CAPTURE " + DEMO_CAPTURE_PATH);
|
|
170
174
|
return;
|
|
171
175
|
} catch (e) {
|
|
172
176
|
console.error("DEMO_SESSION_ERROR " + e);
|
|
@@ -232,8 +236,8 @@ function demoScenario() {
|
|
|
232
236
|
await sleep(600);
|
|
233
237
|
try {
|
|
234
238
|
const img = await win.webContents.capturePage();
|
|
235
|
-
fs.writeFileSync(
|
|
236
|
-
console.error("DEMO_CAPTURE
|
|
239
|
+
fs.writeFileSync(DEMO_CAPTURE_PATH, img.toPNG());
|
|
240
|
+
console.error("DEMO_CAPTURE " + DEMO_CAPTURE_PATH);
|
|
237
241
|
} catch (e) {
|
|
238
242
|
console.error("DEMO_CAPTURE_ERROR " + e);
|
|
239
243
|
}
|
|
@@ -287,10 +291,32 @@ function createWindow() {
|
|
|
287
291
|
|
|
288
292
|
// renderer → backend
|
|
289
293
|
ipcMain.on("gui-send", (_event, msg) => {
|
|
294
|
+
// 工作区目录选择:Electron 原生对话框只能由本进程弹(contextIsolation,渲染层无 Node),
|
|
295
|
+
// 结果回给渲染层(ws_folder_picked),由渲染层决定驱动后端 set_workspace 还是 move_session
|
|
296
|
+
if (msg && msg.type === "pick_workspace_folder") {
|
|
297
|
+
pickWorkspaceFolder();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
290
300
|
send(msg);
|
|
291
301
|
});
|
|
292
302
|
|
|
293
|
-
|
|
303
|
+
async function pickWorkspaceFolder() {
|
|
304
|
+
try {
|
|
305
|
+
const result = await dialog.showOpenDialog(win, {
|
|
306
|
+
title: "选择工作区文件夹",
|
|
307
|
+
properties: ["openDirectory"],
|
|
308
|
+
});
|
|
309
|
+
const picked = result.canceled ? null : (result.filePaths || [])[0];
|
|
310
|
+
if (picked && win && !win.isDestroyed()) {
|
|
311
|
+
win.webContents.send("gui-event", { type: "ws_folder_picked", path: picked });
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error("[gui] pick folder failed:", e);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// backend → renderer(行缓冲解析;Windows 下此路不通——GUI 子系统 Electron 的 stdin 数据不可达,
|
|
319
|
+
// 见下方 HTTP 通道。保留作非 Windows 平台的备用通道)
|
|
294
320
|
let buffer = "";
|
|
295
321
|
process.stdin.on("data", (data) => {
|
|
296
322
|
buffer += data.toString();
|
|
@@ -307,10 +333,41 @@ process.stdin.on("data", (data) => {
|
|
|
307
333
|
}
|
|
308
334
|
});
|
|
309
335
|
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// backend
|
|
313
|
-
|
|
336
|
+
// 本地 HTTP 通道(backend → GUI 主方向):Windows 下 Electron 子进程 stdin 完全不可达(hello 握手
|
|
337
|
+
// 证明进程与 stdout 均正常、数据仍不到达),事件改走 127.0.0.1 随机端口 + 随机 token(随 hello 行
|
|
338
|
+
// 告知 backend,防本机其他进程伪造)。stdout(GUI → backend)方向不受影响,继续走 ###GUI### 行。
|
|
339
|
+
const HTTP_TOKEN = crypto.randomBytes(16).toString("hex");
|
|
340
|
+
let helloSent = false;
|
|
341
|
+
function sendHello(httpPort) {
|
|
342
|
+
if (helloSent) return;
|
|
343
|
+
helloSent = true;
|
|
344
|
+
send({ type: "hello", httpPort: httpPort || 0, token: httpPort ? HTTP_TOKEN : "" });
|
|
345
|
+
}
|
|
346
|
+
const httpServer = http.createServer((req, res) => {
|
|
347
|
+
if (req.method !== "POST" || req.headers["x-arona-token"] !== HTTP_TOKEN) {
|
|
348
|
+
res.writeHead(403);
|
|
349
|
+
res.end();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let body = "";
|
|
353
|
+
req.on("data", (c) => (body += c));
|
|
354
|
+
req.on("end", () => {
|
|
355
|
+
res.writeHead(204);
|
|
356
|
+
res.end();
|
|
357
|
+
try {
|
|
358
|
+
forward(JSON.parse(body));
|
|
359
|
+
} catch {
|
|
360
|
+
// 非 JSON,忽略
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
httpServer.on("error", (e) => {
|
|
365
|
+
console.error("[gui] http server error:", e.message, "→ 退回 stdin 通道");
|
|
366
|
+
sendHello(0); // HTTP 起不来也要发 hello(不带端口,backend 退回 stdin 写入)
|
|
367
|
+
});
|
|
368
|
+
httpServer.listen(0, "127.0.0.1", () => {
|
|
369
|
+
sendHello(httpServer.address().port);
|
|
370
|
+
});
|
|
314
371
|
|
|
315
372
|
// 窗口全关:先发 exit 请求让后端走完整清理,再退出(延迟让协议行先 flush)
|
|
316
373
|
app.on("window-all-closed", () => {
|
package/gui/renderer/app.js
CHANGED
|
@@ -34,7 +34,17 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
34
34
|
let codingRunsByCall = new Map();
|
|
35
35
|
const menu = { open: false, items: [], index: 0 };
|
|
36
36
|
let currentMsg = null;
|
|
37
|
-
let sessionsData = { currentPath: null, sessions: [] };
|
|
37
|
+
let sessionsData = { currentPath: null, currentWorkspace: "", homeDir: "", knownWorkspaces: [], sessions: [] };
|
|
38
|
+
// 工作区分组展示状态(key = 工作区路径,"" = 未分组旧会话):
|
|
39
|
+
// wsCollapsed 折叠的组;wsShown 各组展开显示的条数(默认 WS_PAGE 条)
|
|
40
|
+
const WS_PAGE = 5;
|
|
41
|
+
const wsCollapsed = new Set();
|
|
42
|
+
const wsShown = new Map();
|
|
43
|
+
// 「移动到工作区」弹窗里点了「选择其他文件夹…」:暂存目标会话,
|
|
44
|
+
// 原生目录框回传(ws_folder_picked)时按此分流为 move_session 而非 set_workspace
|
|
45
|
+
let moveTargetSession = null;
|
|
46
|
+
// 已上报过的活动工作区(null = 尚未收到首个 sessions 事件;用于区分「切换」与「首次推送」)
|
|
47
|
+
let activeWsSeen = null;
|
|
38
48
|
let settingsModalOpen = false;
|
|
39
49
|
let settingsInfoEl = null;
|
|
40
50
|
// 录音中手动停止(再点麦克风):stt_result 空串时静默(不发"未检测到语音")
|
|
@@ -80,9 +90,18 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
80
90
|
} catch { return null; }
|
|
81
91
|
})();
|
|
82
92
|
|
|
83
|
-
/** 富文本渲染:Markdown → HTML(回复正文、工具输出共用)。
|
|
93
|
+
/** 富文本渲染:Markdown → HTML(回复正文、工具输出共用)。
|
|
94
|
+
* 例外:完整 HTML 文档(read index.html 等)不交给 marked——marked 会把源码透传成
|
|
95
|
+
* 真实 DOM(script 被净化、meta/link 不可见、空 div),视觉上整块空白;按源码块显示。 */
|
|
84
96
|
function richRender(el, text) {
|
|
85
97
|
const src = String(text);
|
|
98
|
+
if (/^\s*(<!DOCTYPE\s+html|<html[\s>])/i.test(src)) {
|
|
99
|
+
el.innerHTML = "";
|
|
100
|
+
const pre = document.createElement("pre");
|
|
101
|
+
pre.textContent = src;
|
|
102
|
+
el.appendChild(pre);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
86
105
|
if (mdParse) {
|
|
87
106
|
// 不做预转义:&/< 的转义交给 marked,防注入由 mdParse 的 DOM 净化负责(否则实体被二次转义)
|
|
88
107
|
el.innerHTML = mdParse(src);
|
|
@@ -130,6 +149,35 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
130
149
|
document.body.classList.toggle("has-msg", started);
|
|
131
150
|
}
|
|
132
151
|
|
|
152
|
+
// ── 工作区分组 ────────────────────────────────
|
|
153
|
+
/** 工作区显示名:取目录名;家目录「用户目录」;根目录 /;空值 = 旧会话未记录工作区(「未分组」)。 */
|
|
154
|
+
function wsLabel(ws) {
|
|
155
|
+
if (!ws) return "未分组";
|
|
156
|
+
if (ws === "/") return "/";
|
|
157
|
+
if (sessionsData.homeDir && ws === sessionsData.homeDir) return "用户目录";
|
|
158
|
+
const base = String(ws).replace(/\/+$/, "").split("/").pop();
|
|
159
|
+
return base || ws;
|
|
160
|
+
}
|
|
161
|
+
/** 按工作区分组:组内/组间按最新会话时间倒序;未分组恒排最后。与后端 workspace.ts 同构。 */
|
|
162
|
+
function groupSessions(sessions) {
|
|
163
|
+
const byKey = new Map();
|
|
164
|
+
for (const s of sessions) {
|
|
165
|
+
const k = s.workspace || "";
|
|
166
|
+
if (!byKey.has(k)) byKey.set(k, []);
|
|
167
|
+
byKey.get(k).push(s);
|
|
168
|
+
}
|
|
169
|
+
const groups = [...byKey.entries()].map(([workspace, list]) => {
|
|
170
|
+
list.sort((a, b) => String(b.timestamp || "").localeCompare(String(a.timestamp || "")));
|
|
171
|
+
return { workspace: workspace || null, label: wsLabel(workspace), sessions: list };
|
|
172
|
+
});
|
|
173
|
+
groups.sort((a, b) => {
|
|
174
|
+
if (!a.workspace) return 1;
|
|
175
|
+
if (!b.workspace) return -1;
|
|
176
|
+
return String(b.sessions[0]?.timestamp || "").localeCompare(String(a.sessions[0]?.timestamp || ""));
|
|
177
|
+
});
|
|
178
|
+
return groups;
|
|
179
|
+
}
|
|
180
|
+
|
|
133
181
|
// ── SVG 图标库(stroke 风格;工具行 / 思考块共用,须先于 THINK_SVG 初始化)──
|
|
134
182
|
const ICONS = {
|
|
135
183
|
terminal: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>',
|
|
@@ -142,6 +190,7 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
142
190
|
task: '<polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
|
|
143
191
|
box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
|
|
144
192
|
wrench: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
|
193
|
+
folder: '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>',
|
|
145
194
|
chev: '<polyline points="6 9 12 15 18 9"/>',
|
|
146
195
|
};
|
|
147
196
|
function svgIcon(name, size) {
|
|
@@ -612,7 +661,49 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
612
661
|
if (settingsModalOpen) refreshSettingsModal();
|
|
613
662
|
}
|
|
614
663
|
|
|
615
|
-
// ──
|
|
664
|
+
// ── 侧栏会话列表(工作区嵌套会话分组)──────────
|
|
665
|
+
/** 单条会话行(点击 resume;右键重命名/删除/多选;多选模式为勾选行)。 */
|
|
666
|
+
function buildSessionRow(s) {
|
|
667
|
+
const row = document.createElement("button");
|
|
668
|
+
row.className = "session-item" + (s.path === sessionsData.currentPath ? " current" : "");
|
|
669
|
+
row.title = multiSelect ? "" : s.preview;
|
|
670
|
+
if (multiSelect) {
|
|
671
|
+
row.classList.toggle("selected", multiSelected.has(s.path));
|
|
672
|
+
const check = document.createElement("span");
|
|
673
|
+
check.className = "sess-check";
|
|
674
|
+
check.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
675
|
+
row.appendChild(check);
|
|
676
|
+
}
|
|
677
|
+
const title = document.createElement("span");
|
|
678
|
+
title.className = "s-title";
|
|
679
|
+
title.textContent = s.preview || "(untitled)";
|
|
680
|
+
const time = document.createElement("span");
|
|
681
|
+
time.className = "s-time";
|
|
682
|
+
time.textContent = relTime(s.timestamp);
|
|
683
|
+
row.appendChild(title);
|
|
684
|
+
row.appendChild(time);
|
|
685
|
+
if (multiSelect) {
|
|
686
|
+
row.addEventListener("click", () => {
|
|
687
|
+
if (multiSelected.has(s.path)) multiSelected.delete(s.path);
|
|
688
|
+
else multiSelected.add(s.path);
|
|
689
|
+
row.classList.toggle("selected", multiSelected.has(s.path));
|
|
690
|
+
updateMultiBar();
|
|
691
|
+
});
|
|
692
|
+
} else {
|
|
693
|
+
row.addEventListener("click", () => api.send({ type: "resume_session", path: s.path }));
|
|
694
|
+
row.addEventListener("contextmenu", (e) => {
|
|
695
|
+
e.preventDefault();
|
|
696
|
+
openContextMenu(e.clientX, e.clientY, [
|
|
697
|
+
{ label: "重命名", onClick: () => openRenameModal(s) },
|
|
698
|
+
{ label: "移动到工作区", onClick: () => openMoveWsModal(s) },
|
|
699
|
+
{ label: "删除", danger: true, onClick: () => confirmDeleteSessions([s]) },
|
|
700
|
+
{ label: "多选", onClick: () => setMultiSelect(true) },
|
|
701
|
+
]);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return row;
|
|
705
|
+
}
|
|
706
|
+
|
|
616
707
|
function renderSessions() {
|
|
617
708
|
sessionListEl.innerHTML = "";
|
|
618
709
|
sessionListEl.classList.toggle("multi", multiSelect);
|
|
@@ -624,44 +715,67 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
624
715
|
updateMultiBar();
|
|
625
716
|
return;
|
|
626
717
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
718
|
+
const groups = groupSessions(sessionsData.sessions);
|
|
719
|
+
// 当前会话所在组始终展开(组头折叠状态由用户点击驱动)
|
|
720
|
+
const curGroup = groups.find((g) => g.sessions.some((s) => s.path === sessionsData.currentPath));
|
|
721
|
+
if (curGroup) wsCollapsed.delete(curGroup.workspace || "");
|
|
722
|
+
for (const g of groups) {
|
|
723
|
+
const key = g.workspace || "";
|
|
724
|
+
const collapsed = wsCollapsed.has(key);
|
|
725
|
+
const groupEl = document.createElement("div");
|
|
726
|
+
groupEl.className = "ws-group" + (collapsed ? " collapsed" : "");
|
|
727
|
+
groupEl.dataset.ws = key;
|
|
728
|
+
|
|
729
|
+
// 组头:文件夹图标 + 工作区名 + 数量 + 折叠箭头(多选模式下仅展示)
|
|
730
|
+
const head = document.createElement(multiSelect ? "div" : "button");
|
|
731
|
+
head.className = "ws-head";
|
|
732
|
+
head.title = g.workspace || "";
|
|
733
|
+
head.innerHTML = `<span class="ws-folder">${svgIcon("folder", 14)}</span>`
|
|
734
|
+
+ `<span class="ws-name">${esc(g.label)}</span>`
|
|
735
|
+
+ `<span class="ws-count">${g.sessions.length}</span>`
|
|
736
|
+
+ `<span class="ws-chev">${CHEV_SVG}</span>`;
|
|
737
|
+
if (!multiSelect) {
|
|
738
|
+
head.addEventListener("click", () => {
|
|
739
|
+
// 只切状态类:collapse-body 的 grid 过渡自动播放(不重建 DOM,重建会跳过动画)
|
|
740
|
+
const next = !wsCollapsed.has(key);
|
|
741
|
+
if (next) wsCollapsed.add(key);
|
|
742
|
+
else wsCollapsed.delete(key);
|
|
743
|
+
groupEl.classList.toggle("collapsed", next);
|
|
744
|
+
body.classList.toggle("collapsed", next);
|
|
745
|
+
});
|
|
637
746
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
747
|
+
groupEl.appendChild(head);
|
|
748
|
+
|
|
749
|
+
// 组体:collapse-body 工具类包裹(折叠/展开过渡动画,见 style.css 工具类说明)
|
|
750
|
+
const body = document.createElement("div");
|
|
751
|
+
body.className = "collapse-body" + (collapsed ? " collapsed" : "");
|
|
752
|
+
const bodyInner = document.createElement("div");
|
|
753
|
+
bodyInner.className = "collapse-inner ws-body";
|
|
754
|
+
|
|
755
|
+
const shown = Math.min(wsShown.get(key) ?? WS_PAGE, g.sessions.length);
|
|
756
|
+
for (const s of g.sessions.slice(0, shown)) bodyInner.appendChild(buildSessionRow(s));
|
|
757
|
+
if (shown < g.sessions.length) {
|
|
758
|
+
const more = document.createElement("button");
|
|
759
|
+
more.className = "ws-more";
|
|
760
|
+
more.textContent = `显示更多(${g.sessions.length - shown})`;
|
|
761
|
+
more.addEventListener("click", () => {
|
|
762
|
+
wsShown.set(key, shown + WS_PAGE);
|
|
763
|
+
renderSessions();
|
|
652
764
|
});
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
]);
|
|
765
|
+
bodyInner.appendChild(more);
|
|
766
|
+
} else if (shown > WS_PAGE) {
|
|
767
|
+
const less = document.createElement("button");
|
|
768
|
+
less.className = "ws-more";
|
|
769
|
+
less.textContent = "收起";
|
|
770
|
+
less.addEventListener("click", () => {
|
|
771
|
+
wsShown.delete(key);
|
|
772
|
+
renderSessions();
|
|
662
773
|
});
|
|
774
|
+
bodyInner.appendChild(less);
|
|
663
775
|
}
|
|
664
|
-
|
|
776
|
+
body.appendChild(bodyInner);
|
|
777
|
+
groupEl.appendChild(body);
|
|
778
|
+
sessionListEl.appendChild(groupEl);
|
|
665
779
|
}
|
|
666
780
|
updateMultiBar();
|
|
667
781
|
}
|
|
@@ -723,6 +837,7 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
723
837
|
if (e.key === "Escape") {
|
|
724
838
|
closeConfirm();
|
|
725
839
|
closeContextMenu();
|
|
840
|
+
closeWsMenu();
|
|
726
841
|
if (multiSelect) setMultiSelect(false);
|
|
727
842
|
}
|
|
728
843
|
});
|
|
@@ -786,6 +901,24 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
786
901
|
inputEl.select();
|
|
787
902
|
}
|
|
788
903
|
|
|
904
|
+
/** 移动会话到工作区:列已知工作区 + 「选择其他文件夹…」(原生目录框,ws_folder_picked 分流)。 */
|
|
905
|
+
function openMoveWsModal(s) {
|
|
906
|
+
const items = (sessionsData.knownWorkspaces || []).map((ws) => ({
|
|
907
|
+
label: wsLabel(ws),
|
|
908
|
+
meta: ws,
|
|
909
|
+
onPick: () => api.send({ type: "move_session", path: s.path, workspace: ws }),
|
|
910
|
+
}));
|
|
911
|
+
items.push({
|
|
912
|
+
label: "选择其他文件夹…",
|
|
913
|
+
meta: "",
|
|
914
|
+
onPick: () => {
|
|
915
|
+
moveTargetSession = s.path;
|
|
916
|
+
api.send({ type: "pick_workspace_folder" });
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
openModal("移动到工作区", pickList(items, (i) => i.meta || ""));
|
|
920
|
+
}
|
|
921
|
+
|
|
789
922
|
// ── 斜杠菜单(combobox:Enter 只补全不执行)─────
|
|
790
923
|
function menuRefresh() {
|
|
791
924
|
const v = input.value;
|
|
@@ -917,6 +1050,100 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
917
1050
|
});
|
|
918
1051
|
$("#btn-settings").addEventListener("click", openSettingsModal);
|
|
919
1052
|
|
|
1053
|
+
// ── 欢迎页工作区选择器(初始页输入框左下角,麦克风左侧)──
|
|
1054
|
+
$("#ws-picker-icon").innerHTML = svgIcon("folder", 14);
|
|
1055
|
+
$("#ws-picker-chev").innerHTML = CHEV_SVG;
|
|
1056
|
+
|
|
1057
|
+
/** 同步选择器文案:当前工作区名(GUI 后端启动目录)。 */
|
|
1058
|
+
function updateWsPicker() {
|
|
1059
|
+
$("#ws-picker-label").textContent = sessionsData.currentWorkspace
|
|
1060
|
+
? wsLabel(sessionsData.currentWorkspace)
|
|
1061
|
+
: "会话";
|
|
1062
|
+
$("#ws-picker").title = sessionsData.currentWorkspace || "";
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** 侧栏定位展开某工作区分组:全量显示并滚动到组头(下拉「显示更多」/ 组头点击联动)。 */
|
|
1066
|
+
function revealGroupInSidebar(key) {
|
|
1067
|
+
wsCollapsed.delete(key);
|
|
1068
|
+
wsShown.set(key, Infinity);
|
|
1069
|
+
document.body.classList.remove("sb-collapsed");
|
|
1070
|
+
document.body.classList.add("sb-expanded");
|
|
1071
|
+
$("#btn-expand").classList.add("hidden");
|
|
1072
|
+
$("#btn-collapse").classList.remove("hidden");
|
|
1073
|
+
renderSessions();
|
|
1074
|
+
sessionListEl.querySelector(`.ws-group[data-ws="${CSS.escape(key)}"]`)?.scrollIntoView({ block: "start" });
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function buildWsMenu() {
|
|
1078
|
+
const menu = $("#ws-menu");
|
|
1079
|
+
menu.innerHTML = "";
|
|
1080
|
+
// 各工作区的会话数(knownWorkspaces 可能含无会话的历史选择项)
|
|
1081
|
+
const countByWs = new Map();
|
|
1082
|
+
for (const s of sessionsData.sessions) {
|
|
1083
|
+
const k = s.workspace || "";
|
|
1084
|
+
countByWs.set(k, (countByWs.get(k) || 0) + 1);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// 工作区列表(settings 选择历史 ∪ 会话推导,后端已合并去重;当前项高亮)
|
|
1088
|
+
for (const ws of sessionsData.knownWorkspaces || []) {
|
|
1089
|
+
const row = document.createElement("button");
|
|
1090
|
+
row.className = "ws-all" + (ws === sessionsData.currentWorkspace ? " current" : "");
|
|
1091
|
+
row.title = ws;
|
|
1092
|
+
row.innerHTML = `<span class="ws-folder">${svgIcon("folder", 14)}</span>`
|
|
1093
|
+
+ `<span class="ws-name">${esc(wsLabel(ws))}</span>`
|
|
1094
|
+
+ (countByWs.has(ws) ? `<span class="ws-count">${countByWs.get(ws)}</span>` : "");
|
|
1095
|
+
row.addEventListener("click", () => {
|
|
1096
|
+
closeWsMenu();
|
|
1097
|
+
api.send({ type: "set_workspace", path: ws });
|
|
1098
|
+
});
|
|
1099
|
+
menu.appendChild(row);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (!(sessionsData.knownWorkspaces || []).length) {
|
|
1103
|
+
const empty = document.createElement("div");
|
|
1104
|
+
empty.className = "ws-empty";
|
|
1105
|
+
empty.textContent = "暂无工作区";
|
|
1106
|
+
menu.appendChild(empty);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// 选择其他位置的文件夹:Electron 原生目录对话框(main.cjs 弹出后经 ws_folder_picked 回传)
|
|
1110
|
+
const pick = document.createElement("button");
|
|
1111
|
+
pick.className = "ws-all ws-pick";
|
|
1112
|
+
pick.innerHTML = `<span class="ws-folder">${svgIcon("folder", 14)}</span><span class="ws-name">选择本地文件夹…</span>`;
|
|
1113
|
+
pick.addEventListener("click", () => {
|
|
1114
|
+
moveTargetSession = null;
|
|
1115
|
+
closeWsMenu();
|
|
1116
|
+
api.send({ type: "pick_workspace_folder" });
|
|
1117
|
+
});
|
|
1118
|
+
menu.appendChild(pick);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function toggleWsMenu() {
|
|
1122
|
+
const menu = $("#ws-menu");
|
|
1123
|
+
if (menu.classList.contains("hidden")) {
|
|
1124
|
+
buildWsMenu();
|
|
1125
|
+
glassClear(menu);
|
|
1126
|
+
menu.classList.remove("hidden");
|
|
1127
|
+
$("#ws-picker").classList.add("open");
|
|
1128
|
+
} else {
|
|
1129
|
+
closeWsMenu();
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function closeWsMenu() {
|
|
1133
|
+
const menu = $("#ws-menu");
|
|
1134
|
+
if (menu.classList.contains("hidden")) return;
|
|
1135
|
+
glassHide(menu);
|
|
1136
|
+
$("#ws-picker").classList.remove("open");
|
|
1137
|
+
}
|
|
1138
|
+
$("#ws-picker").addEventListener("click", toggleWsMenu);
|
|
1139
|
+
document.addEventListener("pointerdown", (e) => {
|
|
1140
|
+
const menu = $("#ws-menu");
|
|
1141
|
+
if (!menu.classList.contains("hidden")
|
|
1142
|
+
&& !menu.contains(e.target) && !$("#ws-picker").contains(e.target)) {
|
|
1143
|
+
closeWsMenu();
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
|
|
920
1147
|
// ── 弹窗 ──────────────────────────────────────
|
|
921
1148
|
function openModal(title, bodyNode, footerNode) {
|
|
922
1149
|
$("#modal-title-text").textContent = title;
|
|
@@ -1194,9 +1421,32 @@ window.addEventListener("unhandledrejection", (e) => {
|
|
|
1194
1421
|
case "notice":
|
|
1195
1422
|
statusLine(msg.text, msg.level === "info" ? "" : msg.level);
|
|
1196
1423
|
break;
|
|
1197
|
-
case "sessions":
|
|
1198
|
-
|
|
1424
|
+
case "sessions": {
|
|
1425
|
+
const prevWs = sessionsData.currentWorkspace;
|
|
1426
|
+
sessionsData = {
|
|
1427
|
+
currentPath: msg.currentPath,
|
|
1428
|
+
currentWorkspace: msg.currentWorkspace || "",
|
|
1429
|
+
homeDir: msg.homeDir || "",
|
|
1430
|
+
knownWorkspaces: msg.knownWorkspaces || [],
|
|
1431
|
+
sessions: msg.sessions || [],
|
|
1432
|
+
};
|
|
1199
1433
|
renderSessions();
|
|
1434
|
+
updateWsPicker();
|
|
1435
|
+
// 工作区被切换(选择器操作后端确认回推):侧栏展开定位到新工作区分组
|
|
1436
|
+
if (activeWsSeen !== null && prevWs !== sessionsData.currentWorkspace) {
|
|
1437
|
+
revealGroupInSidebar(sessionsData.currentWorkspace);
|
|
1438
|
+
}
|
|
1439
|
+
activeWsSeen = sessionsData.currentWorkspace;
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
case "ws_folder_picked":
|
|
1443
|
+
// 原生目录框回传:移动会话弹窗发起 → move_session;选择器发起 → set_workspace
|
|
1444
|
+
if (moveTargetSession) {
|
|
1445
|
+
api.send({ type: "move_session", path: moveTargetSession, workspace: msg.path });
|
|
1446
|
+
moveTargetSession = null;
|
|
1447
|
+
} else {
|
|
1448
|
+
api.send({ type: "set_workspace", path: msg.path });
|
|
1449
|
+
}
|
|
1200
1450
|
break;
|
|
1201
1451
|
case "skills":
|
|
1202
1452
|
openSkillsModal(msg.skills);
|
package/gui/renderer/index.html
CHANGED
|
@@ -52,10 +52,16 @@
|
|
|
52
52
|
</div>
|
|
53
53
|
</div>
|
|
54
54
|
<div id="composer-wrap">
|
|
55
|
+
<div id="ws-menu" class="hidden"></div>
|
|
55
56
|
<div id="slash-menu" class="hidden"></div>
|
|
56
57
|
<div id="composer">
|
|
57
58
|
<textarea id="input" rows="1" placeholder="输入消息,/ 为命令,Enter 发送"></textarea>
|
|
58
59
|
<div id="composer-bar">
|
|
60
|
+
<button id="ws-picker" class="ws-picker" title="工作区">
|
|
61
|
+
<span class="ws-folder" id="ws-picker-icon"></span>
|
|
62
|
+
<span class="ws-label" id="ws-picker-label"></span>
|
|
63
|
+
<span class="ws-chev" id="ws-picker-chev"></span>
|
|
64
|
+
</button>
|
|
59
65
|
<button id="btn-mic" class="icon-btn" title="录音">
|
|
60
66
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg>
|
|
61
67
|
</button>
|