mulmoclaude 0.3.0 → 0.5.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/bin/mulmoclaude.js +7 -24
- package/client/assets/html2canvas-Cx501zZr-DiKaqnKs.js +5 -0
- package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
- package/client/assets/index-CY-WpQUm.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/client/index.html +2 -4
- package/package.json +17 -15
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/backend/claude-code.ts +170 -0
- package/server/agent/backend/index.ts +14 -0
- package/server/agent/backend/types.ts +65 -0
- package/server/agent/index.ts +31 -159
- package/server/agent/mcp-server.ts +88 -10
- package/server/agent/mcp-tools/index.ts +8 -7
- package/server/agent/mcp-tools/notify.ts +76 -0
- package/server/agent/mcp-tools/x.ts +12 -2
- package/server/agent/plugin-names.ts +10 -4
- package/server/agent/prompt.ts +187 -26
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +3 -3
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +99 -4
- package/server/api/routes/chart.ts +13 -0
- package/server/api/routes/chat-index.ts +2 -1
- package/server/api/routes/config.ts +35 -8
- package/server/api/routes/files.ts +75 -24
- package/server/api/routes/html.ts +15 -2
- package/server/api/routes/image.ts +75 -20
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/news.ts +146 -0
- package/server/api/routes/notifications.ts +58 -2
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +73 -91
- package/server/api/routes/presentHtml.ts +9 -0
- package/server/api/routes/roles.ts +12 -2
- package/server/api/routes/scheduler.ts +20 -11
- package/server/api/routes/schedulerTasks.ts +58 -21
- package/server/api/routes/sessions.ts +15 -4
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +26 -5
- package/server/api/routes/sources.ts +8 -7
- package/server/api/routes/todos.ts +30 -0
- package/server/api/routes/todosColumnsHandlers.ts +13 -27
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki/frontmatter.ts +86 -0
- package/server/api/routes/wiki.ts +335 -75
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +32 -8
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +26 -16
- package/server/events/resolveRelayBridgeOptions.ts +125 -0
- package/server/index.ts +72 -49
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +15 -5
- package/server/system/macosNotify.ts +152 -0
- package/server/utils/errors.ts +11 -2
- package/server/utils/fetch.ts +54 -0
- package/server/utils/files/atomic.ts +18 -17
- package/server/utils/files/image-store.ts +19 -13
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-image-fill.ts +131 -0
- package/server/utils/files/markdown-store.ts +22 -6
- package/server/utils/files/naming.ts +20 -10
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +4 -4
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/spreadsheet-store.ts +15 -10
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +30 -4
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/id.ts +40 -8
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/logPreview.ts +24 -0
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/promptMeta.ts +32 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/slug.ts +65 -4
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/index.ts +1 -1
- package/server/workspace/chat-index/summarizer.ts +5 -5
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/helps/gemini.md +57 -0
- package/server/workspace/helps/index.md +2 -1
- package/server/workspace/helps/sources.md +42 -0
- package/server/workspace/helps/wiki.md +40 -5
- package/server/workspace/journal/archivist-cli.ts +121 -0
- package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
- package/server/workspace/journal/dailyPass.ts +78 -38
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +56 -5
- package/server/workspace/journal/memoryExtractor.ts +1 -1
- package/server/workspace/journal/optimizationPass.ts +4 -5
- package/server/workspace/journal/paths.ts +8 -24
- package/server/workspace/journal/state.ts +18 -8
- package/server/workspace/news/reader.ts +248 -0
- package/server/workspace/paths.ts +4 -3
- package/server/workspace/reference-dirs.ts +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +5 -4
- package/server/workspace/skills/user-tasks.ts +3 -2
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- package/server/workspace/sources/classifier.ts +1 -1
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +3 -3
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +59 -13
- package/server/workspace/sources/pipeline/index.ts +59 -7
- package/server/workspace/sources/pipeline/notify.ts +13 -5
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- package/server/workspace/sources/pipeline/summarize.ts +1 -1
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/sourceState.ts +9 -4
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +477 -251
- package/src/components/CanvasViewToggle.vue +12 -10
- package/src/components/ChatInput.vue +112 -105
- package/src/components/FileContentHeader.vue +10 -7
- package/src/components/FileContentRenderer.vue +37 -10
- package/src/components/FileTree.vue +34 -4
- package/src/components/FileTreePane.vue +32 -27
- package/src/components/FilesView.vue +5 -3
- package/src/components/FilterChip.vue +22 -0
- package/src/components/LockStatusPopup.vue +19 -13
- package/src/components/NewsView.vue +252 -0
- package/src/components/NotificationBell.vue +35 -9
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PageChatComposer.vue +101 -0
- package/src/components/PluginLauncher.vue +36 -62
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +3 -2
- package/src/components/SessionHeaderControls.vue +63 -0
- package/src/components/SessionHistoryExpandButton.vue +30 -0
- package/src/components/SessionHistoryPanel.vue +64 -93
- package/src/components/SessionHistoryToggleButton.vue +40 -0
- package/src/components/SessionRoleIcon.vue +72 -0
- package/src/components/SessionSidebar.vue +96 -0
- package/src/components/SessionTabBar.vue +44 -51
- package/src/components/SettingsMcpTab.vue +361 -52
- package/src/components/SettingsModal.vue +203 -72
- package/src/components/SettingsReferenceDirsTab.vue +72 -51
- package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
- package/src/components/SidebarHeader.vue +50 -16
- package/src/components/SourcesManager.vue +900 -0
- package/src/components/SourcesView.vue +45 -0
- package/src/components/StackView.vue +84 -48
- package/src/components/SuggestionsPanel.vue +25 -36
- package/src/components/SystemFileBanner.vue +106 -0
- package/src/components/ThinkingIndicator.vue +41 -0
- package/src/components/TodoExplorer.vue +72 -22
- package/src/components/todo/TodoAddDialog.vue +17 -12
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +15 -10
- package/src/components/todo/TodoKanbanView.vue +16 -6
- package/src/components/todo/TodoListView.vue +14 -3
- package/src/components/todo/TodoTableView.vue +36 -5
- package/src/composables/favicon/conditions.ts +76 -0
- package/src/composables/favicon/resolveColor.ts +93 -0
- package/src/composables/favicon/types.ts +61 -0
- package/src/composables/useAppApi.ts +23 -0
- package/src/composables/useChatScroll.ts +5 -5
- package/src/composables/useCurrentRole.ts +32 -0
- package/src/composables/useDynamicFavicon.ts +174 -58
- package/src/composables/useEventListeners.ts +7 -12
- package/src/composables/useFaviconState.ts +93 -12
- package/src/composables/useFileSelection.ts +25 -6
- package/src/composables/useHealth.ts +76 -7
- package/src/composables/useLayoutMode.ts +27 -0
- package/src/composables/useNewsItems.ts +38 -0
- package/src/composables/useNewsReadState.ts +75 -0
- package/src/composables/useNotifications.ts +76 -13
- package/src/composables/usePendingCalls.ts +11 -1
- package/src/composables/useRoles.ts +6 -10
- package/src/composables/useRunElapsed.ts +80 -0
- package/src/composables/useSessionDerived.ts +21 -5
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSidePanelVisible.ts +25 -0
- package/src/composables/useViewLayout.ts +16 -37
- package/src/config/apiRoutes.ts +19 -6
- package/src/config/historyFilters.ts +30 -0
- package/src/config/mcpCatalog.ts +285 -0
- package/src/config/mcpTypes.ts +26 -0
- package/src/config/roles.ts +19 -51
- package/src/config/systemFileDescriptors.ts +170 -0
- package/src/config/toolNames.ts +6 -1
- package/src/config/workspacePaths.ts +1 -0
- package/src/index.css +14 -0
- package/src/lang/de.ts +706 -0
- package/src/lang/en.ts +726 -0
- package/src/lang/es.ts +712 -0
- package/src/lang/fr.ts +704 -0
- package/src/lang/ja.ts +707 -0
- package/src/lang/ko.ts +709 -0
- package/src/lang/pt-BR.ts +702 -0
- package/src/lang/zh.ts +705 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +3 -0
- package/src/plugins/canvas/View.vue +104 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/canvas/index.ts +3 -2
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/chart/index.ts +3 -2
- package/src/plugins/editImage/index.ts +3 -2
- package/src/plugins/generateImage/index.ts +3 -2
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +67 -46
- package/src/plugins/manageRoles/index.ts +3 -2
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +39 -34
- package/src/plugins/manageSkills/index.ts +3 -2
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +3 -687
- package/src/plugins/manageSource/index.ts +3 -2
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +164 -73
- package/src/plugins/markdown/definition.ts +6 -4
- package/src/plugins/markdown/index.ts +3 -2
- package/src/plugins/presentForm/Preview.vue +99 -0
- package/src/plugins/presentForm/View.vue +675 -0
- package/src/plugins/presentForm/definition.ts +127 -0
- package/src/plugins/presentForm/index.ts +18 -0
- package/src/plugins/presentForm/plugin.ts +94 -0
- package/src/plugins/presentForm/types.ts +109 -0
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/index.ts +3 -2
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/presentMulmoScript/index.ts +3 -2
- package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
- package/src/plugins/scheduler/AutomationsView.vue +23 -0
- package/src/plugins/scheduler/CalendarView.vue +23 -0
- package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +119 -28
- package/src/plugins/scheduler/View.vue +75 -32
- package/src/plugins/scheduler/automationsDefinition.ts +58 -0
- package/src/plugins/scheduler/calendarDefinition.ts +46 -0
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/scheduler/index.ts +68 -14
- package/src/plugins/scheduler/legacyShape.ts +34 -0
- package/src/plugins/spreadsheet/Preview.vue +9 -5
- package/src/plugins/spreadsheet/View.vue +43 -57
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
- package/src/plugins/spreadsheet/index.ts +3 -2
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +42 -45
- package/src/plugins/textResponse/utils.ts +25 -0
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- package/src/plugins/todo/composables/useTodos.ts +3 -1
- package/src/plugins/todo/index.ts +3 -2
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +5 -2
- package/src/plugins/wiki/View.vue +539 -92
- package/src/plugins/wiki/index.ts +5 -2
- package/src/plugins/wiki/route.ts +121 -0
- package/src/router/guards.ts +43 -24
- package/src/router/index.ts +53 -26
- package/src/router/pageRoutes.ts +23 -0
- package/src/tools/index.ts +12 -5
- package/src/tools/legacyPluginNames.ts +13 -0
- package/src/types/notification.ts +31 -6
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/eventDispatch.ts +3 -6
- package/src/utils/agent/formatElapsed.ts +37 -0
- package/src/utils/agent/request.ts +22 -1
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/canvas/sidePanelVisible.ts +19 -0
- package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
- package/src/utils/errors.ts +9 -2
- package/src/utils/files/filename.ts +24 -0
- package/src/utils/filesPreview/schedulerPreview.ts +9 -3
- package/src/utils/id.ts +18 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/markdown/taskList.ts +175 -0
- package/src/utils/mcp/interpolateSpec.ts +97 -0
- package/src/utils/notification/dispatch.ts +51 -15
- package/src/utils/path/workspaceLinkRouter.ts +99 -0
- package/src/utils/session/mergeSessions.ts +5 -0
- package/src/utils/sources/filter.ts +69 -0
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-Bm70FDU2.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/server/workspace/journal/linkRewrite.ts +0 -4
- package/src/components/ToolResultsPanel.vue +0 -77
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/plugins/scheduler/definition.ts +0 -57
- package/src/utils/canvas/viewMode.ts +0 -46
- package/src/utils/role/plugins.ts +0 -12
- package/src/utils/session/seedRoleDefault.ts +0 -35
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -1,697 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<div class="px-4 py-2 border-b border-gray-100 shrink-0 flex items-center justify-between gap-2">
|
|
4
|
-
<span class="text-sm font-medium text-gray-700 truncate"> Information sources </span>
|
|
5
|
-
<div class="flex items-center gap-2 shrink-0">
|
|
6
|
-
<span class="text-xs text-gray-500"> {{ sources.length }} source{{ sources.length === 1 ? "" : "s" }} </span>
|
|
7
|
-
<button
|
|
8
|
-
class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:opacity-50"
|
|
9
|
-
:disabled="adding || busy === 'rebuild'"
|
|
10
|
-
data-testid="sources-add-btn"
|
|
11
|
-
@click="startAdd"
|
|
12
|
-
>
|
|
13
|
-
<span class="material-icons text-sm align-middle">add</span>
|
|
14
|
-
Add
|
|
15
|
-
</button>
|
|
16
|
-
<button
|
|
17
|
-
class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:opacity-50"
|
|
18
|
-
:disabled="busy === 'rebuild'"
|
|
19
|
-
data-testid="sources-rebuild-btn"
|
|
20
|
-
@click="rebuild"
|
|
21
|
-
>
|
|
22
|
-
<span class="material-icons text-sm align-middle">refresh</span>
|
|
23
|
-
{{ busy === "rebuild" ? "Rebuilding…" : "Rebuild now" }}
|
|
24
|
-
</button>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div v-if="adding" class="px-4 py-3 border-b border-blue-200 bg-blue-50/50 shrink-0 space-y-2" data-testid="sources-add-form">
|
|
29
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
30
|
-
<label class="text-xs text-gray-700">
|
|
31
|
-
Type
|
|
32
|
-
<select v-model="draft.kind" class="ml-1 text-xs border border-gray-300 rounded px-1 py-0.5" data-testid="sources-draft-kind" @change="onKindChange">
|
|
33
|
-
<option value="rss">RSS</option>
|
|
34
|
-
<option value="github-releases">GitHub releases</option>
|
|
35
|
-
<option value="github-issues">GitHub issues</option>
|
|
36
|
-
<option value="arxiv">arXiv</option>
|
|
37
|
-
</select>
|
|
38
|
-
</label>
|
|
39
|
-
<input
|
|
40
|
-
v-model="draft.primary"
|
|
41
|
-
class="flex-1 min-w-[12rem] text-xs border border-gray-300 rounded px-2 py-1 font-mono"
|
|
42
|
-
:placeholder="primaryPlaceholder"
|
|
43
|
-
data-testid="sources-draft-primary"
|
|
44
|
-
@keydown.enter="commitAdd"
|
|
45
|
-
/>
|
|
46
|
-
<input
|
|
47
|
-
v-model="draft.title"
|
|
48
|
-
class="w-40 text-xs border border-gray-300 rounded px-2 py-1"
|
|
49
|
-
placeholder="Title (optional)"
|
|
50
|
-
data-testid="sources-draft-title"
|
|
51
|
-
@keydown.enter="commitAdd"
|
|
52
|
-
/>
|
|
53
|
-
</div>
|
|
54
|
-
<div class="flex items-center justify-between text-xs">
|
|
55
|
-
<span class="text-gray-500">
|
|
56
|
-
{{ primaryHint }}
|
|
57
|
-
</span>
|
|
58
|
-
<div class="flex gap-2">
|
|
59
|
-
<button class="px-2 py-1 rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="sources-draft-cancel" @click="cancelAdd">
|
|
60
|
-
Cancel
|
|
61
|
-
</button>
|
|
62
|
-
<button
|
|
63
|
-
class="px-2 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50"
|
|
64
|
-
:disabled="busy === 'add' || !draft.primary.trim()"
|
|
65
|
-
data-testid="sources-draft-add"
|
|
66
|
-
@click="commitAdd"
|
|
67
|
-
>
|
|
68
|
-
{{ busy === "add" ? "Adding…" : "Add + Rebuild" }}
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
<div v-if="draftError" class="text-xs text-red-600" data-testid="sources-draft-error">
|
|
73
|
-
{{ draftError }}
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div
|
|
78
|
-
v-if="actionMessage"
|
|
79
|
-
class="px-4 py-2 text-xs border-b shrink-0"
|
|
80
|
-
:class="actionError ? 'bg-red-50 text-red-700 border-red-200' : 'bg-green-50 text-green-700 border-green-200'"
|
|
81
|
-
data-testid="sources-action-message"
|
|
82
|
-
>
|
|
83
|
-
{{ actionMessage }}
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div class="flex-1 overflow-y-auto">
|
|
87
|
-
<div v-if="sources.length === 0" class="flex flex-col items-center justify-center h-full p-6 gap-4" data-testid="sources-empty">
|
|
88
|
-
<p class="text-sm text-gray-500 italic text-center max-w-md">
|
|
89
|
-
No sources registered yet. Pick a starter pack below, click
|
|
90
|
-
<strong>+ Add</strong> above, or ask Claude to register one.
|
|
91
|
-
</p>
|
|
92
|
-
<div class="w-full max-w-md space-y-2" data-testid="sources-presets">
|
|
93
|
-
<button
|
|
94
|
-
v-for="preset in PRESETS"
|
|
95
|
-
:key="preset.id"
|
|
96
|
-
class="w-full text-left border border-gray-200 rounded-lg p-3 hover:bg-blue-50 hover:border-blue-300 disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:border-gray-200"
|
|
97
|
-
:disabled="busy === 'preset-' + preset.id"
|
|
98
|
-
:data-testid="`sources-preset-${preset.id}`"
|
|
99
|
-
@click="installPreset(preset)"
|
|
100
|
-
>
|
|
101
|
-
<div class="flex items-baseline justify-between gap-2">
|
|
102
|
-
<span class="text-sm font-medium text-gray-800">
|
|
103
|
-
{{ preset.label }}
|
|
104
|
-
</span>
|
|
105
|
-
<span class="text-[11px] text-gray-500 shrink-0"> {{ preset.entries.length }} source{{ preset.entries.length === 1 ? "" : "s" }} </span>
|
|
106
|
-
</div>
|
|
107
|
-
<div class="text-xs text-gray-500 mt-1">
|
|
108
|
-
{{ preset.description }}
|
|
109
|
-
</div>
|
|
110
|
-
<div v-if="busy === 'preset-' + preset.id" class="text-xs text-blue-600 mt-1 italic">Registering + fetching…</div>
|
|
111
|
-
</button>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
<ul v-else class="divide-y divide-gray-100 border-b border-gray-100">
|
|
115
|
-
<li
|
|
116
|
-
v-for="source in sources"
|
|
117
|
-
:key="source.slug"
|
|
118
|
-
class="px-4 py-3 flex items-start gap-3"
|
|
119
|
-
:class="{
|
|
120
|
-
'bg-amber-50': source.slug === highlightSlug,
|
|
121
|
-
}"
|
|
122
|
-
:data-testid="`source-row-${source.slug}`"
|
|
123
|
-
>
|
|
124
|
-
<span class="text-[10px] uppercase tracking-wide rounded px-1.5 py-0.5 mt-0.5 shrink-0" :class="kindBadgeClass(source.fetcherKind)">
|
|
125
|
-
{{ kindLabel(source.fetcherKind) }}
|
|
126
|
-
</span>
|
|
127
|
-
<div class="min-w-0 flex-1">
|
|
128
|
-
<div class="flex items-baseline gap-2">
|
|
129
|
-
<a :href="source.url" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-blue-700 hover:underline truncate">
|
|
130
|
-
{{ source.title }}
|
|
131
|
-
</a>
|
|
132
|
-
<code class="text-[11px] text-gray-400 shrink-0">
|
|
133
|
-
{{ source.slug }}
|
|
134
|
-
</code>
|
|
135
|
-
</div>
|
|
136
|
-
<div class="text-xs text-gray-500 truncate">
|
|
137
|
-
{{ source.url }}
|
|
138
|
-
</div>
|
|
139
|
-
<div v-if="source.categories.length > 0" class="mt-1 flex flex-wrap gap-1">
|
|
140
|
-
<span v-for="cat in source.categories" :key="cat" class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">
|
|
141
|
-
{{ cat }}
|
|
142
|
-
</span>
|
|
143
|
-
</div>
|
|
144
|
-
<div v-if="source.notes" class="mt-1 text-xs text-gray-600 italic">
|
|
145
|
-
{{ source.notes }}
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
<button
|
|
149
|
-
class="text-xs text-red-600 hover:text-red-800 shrink-0 disabled:opacity-50"
|
|
150
|
-
:disabled="busy === source.slug"
|
|
151
|
-
:data-testid="`source-remove-${source.slug}`"
|
|
152
|
-
@click="remove(source.slug)"
|
|
153
|
-
>
|
|
154
|
-
{{ busy === source.slug ? "Removing…" : "Remove" }}
|
|
155
|
-
</button>
|
|
156
|
-
</li>
|
|
157
|
-
</ul>
|
|
158
|
-
|
|
159
|
-
<!-- Today's brief. Auto-fetched on mount and refreshed after
|
|
160
|
-
every Rebuild. Rendered as markdown so lists / headings
|
|
161
|
-
feel like a document, not a dump. -->
|
|
162
|
-
<div v-if="sources.length > 0 && (briefLoading || briefHtml || briefError)" class="p-4" data-testid="sources-brief">
|
|
163
|
-
<div class="flex items-baseline justify-between mb-2">
|
|
164
|
-
<h3 class="text-sm font-semibold text-gray-800">
|
|
165
|
-
Today's brief
|
|
166
|
-
<span v-if="briefDate" class="text-xs text-gray-400 font-normal"> ({{ briefDate }}) </span>
|
|
167
|
-
</h3>
|
|
168
|
-
<button v-if="briefFilePath" class="text-[11px] text-gray-500 hover:text-gray-700" :title="briefFilePath">
|
|
169
|
-
{{ briefFilePath }}
|
|
170
|
-
</button>
|
|
171
|
-
</div>
|
|
172
|
-
<div v-if="briefLoading" class="text-xs text-gray-500 italic">Loading today's brief…</div>
|
|
173
|
-
<div v-else-if="briefError" class="text-xs text-gray-500 italic" data-testid="sources-brief-empty">
|
|
174
|
-
{{ briefError }}
|
|
175
|
-
</div>
|
|
176
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
177
|
-
<div v-else class="markdown-content" v-html="briefHtml" />
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<div v-if="lastRebuild" class="px-4 py-2 border-t border-gray-100 shrink-0 text-xs text-gray-600" data-testid="sources-rebuild-summary">
|
|
182
|
-
Last rebuild ({{ lastRebuild.isoDate }}): <strong>{{ lastRebuild.itemCount }}</strong> items from <strong>{{ lastRebuild.plannedCount }}</strong> sources,
|
|
183
|
-
<strong>{{ lastRebuild.duplicateCount }}</strong> duplicates dropped.
|
|
184
|
-
<span v-if="lastRebuild.archiveErrors.length > 0" class="text-red-600"> ({{ lastRebuild.archiveErrors.length }} archive errors) </span>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
2
|
+
<SourcesManager mode="plugin" :initial-data="props.selectedResult.data ?? null" />
|
|
187
3
|
</template>
|
|
188
4
|
|
|
189
5
|
<script setup lang="ts">
|
|
190
|
-
import { computed, onMounted, ref, watch } from "vue";
|
|
191
|
-
import { marked } from "marked";
|
|
192
|
-
import DOMPurify from "dompurify";
|
|
193
6
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
194
|
-
import type { ManageSourceData
|
|
195
|
-
import
|
|
196
|
-
import { API_ROUTES } from "../../config/apiRoutes";
|
|
7
|
+
import type { ManageSourceData } from "./index";
|
|
8
|
+
import SourcesManager from "../../components/SourcesManager.vue";
|
|
197
9
|
|
|
198
10
|
const props = defineProps<{
|
|
199
11
|
selectedResult: ToolResultComplete<ManageSourceData>;
|
|
200
12
|
}>();
|
|
201
|
-
|
|
202
|
-
// Local mirror of the source list that we mutate after Remove /
|
|
203
|
-
// Rebuild button clicks, so the UI stays responsive without the LLM
|
|
204
|
-
// having to re-list. Initial value comes from the tool result.
|
|
205
|
-
const localSources = ref<Source[] | null>(null);
|
|
206
|
-
const lastRebuild = ref<RebuildSummary | null>(null);
|
|
207
|
-
const actionMessage = ref("");
|
|
208
|
-
const actionError = ref(false);
|
|
209
|
-
// Tracks the current button-driven request: "rebuild", "add", or a
|
|
210
|
-
// slug (Remove). Used to disable/relabel the matching button.
|
|
211
|
-
const busy = ref<string | null>(null);
|
|
212
|
-
|
|
213
|
-
// --- Add source form state ---------------------------------------------
|
|
214
|
-
|
|
215
|
-
type DraftKind = "rss" | "github-releases" | "github-issues" | "arxiv";
|
|
216
|
-
interface DraftState {
|
|
217
|
-
kind: DraftKind;
|
|
218
|
-
primary: string; // Feed URL / repo URL / repo slug / arxiv query
|
|
219
|
-
title: string;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const adding = ref(false);
|
|
223
|
-
const draft = ref<DraftState>(emptyDraft());
|
|
224
|
-
const draftError = ref("");
|
|
225
|
-
|
|
226
|
-
function emptyDraft(): DraftState {
|
|
227
|
-
return { kind: "rss", primary: "", title: "" };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function startAdd(): void {
|
|
231
|
-
draft.value = emptyDraft();
|
|
232
|
-
draftError.value = "";
|
|
233
|
-
adding.value = true;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function cancelAdd(): void {
|
|
237
|
-
adding.value = false;
|
|
238
|
-
draftError.value = "";
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function onKindChange(): void {
|
|
242
|
-
draftError.value = "";
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const primaryPlaceholder = computed(() => {
|
|
246
|
-
switch (draft.value.kind) {
|
|
247
|
-
case "rss":
|
|
248
|
-
return "https://news.ycombinator.com/rss";
|
|
249
|
-
case "github-releases":
|
|
250
|
-
case "github-issues":
|
|
251
|
-
return "https://github.com/owner/repo (or owner/repo)";
|
|
252
|
-
case "arxiv":
|
|
253
|
-
return "cat:cs.CL";
|
|
254
|
-
}
|
|
255
|
-
return "";
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
const primaryHint = computed(() => {
|
|
259
|
-
switch (draft.value.kind) {
|
|
260
|
-
case "rss":
|
|
261
|
-
return "Feed URL (RSS 2.0 / Atom / RDF)";
|
|
262
|
-
case "github-releases":
|
|
263
|
-
return "GitHub repo URL or owner/repo — fetches releases";
|
|
264
|
-
case "github-issues":
|
|
265
|
-
return "GitHub repo URL or owner/repo — fetches issues";
|
|
266
|
-
case "arxiv":
|
|
267
|
-
return "arXiv search query (e.g. cat:cs.CL or au:hinton)";
|
|
268
|
-
}
|
|
269
|
-
return "";
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// Extract owner/repo from either a full github.com URL or a bare
|
|
273
|
-
// "owner/repo" string. Returns null when the input doesn't look
|
|
274
|
-
// like a recognisable GitHub repo.
|
|
275
|
-
function parseRepoSlug(input: string): string | null {
|
|
276
|
-
const trimmed = input.trim();
|
|
277
|
-
if (!trimmed) return null;
|
|
278
|
-
const urlMatch = trimmed.match(/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s?#]+)/i);
|
|
279
|
-
if (urlMatch) return `${urlMatch[1]}/${urlMatch[2].replace(/\.git$/, "")}`;
|
|
280
|
-
if (/^[^/\s]+\/[^/\s]+$/.test(trimmed)) return trimmed.replace(/\.git$/, "");
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Build the /api/sources body from the draft. Returns an error
|
|
285
|
-
// string when the input is invalid for the chosen kind.
|
|
286
|
-
interface RegisterPayload {
|
|
287
|
-
title: string;
|
|
288
|
-
url: string;
|
|
289
|
-
fetcherKind: DraftKind;
|
|
290
|
-
fetcherParams: Record<string, string>;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function buildRegisterPayload(input: DraftState): RegisterPayload | string {
|
|
294
|
-
const primary = input.primary.trim();
|
|
295
|
-
const title = input.title.trim();
|
|
296
|
-
if (!primary) return "Please fill in the URL / query field.";
|
|
297
|
-
switch (input.kind) {
|
|
298
|
-
case "rss": {
|
|
299
|
-
if (!/^https?:\/\//i.test(primary)) {
|
|
300
|
-
return "RSS feed URL must start with http:// or https://";
|
|
301
|
-
}
|
|
302
|
-
let hostname: string;
|
|
303
|
-
try {
|
|
304
|
-
hostname = new URL(primary).hostname;
|
|
305
|
-
} catch {
|
|
306
|
-
return "RSS feed URL is not a valid URL.";
|
|
307
|
-
}
|
|
308
|
-
if (!hostname) {
|
|
309
|
-
return "RSS feed URL must include a host.";
|
|
310
|
-
}
|
|
311
|
-
return {
|
|
312
|
-
title: title || hostname,
|
|
313
|
-
url: primary,
|
|
314
|
-
fetcherKind: "rss",
|
|
315
|
-
fetcherParams: { rss_url: primary },
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
case "github-releases":
|
|
319
|
-
case "github-issues": {
|
|
320
|
-
const slug = parseRepoSlug(primary);
|
|
321
|
-
if (!slug) {
|
|
322
|
-
return "Enter a GitHub repo URL (https://github.com/owner/repo) or owner/repo.";
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
title: title || slug,
|
|
326
|
-
url: `https://github.com/${slug}`,
|
|
327
|
-
fetcherKind: input.kind,
|
|
328
|
-
fetcherParams: { github_repo: slug },
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
case "arxiv": {
|
|
332
|
-
const query = primary;
|
|
333
|
-
return {
|
|
334
|
-
title: title || `arXiv: ${query}`,
|
|
335
|
-
url: `https://export.arxiv.org/api/query?search_query=${encodeURIComponent(query)}`,
|
|
336
|
-
fetcherKind: "arxiv",
|
|
337
|
-
fetcherParams: { arxiv_query: query },
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return "Unsupported fetcher kind.";
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async function commitAdd(): Promise<void> {
|
|
345
|
-
const payload = buildRegisterPayload(draft.value);
|
|
346
|
-
if (typeof payload === "string") {
|
|
347
|
-
draftError.value = payload;
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
draftError.value = "";
|
|
351
|
-
busy.value = "add";
|
|
352
|
-
const response = await apiPost<unknown>(API_ROUTES.sources.create, payload);
|
|
353
|
-
if (!response.ok) {
|
|
354
|
-
draftError.value = response.error || "Failed to register source";
|
|
355
|
-
busy.value = null;
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
flash(`Registered. Fetching new items…`);
|
|
359
|
-
adding.value = false;
|
|
360
|
-
await refreshList();
|
|
361
|
-
// C: auto-rebuild so the user sees items without an extra click.
|
|
362
|
-
busy.value = "rebuild";
|
|
363
|
-
await rebuildInline();
|
|
364
|
-
busy.value = null;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// --- Starter-pack presets ----------------------------------------------
|
|
368
|
-
|
|
369
|
-
interface PresetEntry {
|
|
370
|
-
slug: string;
|
|
371
|
-
title: string;
|
|
372
|
-
url: string;
|
|
373
|
-
fetcherKind: "rss" | "github-releases" | "github-issues" | "arxiv";
|
|
374
|
-
fetcherParams: Record<string, string>;
|
|
375
|
-
categories?: string[];
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
interface Preset {
|
|
379
|
-
id: string;
|
|
380
|
-
label: string;
|
|
381
|
-
description: string;
|
|
382
|
-
entries: PresetEntry[];
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const PRESETS: Preset[] = [
|
|
386
|
-
{
|
|
387
|
-
id: "tech-news",
|
|
388
|
-
label: "Tech news",
|
|
389
|
-
description: "Hacker News front page — daily tech headlines.",
|
|
390
|
-
entries: [
|
|
391
|
-
{
|
|
392
|
-
slug: "hacker-news",
|
|
393
|
-
title: "Hacker News",
|
|
394
|
-
url: "https://news.ycombinator.com/rss",
|
|
395
|
-
fetcherKind: "rss",
|
|
396
|
-
fetcherParams: { rss_url: "https://news.ycombinator.com/rss" },
|
|
397
|
-
categories: ["tech-news", "startup"],
|
|
398
|
-
},
|
|
399
|
-
],
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
id: "ai-research",
|
|
403
|
-
label: "AI research",
|
|
404
|
-
description: "Latest arXiv papers in NLP (cs.CL) and machine learning (cs.LG).",
|
|
405
|
-
entries: [
|
|
406
|
-
{
|
|
407
|
-
slug: "arxiv-cs-cl",
|
|
408
|
-
title: "arXiv cs.CL",
|
|
409
|
-
url: "https://export.arxiv.org/api/query?search_query=cat:cs.CL",
|
|
410
|
-
fetcherKind: "arxiv",
|
|
411
|
-
fetcherParams: { arxiv_query: "cat:cs.CL" },
|
|
412
|
-
categories: ["ai", "research"],
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
slug: "arxiv-cs-lg",
|
|
416
|
-
title: "arXiv cs.LG",
|
|
417
|
-
url: "https://export.arxiv.org/api/query?search_query=cat:cs.LG",
|
|
418
|
-
fetcherKind: "arxiv",
|
|
419
|
-
fetcherParams: { arxiv_query: "cat:cs.LG" },
|
|
420
|
-
categories: ["ai", "research"],
|
|
421
|
-
},
|
|
422
|
-
],
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
id: "claude-code",
|
|
426
|
-
label: "Claude Code updates",
|
|
427
|
-
description: "New releases of the Claude Code CLI from the anthropics/claude-code repo.",
|
|
428
|
-
entries: [
|
|
429
|
-
{
|
|
430
|
-
slug: "claude-code-releases",
|
|
431
|
-
title: "Claude Code releases",
|
|
432
|
-
url: "https://github.com/anthropics/claude-code",
|
|
433
|
-
fetcherKind: "github-releases",
|
|
434
|
-
fetcherParams: { github_repo: "anthropics/claude-code" },
|
|
435
|
-
categories: ["ai", "tech-news"],
|
|
436
|
-
},
|
|
437
|
-
],
|
|
438
|
-
},
|
|
439
|
-
];
|
|
440
|
-
|
|
441
|
-
async function installPreset(preset: Preset): Promise<void> {
|
|
442
|
-
busy.value = `preset-${preset.id}`;
|
|
443
|
-
const alreadyHave = new Set(sources.value.map((source) => source.slug));
|
|
444
|
-
const toRegister = preset.entries.filter((entry) => !alreadyHave.has(entry.slug));
|
|
445
|
-
if (toRegister.length === 0) {
|
|
446
|
-
flash(`All sources in "${preset.label}" are already registered.`);
|
|
447
|
-
busy.value = null;
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
const failures: string[] = [];
|
|
451
|
-
for (const entry of toRegister) {
|
|
452
|
-
const response = await apiPost<unknown>(API_ROUTES.sources.create, {
|
|
453
|
-
slug: entry.slug,
|
|
454
|
-
title: entry.title,
|
|
455
|
-
url: entry.url,
|
|
456
|
-
fetcherKind: entry.fetcherKind,
|
|
457
|
-
fetcherParams: entry.fetcherParams,
|
|
458
|
-
// Presets know their categories — skip the classifier
|
|
459
|
-
// CLI call so the first brief is ready sooner.
|
|
460
|
-
categories: entry.categories,
|
|
461
|
-
skipClassify: true,
|
|
462
|
-
});
|
|
463
|
-
if (!response.ok) {
|
|
464
|
-
failures.push(`${entry.slug}: ${response.error}`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
if (failures.length > 0) {
|
|
468
|
-
flash(`Registered ${toRegister.length - failures.length}/${toRegister.length}. Errors: ${failures.join("; ")}`, true);
|
|
469
|
-
} else {
|
|
470
|
-
flash(`Registered ${toRegister.length} source${toRegister.length === 1 ? "" : "s"} from "${preset.label}". Fetching…`);
|
|
471
|
-
}
|
|
472
|
-
await refreshList();
|
|
473
|
-
await rebuildInline();
|
|
474
|
-
busy.value = null;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Rebuild step extracted so commitAdd can chain it without recursing
|
|
478
|
-
// into rebuild()'s own busy-state machine.
|
|
479
|
-
async function rebuildInline(): Promise<void> {
|
|
480
|
-
const response = await apiPost<RebuildSummary>(API_ROUTES.sources.rebuild);
|
|
481
|
-
if (!response.ok) {
|
|
482
|
-
flash(`Register succeeded but rebuild failed: ${response.error}`, true);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
const summary = response.data;
|
|
486
|
-
lastRebuild.value = summary;
|
|
487
|
-
flash(`Ready: ${summary.itemCount} items from ${summary.plannedCount} source${summary.plannedCount === 1 ? "" : "s"}.`);
|
|
488
|
-
await loadBrief(summary.isoDate);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const sources = computed<Source[]>(() => {
|
|
492
|
-
if (localSources.value !== null) return localSources.value;
|
|
493
|
-
return props.selectedResult.data?.sources ?? [];
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
const highlightSlug = computed(() => props.selectedResult.data?.highlightSlug ?? null);
|
|
497
|
-
|
|
498
|
-
// Initialize lastRebuild from the result if the LLM-side rebuild
|
|
499
|
-
// landed before any in-View button click — but never overwrite a
|
|
500
|
-
// fresher result the user's own click produced.
|
|
501
|
-
if (lastRebuild.value === null && props.selectedResult.data?.lastRebuild !== undefined) {
|
|
502
|
-
lastRebuild.value = props.selectedResult.data.lastRebuild;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Re-sync the local mirrors when the caller selects a different
|
|
506
|
-
// manageSource result (e.g. a new tool_result from the LLM). The
|
|
507
|
-
// existing "never overwrite fresher in-View state" guard still
|
|
508
|
-
// applies — we only accept the prop value when it's strictly
|
|
509
|
-
// newer than what the View has.
|
|
510
|
-
watch(
|
|
511
|
-
() => props.selectedResult.uuid,
|
|
512
|
-
() => {
|
|
513
|
-
const incoming = props.selectedResult.data;
|
|
514
|
-
if (!incoming) return;
|
|
515
|
-
// Replace the source list wholesale — the prop's snapshot is
|
|
516
|
-
// authoritative when the user switches between results.
|
|
517
|
-
localSources.value = incoming.sources ?? [];
|
|
518
|
-
const nextRebuild = incoming.lastRebuild;
|
|
519
|
-
if (nextRebuild && (!lastRebuild.value || nextRebuild.isoDate >= lastRebuild.value.isoDate)) {
|
|
520
|
-
lastRebuild.value = nextRebuild;
|
|
521
|
-
}
|
|
522
|
-
},
|
|
523
|
-
);
|
|
524
|
-
|
|
525
|
-
function kindLabel(kind: Source["fetcherKind"]): string {
|
|
526
|
-
switch (kind) {
|
|
527
|
-
case "rss":
|
|
528
|
-
return "RSS";
|
|
529
|
-
case "github-releases":
|
|
530
|
-
return "GitHub rel";
|
|
531
|
-
case "github-issues":
|
|
532
|
-
return "GitHub iss";
|
|
533
|
-
case "arxiv":
|
|
534
|
-
return "arXiv";
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function kindBadgeClass(kind: Source["fetcherKind"]): string {
|
|
539
|
-
switch (kind) {
|
|
540
|
-
case "rss":
|
|
541
|
-
return "bg-orange-100 text-orange-700";
|
|
542
|
-
case "github-releases":
|
|
543
|
-
return "bg-purple-100 text-purple-700";
|
|
544
|
-
case "github-issues":
|
|
545
|
-
return "bg-indigo-100 text-indigo-700";
|
|
546
|
-
case "arxiv":
|
|
547
|
-
return "bg-emerald-100 text-emerald-700";
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function flash(message: string, isError = false): void {
|
|
552
|
-
actionMessage.value = message;
|
|
553
|
-
actionError.value = isError;
|
|
554
|
-
setTimeout(() => {
|
|
555
|
-
if (actionMessage.value === message) actionMessage.value = "";
|
|
556
|
-
}, 4000);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async function refreshList(): Promise<void> {
|
|
560
|
-
const response = await apiGet<{ sources: Source[] }>(API_ROUTES.sources.list);
|
|
561
|
-
if (!response.ok) {
|
|
562
|
-
flash(`Failed to refresh sources: ${response.error}`, true);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
localSources.value = response.data.sources;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
async function remove(slug: string): Promise<void> {
|
|
569
|
-
if (!confirm(`Remove source "${slug}"?`)) return;
|
|
570
|
-
busy.value = slug;
|
|
571
|
-
const response = await apiDelete<unknown>(API_ROUTES.sources.remove.replace(":slug", encodeURIComponent(slug)));
|
|
572
|
-
busy.value = null;
|
|
573
|
-
if (!response.ok) {
|
|
574
|
-
flash(`Remove failed: ${response.error}`, true);
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
flash(`Removed "${slug}".`);
|
|
578
|
-
await refreshList();
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
async function rebuild(): Promise<void> {
|
|
582
|
-
busy.value = "rebuild";
|
|
583
|
-
const response = await apiPost<RebuildSummary>(API_ROUTES.sources.rebuild);
|
|
584
|
-
if (!response.ok) {
|
|
585
|
-
flash(`Rebuild failed: ${response.error}`, true);
|
|
586
|
-
busy.value = null;
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
const summary = response.data;
|
|
590
|
-
lastRebuild.value = summary;
|
|
591
|
-
flash(`Rebuild complete: ${summary.itemCount} items from ${summary.plannedCount} sources.`);
|
|
592
|
-
await Promise.all([refreshList(), loadBrief(summary.isoDate)]);
|
|
593
|
-
busy.value = null;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// --- today's brief -------------------------------------------------------
|
|
597
|
-
|
|
598
|
-
// Fetched markdown (rendered via marked() into briefHtml below). Null
|
|
599
|
-
// while idle; "" after a confirmed empty/404 so the template can show
|
|
600
|
-
// a friendly message instead of a stuck spinner.
|
|
601
|
-
const briefMarkdown = ref<string | null>(null);
|
|
602
|
-
const briefError = ref("");
|
|
603
|
-
const briefLoading = ref(false);
|
|
604
|
-
const briefDate = ref("");
|
|
605
|
-
const briefFilePath = ref("");
|
|
606
|
-
|
|
607
|
-
// Build `news/daily/YYYY/MM/DD.md` from an ISO date. Local-time
|
|
608
|
-
// matches how the pipeline writes the file (see toLocalIsoDate).
|
|
609
|
-
function dailyPathFor(isoDate: string): string {
|
|
610
|
-
const [year, month, day] = isoDate.split("-");
|
|
611
|
-
return `news/daily/${year}/${month}/${day}.md`;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function todayIsoDate(): string {
|
|
615
|
-
const now = new Date();
|
|
616
|
-
const year = now.getFullYear();
|
|
617
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
618
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
619
|
-
return `${year}-${month}-${day}`;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Monotonically-increasing token so concurrent loadBrief() calls
|
|
623
|
-
// (mount + rebuild + prop watch racing on slow networks) can drop
|
|
624
|
-
// stale responses that resolve after a newer one has already
|
|
625
|
-
// settled the state. Without this, an older fetch finishing last
|
|
626
|
-
// would clobber the latest brief.
|
|
627
|
-
let briefLoadToken = 0;
|
|
628
|
-
|
|
629
|
-
async function loadBrief(isoDate: string): Promise<void> {
|
|
630
|
-
const token = ++briefLoadToken;
|
|
631
|
-
briefLoading.value = true;
|
|
632
|
-
briefError.value = "";
|
|
633
|
-
briefDate.value = isoDate;
|
|
634
|
-
const relPath = dailyPathFor(isoDate);
|
|
635
|
-
briefFilePath.value = relPath;
|
|
636
|
-
const response = await apiGet<{ content?: string; kind?: string }>(API_ROUTES.files.content, { path: relPath });
|
|
637
|
-
if (token !== briefLoadToken) return;
|
|
638
|
-
if (!response.ok) {
|
|
639
|
-
if (response.status === 404) {
|
|
640
|
-
briefMarkdown.value = "";
|
|
641
|
-
briefError.value = "No brief written for this date yet. Click Rebuild now.";
|
|
642
|
-
} else {
|
|
643
|
-
briefError.value = response.error || "Failed to load brief";
|
|
644
|
-
}
|
|
645
|
-
briefLoading.value = false;
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
briefMarkdown.value = response.data.content ?? "";
|
|
649
|
-
if (!briefMarkdown.value.trim()) {
|
|
650
|
-
briefError.value = "Today's brief is empty.";
|
|
651
|
-
}
|
|
652
|
-
briefLoading.value = false;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// The daily file ends with a trailing ```json block that carries
|
|
656
|
-
// the structured item list for later machine consumption (Q2 of the
|
|
657
|
-
// plan: "Markdown + trailing fenced JSON block"). Strip it for the
|
|
658
|
-
// human-facing render so the UI doesn't dump a 1000-line JSON blob
|
|
659
|
-
// after the brief. The file on disk stays unchanged.
|
|
660
|
-
function stripTrailingJsonBlock(markdown: string): string {
|
|
661
|
-
const marker = "\n```json\n";
|
|
662
|
-
const idx = markdown.lastIndexOf(marker);
|
|
663
|
-
if (idx < 0) return markdown;
|
|
664
|
-
// Only strip if everything after the marker looks like it belongs
|
|
665
|
-
// to that block (i.e. it's the last fenced block in the file).
|
|
666
|
-
const tail = markdown.slice(idx);
|
|
667
|
-
if (!tail.trimEnd().endsWith("```")) return markdown;
|
|
668
|
-
return markdown.slice(0, idx).trimEnd();
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const briefHtml = computed(() => {
|
|
672
|
-
if (!briefMarkdown.value) return "";
|
|
673
|
-
const body = stripTrailingJsonBlock(briefMarkdown.value);
|
|
674
|
-
// marked() preserves raw HTML embedded in the markdown (RSS
|
|
675
|
-
// content:encoded blocks often carry tracking pixels, iframes,
|
|
676
|
-
// inline <script> from scraped sources). Sanitize before
|
|
677
|
-
// binding to v-html.
|
|
678
|
-
return DOMPurify.sanitize(marked(body) as string);
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
// Load on mount — try today's brief first, then last rebuild's date
|
|
682
|
-
// if different (tool result may have been produced earlier in the day
|
|
683
|
-
// but the user only just opened this canvas).
|
|
684
|
-
onMounted(() => {
|
|
685
|
-
const initial = lastRebuild.value?.isoDate ?? todayIsoDate();
|
|
686
|
-
loadBrief(initial);
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
// Re-fetch when the selected result brings a new rebuild summary
|
|
690
|
-
// (e.g. the LLM triggered another rebuild).
|
|
691
|
-
watch(
|
|
692
|
-
() => props.selectedResult.data?.lastRebuild?.isoDate,
|
|
693
|
-
(next) => {
|
|
694
|
-
if (next && next !== briefDate.value) loadBrief(next);
|
|
695
|
-
},
|
|
696
|
-
);
|
|
697
13
|
</script>
|