openclacky 1.3.3 → 1.3.5

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/docs/rich_ui_guide.md +277 -0
  4. data/docs/rich_ui_refactor_plan.md +396 -0
  5. data/lib/clacky/agent/fake_tool_call_detector.rb +52 -0
  6. data/lib/clacky/agent/llm_caller.rb +10 -4
  7. data/lib/clacky/agent/session_serializer.rb +5 -3
  8. data/lib/clacky/agent/tool_executor.rb +0 -12
  9. data/lib/clacky/agent.rb +77 -11
  10. data/lib/clacky/agent_config.rb +2 -14
  11. data/lib/clacky/api_extension.rb +343 -0
  12. data/lib/clacky/api_extension_loader.rb +168 -0
  13. data/lib/clacky/cli.rb +93 -3
  14. data/lib/clacky/client.rb +47 -25
  15. data/lib/clacky/default_agents/_panels/git/panel.js +1 -1
  16. data/lib/clacky/default_agents/_panels/time_machine/panel.js +23 -1
  17. data/lib/clacky/default_agents/base_prompt.md +1 -0
  18. data/lib/clacky/default_extensions/meeting/handler.rb +331 -0
  19. data/lib/clacky/default_extensions/meeting/meeting.js +790 -0
  20. data/lib/clacky/default_extensions/meeting/meta.yml +3 -0
  21. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +44 -0
  22. data/lib/clacky/default_skills/media-gen/SKILL.md +72 -6
  23. data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +114 -0
  24. data/lib/clacky/idle_compression_timer.rb +3 -1
  25. data/lib/clacky/json_ui_controller.rb +1 -1
  26. data/lib/clacky/locales/en.rb +26 -0
  27. data/lib/clacky/locales/i18n.rb +26 -0
  28. data/lib/clacky/locales/zh.rb +26 -0
  29. data/lib/clacky/media/base.rb +60 -0
  30. data/lib/clacky/media/dashscope.rb +385 -21
  31. data/lib/clacky/media/gemini.rb +9 -0
  32. data/lib/clacky/media/generator.rb +52 -0
  33. data/lib/clacky/media/openai_compat.rb +166 -0
  34. data/lib/clacky/null_ui_controller.rb +13 -0
  35. data/lib/clacky/plain_ui_controller.rb +1 -1
  36. data/lib/clacky/providers.rb +50 -2
  37. data/lib/clacky/rich_ui/components/base_component.rb +50 -0
  38. data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
  39. data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
  40. data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
  41. data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
  42. data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
  43. data/lib/clacky/rich_ui/components/status_view.rb +58 -0
  44. data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
  45. data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
  46. data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
  47. data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
  48. data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
  49. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
  50. data/lib/clacky/rich_ui/view_renderer.rb +291 -0
  51. data/lib/clacky/rich_ui.rb +57 -0
  52. data/lib/clacky/rich_ui_controller.rb +3 -1549
  53. data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
  54. data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
  55. data/lib/clacky/server/http_server.rb +291 -109
  56. data/lib/clacky/server/session_registry.rb +5 -3
  57. data/lib/clacky/server/web_ui_controller.rb +3 -2
  58. data/lib/clacky/shell_hook_loader.rb +1 -1
  59. data/lib/clacky/skill_loader.rb +14 -2
  60. data/lib/clacky/tools/edit.rb +14 -2
  61. data/lib/clacky/tools/terminal/output_cleaner.rb +1 -3
  62. data/lib/clacky/tools/terminal.rb +0 -43
  63. data/lib/clacky/ui2/components/modal_component.rb +1 -1
  64. data/lib/clacky/ui2/ui_controller.rb +147 -31
  65. data/lib/clacky/ui_interface.rb +10 -1
  66. data/lib/clacky/utils/encoding.rb +25 -0
  67. data/lib/clacky/version.rb +1 -1
  68. data/lib/clacky/web/app.css +201 -81
  69. data/lib/clacky/web/app.js +65 -7
  70. data/lib/clacky/web/components/onboard.js +19 -16
  71. data/lib/clacky/web/core/aside.js +8 -3
  72. data/lib/clacky/web/core/ext.js +1 -1
  73. data/lib/clacky/web/features/brand/view.js +8 -5
  74. data/lib/clacky/web/features/channels/store.js +1 -20
  75. data/lib/clacky/web/features/mcp/store.js +1 -20
  76. data/lib/clacky/web/features/profile/store.js +1 -13
  77. data/lib/clacky/web/features/profile/view.js +16 -4
  78. data/lib/clacky/web/features/skills/store.js +36 -23
  79. data/lib/clacky/web/features/skills/view.js +32 -1
  80. data/lib/clacky/web/features/version/store.js +2 -0
  81. data/lib/clacky/web/features/workspace/view.js +1 -1
  82. data/lib/clacky/web/i18n.js +48 -13
  83. data/lib/clacky/web/index.html +24 -17
  84. data/lib/clacky/web/sessions.js +427 -79
  85. data/lib/clacky/web/settings.js +143 -113
  86. data/lib/clacky/web/ws-dispatcher.js +18 -6
  87. data/lib/clacky.rb +27 -5
  88. metadata +45 -2
  89. data/lib/clacky/media/output_dir.rb +0 -43
@@ -16,7 +16,7 @@
16
16
  (() => {
17
17
  const WIDTH_KEY = "clacky.aside.width";
18
18
  const OPEN_KEY = "clacky.aside.open";
19
- const MIN_W = 280;
19
+ const MIN_W = 256;
20
20
  const MAX_W = 720;
21
21
 
22
22
  const $ = (id) => document.getElementById(id);
@@ -31,8 +31,11 @@
31
31
  const opener = $("btn-aside-open");
32
32
  const overlay = $("workspace-overlay");
33
33
  if (!aside) return;
34
- let open = true;
35
- try { open = localStorage.getItem(OPEN_KEY) !== "0"; } catch (_e) { /* ignore */ }
34
+ let open = false;
35
+ try {
36
+ const stored = localStorage.getItem(OPEN_KEY);
37
+ open = stored === null ? false : stored !== "0";
38
+ } catch (_e) { /* ignore */ }
36
39
  const empty = slotEmpty();
37
40
  aside.classList.toggle("collapsed", !open);
38
41
  if (opener) opener.style.display = (!open && !empty) ? "" : "none";
@@ -64,6 +67,7 @@
64
67
  startX = e.clientX;
65
68
  startW = parseFloat(getComputedStyle(aside).getPropertyValue("--session-aside-width"));
66
69
  handle.classList.add("active");
70
+ aside.style.transition = "none";
67
71
  document.body.style.cursor = "col-resize";
68
72
  document.body.style.userSelect = "none";
69
73
  });
@@ -79,6 +83,7 @@
79
83
  if (!dragging) return;
80
84
  dragging = false;
81
85
  handle.classList.remove("active");
86
+ aside.style.transition = "";
82
87
  document.body.style.cursor = "";
83
88
  document.body.style.userSelect = "";
84
89
  const w = parseFloat(getComputedStyle(aside).getPropertyValue("--session-aside-width"));
@@ -313,7 +313,7 @@ Clacky.ext = (() => {
313
313
  btn.type = "button";
314
314
  btn.setAttribute("data-tab", id);
315
315
  const label = document.createElement("span");
316
- label.textContent = entry.tab.label || id;
316
+ label.textContent = (typeof entry.tab.label === "function" ? entry.tab.label() : entry.tab.label) || id;
317
317
  btn.appendChild(label);
318
318
  if (entry.tab.badge != null) _setTabBadge(btn, entry.tab.badge);
319
319
  btn.addEventListener("click", () => activate(id));
@@ -18,6 +18,7 @@ const BrandView = (() => {
18
18
  if (!data || !data.branded) return;
19
19
 
20
20
  if (data.needs_activation) {
21
+ if (data.theme_color) Settings.applyAccentColor(data.theme_color, { persist: true });
21
22
  _showActivationBanner(data.product_name);
22
23
  _applyHeaderLogo();
23
24
  if (data.distribution_refresh_pending) _scheduleDistributionRefreshPoll();
@@ -153,6 +154,7 @@ const BrandView = (() => {
153
154
  if (data.ok) {
154
155
  _setResult(true, I18n.t("brand.activate.success"));
155
156
  if (data.product_name) _applyBrandName(data.product_name);
157
+ if (data.theme_color) Settings.applyAccentColor(data.theme_color, { persist: true });
156
158
  Brand.clearBrandCache();
157
159
  _applyHeaderLogo();
158
160
  setTimeout(_bootUI, 800);
@@ -201,13 +203,14 @@ const BrandView = (() => {
201
203
  const brandWrap = document.getElementById("header-brand");
202
204
 
203
205
  if (info.theme_color) {
204
- const root = document.documentElement;
205
- root.style.setProperty("--color-accent-primary", info.theme_color);
206
- root.style.setProperty("--color-accent-hover", info.theme_color);
207
- root.style.setProperty("--color-button-primary", info.theme_color);
208
- root.style.setProperty("--color-button-primary-hover", info.theme_color);
209
206
  const metaTheme = document.querySelector("meta[name='theme-color']");
210
207
  if (metaTheme) metaTheme.setAttribute("content", info.theme_color);
208
+ }
209
+ const userAccent = (() => { try { return localStorage.getItem("clacky-accent-color"); } catch (_) { return null; } })();
210
+ if (userAccent) {
211
+ Settings.applyAccentColor(userAccent, { persist: false });
212
+ } else if (info.theme_color) {
213
+ Settings.applyAccentColor(info.theme_color, { persist: true });
211
214
  } else {
212
215
  const root = document.documentElement;
213
216
  root.style.removeProperty("--color-accent-primary");
@@ -37,26 +37,7 @@ const ChannelsStore = (() => {
37
37
  // Create a session, register it, queue a command, and navigate to it.
38
38
  async function _sendToAgent(command, sessionName) {
39
39
  try {
40
- const maxN = Sessions.all.reduce((max, s) => {
41
- const m = s.name.match(/^Session (\d+)$/);
42
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
43
- }, 0);
44
- const name = sessionName || ("Session " + (maxN + 1));
45
-
46
- const res = await fetch("/api/sessions", {
47
- method: "POST",
48
- headers: { "Content-Type": "application/json" },
49
- body: JSON.stringify({ name, source: "setup" }),
50
- });
51
- const data = await res.json();
52
- if (!res.ok) throw new Error(data.error || I18n.t("channels.sessionError"));
53
- const session = data.session;
54
- if (!session) throw new Error(I18n.t("channels.noSession"));
55
-
56
- Sessions.add(session);
57
- Sessions.renderList();
58
- Sessions.setPendingMessage(session.id, command);
59
- Sessions.select(session.id);
40
+ await Sessions.startWith(command, { name: sessionName });
60
41
  } catch (e) {
61
42
  alert("Error: " + e.message);
62
43
  }
@@ -43,26 +43,7 @@ const McpStore = (() => {
43
43
 
44
44
  async function _sendToAgent(command, sessionName) {
45
45
  try {
46
- const maxN = Sessions.all.reduce((max, s) => {
47
- const m = s.name.match(/^Session (\d+)$/);
48
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
49
- }, 0);
50
- const name = sessionName || ("Session " + (maxN + 1));
51
-
52
- const res = await fetch("/api/sessions", {
53
- method: "POST",
54
- headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify({ name, source: "mcp" }),
56
- });
57
- const data = await res.json();
58
- if (!res.ok) throw new Error(data.error || "failed to create session");
59
- const session = data.session;
60
- if (!session) throw new Error("no session returned");
61
-
62
- Sessions.add(session);
63
- Sessions.renderList();
64
- Sessions.setPendingMessage(session.id, command);
65
- Sessions.select(session.id);
46
+ await Sessions.startWith(command, { name: sessionName });
66
47
  } catch (e) {
67
48
  alert("Error: " + e.message);
68
49
  }
@@ -64,19 +64,7 @@ const ProfileStore = (() => {
64
64
  }
65
65
 
66
66
  async function _openCurateSession(name, command) {
67
- const res = await fetch("/api/sessions", {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json" },
70
- body: JSON.stringify({ name, source: "onboard" })
71
- });
72
- const data = await res.json();
73
- const session = data.session;
74
- if (!session) throw new Error("No session returned");
75
-
76
- Sessions.add(session);
77
- Sessions.renderList();
78
- Sessions.setPendingMessage(session.id, command);
79
- Sessions.select(session.id);
67
+ await Sessions.startWith(command, { name, source: "onboard" });
80
68
  }
81
69
 
82
70
  const Profile = {
@@ -49,7 +49,7 @@ const ProfileView = (() => {
49
49
 
50
50
  function flushPara() {
51
51
  if (paraBuf.length === 0) return;
52
- out.push("<p>" + _renderInline(paraBuf.join(" ")) + "</p>");
52
+ out.push("<p>" + _renderInline(paraBuf.join("<br>")) + "</p>");
53
53
  paraBuf = [];
54
54
  }
55
55
  function openList(type) {
@@ -152,11 +152,17 @@ const ProfileView = (() => {
152
152
  return;
153
153
  }
154
154
 
155
+ const openSet = new Set(
156
+ [...list.querySelectorAll(".memory-card")]
157
+ .filter(c => c.querySelector(".memory-card-body")?.style.display !== "none")
158
+ .map(c => c.dataset.filename)
159
+ );
160
+
155
161
  list.innerHTML = "";
156
- memories.forEach(m => list.appendChild(_buildMemoryCard(m)));
162
+ memories.forEach(m => list.appendChild(_buildMemoryCard(m, openSet.has(m.filename))));
157
163
  }
158
164
 
159
- function _buildMemoryCard(m) {
165
+ function _buildMemoryCard(m, expanded = false) {
160
166
  const card = document.createElement("div");
161
167
  card.className = "memory-card";
162
168
  card.dataset.filename = m.filename;
@@ -254,6 +260,8 @@ const ProfileView = (() => {
254
260
  expandBtn.addEventListener("click", (e) => { e.stopPropagation(); toggle(); });
255
261
  head.querySelector(".memory-card-info").addEventListener("click", toggle);
256
262
 
263
+ if (expanded) toggle();
264
+
257
265
  return card;
258
266
  }
259
267
 
@@ -303,6 +311,7 @@ const ProfileView = (() => {
303
311
  onSave: async (newContent) => {
304
312
  const r = await Profile.updateMemory(m.filename, newContent);
305
313
  if (!r.ok) throw new Error(r.error);
314
+ Modal.toast(_t("profile.savedOk"), "success");
306
315
  }
307
316
  });
308
317
  }
@@ -333,7 +342,10 @@ const ProfileView = (() => {
333
342
  content: file.content || "",
334
343
  title,
335
344
  language: "markdown",
336
- onSave: (newContent) => Profile.updateProfile(kind, newContent)
345
+ onSave: async (newContent) => {
346
+ await Profile.updateProfile(kind, newContent);
347
+ Modal.toast(_t("profile.savedOk"), "success");
348
+ }
337
349
  });
338
350
  }
339
351
 
@@ -71,28 +71,13 @@ const SkillsStore = (() => {
71
71
  // Resolve the next "Session N" name and create a session, then hand off to
72
72
  // Sessions. Used by every "open a session and run a command" action.
73
73
  async function _openSessionWith(message) {
74
- const maxN = Sessions.all.reduce((max, s) => {
75
- const m = s.name.match(/^Session (\d+)$/);
76
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
77
- }, 0);
78
- const res = await fetch("/api/sessions", {
79
- method: "POST",
80
- headers: { "Content-Type": "application/json" },
81
- body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
82
- });
83
- const data = await res.json();
84
- if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return null; }
85
-
86
- const session = data.session;
87
- if (!session) return null;
88
-
89
74
  if (!WS.ready) { WS.connect(); Skills.load(); }
90
-
91
- Sessions.add(session);
92
- Sessions.renderList();
93
- Sessions.setPendingMessage(session.id, message);
94
- Sessions.select(session.id);
95
- return session;
75
+ try {
76
+ return await Sessions.startWith(message, { source: "manual" });
77
+ } catch (e) {
78
+ alert(I18n.t("tasks.sessionError") + (e.message || "unknown"));
79
+ return null;
80
+ }
96
81
  }
97
82
 
98
83
  /** Return a user-friendly message for install/update errors. */
@@ -268,6 +253,35 @@ const SkillsStore = (() => {
268
253
  return _openSessionWith(message || "/skill-creator");
269
254
  },
270
255
 
256
+ /** Fetch a custom skill's SKILL.md content. Returns { ok, content, path, error }. */
257
+ async fetchSkillContent(name) {
258
+ try {
259
+ const res = await fetch(`/api/skills/${encodeURIComponent(name)}/content`);
260
+ const data = await res.json();
261
+ if (!data.ok) throw new Error(data.error || "Load failed");
262
+ return { ok: true, content: data.content || "", path: data.path };
263
+ } catch (e) {
264
+ return { ok: false, error: e.message };
265
+ }
266
+ },
267
+
268
+ /** Update a custom skill's SKILL.md content. Returns { ok, error }. */
269
+ async updateSkillContent(name, content) {
270
+ try {
271
+ const res = await fetch(`/api/skills/${encodeURIComponent(name)}/content`, {
272
+ method: "PUT",
273
+ headers: { "Content-Type": "application/json" },
274
+ body: JSON.stringify({ content })
275
+ });
276
+ const data = await res.json();
277
+ if (!res.ok || !data.ok) throw new Error(data.error || "Save failed");
278
+ await Skills.load();
279
+ return { ok: true };
280
+ } catch (e) {
281
+ return { ok: false, error: e.message };
282
+ }
283
+ },
284
+
271
285
  /** Import a skill: validate url/path, open a session, run /skill-add. */
272
286
  async importSkill(url) {
273
287
  const trimmed = (url || "").trim();
@@ -286,8 +300,7 @@ const SkillsStore = (() => {
286
300
 
287
301
  // ── Cross-feature state resets (called by settings.js) ───────────────────
288
302
 
289
- /** Reset to My Skills and clear brand state after license unbind. */
290
- resetAfterUnbind() {
303
+ resetBrandState() {
291
304
  _brandSkills = [];
292
305
  _brandActivated = false;
293
306
  _activeTab = "my-skills";
@@ -114,6 +114,15 @@ const SkillsView = (() => {
114
114
  </svg>
115
115
  </button>`;
116
116
 
117
+ const editButtonHtml = isSystem
118
+ ? ""
119
+ : `<button class="btn-skill-edit" data-name="${escapeHtml(skill.name)}" title="${I18n.t("skills.btn.edit")}">
120
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
121
+ <path d="M12 20h9"/>
122
+ <path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
123
+ </svg>
124
+ </button>`;
125
+
117
126
  card.innerHTML = `
118
127
  <div class="skill-card-main">
119
128
  <div class="skill-card-info">
@@ -131,6 +140,7 @@ const SkillsView = (() => {
131
140
  <span class="skill-toggle-track"></span>
132
141
  </label>
133
142
  ${useButtonHtml}
143
+ ${editButtonHtml}
134
144
  ${deleteButtonHtml}
135
145
  </div>
136
146
  </div>
@@ -157,6 +167,12 @@ const SkillsView = (() => {
157
167
  const useBtn = card.querySelector(".btn-skill-use");
158
168
  if (useBtn) useBtn.addEventListener("click", () => Skills.useInstalledSkill(skill.name));
159
169
 
170
+ const editBtn = card.querySelector(".btn-skill-edit");
171
+ if (editBtn) editBtn.addEventListener("click", (e) => {
172
+ e.stopPropagation();
173
+ _editSkill(skill);
174
+ });
175
+
160
176
  const deleteBtn = card.querySelector(".btn-skill-delete");
161
177
  if (deleteBtn) deleteBtn.addEventListener("click", (e) => {
162
178
  e.stopPropagation();
@@ -511,12 +527,27 @@ const SkillsView = (() => {
511
527
  },
512
528
 
513
529
  resetAfterUnbind() {
514
- SkillsStore.resetAfterUnbind();
530
+ SkillsStore.resetBrandState();
515
531
  const panel = $("skills-panel");
516
532
  if (panel && panel.style.display !== "none") _applyTab("my-skills");
517
533
  },
518
534
  };
519
535
 
536
+ async function _editSkill(skill) {
537
+ const res = await Skills.fetchSkillContent(skill.name);
538
+ if (!res.ok) { alert(I18n.t("skills.editFail") + ": " + res.error); return; }
539
+
540
+ CodeEditor.open({
541
+ content: res.content,
542
+ title: skill.name,
543
+ language: "markdown",
544
+ onSave: async (newContent) => {
545
+ const r = await Skills.updateSkillContent(skill.name, newContent);
546
+ if (!r.ok) throw new Error(r.error);
547
+ }
548
+ });
549
+ }
550
+
520
551
  async function _doImportFromBar() {
521
552
  const input = $("skill-import-input");
522
553
  const bar = $("skill-import-bar");
@@ -143,6 +143,8 @@ const VersionStore = (() => {
143
143
  _upgradeDone = false;
144
144
  }
145
145
  _emit({ reason: "upgrade-complete", success: !!event.success });
146
+ } else if (event.type === "_ws_connected") {
147
+ checkVersion();
146
148
  }
147
149
  }
148
150
 
@@ -294,7 +294,7 @@ const WorkspaceView = (() => {
294
294
  if (window.Clacky && Clacky.ext) {
295
295
  Clacky.ext.ui.mountBuiltin("session.aside", (ctx) => WorkspaceView.renderFilesTab(ctx), {
296
296
  order: 40,
297
- tab: { id: "files", label: (typeof I18n !== "undefined" ? I18n.t("workspace.title") : "Files") },
297
+ tab: { id: "files", label: () => (typeof I18n !== "undefined" ? I18n.t("workspace.title") : "Files") },
298
298
  });
299
299
  }
300
300
 
@@ -68,11 +68,16 @@ const I18n = (() => {
68
68
  "chat.resetSession": "Reset session",
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
+ "chat.todo.spawn": "Start",
72
+ "chat.todo.spawning": "Starting…",
73
+ "chat.todo.spawnFailed": "Failed to start session: {msg}",
71
74
  "chat.copied": "Copied",
72
75
  "chat.continue": "Continue",
73
76
  "chat.edit": "Edit",
74
77
  "chat.cancel": "Cancel",
75
78
  "chat.send": "Send",
79
+ "chat.edit.warn": "Editing this message will discard the conversation history, file snapshots, and token costs after it. Continue?",
80
+ "chat.edit.warnSkip": "Don't show this again",
76
81
  "chat.empty.title": "Start the conversation",
77
82
  "chat.empty.subtitle": "Ask anything, or use a skill to get going.",
78
83
  "chat.empty.tip1": "Type / to browse skills",
@@ -104,13 +109,18 @@ const I18n = (() => {
104
109
  "sib.dir.up": "Up",
105
110
  "sib.dir.root": "Root",
106
111
  "sib.dir.default": "Default workspace",
107
- "sib.dir.current": "Current workspace", "sib.dir.inputPlaceholder": "Type or select a directory path",
112
+ "sib.dir.current": "Current workspace",
113
+ "sib.dir.inputPlaceholder": "Type or select a directory path",
108
114
  "sib.dir.loading": "Loading…",
109
115
  "sib.dir.empty": "Empty directory",
110
116
  "sib.dir.loadError": "Failed to load",
111
117
  "sib.dir.confirm": "Confirm",
112
118
  "sib.dir.cancel": "Cancel",
113
- "sib.dir.showHidden": "Show hidden files", "workspace.title": "Files",
119
+ "sib.dir.showHidden": "Show hidden files",
120
+ "sib.dir.newFolder": "New folder",
121
+ "sib.dir.newFolderDefault": "New Folder",
122
+ "sib.dir.mkdirError": "Failed to create folder: {{msg}}",
123
+ "workspace.title": "Files",
114
124
  "workspace.expand": "Open workspace",
115
125
  "workspace.collapse": "Collapse workspace",
116
126
  "workspace.refresh": "Refresh",
@@ -128,6 +138,8 @@ const I18n = (() => {
128
138
  "aside.expand": "Open panel",
129
139
  "workspace.revealInFinder": "Reveal in Finder",
130
140
  "workspace.revealFailed": "Failed to reveal file",
141
+ "changes.tab": "Git",
142
+ "tm.tab": "History",
131
143
  "sib.model.tooltip": "Click to switch model",
132
144
  "sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
133
145
  "sib.variant.header": "Quick switch",
@@ -458,6 +470,7 @@ const I18n = (() => {
458
470
  "skills.deleteError": "Delete failed.",
459
471
  "skills.deleted": "Skill \"{{name}}\" deleted.",
460
472
  "skills.btn.delete": "Delete skill",
473
+ "skills.btn.edit": "Edit skill",
461
474
  "skills.ac.empty": "No skills available",
462
475
  "skills.upload.uploading": "Uploading…",
463
476
  "skills.upload.uploaded": "Uploaded",
@@ -465,6 +478,7 @@ const I18n = (() => {
465
478
  "skills.upload.failed": "Failed",
466
479
  "skills.upload.publishTip": "Publish to cloud",
467
480
  "skills.btn.refresh": "Refresh",
481
+ "skills.editFail": "Failed to load skill",
468
482
 
469
483
  // ── Channels panel ──
470
484
  "channels.title": "Channels",
@@ -553,12 +567,14 @@ const I18n = (() => {
553
567
  "settings.models.badge.default": "Default",
554
568
  "settings.models.badge.lite": "Lite",
555
569
  "settings.media.title": "Secondary Models",
556
- "settings.media.desc": "Image / video / audio / vision models (optional)",
570
+ "settings.media.desc": "Optional. Image / video / audio / vision models.",
557
571
  "settings.media.loading": "Loading…",
558
572
  "settings.media.error": "Failed to load: {{msg}}",
559
573
  "settings.media.kind.image": "Image",
560
574
  "settings.media.kind.video": "Video",
561
575
  "settings.media.kind.audio": "Audio",
576
+ "settings.media.kind.stt": "Speech-to-Text",
577
+ "settings.media.kind.video_understanding": "Video Understanding",
562
578
  "settings.media.kind.ocr": "Vision",
563
579
  "settings.media.source.off": "Off",
564
580
  "settings.media.source.auto": "Auto",
@@ -586,13 +602,6 @@ const I18n = (() => {
586
602
  "settings.media.apiKey.required": "API key required",
587
603
  "settings.media.model.required": "Model name required",
588
604
  "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",
596
605
  "settings.models.field.quicksetup": "Quick Setup",
597
606
  "settings.models.field.model": "Model",
598
607
  "settings.models.field.baseurl": "Base URL",
@@ -620,6 +629,7 @@ const I18n = (() => {
620
629
  "settings.models.btn.test": "Test",
621
630
  "settings.models.link.topUp": "Top up / Manage account",
622
631
  "settings.models.btn.edit": "Edit",
632
+ "settings.models.btn.duplicate": "Duplicate",
623
633
  "settings.models.btn.delete": "Delete",
624
634
  "settings.models.btn.cancel": "Cancel",
625
635
  "settings.models.btn.set_default": "Default",
@@ -628,6 +638,7 @@ const I18n = (() => {
628
638
  "settings.models.btn.done": "Done ✓",
629
639
  "settings.models.modal.add": "Add Model",
630
640
  "settings.models.modal.edit": "Edit Model",
641
+ "settings.models.modal.duplicate": "Duplicate Model",
631
642
  "settings.models.field.provider": "Provider",
632
643
  "settings.models.provider.custom": "Custom",
633
644
  "settings.models.unnamed": "Unnamed Model",
@@ -721,6 +732,8 @@ const I18n = (() => {
721
732
  "settings.currency.updated": "Updated from {{source}} on {{date}}",
722
733
  "settings.currency.updateFailed": "Failed to fetch the latest rate. You can still enter it manually.",
723
734
 
735
+ "settings.accentColor.title": "Accent Color",
736
+
724
737
  // ── Onboard ──
725
738
  "onboard.title": "Welcome to {{brand}}",
726
739
  "onboard.subtitle": "Let's get you set up in a minute.",
@@ -914,6 +927,7 @@ const I18n = (() => {
914
927
 
915
928
  "error.insufficient_credit": "Insufficient LLM credit. Please top up your account to continue.",
916
929
  "error.insufficient_credit.action": "Top up",
930
+ "error.show_detail": "Show details",
917
931
 
918
932
  "billing.sessions": "Sessions",
919
933
  "billing.sessionId": "Session",
@@ -980,11 +994,16 @@ const I18n = (() => {
980
994
  "chat.newMessageHint": "有新消息 ↓",
981
995
  "chat.retry": "重试",
982
996
  "chat.copy": "复制",
997
+ "chat.todo.spawn": "开干",
998
+ "chat.todo.spawning": "创建中…",
999
+ "chat.todo.spawnFailed": "创建会话失败:{msg}",
983
1000
  "chat.copied": "已复制",
984
1001
  "chat.continue": "继续",
985
1002
  "chat.edit": "编辑",
986
1003
  "chat.cancel": "取消",
987
1004
  "chat.send": "发送",
1005
+ "chat.edit.warn": "编辑此消息将丢弃后续对话历史、文件快照及已产生的 token 费用,是否继续?",
1006
+ "chat.edit.warnSkip": "不再提示",
988
1007
  "chat.empty.title": "开始新会话",
989
1008
  "chat.empty.subtitle": "直接提问,或用一个 Skill 启动。",
990
1009
  "chat.empty.tip1": "输入 / 浏览 Skill",
@@ -1016,13 +1035,18 @@ const I18n = (() => {
1016
1035
  "sib.dir.up": "上一级",
1017
1036
  "sib.dir.root": "根目录",
1018
1037
  "sib.dir.default": "默认工作目录",
1019
- "sib.dir.current": "当前工作目录", "sib.dir.inputPlaceholder": "输入或选择目录路径",
1038
+ "sib.dir.current": "当前工作目录",
1039
+ "sib.dir.inputPlaceholder": "输入或选择目录路径",
1020
1040
  "sib.dir.loading": "加载中…",
1021
1041
  "sib.dir.empty": "空目录",
1022
1042
  "sib.dir.loadError": "加载失败",
1023
1043
  "sib.dir.confirm": "确认",
1024
1044
  "sib.dir.cancel": "取消",
1025
- "sib.dir.showHidden": "显示隐藏文件", "workspace.title": "文件",
1045
+ "sib.dir.showHidden": "显示隐藏文件",
1046
+ "sib.dir.newFolder": "新建文件夹",
1047
+ "sib.dir.newFolderDefault": "新建文件夹",
1048
+ "sib.dir.mkdirError": "新建文件夹失败:{{msg}}",
1049
+ "workspace.title": "文件",
1026
1050
  "workspace.expand": "打开工作区",
1027
1051
  "workspace.collapse": "收起工作区",
1028
1052
  "workspace.refresh": "刷新",
@@ -1040,6 +1064,8 @@ const I18n = (() => {
1040
1064
  "aside.expand": "打开面板",
1041
1065
  "workspace.revealInFinder": "打开所在文件夹",
1042
1066
  "workspace.revealFailed": "无法打开文件位置",
1067
+ "changes.tab": "Git 管理",
1068
+ "tm.tab": "时光机",
1043
1069
  "sib.model.tooltip": "点击切换模型",
1044
1070
  "sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
1045
1071
  "sib.variant.header": "快速切换",
@@ -1369,6 +1395,7 @@ const I18n = (() => {
1369
1395
  "skills.deleteError": "删除失败。",
1370
1396
  "skills.deleted": "技能 \"{{name}}\" 已删除。",
1371
1397
  "skills.btn.delete": "删除技能",
1398
+ "skills.btn.edit": "编辑技能",
1372
1399
  "skills.ac.empty": "暂无可用技能",
1373
1400
  "skills.upload.uploading": "上传中…",
1374
1401
  "skills.upload.uploaded": "已上传",
@@ -1376,6 +1403,7 @@ const I18n = (() => {
1376
1403
  "skills.upload.failed": "失败",
1377
1404
  "skills.upload.publishTip": "发布到云端",
1378
1405
  "skills.btn.refresh": "刷新",
1406
+ "skills.editFail": "加载技能失败",
1379
1407
 
1380
1408
  // ── Channels panel ──
1381
1409
  "channels.title": "频道",
@@ -1464,12 +1492,14 @@ const I18n = (() => {
1464
1492
  "settings.models.badge.default": "默认",
1465
1493
  "settings.models.badge.lite": "轻量",
1466
1494
  "settings.media.title": "配置副模型",
1467
- "settings.media.desc": "图片生成 / 视频生成 / 音频生成 / 视觉理解(可选)",
1495
+ "settings.media.desc": "图片生成 / 视频生成 / 音频生成 / 语音转写 / 视频理解 / 视觉理解(可选)",
1468
1496
  "settings.media.loading": "加载中…",
1469
1497
  "settings.media.error": "加载失败:{{msg}}",
1470
1498
  "settings.media.kind.image": "图片生成",
1471
1499
  "settings.media.kind.video": "视频生成",
1472
1500
  "settings.media.kind.audio": "音频生成",
1501
+ "settings.media.kind.stt": "语音转写",
1502
+ "settings.media.kind.video_understanding": "视频理解",
1473
1503
  "settings.media.kind.ocr": "视觉理解",
1474
1504
  "settings.media.source.off": "关闭",
1475
1505
  "settings.media.source.auto": "自动",
@@ -1531,6 +1561,7 @@ const I18n = (() => {
1531
1561
  "settings.models.btn.test": "测试",
1532
1562
  "settings.models.link.topUp": "充值 / 账户管理",
1533
1563
  "settings.models.btn.edit": "编辑",
1564
+ "settings.models.btn.duplicate": "复制",
1534
1565
  "settings.models.btn.delete": "删除",
1535
1566
  "settings.models.btn.cancel": "取消",
1536
1567
  "settings.models.btn.set_default": "设为默认",
@@ -1539,6 +1570,7 @@ const I18n = (() => {
1539
1570
  "settings.models.btn.done": "完成 ✓",
1540
1571
  "settings.models.modal.add": "添加模型",
1541
1572
  "settings.models.modal.edit": "编辑模型",
1573
+ "settings.models.modal.duplicate": "复制模型",
1542
1574
  "settings.models.field.provider": "供应商",
1543
1575
  "settings.models.provider.custom": "自定义",
1544
1576
  "settings.models.unnamed": "未命名模型",
@@ -1632,6 +1664,8 @@ const I18n = (() => {
1632
1664
  "settings.currency.updated": "已从 {{source}} 更新,日期 {{date}}",
1633
1665
  "settings.currency.updateFailed": "获取最新汇率失败,仍可手动输入。",
1634
1666
 
1667
+ "settings.accentColor.title": "主色",
1668
+
1635
1669
  // ── Onboard ──
1636
1670
  "onboard.title": "欢迎使用 {{brand}}",
1637
1671
  "onboard.subtitle": "一分钟完成配置,马上开始。",
@@ -1826,6 +1860,7 @@ const I18n = (() => {
1826
1860
 
1827
1861
  "error.insufficient_credit": "LLM(大模型)余额不足,请充值后继续使用。",
1828
1862
  "error.insufficient_credit.action": "去充值",
1863
+ "error.show_detail": "查看详情",
1829
1864
  "billing.sessions": "会话消耗",
1830
1865
  "billing.sessionId": "会话",
1831
1866
  "billing.tokens": "Token",