mulmoclaude 0.1.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.
- package/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="text-sm">
|
|
3
|
+
<div class="flex items-center gap-1 font-medium text-gray-700 mb-1">
|
|
4
|
+
<span>☑</span>
|
|
5
|
+
<span>{{ completedCount }}/{{ items.length }} completed</span>
|
|
6
|
+
</div>
|
|
7
|
+
<div
|
|
8
|
+
v-for="item in preview"
|
|
9
|
+
:key="item.id"
|
|
10
|
+
class="text-xs truncate flex items-center gap-1"
|
|
11
|
+
:class="item.completed ? 'line-through text-gray-400' : 'text-gray-600'"
|
|
12
|
+
>
|
|
13
|
+
<span class="shrink-0">{{ item.completed ? "✓" : "○" }}</span>
|
|
14
|
+
<span class="truncate">{{ item.text }}</span>
|
|
15
|
+
<template v-if="(item.labels?.length ?? 0) > 0">
|
|
16
|
+
<span
|
|
17
|
+
v-for="label in (item.labels ?? []).slice(0, 2)"
|
|
18
|
+
:key="label"
|
|
19
|
+
class="px-1 rounded-full text-[9px] font-medium shrink-0"
|
|
20
|
+
:class="colorForLabel(label)"
|
|
21
|
+
>{{ label }}</span
|
|
22
|
+
>
|
|
23
|
+
<span v-if="(item.labels?.length ?? 0) > 2" class="text-[9px] text-gray-400 shrink-0">+{{ (item.labels?.length ?? 0) - 2 }}</span>
|
|
24
|
+
</template>
|
|
25
|
+
</div>
|
|
26
|
+
<div v-if="more > 0" class="text-xs text-gray-400">+ {{ more }} more…</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { computed, ref, watch } from "vue";
|
|
32
|
+
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
33
|
+
import type { TodoData, TodoItem } from "./index";
|
|
34
|
+
import { useFreshPluginData } from "../../composables/useFreshPluginData";
|
|
35
|
+
import { API_ROUTES } from "../../config/apiRoutes";
|
|
36
|
+
import { colorForLabel } from "./labels";
|
|
37
|
+
|
|
38
|
+
const props = defineProps<{ result: ToolResultComplete<TodoData> }>();
|
|
39
|
+
|
|
40
|
+
const items = ref<TodoItem[]>(props.result.data?.items ?? []);
|
|
41
|
+
|
|
42
|
+
const { refresh } = useFreshPluginData<TodoItem[]>({
|
|
43
|
+
endpoint: () => API_ROUTES.todos.list,
|
|
44
|
+
extract: (json) => {
|
|
45
|
+
const v = (json as { data?: { items?: TodoItem[] } }).data?.items;
|
|
46
|
+
return Array.isArray(v) ? v : null;
|
|
47
|
+
},
|
|
48
|
+
apply: (data) => {
|
|
49
|
+
items.value = data;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
watch(
|
|
54
|
+
() => props.result.uuid,
|
|
55
|
+
() => {
|
|
56
|
+
items.value = props.result.data?.items ?? [];
|
|
57
|
+
void refresh();
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
const completedCount = computed(() => items.value.filter((i) => i.completed).length);
|
|
61
|
+
const preview = computed(() => items.value.slice(0, 3));
|
|
62
|
+
const more = computed(() => Math.max(0, items.value.length - 3));
|
|
63
|
+
</script>
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full bg-white flex flex-col">
|
|
3
|
+
<!-- API error banner — surfaces POST /api/todos failures so a
|
|
4
|
+
silent add/remove/toggle becomes diagnosable. -->
|
|
5
|
+
<div v-if="todoApiError" class="px-4 py-2 bg-red-50 border-b border-red-200 text-sm text-red-700" role="alert" data-testid="todo-api-error">
|
|
6
|
+
⚠ Failed to update todos: {{ todoApiError }}
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
|
|
9
|
+
<h2 class="text-lg font-semibold text-gray-800">Todo List</h2>
|
|
10
|
+
<span class="text-sm text-gray-500">{{ completedCount }}/{{ items.length }} completed</span>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Filter bar: only shown when at least one label is in use. -->
|
|
14
|
+
<div v-if="labelInventory.length > 0" class="flex flex-wrap items-center gap-1.5 px-6 py-2 border-b border-gray-100 bg-gray-50">
|
|
15
|
+
<span class="text-xs text-gray-500 mr-1">Filter:</span>
|
|
16
|
+
<button
|
|
17
|
+
v-for="entry in labelInventory"
|
|
18
|
+
:key="entry.label"
|
|
19
|
+
class="px-2 py-0.5 rounded-full text-xs font-medium transition-all"
|
|
20
|
+
:class="
|
|
21
|
+
activeFilters.has(entry.label.toLowerCase())
|
|
22
|
+
? 'ring-2 ring-blue-400 ' + colorForLabel(entry.label)
|
|
23
|
+
: colorForLabel(entry.label) + ' opacity-70 hover:opacity-100'
|
|
24
|
+
"
|
|
25
|
+
@click="toggleFilter(entry.label)"
|
|
26
|
+
>
|
|
27
|
+
{{ entry.label }}
|
|
28
|
+
<span class="opacity-60">{{ entry.count }}</span>
|
|
29
|
+
</button>
|
|
30
|
+
<button v-if="activeFilters.size > 0" class="ml-auto text-xs text-gray-500 hover:text-gray-700" title="Clear all filters" @click="clearFilters">
|
|
31
|
+
Clear ✕
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div v-if="items.length === 0" class="flex-1 flex items-center justify-center text-gray-400">No todo items yet</div>
|
|
36
|
+
|
|
37
|
+
<div v-else-if="filteredItems.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm">No items match the active filter</div>
|
|
38
|
+
|
|
39
|
+
<ul v-else class="flex-1 overflow-y-auto p-4 space-y-2">
|
|
40
|
+
<li v-for="item in filteredItems" :key="item.id" class="rounded-lg border" :class="selectedId === item.id ? 'border-blue-400' : 'border-gray-200'">
|
|
41
|
+
<!-- Item row -->
|
|
42
|
+
<div
|
|
43
|
+
class="flex items-center gap-3 p-3 cursor-pointer group hover:bg-gray-50 rounded-lg"
|
|
44
|
+
:class="selectedId === item.id ? 'rounded-b-none' : ''"
|
|
45
|
+
@click="selectItem(item)"
|
|
46
|
+
>
|
|
47
|
+
<input type="checkbox" :checked="item.completed" class="cursor-pointer shrink-0" @click.stop @change="toggle(item)" />
|
|
48
|
+
<div class="flex-1 min-w-0">
|
|
49
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
50
|
+
<span class="text-sm" :class="item.completed ? 'line-through text-gray-400' : 'text-gray-800'">{{ item.text }}</span>
|
|
51
|
+
<span
|
|
52
|
+
v-for="label in item.labels ?? []"
|
|
53
|
+
:key="label"
|
|
54
|
+
class="px-1.5 py-0.5 rounded-full text-[10px] font-medium shrink-0"
|
|
55
|
+
:class="colorForLabel(label)"
|
|
56
|
+
>{{ label }}</span
|
|
57
|
+
>
|
|
58
|
+
</div>
|
|
59
|
+
<div v-if="item.note" class="text-xs text-gray-400 mt-0.5">
|
|
60
|
+
{{ item.note }}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<button
|
|
64
|
+
class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0"
|
|
65
|
+
title="Delete item"
|
|
66
|
+
@click.stop="remove(item)"
|
|
67
|
+
>
|
|
68
|
+
✕
|
|
69
|
+
</button>
|
|
70
|
+
<span class="material-icons text-gray-400 text-sm" :title="selectedId === item.id ? 'Collapse' : 'Expand'">
|
|
71
|
+
{{ selectedId === item.id ? "expand_less" : "expand_more" }}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Inline editor -->
|
|
76
|
+
<div v-if="selectedId === item.id" class="border-t border-blue-100 bg-blue-50 p-4 space-y-3 rounded-b-lg">
|
|
77
|
+
<textarea
|
|
78
|
+
v-model="yamlText"
|
|
79
|
+
class="w-full h-24 p-3 font-mono text-xs bg-white border border-blue-300 rounded resize-y focus:outline-none focus:border-blue-500"
|
|
80
|
+
spellcheck="false"
|
|
81
|
+
/>
|
|
82
|
+
<div class="flex items-center gap-2">
|
|
83
|
+
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">Update</button>
|
|
84
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="selectedId = null">Cancel</button>
|
|
85
|
+
<span v-if="yamlError" class="text-xs text-red-500">{{ yamlError }}</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</li>
|
|
89
|
+
</ul>
|
|
90
|
+
|
|
91
|
+
<button v-if="hasCompleted" class="mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start" @click="clearCompleted">Clear completed</button>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script setup lang="ts">
|
|
96
|
+
import { computed, ref, watch } from "vue";
|
|
97
|
+
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
98
|
+
import type { TodoData, TodoItem } from "./index";
|
|
99
|
+
import { useFreshPluginData } from "../../composables/useFreshPluginData";
|
|
100
|
+
import { apiPost } from "../../utils/api";
|
|
101
|
+
import { API_ROUTES } from "../../config/apiRoutes";
|
|
102
|
+
import { colorForLabel, filterByLabels, listLabelsWithCount, subtractLabels } from "./labels";
|
|
103
|
+
|
|
104
|
+
const props = defineProps<{
|
|
105
|
+
selectedResult: ToolResultComplete<TodoData>;
|
|
106
|
+
}>();
|
|
107
|
+
const emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();
|
|
108
|
+
|
|
109
|
+
const items = ref<TodoItem[]>(props.selectedResult.data?.items ?? []);
|
|
110
|
+
|
|
111
|
+
const { refresh } = useFreshPluginData<TodoItem[]>({
|
|
112
|
+
endpoint: () => API_ROUTES.todos.list,
|
|
113
|
+
extract: (json) => {
|
|
114
|
+
const v = (json as { data?: { items?: TodoItem[] } }).data?.items;
|
|
115
|
+
return Array.isArray(v) ? v : null;
|
|
116
|
+
},
|
|
117
|
+
apply: (data) => {
|
|
118
|
+
items.value = data;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Re-fetch when the caller swaps in a different tool result.
|
|
123
|
+
// Watch the uuid (not data?.items) so the trigger fires even when
|
|
124
|
+
// the old and new results both have `undefined` items — e.g.
|
|
125
|
+
// toggling between two empty results — per CodeRabbit V2's
|
|
126
|
+
// "watch key miss" finding.
|
|
127
|
+
watch(
|
|
128
|
+
() => props.selectedResult.uuid,
|
|
129
|
+
() => {
|
|
130
|
+
items.value = props.selectedResult.data?.items ?? [];
|
|
131
|
+
void refresh();
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
const completedCount = computed(() => items.value.filter((i) => i.completed).length);
|
|
135
|
+
const hasCompleted = computed(() => items.value.some((i) => i.completed));
|
|
136
|
+
|
|
137
|
+
// ── Label filter state ──────────────────────────────────────────────────────
|
|
138
|
+
// Filters are local to this View instance — intentional, so that
|
|
139
|
+
// switching sessions or reopening a tool result doesn't drag state
|
|
140
|
+
// across contexts. Active filters are stored lowercased to match
|
|
141
|
+
// `filterByLabels`' case-insensitive semantics.
|
|
142
|
+
|
|
143
|
+
const activeFilters = ref<Set<string>>(new Set());
|
|
144
|
+
|
|
145
|
+
const labelInventory = computed(() => listLabelsWithCount(items.value));
|
|
146
|
+
|
|
147
|
+
const filteredItems = computed(() => filterByLabels(items.value, [...activeFilters.value]));
|
|
148
|
+
|
|
149
|
+
function toggleFilter(label: string): void {
|
|
150
|
+
const key = label.toLowerCase();
|
|
151
|
+
const next = new Set(activeFilters.value);
|
|
152
|
+
if (next.has(key)) {
|
|
153
|
+
next.delete(key);
|
|
154
|
+
} else {
|
|
155
|
+
next.add(key);
|
|
156
|
+
}
|
|
157
|
+
activeFilters.value = next;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function clearFilters(): void {
|
|
161
|
+
activeFilters.value = new Set();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── YAML helpers ─────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function yamlStringValue(v: string): string {
|
|
167
|
+
const needsQuotes = v === "" || /[:#[\]{},&*?|<>=!%@`]/.test(v) || /^\s|\s$/.test(v) || /^(true|false|null|~)$/i.test(v) || /^\d/.test(v);
|
|
168
|
+
if (needsQuotes) {
|
|
169
|
+
return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
170
|
+
}
|
|
171
|
+
return v;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function serializeYaml(item: TodoItem): string {
|
|
175
|
+
const labels = item.labels ?? [];
|
|
176
|
+
const labelsLine = labels.length > 0 ? `labels: [${labels.map(yamlStringValue).join(", ")}]` : "labels: []";
|
|
177
|
+
return [`text: ${yamlStringValue(item.text)}`, `note: ${item.note ? yamlStringValue(item.note) : ""}`, labelsLine].join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Parse a YAML flow sequence `[a, "b", c]` into an array of strings.
|
|
181
|
+
// Handles quoted and unquoted entries. Whitespace-only input → empty.
|
|
182
|
+
function parseFlowSequence(raw: string): string[] {
|
|
183
|
+
const trimmed = raw.trim();
|
|
184
|
+
if (!trimmed || trimmed === "[]") return [];
|
|
185
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
|
|
186
|
+
const inner = trimmed.slice(1, -1);
|
|
187
|
+
// Split on commas that are NOT inside double quotes. Cheap scan;
|
|
188
|
+
// fine for our label use case where items don't contain commas
|
|
189
|
+
// (stored labels are normalised strings without commas).
|
|
190
|
+
const result: string[] = [];
|
|
191
|
+
let buffer = "";
|
|
192
|
+
let inQuotes = false;
|
|
193
|
+
for (let i = 0; i < inner.length; i++) {
|
|
194
|
+
const ch = inner[i];
|
|
195
|
+
if (ch === '"' && inner[i - 1] !== "\\") {
|
|
196
|
+
inQuotes = !inQuotes;
|
|
197
|
+
buffer += ch;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (ch === "," && !inQuotes) {
|
|
201
|
+
const piece = parseYamlValue(buffer.trim());
|
|
202
|
+
if (piece) result.push(piece);
|
|
203
|
+
buffer = "";
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
buffer += ch;
|
|
207
|
+
}
|
|
208
|
+
const last = parseYamlValue(buffer.trim());
|
|
209
|
+
if (last) result.push(last);
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseYamlValue(raw: string): string {
|
|
214
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
215
|
+
return raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
216
|
+
}
|
|
217
|
+
if (raw.startsWith("'") && raw.endsWith("'")) {
|
|
218
|
+
return raw.slice(1, -1);
|
|
219
|
+
}
|
|
220
|
+
return raw;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseYaml(text: string): { text: string; note: string; labels: string[] } | null {
|
|
224
|
+
const result: Record<string, string> = {};
|
|
225
|
+
for (const line of text.split("\n")) {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
228
|
+
const colonIdx = line.indexOf(": ");
|
|
229
|
+
if (colonIdx === -1) {
|
|
230
|
+
// "key:" with empty value
|
|
231
|
+
const colonEnd = line.indexOf(":");
|
|
232
|
+
if (colonEnd !== -1) result[line.slice(0, colonEnd).trim()] = "";
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// `labels:` is a flow sequence (`[a, b]`) — parse it as a list
|
|
236
|
+
// instead of running it through `parseYamlValue` which strips
|
|
237
|
+
// brackets as if they were quotes.
|
|
238
|
+
const key = line.slice(0, colonIdx).trim();
|
|
239
|
+
const raw = line.slice(colonIdx + 2).trim();
|
|
240
|
+
if (key === "labels") {
|
|
241
|
+
result[key] = raw;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
result[key] = parseYamlValue(raw);
|
|
245
|
+
}
|
|
246
|
+
if (typeof result["text"] !== "string" || !result["text"]) return null;
|
|
247
|
+
const labels = parseFlowSequence(result["labels"] ?? "[]");
|
|
248
|
+
return {
|
|
249
|
+
text: result["text"],
|
|
250
|
+
note: result["note"] ?? "",
|
|
251
|
+
labels,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Item selection & YAML edit ────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
const selectedId = ref<string | null>(null);
|
|
258
|
+
const selectedOriginalText = ref<string>("");
|
|
259
|
+
const selectedOriginalLabels = ref<string[]>([]);
|
|
260
|
+
const yamlText = ref("");
|
|
261
|
+
const yamlError = ref("");
|
|
262
|
+
|
|
263
|
+
function selectItem(item: TodoItem) {
|
|
264
|
+
if (selectedId.value === item.id) {
|
|
265
|
+
selectedId.value = null;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
selectedId.value = item.id;
|
|
269
|
+
selectedOriginalText.value = item.text;
|
|
270
|
+
selectedOriginalLabels.value = [...(item.labels ?? [])];
|
|
271
|
+
yamlText.value = serializeYaml(item);
|
|
272
|
+
yamlError.value = "";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
watch(items, () => {
|
|
276
|
+
if (selectedId.value) {
|
|
277
|
+
const item = items.value.find((i) => i.id === selectedId.value);
|
|
278
|
+
if (item) {
|
|
279
|
+
yamlText.value = serializeYaml(item);
|
|
280
|
+
selectedOriginalText.value = item.text;
|
|
281
|
+
selectedOriginalLabels.value = [...(item.labels ?? [])];
|
|
282
|
+
} else {
|
|
283
|
+
selectedId.value = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
async function applyItemEdit() {
|
|
289
|
+
yamlError.value = "";
|
|
290
|
+
const parsed = parseYaml(yamlText.value);
|
|
291
|
+
if (!parsed) {
|
|
292
|
+
yamlError.value = "Could not parse YAML — 'text' field is required";
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// 1. text / note go through `update` (label-agnostic on the server).
|
|
296
|
+
const ok = await callApi({
|
|
297
|
+
action: "update",
|
|
298
|
+
text: selectedOriginalText.value,
|
|
299
|
+
newText: parsed.text,
|
|
300
|
+
note: parsed.note,
|
|
301
|
+
});
|
|
302
|
+
if (!ok) return;
|
|
303
|
+
// 2. labels are diffed against the prior state and applied as
|
|
304
|
+
// `add_label` / `remove_label` calls. `update` deliberately
|
|
305
|
+
// does not touch labels — this keeps each LLM action
|
|
306
|
+
// single-purpose and matches the add_label/remove_label
|
|
307
|
+
// semantics the LLM uses.
|
|
308
|
+
const originalLabels = selectedOriginalLabels.value;
|
|
309
|
+
const removed = subtractLabels(originalLabels, parsed.labels);
|
|
310
|
+
const added = subtractLabels(parsed.labels, originalLabels);
|
|
311
|
+
// Use the already-renamed `parsed.text` to match the updated item.
|
|
312
|
+
if (removed.length > 0) {
|
|
313
|
+
await callApi({
|
|
314
|
+
action: "remove_label",
|
|
315
|
+
text: parsed.text,
|
|
316
|
+
labels: removed,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (added.length > 0) {
|
|
320
|
+
await callApi({
|
|
321
|
+
action: "add_label",
|
|
322
|
+
text: parsed.text,
|
|
323
|
+
labels: added,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
selectedId.value = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── API ───────────────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
// Last POST /api/todos failure. Cleared on the next successful call so
|
|
332
|
+
// the banner disappears as soon as things recover.
|
|
333
|
+
const todoApiError = ref<string | null>(null);
|
|
334
|
+
|
|
335
|
+
async function callApi(body: Record<string, unknown>): Promise<boolean> {
|
|
336
|
+
const response = await apiPost<{ data?: { items?: TodoItem[] } }>(API_ROUTES.todos.dispatch, body);
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
todoApiError.value = response.error;
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
todoApiError.value = null;
|
|
342
|
+
const result = response.data;
|
|
343
|
+
items.value = result.data?.items ?? [];
|
|
344
|
+
emit("updateResult", {
|
|
345
|
+
...props.selectedResult,
|
|
346
|
+
...result,
|
|
347
|
+
uuid: props.selectedResult.uuid,
|
|
348
|
+
});
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function toggle(item: TodoItem) {
|
|
353
|
+
callApi({ action: item.completed ? "uncheck" : "check", text: item.text });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function remove(item: TodoItem) {
|
|
357
|
+
if (selectedId.value === item.id) selectedId.value = null;
|
|
358
|
+
callApi({ action: "delete", text: item.text });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function clearCompleted() {
|
|
362
|
+
callApi({ action: "clear_completed" });
|
|
363
|
+
}
|
|
364
|
+
</script>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Shared state + REST helpers for the file-explorer Todo views
|
|
2
|
+
// (TodoExplorer.vue, TodoKanbanView.vue, TodoTableView.vue,
|
|
3
|
+
// TodoListView.vue). Centralising the data layer here keeps each view
|
|
4
|
+
// focused on rendering, and means there's only one place to add
|
|
5
|
+
// optimistic-update / error-recovery logic.
|
|
6
|
+
//
|
|
7
|
+
// All mutating helpers POST/PATCH against the Web-UI REST routes added
|
|
8
|
+
// in Phase 1 (server/routes/todos.ts) — the MCP `manageTodoList`
|
|
9
|
+
// action route is intentionally NOT used by the explorer.
|
|
10
|
+
|
|
11
|
+
import { ref, type Ref } from "vue";
|
|
12
|
+
import { API_ROUTES } from "../../../config/apiRoutes";
|
|
13
|
+
import { useFreshPluginData } from "../../../composables/useFreshPluginData";
|
|
14
|
+
import { errorMessage } from "../../../utils/errors";
|
|
15
|
+
import { apiCall } from "../../../utils/api";
|
|
16
|
+
import type { StatusColumn, TodoItem } from "../index";
|
|
17
|
+
|
|
18
|
+
interface TodosResponse {
|
|
19
|
+
data?: { items?: TodoItem[]; columns?: StatusColumn[] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isTodoItemArray(value: unknown): value is TodoItem[] {
|
|
23
|
+
return Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isStatusColumnArray(value: unknown): value is StatusColumn[] {
|
|
27
|
+
return Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractItems(json: unknown): TodoItem[] | null {
|
|
31
|
+
const items = (json as TodosResponse).data?.items;
|
|
32
|
+
return isTodoItemArray(items) ? items : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractColumns(json: unknown): StatusColumn[] | null {
|
|
36
|
+
const cols = (json as TodosResponse).data?.columns;
|
|
37
|
+
return isStatusColumnArray(cols) ? cols : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Apply parsed JSON payload (always { data: { items, columns } } for
|
|
41
|
+
// the new REST routes) into the local refs. Centralised so every
|
|
42
|
+
// helper uses the same parser and error guard. Returns false on a
|
|
43
|
+
// payload missing the items array.
|
|
44
|
+
function applyPayload(json: unknown, items: Ref<TodoItem[]>, columns: Ref<StatusColumn[]>): boolean {
|
|
45
|
+
const nextItems = extractItems(json);
|
|
46
|
+
const nextColumns = extractColumns(json);
|
|
47
|
+
if (nextItems) items.value = nextItems;
|
|
48
|
+
if (nextColumns) columns.value = nextColumns;
|
|
49
|
+
return nextItems !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface UseTodosHandle {
|
|
53
|
+
items: Ref<TodoItem[]>;
|
|
54
|
+
columns: Ref<StatusColumn[]>;
|
|
55
|
+
error: Ref<string | null>;
|
|
56
|
+
refresh: () => Promise<boolean>;
|
|
57
|
+
// ── item ops ──
|
|
58
|
+
createItem: (input: CreateItemInput) => Promise<boolean>;
|
|
59
|
+
patchItem: (id: string, input: PatchItemInput) => Promise<boolean>;
|
|
60
|
+
moveItem: (id: string, input: MoveItemInput) => Promise<boolean>;
|
|
61
|
+
deleteItem: (id: string) => Promise<boolean>;
|
|
62
|
+
// ── column ops ──
|
|
63
|
+
addColumn: (input: AddColumnInput) => Promise<boolean>;
|
|
64
|
+
patchColumn: (id: string, input: PatchColumnInput) => Promise<boolean>;
|
|
65
|
+
deleteColumn: (id: string) => Promise<boolean>;
|
|
66
|
+
reorderColumns: (ids: string[]) => Promise<boolean>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CreateItemInput {
|
|
70
|
+
text: string;
|
|
71
|
+
note?: string;
|
|
72
|
+
status?: string;
|
|
73
|
+
priority?: string;
|
|
74
|
+
dueDate?: string;
|
|
75
|
+
labels?: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PatchItemInput {
|
|
79
|
+
text?: string;
|
|
80
|
+
note?: string | null;
|
|
81
|
+
status?: string;
|
|
82
|
+
priority?: string | null;
|
|
83
|
+
dueDate?: string | null;
|
|
84
|
+
labels?: string[];
|
|
85
|
+
completed?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface MoveItemInput {
|
|
89
|
+
status?: string;
|
|
90
|
+
position?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface AddColumnInput {
|
|
94
|
+
label: string;
|
|
95
|
+
isDone?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface PatchColumnInput {
|
|
99
|
+
label?: string;
|
|
100
|
+
isDone?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Initial values come from the caller (e.g. the parent View receives
|
|
104
|
+
// items via its `selectedResult` prop). The composable then refreshes
|
|
105
|
+
// from the server on mount and updates the refs in place.
|
|
106
|
+
export function useTodos(initialItems: TodoItem[] = [], initialColumns: StatusColumn[] = []): UseTodosHandle {
|
|
107
|
+
const items = ref<TodoItem[]>(initialItems);
|
|
108
|
+
const columns = ref<StatusColumn[]>(initialColumns);
|
|
109
|
+
const error = ref<string | null>(null);
|
|
110
|
+
|
|
111
|
+
const { refresh: rawRefresh } = useFreshPluginData<{
|
|
112
|
+
items: TodoItem[];
|
|
113
|
+
columns: StatusColumn[];
|
|
114
|
+
}>({
|
|
115
|
+
endpoint: () => API_ROUTES.todos.list,
|
|
116
|
+
extract: (json) => {
|
|
117
|
+
const extractedItems = extractItems(json);
|
|
118
|
+
const extractedColumns = extractColumns(json);
|
|
119
|
+
if (!extractedItems) return null;
|
|
120
|
+
return { items: extractedItems, columns: extractedColumns ?? [] };
|
|
121
|
+
},
|
|
122
|
+
apply: ({ items: nextItems, columns: nextColumns }) => {
|
|
123
|
+
items.value = nextItems;
|
|
124
|
+
if (nextColumns.length > 0) columns.value = nextColumns;
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// useFreshPluginData swallows fetch errors silently — its refresh
|
|
129
|
+
// returns false on failure but never updates anything callers can
|
|
130
|
+
// observe. Wrap it so the initial GET / manual reloads surface
|
|
131
|
+
// through the same `error` ref the rest of the composable uses.
|
|
132
|
+
async function refresh(): Promise<boolean> {
|
|
133
|
+
error.value = null;
|
|
134
|
+
const success = await rawRefresh();
|
|
135
|
+
if (!success) error.value = "Failed to load todos";
|
|
136
|
+
return success;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Thin wrapper around apiCall that applies the response payload
|
|
140
|
+
// into the local refs and surfaces errors through `error.value`.
|
|
141
|
+
// Using apiCall (not raw fetch) ensures the #272 bearer token is
|
|
142
|
+
// attached to every request.
|
|
143
|
+
async function call(url: string, method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", body?: unknown): Promise<boolean> {
|
|
144
|
+
error.value = null;
|
|
145
|
+
try {
|
|
146
|
+
const result = await apiCall<unknown>(url, { method, body });
|
|
147
|
+
if (!result.ok) {
|
|
148
|
+
error.value = result.error;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const applied = applyPayload(result.data, items, columns);
|
|
152
|
+
if (!applied) {
|
|
153
|
+
error.value = `Request failed: unexpected payload shape`;
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
error.value = errorMessage(err);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
items,
|
|
165
|
+
columns,
|
|
166
|
+
error,
|
|
167
|
+
refresh,
|
|
168
|
+
createItem: (input) => call(API_ROUTES.todos.items, "POST", input),
|
|
169
|
+
patchItem: (id, input) => call(API_ROUTES.todos.item.replace(":id", encodeURIComponent(id)), "PATCH", input),
|
|
170
|
+
moveItem: (id, input) => call(API_ROUTES.todos.itemMove.replace(":id", encodeURIComponent(id)), "POST", input),
|
|
171
|
+
deleteItem: (id) => call(API_ROUTES.todos.item.replace(":id", encodeURIComponent(id)), "DELETE"),
|
|
172
|
+
addColumn: (input) => call(API_ROUTES.todos.columns, "POST", input),
|
|
173
|
+
patchColumn: (id, input) => call(API_ROUTES.todos.column.replace(":id", encodeURIComponent(id)), "PATCH", input),
|
|
174
|
+
deleteColumn: (id) => call(API_ROUTES.todos.column.replace(":id", encodeURIComponent(id)), "DELETE"),
|
|
175
|
+
reorderColumns: (ids) => call(API_ROUTES.todos.columnsOrder, "PUT", { ids }),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ToolDefinition } from "gui-chat-protocol";
|
|
2
|
+
|
|
3
|
+
const toolDefinition: ToolDefinition = {
|
|
4
|
+
type: "function",
|
|
5
|
+
name: "manageTodoList",
|
|
6
|
+
prompt: "When users mention tasks, things to do, or ask about their todo list, use manageTodoList to help them track items.",
|
|
7
|
+
description:
|
|
8
|
+
"Manage a todo list — show items, add, update, check/uncheck, or delete them. Items can optionally carry labels (tags) for categorisation; use labels to group related todos (e.g. 'Work', 'Groceries', 'Urgent') and filter the list at read time.",
|
|
9
|
+
parameters: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
action: {
|
|
13
|
+
type: "string",
|
|
14
|
+
enum: ["show", "add", "delete", "update", "check", "uncheck", "clear_completed", "add_label", "remove_label", "list_labels"],
|
|
15
|
+
description: "Action to perform on the todo list.",
|
|
16
|
+
},
|
|
17
|
+
text: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "For 'add': the todo item text. For 'delete', 'update', 'check', 'uncheck', 'add_label', 'remove_label': partial text to find the item.",
|
|
20
|
+
},
|
|
21
|
+
newText: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "For 'update' only: the replacement text.",
|
|
24
|
+
},
|
|
25
|
+
note: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "For 'add' or 'update': an optional note or extra detail for the item.",
|
|
28
|
+
},
|
|
29
|
+
labels: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string" },
|
|
32
|
+
description:
|
|
33
|
+
"For 'add': labels to tag the new item with. For 'add_label' / 'remove_label': labels to add to / remove from the item matched by 'text'. Labels are case-insensitive for matching but stored with their original case.",
|
|
34
|
+
},
|
|
35
|
+
filterLabels: {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: { type: "string" },
|
|
38
|
+
description: "For 'show' only: return only items that have at least one of these labels (OR semantics, case-insensitive). Omit to show all items.",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ["action"],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default toolDefinition;
|