openclacky 1.3.2 → 1.3.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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/Dockerfile +3 -0
  4. data/README.md +1 -1
  5. data/README_JA.md +237 -0
  6. data/docs/rich_ui_guide.md +277 -0
  7. data/docs/rich_ui_refactor_plan.md +396 -0
  8. data/lib/clacky/agent/llm_caller.rb +10 -4
  9. data/lib/clacky/agent/session_serializer.rb +52 -7
  10. data/lib/clacky/agent/time_machine.rb +247 -26
  11. data/lib/clacky/agent.rb +15 -3
  12. data/lib/clacky/api_extension.rb +262 -0
  13. data/lib/clacky/api_extension_loader.rb +156 -0
  14. data/lib/clacky/cli.rb +93 -3
  15. data/lib/clacky/client.rb +38 -13
  16. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  17. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  18. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  19. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  20. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  21. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  22. data/lib/clacky/default_skills/media-gen/SKILL.md +37 -10
  23. data/lib/clacky/idle_compression_timer.rb +3 -1
  24. data/lib/clacky/locales/en.rb +26 -0
  25. data/lib/clacky/locales/i18n.rb +26 -0
  26. data/lib/clacky/locales/zh.rb +26 -0
  27. data/lib/clacky/media/openai_compat.rb +64 -1
  28. data/lib/clacky/message_history.rb +9 -0
  29. data/lib/clacky/rich_ui/components/base_component.rb +50 -0
  30. data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
  31. data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
  32. data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
  33. data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
  34. data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
  35. data/lib/clacky/rich_ui/components/status_view.rb +58 -0
  36. data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
  37. data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
  38. data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
  39. data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
  40. data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
  41. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
  42. data/lib/clacky/rich_ui/view_renderer.rb +291 -0
  43. data/lib/clacky/rich_ui.rb +57 -0
  44. data/lib/clacky/rich_ui_controller.rb +3 -1549
  45. data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
  46. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  47. data/lib/clacky/server/git_panel.rb +115 -0
  48. data/lib/clacky/server/http_server.rb +547 -15
  49. data/lib/clacky/server/server_master.rb +6 -4
  50. data/lib/clacky/server/session_registry.rb +1 -1
  51. data/lib/clacky/shell_hook_loader.rb +1 -1
  52. data/lib/clacky/tools/edit.rb +14 -2
  53. data/lib/clacky/ui2/ui_controller.rb +7 -0
  54. data/lib/clacky/version.rb +1 -1
  55. data/lib/clacky/web/app.css +511 -101
  56. data/lib/clacky/web/app.js +95 -14
  57. data/lib/clacky/web/components/code-editor.js +197 -0
  58. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  59. data/lib/clacky/web/{onboard.js → components/onboard.js} +18 -2
  60. data/lib/clacky/web/core/aside.js +117 -0
  61. data/lib/clacky/web/core/ext.js +387 -0
  62. data/lib/clacky/web/features/backup/store.js +92 -0
  63. data/lib/clacky/web/features/backup/view.js +94 -0
  64. data/lib/clacky/web/features/billing/store.js +163 -0
  65. data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
  66. data/lib/clacky/web/features/brand/store.js +110 -0
  67. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  68. data/lib/clacky/web/features/channels/store.js +103 -0
  69. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  70. data/lib/clacky/web/features/creator/store.js +81 -0
  71. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  72. data/lib/clacky/web/features/mcp/store.js +158 -0
  73. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  74. data/lib/clacky/web/features/model-tester/store.js +77 -0
  75. data/lib/clacky/web/features/model-tester/view.js +7 -0
  76. data/lib/clacky/web/features/profile/store.js +170 -0
  77. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  78. data/lib/clacky/web/features/share/store.js +145 -0
  79. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  80. data/lib/clacky/web/features/skills/store.js +331 -0
  81. data/lib/clacky/web/features/skills/view.js +581 -0
  82. data/lib/clacky/web/features/tasks/store.js +135 -0
  83. data/lib/clacky/web/features/tasks/view.js +241 -0
  84. data/lib/clacky/web/features/trash/store.js +242 -0
  85. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  86. data/lib/clacky/web/features/version/store.js +165 -0
  87. data/lib/clacky/web/features/version/view.js +323 -0
  88. data/lib/clacky/web/features/workspace/store.js +99 -0
  89. data/lib/clacky/web/features/workspace/view.js +305 -0
  90. data/lib/clacky/web/i18n.js +68 -6
  91. data/lib/clacky/web/index.html +113 -62
  92. data/lib/clacky/web/sessions.js +493 -39
  93. data/lib/clacky/web/settings.js +143 -49
  94. data/lib/clacky/web/skills.js +3 -863
  95. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  96. data/lib/clacky/web/ws-dispatcher.js +7 -3
  97. data/lib/clacky.rb +17 -1
  98. metadata +81 -20
  99. data/lib/clacky/web/backup.js +0 -119
  100. data/lib/clacky/web/model-tester.js +0 -66
  101. data/lib/clacky/web/tasks.js +0 -373
  102. data/lib/clacky/web/version.js +0 -449
  103. data/lib/clacky/web/workspace.js +0 -316
  104. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  105. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  106. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  107. /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,12 @@ 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",
76
+ "chat.edit.warn": "Editing this message will discard the conversation history, file snapshots, and token costs after it. Continue?",
77
+ "chat.edit.warnSkip": "Don't show this again",
72
78
  "chat.empty.title": "Start the conversation",
73
79
  "chat.empty.subtitle": "Ask anything, or use a skill to get going.",
74
80
  "chat.empty.tip1": "Type / to browse skills",
@@ -100,13 +106,18 @@ const I18n = (() => {
100
106
  "sib.dir.up": "Up",
101
107
  "sib.dir.root": "Root",
102
108
  "sib.dir.default": "Default workspace",
103
- "sib.dir.current": "Current workspace", "sib.dir.inputPlaceholder": "Type or select a directory path",
109
+ "sib.dir.current": "Current workspace",
110
+ "sib.dir.inputPlaceholder": "Type or select a directory path",
104
111
  "sib.dir.loading": "Loading…",
105
112
  "sib.dir.empty": "Empty directory",
106
113
  "sib.dir.loadError": "Failed to load",
107
114
  "sib.dir.confirm": "Confirm",
108
115
  "sib.dir.cancel": "Cancel",
109
- "sib.dir.showHidden": "Show hidden files", "workspace.title": "Workspace",
116
+ "sib.dir.showHidden": "Show hidden files",
117
+ "sib.dir.newFolder": "New folder",
118
+ "sib.dir.newFolderDefault": "New Folder",
119
+ "sib.dir.mkdirError": "Failed to create folder: {{msg}}",
120
+ "workspace.title": "Files",
110
121
  "workspace.expand": "Open workspace",
111
122
  "workspace.collapse": "Collapse workspace",
112
123
  "workspace.refresh": "Refresh",
@@ -114,8 +125,18 @@ const I18n = (() => {
114
125
  "workspace.loading": "Loading…",
115
126
  "workspace.error": "Failed to load files",
116
127
  "workspace.downloadFailed": "Download failed",
128
+ "workspace.previewFailed": "Failed to preview file",
129
+ "workspace.previewUnsupported": "Cannot preview this file type",
130
+ "workspace.download": "Download",
131
+ "workspace.contextMenuHint": "Right-click for more options",
132
+ "workspace.copyPath": "Copy Path",
133
+ "workspace.copyRelPath": "Copy Relative Path",
134
+ "aside.collapse": "Collapse panel",
135
+ "aside.expand": "Open panel",
117
136
  "workspace.revealInFinder": "Reveal in Finder",
118
137
  "workspace.revealFailed": "Failed to reveal file",
138
+ "changes.tab": "Git",
139
+ "tm.tab": "History",
119
140
  "sib.model.tooltip": "Click to switch model",
120
141
  "sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
121
142
  "sib.variant.header": "Quick switch",
@@ -161,6 +182,9 @@ const I18n = (() => {
161
182
  "modal.no": "No",
162
183
  "modal.ok": "OK",
163
184
  "modal.cancel": "Cancel",
185
+ "modal.close": "Close",
186
+ "modal.save": "Save",
187
+ "modal.saving": "Saving…",
164
188
 
165
189
  // ── Auth ──
166
190
  "auth.accessKeyRequired": "Access key required:",
@@ -342,8 +366,11 @@ const I18n = (() => {
342
366
  // Per-tab curate actions
343
367
  "profile.soul.curateHint": "Not quite right? Let the assistant curate this through a short conversation.",
344
368
  "profile.soul.curateBtn": "Have the assistant curate this",
369
+ "profile.soul.editBtn": "Edit directly",
345
370
  "profile.user.curateHint": "Changed jobs? Picked up new interests? Let the assistant update your profile.",
346
371
  "profile.user.curateBtn": "Have the assistant update this",
372
+ "profile.user.editBtn": "Edit directly",
373
+
347
374
  "profile.curateFail": "Could not start the curation session",
348
375
  "profile.curateName.soul": "Curate soul",
349
376
  "profile.curateName.user": "Curate profile",
@@ -440,6 +467,7 @@ const I18n = (() => {
440
467
  "skills.deleteError": "Delete failed.",
441
468
  "skills.deleted": "Skill \"{{name}}\" deleted.",
442
469
  "skills.btn.delete": "Delete skill",
470
+ "skills.btn.edit": "Edit skill",
443
471
  "skills.ac.empty": "No skills available",
444
472
  "skills.upload.uploading": "Uploading…",
445
473
  "skills.upload.uploaded": "Uploaded",
@@ -447,6 +475,7 @@ const I18n = (() => {
447
475
  "skills.upload.failed": "Failed",
448
476
  "skills.upload.publishTip": "Publish to cloud",
449
477
  "skills.btn.refresh": "Refresh",
478
+ "skills.editFail": "Failed to load skill",
450
479
 
451
480
  // ── Channels panel ──
452
481
  "channels.title": "Channels",
@@ -595,6 +624,7 @@ const I18n = (() => {
595
624
  "settings.models.btn.test": "Test",
596
625
  "settings.models.link.topUp": "Top up / Manage account",
597
626
  "settings.models.btn.edit": "Edit",
627
+ "settings.models.btn.duplicate": "Duplicate",
598
628
  "settings.models.btn.delete": "Delete",
599
629
  "settings.models.btn.cancel": "Cancel",
600
630
  "settings.models.btn.set_default": "Default",
@@ -603,6 +633,7 @@ const I18n = (() => {
603
633
  "settings.models.btn.done": "Done ✓",
604
634
  "settings.models.modal.add": "Add Model",
605
635
  "settings.models.modal.edit": "Edit Model",
636
+ "settings.models.modal.duplicate": "Duplicate Model",
606
637
  "settings.models.field.provider": "Provider",
607
638
  "settings.models.provider.custom": "Custom",
608
639
  "settings.models.unnamed": "Unnamed Model",
@@ -642,7 +673,7 @@ const I18n = (() => {
642
673
  "settings.backup.includeSessions": "Include session history (larger archive)",
643
674
  "settings.backup.autoLabel": "Automatic backup",
644
675
  "settings.backup.autoHint": "Daily at 03:00, keeps the latest 7",
645
- "settings.backup.runNow": "Download backup",
676
+ "settings.backup.runNow": "Download backup (full snapshot)",
646
677
  "settings.backup.running": "Preparing backup…",
647
678
  "settings.backup.downloaded": "Backup downloaded.",
648
679
  "settings.backup.lastOk": "Last backup: {{time}}",
@@ -956,6 +987,12 @@ const I18n = (() => {
956
987
  "chat.retry": "重试",
957
988
  "chat.copy": "复制",
958
989
  "chat.copied": "已复制",
990
+ "chat.continue": "继续",
991
+ "chat.edit": "编辑",
992
+ "chat.cancel": "取消",
993
+ "chat.send": "发送",
994
+ "chat.edit.warn": "编辑此消息将丢弃后续对话历史、文件快照及已产生的 token 费用,是否继续?",
995
+ "chat.edit.warnSkip": "不再提示",
959
996
  "chat.empty.title": "开始新会话",
960
997
  "chat.empty.subtitle": "直接提问,或用一个 Skill 启动。",
961
998
  "chat.empty.tip1": "输入 / 浏览 Skill",
@@ -987,13 +1024,18 @@ const I18n = (() => {
987
1024
  "sib.dir.up": "上一级",
988
1025
  "sib.dir.root": "根目录",
989
1026
  "sib.dir.default": "默认工作目录",
990
- "sib.dir.current": "当前工作目录", "sib.dir.inputPlaceholder": "输入或选择目录路径",
1027
+ "sib.dir.current": "当前工作目录",
1028
+ "sib.dir.inputPlaceholder": "输入或选择目录路径",
991
1029
  "sib.dir.loading": "加载中…",
992
1030
  "sib.dir.empty": "空目录",
993
1031
  "sib.dir.loadError": "加载失败",
994
1032
  "sib.dir.confirm": "确认",
995
1033
  "sib.dir.cancel": "取消",
996
- "sib.dir.showHidden": "显示隐藏文件", "workspace.title": "工作区",
1034
+ "sib.dir.showHidden": "显示隐藏文件",
1035
+ "sib.dir.newFolder": "新建文件夹",
1036
+ "sib.dir.newFolderDefault": "新建文件夹",
1037
+ "sib.dir.mkdirError": "新建文件夹失败:{{msg}}",
1038
+ "workspace.title": "文件",
997
1039
  "workspace.expand": "打开工作区",
998
1040
  "workspace.collapse": "收起工作区",
999
1041
  "workspace.refresh": "刷新",
@@ -1001,8 +1043,18 @@ const I18n = (() => {
1001
1043
  "workspace.loading": "加载中…",
1002
1044
  "workspace.error": "加载文件失败",
1003
1045
  "workspace.downloadFailed": "下载失败",
1046
+ "workspace.previewFailed": "文件预览失败",
1047
+ "workspace.previewUnsupported": "无法预览此类型文件",
1048
+ "workspace.download": "下载",
1049
+ "workspace.contextMenuHint": "右键显示更多菜单",
1050
+ "workspace.copyPath": "复制路径",
1051
+ "workspace.copyRelPath": "复制相对路径",
1052
+ "aside.collapse": "收起面板",
1053
+ "aside.expand": "打开面板",
1004
1054
  "workspace.revealInFinder": "打开所在文件夹",
1005
1055
  "workspace.revealFailed": "无法打开文件位置",
1056
+ "changes.tab": "Git 管理",
1057
+ "tm.tab": "时光机",
1006
1058
  "sib.model.tooltip": "点击切换模型",
1007
1059
  "sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
1008
1060
  "sib.variant.header": "快速切换",
@@ -1046,6 +1098,9 @@ const I18n = (() => {
1046
1098
  "modal.no": "取消",
1047
1099
  "modal.ok": "确定",
1048
1100
  "modal.cancel": "取消",
1101
+ "modal.close": "关闭",
1102
+ "modal.save": "保存",
1103
+ "modal.saving": "保存中…",
1049
1104
 
1050
1105
  // ── Auth ──
1051
1106
  "auth.accessKeyRequired": "请输入访问密钥:",
@@ -1229,8 +1284,11 @@ const I18n = (() => {
1229
1284
  // Per-tab curate actions
1230
1285
  "profile.soul.curateHint": "感觉不太对?让助理通过一段简短对话帮你调整。",
1231
1286
  "profile.soul.curateBtn": "让助理整理性格",
1287
+ "profile.soul.editBtn": "直接编辑",
1232
1288
  "profile.user.curateHint": "换工作了?有新兴趣了?让助理帮你更新主人档案。",
1233
1289
  "profile.user.curateBtn": "让助理更新档案",
1290
+ "profile.user.editBtn": "直接编辑",
1291
+
1234
1292
  "profile.curateFail": "无法启动整理会话",
1235
1293
  "profile.curateName.soul": "整理性格",
1236
1294
  "profile.curateName.user": "整理主人档案",
@@ -1326,6 +1384,7 @@ const I18n = (() => {
1326
1384
  "skills.deleteError": "删除失败。",
1327
1385
  "skills.deleted": "技能 \"{{name}}\" 已删除。",
1328
1386
  "skills.btn.delete": "删除技能",
1387
+ "skills.btn.edit": "编辑技能",
1329
1388
  "skills.ac.empty": "暂无可用技能",
1330
1389
  "skills.upload.uploading": "上传中…",
1331
1390
  "skills.upload.uploaded": "已上传",
@@ -1333,6 +1392,7 @@ const I18n = (() => {
1333
1392
  "skills.upload.failed": "失败",
1334
1393
  "skills.upload.publishTip": "发布到云端",
1335
1394
  "skills.btn.refresh": "刷新",
1395
+ "skills.editFail": "加载技能失败",
1336
1396
 
1337
1397
  // ── Channels panel ──
1338
1398
  "channels.title": "频道",
@@ -1481,6 +1541,7 @@ const I18n = (() => {
1481
1541
  "settings.models.btn.test": "测试",
1482
1542
  "settings.models.link.topUp": "充值 / 账户管理",
1483
1543
  "settings.models.btn.edit": "编辑",
1544
+ "settings.models.btn.duplicate": "复制",
1484
1545
  "settings.models.btn.delete": "删除",
1485
1546
  "settings.models.btn.cancel": "取消",
1486
1547
  "settings.models.btn.set_default": "设为默认",
@@ -1489,6 +1550,7 @@ const I18n = (() => {
1489
1550
  "settings.models.btn.done": "完成 ✓",
1490
1551
  "settings.models.modal.add": "添加模型",
1491
1552
  "settings.models.modal.edit": "编辑模型",
1553
+ "settings.models.modal.duplicate": "复制模型",
1492
1554
  "settings.models.field.provider": "供应商",
1493
1555
  "settings.models.provider.custom": "自定义",
1494
1556
  "settings.models.unnamed": "未命名模型",
@@ -1528,7 +1590,7 @@ const I18n = (() => {
1528
1590
  "settings.backup.includeSessions": "包含会话历史(归档更大)",
1529
1591
  "settings.backup.autoLabel": "自动备份",
1530
1592
  "settings.backup.autoHint": "每天 03:00 自动备份,保留最近 7 份",
1531
- "settings.backup.runNow": "下载备份",
1593
+ "settings.backup.runNow": "下载备份(完整快照)",
1532
1594
  "settings.backup.running": "正在准备备份…",
1533
1595
  "settings.backup.downloaded": "备份已下载。",
1534
1596
  "settings.backup.lastOk": "上次备份:{{time}}",