openclacky 1.3.2 → 1.3.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/Dockerfile +3 -0
  4. data/README.md +1 -1
  5. data/README_JA.md +237 -0
  6. data/lib/clacky/agent/session_serializer.rb +49 -5
  7. data/lib/clacky/agent/time_machine.rb +247 -26
  8. data/lib/clacky/agent.rb +12 -1
  9. data/lib/clacky/agent_config.rb +14 -2
  10. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  11. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  12. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  13. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  14. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  15. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  16. data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
  17. data/lib/clacky/media/openai_compat.rb +64 -1
  18. data/lib/clacky/media/output_dir.rb +43 -0
  19. data/lib/clacky/message_history.rb +9 -0
  20. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  21. data/lib/clacky/server/git_panel.rb +115 -0
  22. data/lib/clacky/server/http_server.rb +497 -12
  23. data/lib/clacky/server/server_master.rb +6 -4
  24. data/lib/clacky/version.rb +1 -1
  25. data/lib/clacky/web/app.css +473 -60
  26. data/lib/clacky/web/app.js +30 -7
  27. data/lib/clacky/web/components/code-editor.js +197 -0
  28. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  29. data/lib/clacky/web/core/aside.js +112 -0
  30. data/lib/clacky/web/core/ext.js +387 -0
  31. data/lib/clacky/web/features/backup/store.js +92 -0
  32. data/lib/clacky/web/features/backup/view.js +94 -0
  33. data/lib/clacky/web/features/billing/store.js +163 -0
  34. data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
  35. data/lib/clacky/web/features/brand/store.js +110 -0
  36. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  37. data/lib/clacky/web/features/channels/store.js +103 -0
  38. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  39. data/lib/clacky/web/features/creator/store.js +81 -0
  40. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  41. data/lib/clacky/web/features/mcp/store.js +158 -0
  42. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  43. data/lib/clacky/web/features/model-tester/store.js +77 -0
  44. data/lib/clacky/web/features/model-tester/view.js +7 -0
  45. data/lib/clacky/web/features/profile/store.js +170 -0
  46. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  47. data/lib/clacky/web/features/share/store.js +145 -0
  48. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  49. data/lib/clacky/web/features/skills/store.js +303 -0
  50. data/lib/clacky/web/features/skills/view.js +550 -0
  51. data/lib/clacky/web/features/tasks/store.js +135 -0
  52. data/lib/clacky/web/features/tasks/view.js +241 -0
  53. data/lib/clacky/web/features/trash/store.js +242 -0
  54. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  55. data/lib/clacky/web/features/version/store.js +165 -0
  56. data/lib/clacky/web/features/version/view.js +323 -0
  57. data/lib/clacky/web/features/workspace/store.js +99 -0
  58. data/lib/clacky/web/features/workspace/view.js +305 -0
  59. data/lib/clacky/web/i18n.js +56 -6
  60. data/lib/clacky/web/index.html +117 -58
  61. data/lib/clacky/web/sessions.js +221 -25
  62. data/lib/clacky/web/settings.js +118 -22
  63. data/lib/clacky/web/skills.js +3 -863
  64. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  65. data/lib/clacky.rb +1 -0
  66. metadata +45 -20
  67. data/lib/clacky/web/backup.js +0 -119
  68. data/lib/clacky/web/model-tester.js +0 -66
  69. data/lib/clacky/web/tasks.js +0 -373
  70. data/lib/clacky/web/version.js +0 -449
  71. data/lib/clacky/web/workspace.js +0 -316
  72. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  73. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  74. /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
  75. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  76. /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
@@ -0,0 +1,305 @@
1
+ // ── Workspace · view — Files tab (download artifacts) ─────────────────────
2
+ //
3
+ // Renders the working-directory file tree as the "Files" tab in the session
4
+ // aside. The tab is product-positioned as "see what the AI produced and
5
+ // download it": clicking a file downloads it; directories expand lazily;
6
+ // right-click reveals in the OS file manager (desktop only).
7
+ //
8
+ // Registered as a host-owned (built-in) tab via Clacky.ext.ui.mountBuiltin so
9
+ // it shows for every session regardless of agent profile. All I/O goes through
10
+ // WorkspaceStore.
11
+ //
12
+ // Depends on: WorkspaceStore, Clacky.ext, I18n, Modal.
13
+ // ───────────────────────────────────────────────────────────────────────────
14
+ "use strict";
15
+
16
+ const WorkspaceView = (() => {
17
+ const t = (key) => (typeof I18n !== "undefined" ? I18n.t(key) : key);
18
+
19
+ const ICON_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>';
20
+ const ICON_FILE = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
21
+ const ICON_CARET = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
22
+
23
+ function formatSize(bytes) {
24
+ if (bytes == null) return "";
25
+ if (bytes < 1024) return `${bytes} B`;
26
+ const units = ["KB", "MB", "GB", "TB"];
27
+ let n = bytes / 1024, i = 0;
28
+ while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
29
+ return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
30
+ }
31
+
32
+ function renderEntries(entries) {
33
+ const frag = document.createDocumentFragment();
34
+ if (!entries.length) {
35
+ const empty = document.createElement("div");
36
+ empty.className = "wt-empty";
37
+ empty.textContent = t("workspace.empty");
38
+ frag.appendChild(empty);
39
+ return frag;
40
+ }
41
+ for (const entry of entries) frag.appendChild(buildNode(entry));
42
+ return frag;
43
+ }
44
+
45
+ function buildNode(entry) {
46
+ const node = document.createElement("div");
47
+ node.className = "wt-node";
48
+
49
+ const row = document.createElement("div");
50
+ row.className = "wt-row";
51
+ row.title = entry.name;
52
+
53
+ const caret = document.createElement("span");
54
+ caret.className = "wt-caret" + (entry.type === "dir" ? "" : " leaf");
55
+ if (entry.type === "dir") caret.innerHTML = ICON_CARET;
56
+
57
+ const icon = document.createElement("span");
58
+ icon.className = "wt-icon";
59
+ icon.innerHTML = entry.type === "dir" ? ICON_FOLDER : ICON_FILE;
60
+
61
+ const name = document.createElement("span");
62
+ name.className = "wt-name";
63
+ name.textContent = entry.name;
64
+
65
+ row.appendChild(caret);
66
+ row.appendChild(icon);
67
+ row.appendChild(name);
68
+
69
+ if (entry.type === "file") {
70
+ const size = document.createElement("span");
71
+ size.className = "wt-size";
72
+ size.textContent = formatSize(entry.size);
73
+ row.appendChild(size);
74
+ }
75
+
76
+ node.appendChild(row);
77
+
78
+ if (entry.type === "dir") {
79
+ const children = document.createElement("div");
80
+ children.className = "wt-children";
81
+ children.style.display = "none";
82
+ node.appendChild(children);
83
+ row.addEventListener("click", () => toggleDir(entry, caret, children));
84
+ } else {
85
+ row.addEventListener("click", () => openFile(entry));
86
+ }
87
+
88
+ row.addEventListener("contextmenu", (e) => {
89
+ e.preventDefault();
90
+ showContextMenu(e, entry);
91
+ });
92
+
93
+ return node;
94
+ }
95
+
96
+ function showContextMenu(e, entry) {
97
+ closeContextMenu();
98
+
99
+ const iconFolder = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>';
100
+ const iconDownload = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="M7.5 11l4.5 4.5 4.5-4.5"/><path d="M5 20h14"/></svg>';
101
+ const iconCopy = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
102
+ const iconRelPath = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/><path d="M12 19l3-3"/></svg>';
103
+
104
+ const downloadItem = entry.type === "file" ? `
105
+ <div class="session-actions-menu-item" data-action="download">
106
+ <span class="session-actions-menu-icon">${iconDownload}</span>
107
+ <span class="session-actions-menu-label">${t("workspace.download")}</span>
108
+ </div>` : "";
109
+
110
+ const menu = document.createElement("div");
111
+ menu.className = "wt-context-menu session-context-menu";
112
+ menu.innerHTML = `
113
+ <div class="session-actions-menu-item" data-action="reveal">
114
+ <span class="session-actions-menu-icon">${iconFolder}</span>
115
+ <span class="session-actions-menu-label">${t("workspace.revealInFinder")}</span>
116
+ </div>
117
+ <div class="session-actions-menu-item" data-action="copypath">
118
+ <span class="session-actions-menu-icon">${iconCopy}</span>
119
+ <span class="session-actions-menu-label">${t("workspace.copyPath")}</span>
120
+ </div>
121
+ <div class="session-actions-menu-item" data-action="copyrelpath">
122
+ <span class="session-actions-menu-icon">${iconRelPath}</span>
123
+ <span class="session-actions-menu-label">${t("workspace.copyRelPath")}</span>
124
+ </div>
125
+ ${downloadItem}
126
+ `;
127
+
128
+ document.body.appendChild(menu);
129
+ menu.addEventListener("contextmenu", (ev) => ev.preventDefault());
130
+ menu.style.position = "fixed";
131
+ menu.style.top = e.clientY + "px";
132
+ menu.style.left = e.clientX + "px";
133
+ requestAnimationFrame(() => {
134
+ const r = menu.getBoundingClientRect();
135
+ if (r.right > window.innerWidth) menu.style.left = (window.innerWidth - r.width - 8) + "px";
136
+ if (r.bottom > window.innerHeight) menu.style.top = (window.innerHeight - r.height - 8) + "px";
137
+ });
138
+
139
+ menu.addEventListener("click", async (ev) => {
140
+ const item = ev.target.closest(".session-actions-menu-item");
141
+ if (!item) return;
142
+ closeContextMenu();
143
+ if (item.dataset.action === "reveal") await revealFile(entry);
144
+ if (item.dataset.action === "download") await downloadFile(entry);
145
+ if (item.dataset.action === "copypath") copyPath(entry);
146
+ if (item.dataset.action === "copyrelpath") copyRelPath(entry);
147
+ });
148
+
149
+ setTimeout(() => {
150
+ document.addEventListener("click", closeContextMenu, { once: true });
151
+ }, 0);
152
+ }
153
+
154
+ function closeContextMenu() {
155
+ const existing = document.querySelector(".wt-context-menu");
156
+ if (existing) existing.remove();
157
+ }
158
+
159
+ function copyPath(entry) {
160
+ const absPath = Workspace.state.workingDir.replace(/\/+$/, "") + "/" + entry.path.replace(/^\/+/, "");
161
+ navigator.clipboard.writeText(absPath).then(() => {
162
+ Modal.toast(absPath, "info");
163
+ });
164
+ }
165
+
166
+ function copyRelPath(entry) {
167
+ navigator.clipboard.writeText(entry.path).then(() => {
168
+ Modal.toast(entry.path, "info");
169
+ });
170
+ }
171
+
172
+ async function revealFile(entry) {
173
+ try {
174
+ await Workspace.revealFile(entry);
175
+ } catch (err) {
176
+ console.error("reveal failed:", err);
177
+ if (typeof Modal !== "undefined") Modal.toast(t("workspace.revealFailed"), "error");
178
+ }
179
+ }
180
+
181
+ async function toggleDir(entry, caret, children) {
182
+ const isOpen = caret.classList.contains("open");
183
+ if (isOpen) {
184
+ caret.classList.remove("open");
185
+ children.style.display = "none";
186
+ return;
187
+ }
188
+ caret.classList.add("open");
189
+ children.style.display = "";
190
+ if (children.dataset.loaded === "1") return;
191
+
192
+ children.innerHTML = `<div class="wt-loading">${t("workspace.loading")}</div>`;
193
+ try {
194
+ const entries = await Workspace.fetchEntries(entry.path);
195
+ children.innerHTML = "";
196
+ children.appendChild(renderEntries(entries));
197
+ children.dataset.loaded = "1";
198
+ } catch (err) {
199
+ console.error("workspace load failed:", err);
200
+ children.innerHTML = `<div class="wt-error">${t("workspace.error")}</div>`;
201
+ }
202
+ }
203
+
204
+ async function openFile(entry) {
205
+ const kind = CodeEditor.fileKind(entry.name);
206
+ if (kind === "binary") {
207
+ Modal.toast(t("workspace.previewUnsupported"), "info");
208
+ return;
209
+ }
210
+ if (kind === "image") {
211
+ try {
212
+ const blob = await Workspace.fetchFileBlob(entry);
213
+ const url = URL.createObjectURL(blob);
214
+ CodeEditor.open({ filename: entry.name, title: entry.name, kind: "image", imageUrl: url, onClose: () => URL.revokeObjectURL(url) });
215
+ } catch (err) {
216
+ console.error("preview failed:", err);
217
+ Modal.toast(t("workspace.previewFailed"), "error");
218
+ }
219
+ return;
220
+ }
221
+ try {
222
+ const text = await Workspace.fetchFileText(entry);
223
+ CodeEditor.open({ filename: entry.name, title: entry.name, content: text, readOnly: true });
224
+ } catch (err) {
225
+ console.error("preview failed:", err);
226
+ Modal.toast(t("workspace.previewFailed"), "error");
227
+ }
228
+ }
229
+
230
+ async function downloadFile(entry) {
231
+ try {
232
+ const blob = await Workspace.fetchFileBlob(entry);
233
+ const url = URL.createObjectURL(blob);
234
+ const a = document.createElement("a");
235
+ a.href = url;
236
+ a.download = entry.name;
237
+ document.body.appendChild(a);
238
+ a.click();
239
+ a.remove();
240
+ URL.revokeObjectURL(url);
241
+ } catch (err) {
242
+ console.error("download failed:", err);
243
+ if (typeof Modal !== "undefined") Modal.toast(t("workspace.downloadFailed"), "error");
244
+ }
245
+ }
246
+
247
+ async function loadRoot(tree) {
248
+ if (!tree || !Workspace.state.hasSession()) return;
249
+ tree.innerHTML = `<div class="wt-loading">${t("workspace.loading")}</div>`;
250
+ try {
251
+ const entries = await Workspace.fetchEntries("");
252
+ tree.innerHTML = "";
253
+ tree.appendChild(renderEntries(entries));
254
+ } catch (err) {
255
+ console.error("workspace load failed:", err);
256
+ tree.innerHTML = `<div class="wt-error">${t("workspace.error")}</div>`;
257
+ }
258
+ }
259
+
260
+ // Build the Files tab body for the current session.
261
+ function renderFilesTab(_ctx) {
262
+ const wrap = document.createElement("div");
263
+ wrap.className = "wt-panel";
264
+
265
+ const bar = document.createElement("div");
266
+ bar.className = "wt-bar";
267
+ const hint = document.createElement("span");
268
+ hint.className = "wt-bar-hint";
269
+ hint.textContent = t("workspace.contextMenuHint");
270
+ const refresh = document.createElement("button");
271
+ refresh.type = "button";
272
+ refresh.className = "wt-bar-btn";
273
+ refresh.textContent = t("workspace.refresh");
274
+ bar.appendChild(hint);
275
+ bar.appendChild(refresh);
276
+
277
+ const tree = document.createElement("div");
278
+ tree.className = "wt-tree";
279
+ tree.setAttribute("role", "tree");
280
+
281
+ refresh.addEventListener("click", () => loadRoot(tree));
282
+
283
+ wrap.appendChild(bar);
284
+ wrap.appendChild(tree);
285
+ loadRoot(tree);
286
+ return wrap;
287
+ }
288
+
289
+ return { renderFilesTab };
290
+ })();
291
+
292
+ // Files is a built-in tab: visible for every session, after the agent-scoped
293
+ // panels (git/time-machine use orders 10/20).
294
+ if (window.Clacky && Clacky.ext) {
295
+ Clacky.ext.ui.mountBuiltin("session.aside", (ctx) => WorkspaceView.renderFilesTab(ctx), {
296
+ order: 40,
297
+ tab: { id: "files", label: (typeof I18n !== "undefined" ? I18n.t("workspace.title") : "Files") },
298
+ });
299
+ }
300
+
301
+ // Keep the store's session context in sync (sessions.js still calls
302
+ // Workspace.onSession on every session switch). Rendering is driven by the
303
+ // slot re-render, so this only updates state.
304
+ Workspace.onSession = (session) => { Workspace.setSession(session); };
305
+ window.Workspace = Workspace;
@@ -69,6 +69,10 @@ const I18n = (() => {
69
69
  "chat.resetSessionConfirm": "Reset will start a brand-new session. The current conversation history stays in your sidebar but will no longer be active. Continue?",
70
70
  "chat.copy": "Copy",
71
71
  "chat.copied": "Copied",
72
+ "chat.continue": "Continue",
73
+ "chat.edit": "Edit",
74
+ "chat.cancel": "Cancel",
75
+ "chat.send": "Send",
72
76
  "chat.empty.title": "Start the conversation",
73
77
  "chat.empty.subtitle": "Ask anything, or use a skill to get going.",
74
78
  "chat.empty.tip1": "Type / to browse skills",
@@ -106,7 +110,7 @@ const I18n = (() => {
106
110
  "sib.dir.loadError": "Failed to load",
107
111
  "sib.dir.confirm": "Confirm",
108
112
  "sib.dir.cancel": "Cancel",
109
- "sib.dir.showHidden": "Show hidden files", "workspace.title": "Workspace",
113
+ "sib.dir.showHidden": "Show hidden files", "workspace.title": "Files",
110
114
  "workspace.expand": "Open workspace",
111
115
  "workspace.collapse": "Collapse workspace",
112
116
  "workspace.refresh": "Refresh",
@@ -114,6 +118,14 @@ const I18n = (() => {
114
118
  "workspace.loading": "Loading…",
115
119
  "workspace.error": "Failed to load files",
116
120
  "workspace.downloadFailed": "Download failed",
121
+ "workspace.previewFailed": "Failed to preview file",
122
+ "workspace.previewUnsupported": "Cannot preview this file type",
123
+ "workspace.download": "Download",
124
+ "workspace.contextMenuHint": "Right-click for more options",
125
+ "workspace.copyPath": "Copy Path",
126
+ "workspace.copyRelPath": "Copy Relative Path",
127
+ "aside.collapse": "Collapse panel",
128
+ "aside.expand": "Open panel",
117
129
  "workspace.revealInFinder": "Reveal in Finder",
118
130
  "workspace.revealFailed": "Failed to reveal file",
119
131
  "sib.model.tooltip": "Click to switch model",
@@ -161,6 +173,9 @@ const I18n = (() => {
161
173
  "modal.no": "No",
162
174
  "modal.ok": "OK",
163
175
  "modal.cancel": "Cancel",
176
+ "modal.close": "Close",
177
+ "modal.save": "Save",
178
+ "modal.saving": "Saving…",
164
179
 
165
180
  // ── Auth ──
166
181
  "auth.accessKeyRequired": "Access key required:",
@@ -342,8 +357,11 @@ const I18n = (() => {
342
357
  // Per-tab curate actions
343
358
  "profile.soul.curateHint": "Not quite right? Let the assistant curate this through a short conversation.",
344
359
  "profile.soul.curateBtn": "Have the assistant curate this",
360
+ "profile.soul.editBtn": "Edit directly",
345
361
  "profile.user.curateHint": "Changed jobs? Picked up new interests? Let the assistant update your profile.",
346
362
  "profile.user.curateBtn": "Have the assistant update this",
363
+ "profile.user.editBtn": "Edit directly",
364
+
347
365
  "profile.curateFail": "Could not start the curation session",
348
366
  "profile.curateName.soul": "Curate soul",
349
367
  "profile.curateName.user": "Curate profile",
@@ -535,7 +553,7 @@ const I18n = (() => {
535
553
  "settings.models.badge.default": "Default",
536
554
  "settings.models.badge.lite": "Lite",
537
555
  "settings.media.title": "Secondary Models",
538
- "settings.media.desc": "Optional. Image / video / audio / vision models.",
556
+ "settings.media.desc": "Image / video / audio / vision models (optional)",
539
557
  "settings.media.loading": "Loading…",
540
558
  "settings.media.error": "Failed to load: {{msg}}",
541
559
  "settings.media.kind.image": "Image",
@@ -568,6 +586,13 @@ const I18n = (() => {
568
586
  "settings.media.apiKey.required": "API key required",
569
587
  "settings.media.model.required": "Model name required",
570
588
  "settings.media.baseUrl.required": "Base URL required",
589
+ "settings.media.output_dir.desc": "Where generated images, videos and audio are saved (optional)",
590
+ "settings.media.output_dir.browse": "Browse…",
591
+ "settings.media.output_dir.picker": "Select Media Output Directory",
592
+ "settings.media.output_dir.clear": "Clear",
593
+ "settings.media.output_dir.saved": "Saved",
594
+ "settings.media.output_dir.cleared": "Cleared",
595
+ "settings.media.output_dir.invalid": "Invalid directory",
571
596
  "settings.models.field.quicksetup": "Quick Setup",
572
597
  "settings.models.field.model": "Model",
573
598
  "settings.models.field.baseurl": "Base URL",
@@ -642,7 +667,7 @@ const I18n = (() => {
642
667
  "settings.backup.includeSessions": "Include session history (larger archive)",
643
668
  "settings.backup.autoLabel": "Automatic backup",
644
669
  "settings.backup.autoHint": "Daily at 03:00, keeps the latest 7",
645
- "settings.backup.runNow": "Download backup",
670
+ "settings.backup.runNow": "Download backup (full snapshot)",
646
671
  "settings.backup.running": "Preparing backup…",
647
672
  "settings.backup.downloaded": "Backup downloaded.",
648
673
  "settings.backup.lastOk": "Last backup: {{time}}",
@@ -956,6 +981,10 @@ const I18n = (() => {
956
981
  "chat.retry": "重试",
957
982
  "chat.copy": "复制",
958
983
  "chat.copied": "已复制",
984
+ "chat.continue": "继续",
985
+ "chat.edit": "编辑",
986
+ "chat.cancel": "取消",
987
+ "chat.send": "发送",
959
988
  "chat.empty.title": "开始新会话",
960
989
  "chat.empty.subtitle": "直接提问,或用一个 Skill 启动。",
961
990
  "chat.empty.tip1": "输入 / 浏览 Skill",
@@ -993,7 +1022,7 @@ const I18n = (() => {
993
1022
  "sib.dir.loadError": "加载失败",
994
1023
  "sib.dir.confirm": "确认",
995
1024
  "sib.dir.cancel": "取消",
996
- "sib.dir.showHidden": "显示隐藏文件", "workspace.title": "工作区",
1025
+ "sib.dir.showHidden": "显示隐藏文件", "workspace.title": "文件",
997
1026
  "workspace.expand": "打开工作区",
998
1027
  "workspace.collapse": "收起工作区",
999
1028
  "workspace.refresh": "刷新",
@@ -1001,6 +1030,14 @@ const I18n = (() => {
1001
1030
  "workspace.loading": "加载中…",
1002
1031
  "workspace.error": "加载文件失败",
1003
1032
  "workspace.downloadFailed": "下载失败",
1033
+ "workspace.previewFailed": "文件预览失败",
1034
+ "workspace.previewUnsupported": "无法预览此类型文件",
1035
+ "workspace.download": "下载",
1036
+ "workspace.contextMenuHint": "右键显示更多菜单",
1037
+ "workspace.copyPath": "复制路径",
1038
+ "workspace.copyRelPath": "复制相对路径",
1039
+ "aside.collapse": "收起面板",
1040
+ "aside.expand": "打开面板",
1004
1041
  "workspace.revealInFinder": "打开所在文件夹",
1005
1042
  "workspace.revealFailed": "无法打开文件位置",
1006
1043
  "sib.model.tooltip": "点击切换模型",
@@ -1046,6 +1083,9 @@ const I18n = (() => {
1046
1083
  "modal.no": "取消",
1047
1084
  "modal.ok": "确定",
1048
1085
  "modal.cancel": "取消",
1086
+ "modal.close": "关闭",
1087
+ "modal.save": "保存",
1088
+ "modal.saving": "保存中…",
1049
1089
 
1050
1090
  // ── Auth ──
1051
1091
  "auth.accessKeyRequired": "请输入访问密钥:",
@@ -1229,8 +1269,11 @@ const I18n = (() => {
1229
1269
  // Per-tab curate actions
1230
1270
  "profile.soul.curateHint": "感觉不太对?让助理通过一段简短对话帮你调整。",
1231
1271
  "profile.soul.curateBtn": "让助理整理性格",
1272
+ "profile.soul.editBtn": "直接编辑",
1232
1273
  "profile.user.curateHint": "换工作了?有新兴趣了?让助理帮你更新主人档案。",
1233
1274
  "profile.user.curateBtn": "让助理更新档案",
1275
+ "profile.user.editBtn": "直接编辑",
1276
+
1234
1277
  "profile.curateFail": "无法启动整理会话",
1235
1278
  "profile.curateName.soul": "整理性格",
1236
1279
  "profile.curateName.user": "整理主人档案",
@@ -1421,7 +1464,7 @@ const I18n = (() => {
1421
1464
  "settings.models.badge.default": "默认",
1422
1465
  "settings.models.badge.lite": "轻量",
1423
1466
  "settings.media.title": "配置副模型",
1424
- "settings.media.desc": "可选。图片生成 / 视频生成 / 音频生成 / 视觉理解。",
1467
+ "settings.media.desc": "图片生成 / 视频生成 / 音频生成 / 视觉理解(可选)",
1425
1468
  "settings.media.loading": "加载中…",
1426
1469
  "settings.media.error": "加载失败:{{msg}}",
1427
1470
  "settings.media.kind.image": "图片生成",
@@ -1454,6 +1497,13 @@ const I18n = (() => {
1454
1497
  "settings.media.apiKey.required": "请填写 API Key",
1455
1498
  "settings.media.model.required": "请填写模型名称",
1456
1499
  "settings.media.baseUrl.required": "请填写 Base URL",
1500
+ "settings.media.output_dir.desc": "生成的图片、视频、音频文件保存位置(可选)",
1501
+ "settings.media.output_dir.browse": "选择目录…",
1502
+ "settings.media.output_dir.picker": "选择媒体输出目录",
1503
+ "settings.media.output_dir.clear": "清除",
1504
+ "settings.media.output_dir.saved": "已保存",
1505
+ "settings.media.output_dir.cleared": "已清除",
1506
+ "settings.media.output_dir.invalid": "目录无效",
1457
1507
  "settings.models.field.quicksetup": "快速配置",
1458
1508
  "settings.models.field.model": "Model",
1459
1509
  "settings.models.field.baseurl": "Base URL",
@@ -1528,7 +1578,7 @@ const I18n = (() => {
1528
1578
  "settings.backup.includeSessions": "包含会话历史(归档更大)",
1529
1579
  "settings.backup.autoLabel": "自动备份",
1530
1580
  "settings.backup.autoHint": "每天 03:00 自动备份,保留最近 7 份",
1531
- "settings.backup.runNow": "下载备份",
1581
+ "settings.backup.runNow": "下载备份(完整快照)",
1532
1582
  "settings.backup.running": "正在准备备份…",
1533
1583
  "settings.backup.downloaded": "备份已下载。",
1534
1584
  "settings.backup.lastOk": "上次备份:{{time}}",