mulmoclaude 0.1.2 → 0.4.0

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 (251) hide show
  1. package/bin/mulmoclaude.js +7 -24
  2. package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
  3. package/client/assets/index-CubzmCVK.css +2 -0
  4. package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
  5. package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
  6. package/client/index.html +2 -4
  7. package/package.json +13 -13
  8. package/server/agent/attachmentConverter.ts +2 -2
  9. package/server/agent/config.ts +12 -12
  10. package/server/agent/index.ts +9 -3
  11. package/server/agent/mcp-server.ts +19 -19
  12. package/server/agent/mcp-tools/index.ts +6 -6
  13. package/server/agent/mcp-tools/x.ts +7 -6
  14. package/server/agent/prompt.ts +195 -29
  15. package/server/agent/resumeFailover.ts +5 -5
  16. package/server/agent/sandboxMounts.ts +10 -10
  17. package/server/agent/stream.ts +4 -4
  18. package/server/api/auth/bearerAuth.ts +3 -3
  19. package/server/api/auth/token.ts +2 -2
  20. package/server/api/routes/agent.ts +21 -3
  21. package/server/api/routes/config.ts +1 -1
  22. package/server/api/routes/files.ts +22 -21
  23. package/server/api/routes/html.ts +2 -2
  24. package/server/api/routes/image.ts +7 -7
  25. package/server/api/routes/mulmo-script.ts +33 -31
  26. package/server/api/routes/pdf.ts +2 -2
  27. package/server/api/routes/plugins.ts +16 -6
  28. package/server/api/routes/roles.ts +2 -2
  29. package/server/api/routes/scheduler.ts +14 -12
  30. package/server/api/routes/schedulerHandlers.ts +12 -12
  31. package/server/api/routes/schedulerTasks.ts +19 -17
  32. package/server/api/routes/sessions.ts +26 -26
  33. package/server/api/routes/sessionsCursor.ts +4 -4
  34. package/server/api/routes/skills.ts +5 -5
  35. package/server/api/routes/sources.ts +3 -3
  36. package/server/api/routes/todosColumnsHandlers.ts +30 -30
  37. package/server/api/routes/todosHandlers.ts +1 -1
  38. package/server/api/routes/todosItemsHandlers.ts +14 -14
  39. package/server/api/routes/wiki.ts +36 -22
  40. package/server/api/sandboxStatus.ts +1 -1
  41. package/server/events/notifications.ts +6 -6
  42. package/server/events/pub-sub/index.ts +3 -3
  43. package/server/events/relay-client.ts +17 -16
  44. package/server/events/scheduler-adapter.ts +20 -20
  45. package/server/events/session-store/index.ts +10 -10
  46. package/server/events/task-manager/index.ts +7 -7
  47. package/server/index.ts +59 -65
  48. package/server/system/config.ts +5 -5
  49. package/server/system/credentials.ts +7 -5
  50. package/server/system/env.ts +5 -5
  51. package/server/utils/date.ts +18 -18
  52. package/server/utils/files/atomic.ts +16 -16
  53. package/server/utils/files/html-io.ts +5 -5
  54. package/server/utils/files/image-store.ts +19 -8
  55. package/server/utils/files/journal-io.ts +4 -4
  56. package/server/utils/files/json.ts +5 -5
  57. package/server/utils/files/markdown-store.ts +4 -4
  58. package/server/utils/files/naming.ts +2 -2
  59. package/server/utils/files/reference-dirs-io.ts +3 -3
  60. package/server/utils/files/roles-io.ts +12 -12
  61. package/server/utils/files/safe.ts +14 -14
  62. package/server/utils/files/scheduler-io.ts +5 -5
  63. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  64. package/server/utils/files/session-io.ts +35 -35
  65. package/server/utils/files/spreadsheet-store.ts +7 -7
  66. package/server/utils/files/todos-io.ts +9 -9
  67. package/server/utils/files/user-tasks-io.ts +5 -5
  68. package/server/utils/files/workspace-io.ts +12 -12
  69. package/server/utils/gemini.ts +2 -2
  70. package/server/utils/gitignore.ts +9 -9
  71. package/server/utils/json.ts +5 -5
  72. package/server/utils/logBackgroundError.ts +12 -3
  73. package/server/utils/markdown.ts +5 -5
  74. package/server/utils/port.d.mts +6 -0
  75. package/server/utils/port.mjs +48 -0
  76. package/server/utils/request.ts +12 -6
  77. package/server/utils/spawn.ts +1 -1
  78. package/server/utils/types.ts +2 -2
  79. package/server/workspace/chat-index/indexer.ts +15 -15
  80. package/server/workspace/chat-index/summarizer.ts +4 -4
  81. package/server/workspace/custom-dirs.ts +16 -16
  82. package/server/workspace/journal/archivist.ts +35 -35
  83. package/server/workspace/journal/dailyPass.ts +31 -28
  84. package/server/workspace/journal/diff.ts +2 -2
  85. package/server/workspace/journal/index.ts +4 -4
  86. package/server/workspace/journal/indexFile.ts +29 -25
  87. package/server/workspace/journal/optimizationPass.ts +2 -2
  88. package/server/workspace/journal/state.ts +6 -6
  89. package/server/workspace/paths.ts +3 -3
  90. package/server/workspace/reference-dirs.ts +20 -20
  91. package/server/workspace/roles.ts +6 -6
  92. package/server/workspace/skills/discovery.ts +4 -4
  93. package/server/workspace/skills/parser.ts +6 -6
  94. package/server/workspace/skills/scheduler.ts +3 -3
  95. package/server/workspace/skills/user-tasks.ts +34 -34
  96. package/server/workspace/skills/writer.ts +3 -3
  97. package/server/workspace/sources/arxivDiscovery.ts +10 -10
  98. package/server/workspace/sources/classifier.ts +7 -7
  99. package/server/workspace/sources/fetchers/arxiv.ts +7 -7
  100. package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
  101. package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
  102. package/server/workspace/sources/fetchers/rss.ts +5 -5
  103. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  104. package/server/workspace/sources/interests.ts +12 -12
  105. package/server/workspace/sources/paths.ts +6 -6
  106. package/server/workspace/sources/pipeline/fetch.ts +36 -13
  107. package/server/workspace/sources/pipeline/index.ts +8 -13
  108. package/server/workspace/sources/pipeline/notify.ts +3 -3
  109. package/server/workspace/sources/pipeline/plan.ts +15 -13
  110. package/server/workspace/sources/pipeline/write.ts +5 -5
  111. package/server/workspace/sources/rateLimiter.ts +1 -1
  112. package/server/workspace/sources/registry.ts +16 -16
  113. package/server/workspace/sources/robots.ts +14 -14
  114. package/server/workspace/sources/sourceState.ts +17 -10
  115. package/server/workspace/sources/types.ts +9 -0
  116. package/server/workspace/sources/urls.ts +1 -1
  117. package/server/workspace/tool-trace/classify.ts +4 -4
  118. package/server/workspace/tool-trace/index.ts +1 -1
  119. package/server/workspace/tool-trace/writeSearch.ts +26 -16
  120. package/server/workspace/wiki-backlinks/index.ts +8 -8
  121. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
  122. package/server/workspace/workspace.ts +7 -7
  123. package/src/App.vue +315 -141
  124. package/src/components/CanvasViewToggle.vue +10 -7
  125. package/src/components/ChatInput.vue +67 -33
  126. package/src/components/FileContentHeader.vue +7 -4
  127. package/src/components/FileContentRenderer.vue +20 -6
  128. package/src/components/FileTree.vue +6 -3
  129. package/src/components/FileTreePane.vue +11 -8
  130. package/src/components/FilesView.vue +5 -3
  131. package/src/components/LockStatusPopup.vue +17 -14
  132. package/src/components/NotificationBell.vue +14 -5
  133. package/src/components/NotificationToast.vue +6 -3
  134. package/src/components/PluginLauncher.vue +19 -56
  135. package/src/components/RightSidebar.vue +13 -10
  136. package/src/components/RoleSelector.vue +2 -2
  137. package/src/components/SessionHistoryPanel.vue +38 -34
  138. package/src/components/SessionTabBar.vue +8 -10
  139. package/src/components/SettingsMcpTab.vue +49 -36
  140. package/src/components/SettingsModal.vue +24 -22
  141. package/src/components/SettingsReferenceDirsTab.vue +39 -34
  142. package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
  143. package/src/components/SidebarHeader.vue +25 -4
  144. package/src/components/StackView.vue +4 -1
  145. package/src/components/SuggestionsPanel.vue +7 -4
  146. package/src/components/TodoExplorer.vue +26 -15
  147. package/src/components/ToolResultsPanel.vue +27 -13
  148. package/src/components/todo/TodoAddDialog.vue +19 -14
  149. package/src/components/todo/TodoEditDialog.vue +7 -2
  150. package/src/components/todo/TodoEditPanel.vue +17 -12
  151. package/src/components/todo/TodoKanbanView.vue +10 -5
  152. package/src/components/todo/TodoListView.vue +10 -7
  153. package/src/components/todo/TodoTableView.vue +5 -2
  154. package/src/composables/useAppApi.ts +9 -0
  155. package/src/composables/useClickOutside.ts +2 -2
  156. package/src/composables/useDynamicFavicon.ts +172 -37
  157. package/src/composables/useEventListeners.ts +7 -8
  158. package/src/composables/useFaviconState.ts +13 -2
  159. package/src/composables/useFileSelection.ts +24 -6
  160. package/src/composables/useFreshPluginData.ts +3 -3
  161. package/src/composables/useKeyNavigation.ts +11 -11
  162. package/src/composables/useLayoutMode.ts +32 -0
  163. package/src/composables/useMcpTools.ts +2 -2
  164. package/src/composables/useNotifications.ts +3 -3
  165. package/src/composables/usePdfDownload.ts +4 -4
  166. package/src/composables/usePendingCalls.ts +1 -1
  167. package/src/composables/usePubSub.ts +10 -10
  168. package/src/composables/useRoles.ts +1 -1
  169. package/src/composables/useSandboxStatus.ts +1 -1
  170. package/src/composables/useSessionDerived.ts +3 -3
  171. package/src/composables/useSessionHistory.ts +7 -17
  172. package/src/composables/useSessionSync.ts +8 -8
  173. package/src/composables/useViewLayout.ts +20 -34
  174. package/src/config/roles.ts +2 -2
  175. package/src/lang/de.ts +536 -0
  176. package/src/lang/en.ts +558 -0
  177. package/src/lang/es.ts +543 -0
  178. package/src/lang/fr.ts +536 -0
  179. package/src/lang/ja.ts +536 -0
  180. package/src/lang/ko.ts +540 -0
  181. package/src/lang/pt-BR.ts +534 -0
  182. package/src/lang/zh.ts +537 -0
  183. package/src/lib/vue-i18n.ts +97 -0
  184. package/src/main.ts +2 -0
  185. package/src/plugins/canvas/View.vue +102 -186
  186. package/src/plugins/canvas/definition.ts +0 -8
  187. package/src/plugins/chart/Preview.vue +5 -5
  188. package/src/plugins/chart/View.vue +9 -4
  189. package/src/plugins/manageRoles/Preview.vue +4 -1
  190. package/src/plugins/manageRoles/View.vue +59 -43
  191. package/src/plugins/manageSkills/Preview.vue +8 -3
  192. package/src/plugins/manageSkills/View.vue +29 -25
  193. package/src/plugins/manageSource/Preview.vue +2 -2
  194. package/src/plugins/manageSource/View.vue +73 -52
  195. package/src/plugins/markdown/Preview.vue +1 -1
  196. package/src/plugins/markdown/View.vue +26 -36
  197. package/src/plugins/presentHtml/Preview.vue +1 -1
  198. package/src/plugins/presentHtml/View.vue +7 -4
  199. package/src/plugins/presentHtml/helpers.ts +8 -8
  200. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  201. package/src/plugins/presentMulmoScript/View.vue +40 -30
  202. package/src/plugins/presentMulmoScript/helpers.ts +1 -1
  203. package/src/plugins/scheduler/Preview.vue +13 -10
  204. package/src/plugins/scheduler/TasksTab.vue +57 -28
  205. package/src/plugins/scheduler/View.vue +28 -19
  206. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  207. package/src/plugins/spreadsheet/Preview.vue +8 -3
  208. package/src/plugins/spreadsheet/View.vue +21 -12
  209. package/src/plugins/textResponse/Preview.vue +15 -58
  210. package/src/plugins/textResponse/View.vue +29 -9
  211. package/src/plugins/todo/Preview.vue +13 -8
  212. package/src/plugins/todo/View.vue +38 -24
  213. package/src/plugins/todo/composables/useTodos.ts +5 -5
  214. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  215. package/src/plugins/ui-image/ImageView.vue +7 -4
  216. package/src/plugins/wiki/Preview.vue +10 -7
  217. package/src/plugins/wiki/View.vue +202 -81
  218. package/src/plugins/wiki/helpers.ts +4 -4
  219. package/src/plugins/wiki/route.ts +112 -0
  220. package/src/router/guards.ts +46 -28
  221. package/src/router/index.ts +41 -26
  222. package/src/types/session.ts +4 -3
  223. package/src/types/vue-i18n.d.ts +20 -0
  224. package/src/utils/agent/request.ts +22 -3
  225. package/src/utils/canvas/layoutMode.ts +26 -0
  226. package/src/utils/dom/scrollable.ts +2 -2
  227. package/src/utils/files/expandedDirs.ts +1 -1
  228. package/src/utils/files/sortChildren.ts +6 -6
  229. package/src/utils/format/frontmatter.ts +6 -6
  230. package/src/utils/image/cacheBust.ts +16 -0
  231. package/src/utils/image/resolve.ts +16 -0
  232. package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
  233. package/src/utils/markdown/extractFirstH1.ts +2 -2
  234. package/src/utils/path/relativeLink.ts +15 -15
  235. package/src/utils/path/workspaceLinkRouter.ts +81 -0
  236. package/src/utils/role/icon.ts +2 -2
  237. package/src/utils/role/merge.ts +2 -2
  238. package/src/utils/role/plugins.ts +1 -1
  239. package/src/utils/session/sessionFactory.ts +2 -2
  240. package/src/utils/session/sessionHelpers.ts +2 -2
  241. package/src/utils/tools/dedup.ts +4 -4
  242. package/src/utils/tools/result.ts +3 -3
  243. package/src/utils/types.ts +2 -2
  244. package/src/vite-env.d.ts +9 -0
  245. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  246. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  247. package/client/assets/index-KNLBjwuh.css +0 -1
  248. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  249. package/src/composables/useCanvasViewMode.ts +0 -121
  250. package/src/utils/canvas/viewMode.ts +0 -46
  251. /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
@@ -31,24 +31,36 @@
31
31
  </div>
32
32
 
33
33
  <div class="flex items-center gap-1">
34
- <button class="w-8 h-8 flex items-center justify-center rounded border-2 border-gray-300 bg-white hover:bg-gray-50" title="Undo" @click="undo">
34
+ <button
35
+ class="w-8 h-8 flex items-center justify-center rounded border-2 border-gray-300 bg-white hover:bg-gray-50"
36
+ :title="t('pluginCanvas.undo')"
37
+ @click="undo"
38
+ >
35
39
  <span class="material-icons text-sm">undo</span>
36
40
  </button>
37
- <button class="w-8 h-8 flex items-center justify-center rounded border-2 border-gray-300 bg-white hover:bg-gray-50" title="Redo" @click="redo">
41
+ <button
42
+ class="w-8 h-8 flex items-center justify-center rounded border-2 border-gray-300 bg-white hover:bg-gray-50"
43
+ :title="t('pluginCanvas.redo')"
44
+ @click="redo"
45
+ >
38
46
  <span class="material-icons text-sm">redo</span>
39
47
  </button>
40
- <button class="w-8 h-8 flex items-center justify-center rounded border-2 border-red-300 bg-white hover:bg-red-50" title="Clear" @click="clear">
48
+ <button
49
+ class="w-8 h-8 flex items-center justify-center rounded border-2 border-red-300 bg-white hover:bg-red-50"
50
+ :title="t('pluginCanvas.clear')"
51
+ @click="clear"
52
+ >
41
53
  <span class="material-icons text-sm">delete</span>
42
54
  </button>
43
55
  </div>
44
56
  </div>
45
57
  </div>
46
58
 
47
- <div class="flex-1 p-4 overflow-hidden">
59
+ <div ref="containerRef" class="flex-1 p-4 overflow-hidden">
48
60
  <VueDrawingCanvas
61
+ v-if="canvasWidth > 0"
49
62
  ref="canvasRef"
50
63
  :key="`${selectedResult?.uuid || 'default'}-${canvasRenderKey}`"
51
- v-model:image="canvasImage"
52
64
  :width="canvasWidth"
53
65
  :height="canvasHeight"
54
66
  :stroke-type="'dash'"
@@ -59,20 +71,19 @@
59
71
  :line-width="brushSize"
60
72
  :color="brushColor"
61
73
  :background-color="'#FFFFFF'"
62
- :background-image="undefined"
74
+ :background-image="backgroundImage"
63
75
  :watermark="undefined"
64
- :initial-image="initialStrokes"
65
76
  save-as="png"
66
77
  :styles="{
67
78
  border: '1px solid #ddd',
68
79
  borderRadius: '8px',
69
80
  }"
70
81
  :lock="false"
71
- @mouseup="handleDrawingEnd"
72
- @touchend="handleDrawingEnd"
82
+ @mouseup="saveDrawing"
83
+ @touchend="saveDrawing"
73
84
  />
74
85
  <div class="flex items-center gap-2 flex-wrap mt-3">
75
- <span class="text-xs text-gray-500 mr-1">Style:</span>
86
+ <span class="text-xs text-gray-500 mr-1">{{ t("pluginCanvas.styleLabel") }}</span>
76
87
  <button
77
88
  v-for="style in artStyles"
78
89
  :key="style.id"
@@ -87,22 +98,23 @@
87
98
  </template>
88
99
 
89
100
  <script setup lang="ts">
90
- import { ref, onMounted, onUnmounted, nextTick, watch } from "vue";
101
+ import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
102
+ import { useI18n } from "vue-i18n";
91
103
  import VueDrawingCanvas from "vue-drawing-canvas";
92
104
  import type { ToolResult } from "gui-chat-protocol/vue";
93
- import type { ImageToolData, CanvasDrawingState } from "./definition";
94
- import { apiPost, apiPut } from "../../utils/api";
105
+ import type { ImageToolData } from "./definition";
106
+ import { apiPut } from "../../utils/api";
95
107
  import { API_ROUTES } from "../../config/apiRoutes";
108
+ import { resolveImageSrc } from "../../utils/image/resolve";
109
+ import { bumpImage } from "../../utils/image/cacheBust";
110
+
111
+ const { t } = useI18n();
96
112
 
97
113
  const props = defineProps<{
98
114
  selectedResult: ToolResult<ImageToolData> | null;
99
115
  sendTextMessage?: (text: string) => void;
100
116
  }>();
101
117
 
102
- const emit = defineEmits<{
103
- updateResult: [result: ToolResult<ImageToolData>];
104
- }>();
105
-
106
118
  const artStyles = [
107
119
  { id: "ghibli", label: "Ghibli" },
108
120
  { id: "ukiyoe", label: "Ukiyoe" },
@@ -118,215 +130,119 @@ const artStyles = [
118
130
  { id: "pixelart", label: "Pixel Art" },
119
131
  ];
120
132
 
121
- const applyStyle = async (style: { id: string; label: string }) => {
122
- const saved = await saveDrawingState();
123
- if (!saved) return;
124
- if (props.sendTextMessage) {
125
- props.sendTextMessage(`Turn my drawing on the canvas into a ${style.label} style image.`);
126
- }
133
+ const applyStyle = (style: { id: string; label: string }) => {
134
+ props.sendTextMessage?.(`Turn my drawing on the canvas into a ${style.label} style image.`);
127
135
  };
128
136
 
129
137
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
138
  const canvasRef = ref<any>(null);
131
- const canvasImage = ref("");
139
+ const containerRef = ref<HTMLDivElement | null>(null);
132
140
  const brushSize = ref(5);
133
141
  const brushColor = ref("#000000");
134
- const initialStrokes = ref([]);
135
- const canvasWidth = ref(800);
136
- const canvasHeight = ref(600);
142
+ // `canvasWidth === 0` doubles as "not measured yet" — the child has a
143
+ // v-if on `canvasWidth > 0` so it mounts exactly once, with the
144
+ // correct dimensions. Mounting at a default 800×600 and resizing a
145
+ // tick later races the in-flight background fetch and blanks the
146
+ // canvas on reload.
147
+ const canvasWidth = ref(0);
148
+ const canvasHeight = ref(0);
137
149
  const canvasRenderKey = ref(0);
138
150
 
139
- // Track the server-side image path for this canvas instance.
140
- // Initialized from existing result data (if reopening a saved canvas).
141
- const imagePath = ref(
142
- props.selectedResult?.data?.imageData && !props.selectedResult.data.imageData.startsWith("data:") ? props.selectedResult.data.imageData : "",
143
- );
144
- let uploadInFlight = false;
145
- let pendingSave = false;
146
-
147
- const restoreDrawingState = () => {
148
- if (props.selectedResult?.viewState?.drawingState) {
149
- const state = props.selectedResult.viewState.drawingState as CanvasDrawingState;
151
+ // The PNG on disk is the source of truth. The path is baked into
152
+ // the tool result at openCanvas time (server-allocated), so reload
153
+ // finds the file with zero client→server sync. Every stroke PUTs
154
+ // back to this same path.
155
+ const imagePath = computed(() => {
156
+ const stored = props.selectedResult?.data?.imageData;
157
+ if (!stored || stored.startsWith("data:")) return "";
158
+ return stored;
159
+ });
150
160
 
151
- brushSize.value = state.brushSize || 5;
152
- brushColor.value = state.brushColor || "#000000";
153
- canvasWidth.value = state.canvasWidth || 800;
154
- canvasHeight.value = state.canvasHeight || 600;
161
+ // Per-mount cache buster for the VueDrawingCanvas child. The URL
162
+ // must be stable for the lifetime of one canvas instance — if it
163
+ // changes while the child is alive (e.g. from a post-save bump),
164
+ // the library nulls its cached `loadedImage` and the next redraw
165
+ // races a fresh re-fetch against stroke painting, blanking the
166
+ // canvas. Tying the token to `canvasRenderKey` (which increments
167
+ // when we explicitly remount via :key on resize) gives us fresh
168
+ // bytes on page reload and on resize, without mid-session churn.
169
+ const setupTime = Date.now();
170
+ const backgroundImage = computed(() => {
171
+ if (!imagePath.value) return undefined;
172
+ const base = resolveImageSrc(imagePath.value);
173
+ const sep = base.includes("?") ? "&" : "?";
174
+ return `${base}${sep}mt=${setupTime}-${canvasRenderKey.value}`;
175
+ });
155
176
 
156
- if (state.strokes) {
157
- initialStrokes.value = state.strokes as never[];
158
- } else {
159
- initialStrokes.value = [];
160
- }
161
- } else {
162
- initialStrokes.value = [];
163
- }
164
- // Reinitialise imagePath from the incoming result so autosaves
165
- // after a selectedResult change don't upload against the old path.
166
- imagePath.value =
167
- props.selectedResult?.data?.imageData && !props.selectedResult.data.imageData.startsWith("data:") ? props.selectedResult.data.imageData : "";
168
- };
169
- restoreDrawingState();
177
+ let uploadInFlight = false;
178
+ let pendingSave = false;
170
179
 
171
- const undo = async () => {
172
- if (canvasRef.value) {
173
- try {
174
- canvasRef.value.undo();
175
- // Wait for the canvas to update, then save state
176
- setTimeout(saveDrawingState, 50);
177
- } catch (error) {
178
- console.warn("Undo operation failed:", error);
179
- }
180
- }
180
+ // Undo/redo kick off the library's async redraw; give it a tick to
181
+ // composite before we snapshot-and-upload.
182
+ const undo = () => {
183
+ canvasRef.value?.undo();
184
+ setTimeout(saveDrawing, 50);
181
185
  };
182
-
183
- const redo = async () => {
184
- if (canvasRef.value) {
185
- try {
186
- canvasRef.value.redo();
187
- // Wait for the canvas to update, then save state
188
- setTimeout(saveDrawingState, 50);
189
- } catch (error) {
190
- console.warn("Redo operation failed:", error);
191
- }
192
- }
186
+ const redo = () => {
187
+ canvasRef.value?.redo();
188
+ setTimeout(saveDrawing, 50);
193
189
  };
194
-
195
190
  const clear = () => {
196
- if (canvasRef.value) {
197
- try {
198
- canvasRef.value.reset();
199
- saveDrawingState();
200
- } catch (error) {
201
- console.warn("Clear operation failed:", error);
202
- }
203
- }
204
- };
205
-
206
- const handleDrawingEnd = () => {
207
- saveDrawingState();
191
+ canvasRef.value?.reset();
192
+ saveDrawing();
208
193
  };
209
194
 
210
- // Returns true if the drawing was successfully persisted, false
211
- // otherwise. Callers like applyStyle check the return value to avoid
212
- // sending a style request against stale image data.
213
- //
214
- // Captures a local snapshot of props.selectedResult and imagePath at
215
- // the start so an async upload never accidentally writes against a
216
- // different result that was swapped in mid-flight.
217
- const saveDrawingState = async (): Promise<boolean> => {
218
- if (!canvasRef.value || !props.selectedResult) return false;
195
+ // Snapshot the current bitmap and PUT it back to the pre-allocated
196
+ // file. No result mutation the path is fixed from canvas creation,
197
+ // so nothing upstream needs to know about saves.
198
+ const saveDrawing = async (): Promise<void> => {
199
+ if (!canvasRef.value || !imagePath.value) return;
219
200
  if (uploadInFlight) {
220
201
  pendingSave = true;
221
- return false;
202
+ return;
222
203
  }
223
- // Snapshot the current result and path *before* any await so a
224
- // concurrent selectedResult swap doesn't silently redirect the
225
- // upload.
226
- const boundResult = props.selectedResult;
227
- const boundImagePath = imagePath.value;
228
204
  uploadInFlight = true;
229
205
  try {
230
206
  const imageDataUri: string = await canvasRef.value.save();
231
- const strokes = canvasRef.value.getAllStrokes();
232
- const drawingState = {
233
- strokes,
234
- brushSize: brushSize.value,
235
- brushColor: brushColor.value,
236
- canvasWidth: canvasWidth.value,
237
- canvasHeight: canvasHeight.value,
238
- };
239
-
240
- // If selectedResult changed while we were saving the canvas
241
- // bitmap, abort — the upload would go to the wrong session.
242
- if (boundResult !== props.selectedResult) return false;
243
-
244
- const savedPath = boundImagePath
245
- ? await (async () => {
246
- const filename = boundImagePath.replace(/^images\//, "");
247
- const result = await apiPut<{ path: string }>(API_ROUTES.image.update.replace(":filename", filename), { imageData: imageDataUri });
248
- if (!result.ok) throw new Error(`PUT failed: ${result.error}`);
249
- return result.data.path;
250
- })()
251
- : await (async () => {
252
- const result = await apiPost<{ path: string }>(API_ROUTES.image.upload, {
253
- imageData: imageDataUri,
254
- });
255
- if (!result.ok) throw new Error(`POST failed: ${result.error}`);
256
- return result.data.path;
257
- })();
258
-
259
- imagePath.value = savedPath;
260
-
261
- const updatedResult: ToolResult<ImageToolData> = {
262
- ...boundResult,
263
- data: {
264
- prompt: boundResult.data?.prompt || "",
265
- imageData: savedPath,
266
- },
267
- viewState: { drawingState },
268
- };
269
-
270
- emit("updateResult", updatedResult);
271
- return true;
207
+ const filename = imagePath.value.split("/").pop() ?? "";
208
+ const result = await apiPut<{ path: string }>(API_ROUTES.image.update.replace(":filename", filename), { imageData: imageDataUri });
209
+ if (!result.ok) throw new Error(`PUT failed: ${result.error}`);
210
+ bumpImage(imagePath.value);
272
211
  } catch (error) {
273
- console.error("Failed to save drawing state:", error);
274
- return false;
212
+ console.error("Failed to save drawing:", error);
275
213
  } finally {
276
214
  uploadInFlight = false;
277
215
  if (pendingSave) {
278
216
  pendingSave = false;
279
- void saveDrawingState();
217
+ void saveDrawing();
280
218
  }
281
219
  }
282
220
  };
283
221
 
284
- // Watch for selectedResult changes to restore drawing state
285
- watch(
286
- () => props.selectedResult,
287
- () => {
288
- restoreDrawingState();
289
- },
290
- { immediate: false },
291
- );
292
-
293
- // Watch for changes to automatically save drawing state
294
- watch([brushSize, brushColor], () => {
295
- saveDrawingState();
296
- });
297
-
298
- // Watch for canvas size changes and force re-mount
299
- watch([canvasWidth, canvasHeight], () => {
300
- // Force canvas to re-mount with new dimensions by changing the key
301
- canvasRenderKey.value++;
302
- });
303
222
  const updateCanvasSize = () => {
304
- // Get the canvas container (the div with flex-1 p-4 overflow-hidden)
305
- const canvasContainer = canvasRef.value?.$el?.parentElement;
306
- if (canvasContainer) {
307
- const containerRect = canvasContainer.getBoundingClientRect();
308
-
309
- const padding = 32; // p-4 = 16px each side
310
- const newWidth = Math.floor(containerRect.width - padding);
311
- const newHeight = Math.floor((newWidth * 9) / 16);
312
-
313
- // Only update if the size actually changed to avoid unnecessary re-renders
314
- if (newWidth !== canvasWidth.value || newHeight !== canvasHeight.value) {
315
- canvasWidth.value = newWidth;
316
- canvasHeight.value = newHeight;
317
- }
318
- }
223
+ const container = containerRef.value;
224
+ if (!container) return;
225
+ const containerRect = container.getBoundingClientRect();
226
+ const padding = 32;
227
+ const newWidth = Math.floor(containerRect.width - padding);
228
+ const newHeight = Math.floor((newWidth * 9) / 16);
229
+ if (newWidth <= 0) return;
230
+ if (newWidth === canvasWidth.value && newHeight === canvasHeight.value) return;
231
+ const firstPaint = canvasWidth.value === 0;
232
+ canvasWidth.value = newWidth;
233
+ canvasHeight.value = newHeight;
234
+ // Remount the child on every size change *after* the first paint so
235
+ // it re-reads the background image at the new dimensions. The first
236
+ // paint doesn't need this — v-if gates the mount on `canvasWidth > 0`.
237
+ if (!firstPaint) canvasRenderKey.value++;
319
238
  };
320
239
 
321
240
  onMounted(async () => {
322
241
  await nextTick();
323
242
  updateCanvasSize();
324
-
325
- // Listen for window resize to update canvas size
326
243
  window.addEventListener("resize", updateCanvasSize);
327
244
  });
328
245
 
329
- // Clean up resize listener
330
246
  onUnmounted(() => {
331
247
  window.removeEventListener("resize", updateCanvasSize);
332
248
  });
@@ -7,14 +7,6 @@ export interface ImageToolData {
7
7
  prompt: string;
8
8
  }
9
9
 
10
- export interface CanvasDrawingState {
11
- brushSize?: number;
12
- brushColor?: string;
13
- canvasWidth?: number;
14
- canvasHeight?: number;
15
- strokes?: unknown[];
16
- }
17
-
18
10
  const toolDefinition: ToolDefinition = {
19
11
  type: "function",
20
12
  name: TOOL_NAME,
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="text-sm">
2
+ <div class="p-2 text-sm">
3
3
  <div class="font-medium text-gray-700 truncate mb-1">
4
4
  {{ title }}
5
5
  </div>
@@ -25,8 +25,8 @@ const hint = computed(() => {
25
25
  const charts = data.value?.document?.charts ?? [];
26
26
  if (charts.length === 0) return "";
27
27
  const types = charts
28
- .map((c) => c.type ?? inferTypeFromOption(c.option))
29
- .filter((t): t is string => Boolean(t))
28
+ .map((chart) => chart.type ?? inferTypeFromOption(chart.option))
29
+ .filter((chartType): chartType is string => Boolean(chartType))
30
30
  .slice(0, 3);
31
31
  const suffix = charts.length > types.length ? ", …" : "";
32
32
  const typeList = types.join(", ");
@@ -41,8 +41,8 @@ function inferTypeFromOption(option: Record<string, unknown>): string | null {
41
41
  const first = series[0] as { type?: unknown };
42
42
  if (typeof first.type === "string") return first.type;
43
43
  } else if (series && typeof series === "object") {
44
- const t = (series as { type?: unknown }).type;
45
- if (typeof t === "string") return t;
44
+ const seriesType = (series as { type?: unknown }).type;
45
+ if (typeof seriesType === "string") return seriesType;
46
46
  }
47
47
  return null;
48
48
  }
@@ -2,16 +2,18 @@
2
2
  <div class="h-full flex flex-col overflow-hidden">
3
3
  <div class="px-4 py-2 border-b border-gray-100 shrink-0 flex items-center justify-between">
4
4
  <span class="text-sm font-medium text-gray-700 truncate">
5
- {{ title ?? "Chart" }}
5
+ {{ title ?? t("pluginChart.untitled") }}
6
+ </span>
7
+ <span class="text-xs text-gray-500 shrink-0">
8
+ {{ t("pluginChart.chartCount", charts.length, { named: { count: charts.length } }) }}
6
9
  </span>
7
- <span class="text-xs text-gray-500 shrink-0"> {{ charts.length }} chart{{ charts.length === 1 ? "" : "s" }} </span>
8
10
  </div>
9
11
  <div class="flex-1 overflow-y-auto p-4 space-y-4">
10
12
  <div v-for="(chart, idx) in charts" :key="idx" class="border border-gray-200 rounded-lg bg-white" :data-testid="`chart-card-${idx}`">
11
13
  <div class="px-3 py-2 border-b border-gray-100 flex items-center justify-between gap-2">
12
14
  <div class="flex items-center gap-2 min-w-0">
13
15
  <span class="text-sm font-medium text-gray-800 truncate">
14
- {{ chart.title ?? `Chart ${idx + 1}` }}
16
+ {{ chart.title ?? t("pluginChart.chartTitle", { num: idx + 1 }) }}
15
17
  </span>
16
18
  <span v-if="chart.type" class="text-[10px] uppercase tracking-wide rounded px-1.5 py-0.5 bg-blue-50 text-blue-700 shrink-0">
17
19
  {{ chart.type }}
@@ -23,7 +25,7 @@
23
25
  @click="exportPng(idx, chart.title)"
24
26
  >
25
27
  <span class="material-icons text-sm align-middle">download</span>
26
- PNG
28
+ {{ t("pluginChart.png") }}
27
29
  </button>
28
30
  </div>
29
31
  <div :ref="(el) => setChartRef(idx, el as HTMLDivElement | null)" class="w-full h-[400px]" :data-testid="`chart-canvas-${idx}`" />
@@ -34,11 +36,14 @@
34
36
 
35
37
  <script setup lang="ts">
36
38
  import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
39
+ import { useI18n } from "vue-i18n";
37
40
  import * as echarts from "echarts";
38
41
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
39
42
  import type { ChartEntry, PresentChartData } from "./index";
40
43
  import { isRecord } from "../../utils/types";
41
44
 
45
+ const { t } = useI18n();
46
+
42
47
  const props = defineProps<{
43
48
  selectedResult: ToolResultComplete<PresentChartData>;
44
49
  }>();
@@ -2,7 +2,7 @@
2
2
  <div class="text-sm">
3
3
  <div class="flex items-center gap-1 font-medium text-gray-700 mb-1">
4
4
  <span class="material-icons" style="font-size: 14px">manage_accounts</span>
5
- <span>{{ customRoles.length }} custom role{{ customRoles.length !== 1 ? "s" : "" }}</span>
5
+ <span>{{ t("pluginManageRoles.previewCount", customRoles.length, { named: { count: customRoles.length } }) }}</span>
6
6
  </div>
7
7
  <div v-for="role in customRoles" :key="role.id" class="text-xs text-gray-600 flex items-center gap-1">
8
8
  <span class="material-icons" style="font-size: 12px">{{ role.icon }}</span>
@@ -13,7 +13,10 @@
13
13
 
14
14
  <script setup lang="ts">
15
15
  import { ref, watch } from "vue";
16
+ import { useI18n } from "vue-i18n";
16
17
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
18
+
19
+ const { t } = useI18n();
17
20
  import type { ManageRolesData, CustomRole } from "./index";
18
21
  import { useFreshPluginData } from "../../composables/useFreshPluginData";
19
22
  import { API_ROUTES } from "../../config/apiRoutes";