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
|
@@ -7,20 +7,20 @@
|
|
|
7
7
|
aria-labelledby="todo-add-dialog-title"
|
|
8
8
|
@click.stop
|
|
9
9
|
>
|
|
10
|
-
<h3 id="todo-add-dialog-title" class="text-base font-semibold text-gray-800">
|
|
10
|
+
<h3 id="todo-add-dialog-title" class="text-base font-semibold text-gray-800">{{ t("todoDialogs.addTitle") }}</h3>
|
|
11
11
|
<label class="block text-xs text-gray-600">
|
|
12
|
-
|
|
12
|
+
{{ t("todoDialogs.fieldText") }}
|
|
13
13
|
<input
|
|
14
14
|
ref="textInput"
|
|
15
15
|
v-model="text"
|
|
16
16
|
type="text"
|
|
17
|
-
placeholder="
|
|
17
|
+
:placeholder="t('todoDialogs.textPlaceholder')"
|
|
18
18
|
class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
|
|
19
19
|
@keydown.enter="submit"
|
|
20
20
|
/>
|
|
21
21
|
</label>
|
|
22
22
|
<label class="block text-xs text-gray-600">
|
|
23
|
-
|
|
23
|
+
{{ t("todoDialogs.fieldNote") }}
|
|
24
24
|
<textarea
|
|
25
25
|
v-model="note"
|
|
26
26
|
rows="2"
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
</label>
|
|
30
30
|
<div class="grid grid-cols-2 gap-3">
|
|
31
31
|
<label class="block text-xs text-gray-600">
|
|
32
|
-
|
|
32
|
+
{{ t("todoDialogs.fieldStatus") }}
|
|
33
33
|
<select v-model="status" class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400">
|
|
34
34
|
<option v-for="col in columns" :key="col.id" :value="col.id">
|
|
35
35
|
{{ col.label }}
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
</select>
|
|
38
38
|
</label>
|
|
39
39
|
<label class="block text-xs text-gray-600">
|
|
40
|
-
|
|
40
|
+
{{ t("todoDialogs.fieldPriority") }}
|
|
41
41
|
<select v-model="priority" class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400">
|
|
42
|
-
<option value=""
|
|
42
|
+
<option value="">{{ t("todoDialogs.noneOption") }}</option>
|
|
43
43
|
<option v-for="p in PRIORITIES" :key="p" :value="p">
|
|
44
44
|
{{ PRIORITY_LABELS[p] }}
|
|
45
45
|
</option>
|
|
46
46
|
</select>
|
|
47
47
|
</label>
|
|
48
48
|
<label class="block text-xs text-gray-600">
|
|
49
|
-
|
|
49
|
+
{{ t("todoDialogs.fieldDueDate") }}
|
|
50
50
|
<input
|
|
51
51
|
v-model="dueDate"
|
|
52
52
|
type="date"
|
|
@@ -54,18 +54,20 @@
|
|
|
54
54
|
/>
|
|
55
55
|
</label>
|
|
56
56
|
<label class="block text-xs text-gray-600">
|
|
57
|
-
|
|
57
|
+
{{ t("todoDialogs.fieldLabels") }}
|
|
58
58
|
<input
|
|
59
59
|
v-model="labelsText"
|
|
60
60
|
type="text"
|
|
61
|
-
placeholder="
|
|
61
|
+
:placeholder="t('todoDialogs.labelsPlaceholder')"
|
|
62
62
|
class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
|
|
63
63
|
/>
|
|
64
64
|
</label>
|
|
65
65
|
</div>
|
|
66
66
|
<div class="flex justify-end gap-2 pt-1">
|
|
67
|
-
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="emit('cancel')">
|
|
68
|
-
|
|
67
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="emit('cancel')">
|
|
68
|
+
{{ t("common.cancel") }}
|
|
69
|
+
</button>
|
|
70
|
+
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="submit">{{ t("common.add") }}</button>
|
|
69
71
|
</div>
|
|
70
72
|
</div>
|
|
71
73
|
</div>
|
|
@@ -73,10 +75,13 @@
|
|
|
73
75
|
|
|
74
76
|
<script setup lang="ts">
|
|
75
77
|
import { onMounted, onUnmounted, ref } from "vue";
|
|
78
|
+
import { useI18n } from "vue-i18n";
|
|
76
79
|
import type { StatusColumn } from "../../plugins/todo/index";
|
|
77
80
|
import { PRIORITIES, PRIORITY_LABELS } from "../../plugins/todo/priority";
|
|
78
81
|
import type { CreateItemInput } from "../../plugins/todo/composables/useTodos";
|
|
79
82
|
|
|
83
|
+
const { t } = useI18n();
|
|
84
|
+
|
|
80
85
|
const props = defineProps<{
|
|
81
86
|
columns: StatusColumn[];
|
|
82
87
|
defaultStatus?: string;
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
@click.stop
|
|
9
9
|
>
|
|
10
10
|
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
|
11
|
-
<h3 id="todo-edit-dialog-title" class="text-base font-semibold text-gray-800">
|
|
12
|
-
<button class="text-gray-400 hover:text-red-500 text-xs px-2 py-0.5" title="
|
|
11
|
+
<h3 id="todo-edit-dialog-title" class="text-base font-semibold text-gray-800">{{ t("todoDialogs.editTitle") }}</h3>
|
|
12
|
+
<button class="text-gray-400 hover:text-red-500 text-xs px-2 py-0.5" :title="t('todoDialogs.deleteTitle')" @click="emit('delete', item.id)">
|
|
13
|
+
{{ t("todoDialogs.deleteButton") }}
|
|
14
|
+
</button>
|
|
13
15
|
</div>
|
|
14
16
|
<TodoEditPanel :item="item" :columns="columns" @save="(input) => emit('save', input)" @cancel="emit('cancel')" />
|
|
15
17
|
</div>
|
|
@@ -18,10 +20,13 @@
|
|
|
18
20
|
|
|
19
21
|
<script setup lang="ts">
|
|
20
22
|
import { onMounted, onUnmounted } from "vue";
|
|
23
|
+
import { useI18n } from "vue-i18n";
|
|
21
24
|
import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
|
|
22
25
|
import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
|
|
23
26
|
import TodoEditPanel from "./TodoEditPanel.vue";
|
|
24
27
|
|
|
28
|
+
const { t } = useI18n();
|
|
29
|
+
|
|
25
30
|
defineProps<{
|
|
26
31
|
item: TodoItem;
|
|
27
32
|
columns: StatusColumn[];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="border-t border-blue-100 bg-blue-50 p-4 space-y-3 rounded-b-lg">
|
|
3
3
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
4
4
|
<label class="block text-xs text-gray-600 sm:col-span-2">
|
|
5
|
-
|
|
5
|
+
{{ t("todoDialogs.fieldText") }}
|
|
6
6
|
<input
|
|
7
7
|
v-model="text"
|
|
8
8
|
type="text"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/>
|
|
11
11
|
</label>
|
|
12
12
|
<label class="block text-xs text-gray-600 sm:col-span-2">
|
|
13
|
-
|
|
13
|
+
{{ t("todoDialogs.fieldNote") }}
|
|
14
14
|
<textarea
|
|
15
15
|
v-model="note"
|
|
16
16
|
rows="2"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
/>
|
|
19
19
|
</label>
|
|
20
20
|
<label class="block text-xs text-gray-600">
|
|
21
|
-
|
|
21
|
+
{{ t("todoDialogs.fieldStatus") }}
|
|
22
22
|
<select v-model="status" class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500">
|
|
23
23
|
<option v-for="col in columns" :key="col.id" :value="col.id">
|
|
24
24
|
{{ col.label }}
|
|
@@ -26,16 +26,16 @@
|
|
|
26
26
|
</select>
|
|
27
27
|
</label>
|
|
28
28
|
<label class="block text-xs text-gray-600">
|
|
29
|
-
|
|
29
|
+
{{ t("todoDialogs.fieldPriority") }}
|
|
30
30
|
<select v-model="priority" class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500">
|
|
31
|
-
<option value=""
|
|
31
|
+
<option value="">{{ t("todoDialogs.noneOption") }}</option>
|
|
32
32
|
<option v-for="p in PRIORITIES" :key="p" :value="p">
|
|
33
33
|
{{ PRIORITY_LABELS[p] }}
|
|
34
34
|
</option>
|
|
35
35
|
</select>
|
|
36
36
|
</label>
|
|
37
37
|
<label class="block text-xs text-gray-600">
|
|
38
|
-
|
|
38
|
+
{{ t("todoDialogs.fieldDueDate") }}
|
|
39
39
|
<input
|
|
40
40
|
v-model="dueDate"
|
|
41
41
|
type="date"
|
|
@@ -43,28 +43,33 @@
|
|
|
43
43
|
/>
|
|
44
44
|
</label>
|
|
45
45
|
<label class="block text-xs text-gray-600">
|
|
46
|
-
|
|
46
|
+
{{ t("todoDialogs.fieldLabelsCommaSeparated") }}
|
|
47
47
|
<input
|
|
48
48
|
v-model="labelsText"
|
|
49
49
|
type="text"
|
|
50
|
-
placeholder="
|
|
50
|
+
:placeholder="t('todoDialogs.labelsPlaceholder')"
|
|
51
51
|
class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500"
|
|
52
52
|
/>
|
|
53
53
|
</label>
|
|
54
54
|
</div>
|
|
55
55
|
<div class="flex items-center gap-2 pt-1">
|
|
56
|
-
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="save">
|
|
57
|
-
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="emit('cancel')">
|
|
56
|
+
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="save">{{ t("common.save") }}</button>
|
|
57
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="emit('cancel')">
|
|
58
|
+
{{ t("common.cancel") }}
|
|
59
|
+
</button>
|
|
58
60
|
</div>
|
|
59
61
|
</div>
|
|
60
62
|
</template>
|
|
61
63
|
|
|
62
64
|
<script setup lang="ts">
|
|
63
65
|
import { ref } from "vue";
|
|
66
|
+
import { useI18n } from "vue-i18n";
|
|
64
67
|
import type { StatusColumn, TodoItem, TodoPriority } from "../../plugins/todo/index";
|
|
65
68
|
import { PRIORITIES, PRIORITY_LABELS } from "../../plugins/todo/priority";
|
|
66
69
|
import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
|
|
67
70
|
|
|
71
|
+
const { t } = useI18n();
|
|
72
|
+
|
|
68
73
|
const props = defineProps<{
|
|
69
74
|
item: TodoItem;
|
|
70
75
|
columns: StatusColumn[];
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<span class="text-xs text-gray-500 shrink-0">{{ itemsByColumn(col.id).length }}</span>
|
|
33
33
|
</div>
|
|
34
34
|
<div class="relative">
|
|
35
|
-
<button class="text-gray-400 hover:text-gray-600 px-1" title="
|
|
35
|
+
<button class="text-gray-400 hover:text-gray-600 px-1" :title="t('todoKanban.columnActions')" @click="toggleMenu(col.id)">
|
|
36
36
|
<span class="material-icons text-base">more_horiz</span>
|
|
37
37
|
</button>
|
|
38
38
|
<div
|
|
@@ -40,11 +40,13 @@
|
|
|
40
40
|
class="absolute right-0 top-6 z-20 bg-white border border-gray-200 rounded shadow-md text-xs w-40 py-1"
|
|
41
41
|
@click.stop
|
|
42
42
|
>
|
|
43
|
-
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-50" @click="startRename(col)">
|
|
43
|
+
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-50" @click="startRename(col)">{{ t("todoKanban.rename") }}</button>
|
|
44
44
|
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-50" @click="markAsDone(col.id)">
|
|
45
|
-
{{ col.isDone ? "
|
|
45
|
+
{{ col.isDone ? t("todoKanban.alreadyDoneColumn") : t("todoKanban.markAsDoneColumn") }}
|
|
46
|
+
</button>
|
|
47
|
+
<button class="w-full text-left px-3 py-1.5 text-red-600 hover:bg-red-50" @click="deleteColumn(col.id)">
|
|
48
|
+
{{ t("todoKanban.deleteColumn") }}
|
|
46
49
|
</button>
|
|
47
|
-
<button class="w-full text-left px-3 py-1.5 text-red-600 hover:bg-red-50" @click="deleteColumn(col.id)">Delete column</button>
|
|
48
50
|
</div>
|
|
49
51
|
</div>
|
|
50
52
|
</div>
|
|
@@ -61,9 +63,14 @@
|
|
|
61
63
|
<template #item="{ element }: { element: TodoItem }">
|
|
62
64
|
<div
|
|
63
65
|
:data-testid="`todo-card-${element.id}`"
|
|
64
|
-
|
|
66
|
+
tabindex="0"
|
|
67
|
+
role="button"
|
|
68
|
+
:aria-label="t('todoKanban.openCardAria', { task: element.text })"
|
|
69
|
+
class="bg-white border border-l-4 border-gray-200 rounded shadow-sm p-2 cursor-grab hover:shadow active:cursor-grabbing focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
|
|
65
70
|
:class="element.priority ? PRIORITY_BORDER[element.priority] : 'border-l-gray-200'"
|
|
66
71
|
@click="emit('open', element)"
|
|
72
|
+
@keydown.enter.prevent.self="(e) => !e.repeat && emit('open', element)"
|
|
73
|
+
@keydown.space.prevent.self="(e) => !e.repeat && emit('open', element)"
|
|
67
74
|
>
|
|
68
75
|
<div class="flex items-start gap-2">
|
|
69
76
|
<input
|
|
@@ -103,7 +110,7 @@
|
|
|
103
110
|
|
|
104
111
|
<!-- Add card stub -->
|
|
105
112
|
<button class="m-2 text-xs text-gray-500 hover:text-gray-800 hover:bg-gray-200 rounded py-1.5 transition-colors" @click="emit('quickAdd', col.id)">
|
|
106
|
-
|
|
113
|
+
{{ t("todoKanban.addCard") }}
|
|
107
114
|
</button>
|
|
108
115
|
</div>
|
|
109
116
|
</template>
|
|
@@ -113,11 +120,14 @@
|
|
|
113
120
|
|
|
114
121
|
<script setup lang="ts">
|
|
115
122
|
import { computed, nextTick, ref, watch } from "vue";
|
|
123
|
+
import { useI18n } from "vue-i18n";
|
|
116
124
|
import draggable from "vuedraggable";
|
|
117
125
|
import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
|
|
118
126
|
import { colorForLabel } from "../../plugins/todo/labels";
|
|
119
127
|
import { PRIORITY_BORDER, PRIORITY_CLASSES, PRIORITY_LABELS, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
|
|
120
128
|
|
|
129
|
+
const { t } = useI18n();
|
|
130
|
+
|
|
121
131
|
// vuedraggable @change event shape. The library emits one of these
|
|
122
132
|
// three keys depending on whether the move was within the same list,
|
|
123
133
|
// added from another list, or removed from the current list. We only
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="h-full overflow-y-auto p-4">
|
|
3
|
-
<div v-if="filteredItems.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">
|
|
3
|
+
<div v-if="filteredItems.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">{{ t("todoTableList.noMatchingFilter") }}</div>
|
|
4
4
|
<ul v-else class="space-y-2 max-w-3xl mx-auto">
|
|
5
5
|
<li v-for="item in filteredItems" :key="item.id" class="rounded-lg border border-gray-200 hover:border-gray-300 transition-colors">
|
|
6
|
-
<div
|
|
6
|
+
<div
|
|
7
|
+
tabindex="0"
|
|
8
|
+
role="button"
|
|
9
|
+
:aria-label="t('todoTableList.expandRowAria', { task: item.text })"
|
|
10
|
+
class="flex items-center gap-3 p-3 cursor-pointer group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded"
|
|
11
|
+
@click="toggleExpand(item.id)"
|
|
12
|
+
@keydown.enter.prevent.self="(e) => !e.repeat && toggleExpand(item.id)"
|
|
13
|
+
@keydown.space.prevent.self="(e) => !e.repeat && toggleExpand(item.id)"
|
|
14
|
+
>
|
|
7
15
|
<input type="checkbox" :checked="item.completed" class="cursor-pointer shrink-0" @click.stop @change="toggleComplete(item)" />
|
|
8
16
|
<div class="flex-1 min-w-0">
|
|
9
17
|
<div class="flex items-center gap-2 flex-wrap">
|
|
@@ -25,7 +33,7 @@
|
|
|
25
33
|
</div>
|
|
26
34
|
<button
|
|
27
35
|
class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0"
|
|
28
|
-
title="
|
|
36
|
+
:title="t('pluginTodo.deleteItem')"
|
|
29
37
|
@click.stop="emit('delete', item.id)"
|
|
30
38
|
>
|
|
31
39
|
✕
|
|
@@ -39,12 +47,15 @@
|
|
|
39
47
|
|
|
40
48
|
<script setup lang="ts">
|
|
41
49
|
import { ref } from "vue";
|
|
50
|
+
import { useI18n } from "vue-i18n";
|
|
42
51
|
import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
|
|
43
52
|
import { colorForLabel } from "../../plugins/todo/labels";
|
|
44
53
|
import { PRIORITY_CLASSES, PRIORITY_LABELS, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
|
|
45
54
|
import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
|
|
46
55
|
import TodoEditPanel from "./TodoEditPanel.vue";
|
|
47
56
|
|
|
57
|
+
const { t } = useI18n();
|
|
58
|
+
|
|
48
59
|
const props = defineProps<{
|
|
49
60
|
filteredItems: TodoItem[];
|
|
50
61
|
columns: StatusColumn[];
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="h-full overflow-auto">
|
|
3
|
-
<div v-if="filteredItems.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">
|
|
3
|
+
<div v-if="filteredItems.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">{{ t("todoTableList.noMatchingFilter") }}</div>
|
|
4
4
|
<table v-else class="min-w-full text-sm">
|
|
5
5
|
<thead class="bg-gray-50 sticky top-0 z-10">
|
|
6
6
|
<tr class="text-left text-xs font-medium text-gray-500 uppercase">
|
|
7
|
-
|
|
7
|
+
<!-- Keep the native <th> column-header role so aria-sort and
|
|
8
|
+
table navigation work for screen readers. role="button"
|
|
9
|
+
would overwrite the header role and defeat aria-sort.
|
|
10
|
+
Keyboard activation goes via tabindex + key handlers. -->
|
|
11
|
+
<th
|
|
12
|
+
v-for="col in COLUMNS"
|
|
13
|
+
:key="col.key"
|
|
14
|
+
tabindex="0"
|
|
15
|
+
:aria-sort="sortKey === col.key ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'"
|
|
16
|
+
:aria-label="t('todoTableList.sortColumnAria', { column: col.ariaLabel ?? col.label })"
|
|
17
|
+
class="px-3 py-2 cursor-pointer hover:bg-gray-100 select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
|
|
18
|
+
@click="setSort(col.key)"
|
|
19
|
+
@keydown.enter.prevent.self="(e) => !e.repeat && setSort(col.key)"
|
|
20
|
+
@keydown.space.prevent.self="(e) => !e.repeat && setSort(col.key)"
|
|
21
|
+
>
|
|
8
22
|
{{ col.label }}
|
|
9
23
|
<span v-if="sortKey === col.key" class="material-icons text-xs align-middle">{{ sortDir === "asc" ? "arrow_upward" : "arrow_downward" }}</span>
|
|
10
24
|
</th>
|
|
@@ -17,7 +31,18 @@
|
|
|
17
31
|
<td class="px-3 py-2">
|
|
18
32
|
<input type="checkbox" :checked="item.completed" @change="emit('toggleComplete', item)" />
|
|
19
33
|
</td>
|
|
20
|
-
<
|
|
34
|
+
<!-- Same rationale as the <th> above: keep the native <td>
|
|
35
|
+
cell role so screen-reader table navigation still works;
|
|
36
|
+
activation goes via tabindex + key handlers, described
|
|
37
|
+
via aria-label. -->
|
|
38
|
+
<td
|
|
39
|
+
tabindex="0"
|
|
40
|
+
:aria-label="t('todoTableList.expandRowAria', { task: item.text })"
|
|
41
|
+
class="px-3 py-2 max-w-md cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
|
|
42
|
+
@click="toggleExpand(item.id)"
|
|
43
|
+
@keydown.enter.prevent.self="(e) => !e.repeat && toggleExpand(item.id)"
|
|
44
|
+
@keydown.space.prevent.self="(e) => !e.repeat && toggleExpand(item.id)"
|
|
45
|
+
>
|
|
21
46
|
<div :class="item.completed ? 'line-through text-gray-400' : 'text-gray-800'">
|
|
22
47
|
{{ item.text }}
|
|
23
48
|
</div>
|
|
@@ -53,7 +78,7 @@
|
|
|
53
78
|
{{ formatShortDate(item.createdAt) }}
|
|
54
79
|
</td>
|
|
55
80
|
<td class="px-3 py-2 text-right">
|
|
56
|
-
<button class="text-gray-300 hover:text-red-500 text-xs" title="
|
|
81
|
+
<button class="text-gray-300 hover:text-red-500 text-xs" :title="t('pluginTodo.deleteItem')" @click="emit('delete', item.id)">✕</button>
|
|
57
82
|
</td>
|
|
58
83
|
</tr>
|
|
59
84
|
<tr v-if="expandedId === item.id">
|
|
@@ -69,6 +94,7 @@
|
|
|
69
94
|
|
|
70
95
|
<script setup lang="ts">
|
|
71
96
|
import { computed, ref } from "vue";
|
|
97
|
+
import { useI18n } from "vue-i18n";
|
|
72
98
|
import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
|
|
73
99
|
import { colorForLabel } from "../../plugins/todo/labels";
|
|
74
100
|
import { PRIORITY_CLASSES, PRIORITY_LABELS, PRIORITY_ORDER, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
|
|
@@ -76,16 +102,21 @@ import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
|
|
|
76
102
|
import TodoEditPanel from "./TodoEditPanel.vue";
|
|
77
103
|
import { formatShortDate } from "../../utils/format/date";
|
|
78
104
|
|
|
105
|
+
const { t } = useI18n();
|
|
106
|
+
|
|
79
107
|
type SortKey = "completed" | "text" | "status" | "priority" | "labels" | "dueDate" | "createdAt";
|
|
80
108
|
type SortDir = "asc" | "desc";
|
|
81
109
|
|
|
82
110
|
interface ColumnDef {
|
|
83
111
|
key: SortKey;
|
|
84
112
|
label: string;
|
|
113
|
+
// Fallback name for screen readers when `label` is empty or is a
|
|
114
|
+
// glyph / checkbox indicator that wouldn't survive being spoken.
|
|
115
|
+
ariaLabel?: string;
|
|
85
116
|
}
|
|
86
117
|
|
|
87
118
|
const COLUMNS: ColumnDef[] = [
|
|
88
|
-
{ key: "completed", label: "" },
|
|
119
|
+
{ key: "completed", label: "", ariaLabel: "Completion" },
|
|
89
120
|
{ key: "text", label: "Text" },
|
|
90
121
|
{ key: "status", label: "Status" },
|
|
91
122
|
{ key: "priority", label: "Priority" },
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Pure predicates for the favicon resolver. Every function takes
|
|
2
|
+
// only its inputs — no global `Date`, no env, no network — so a test
|
|
3
|
+
// can pin an arbitrary moment and assert the branch deterministically.
|
|
4
|
+
|
|
5
|
+
// Night runs from 22:00 (inclusive) to 05:00 (exclusive). Chosen so a
|
|
6
|
+
// typical "I'm winding down" window (22–24) and a "still up" window
|
|
7
|
+
// (00–05) share the same deep-work indigo.
|
|
8
|
+
export function isLateNight(now: Date): boolean {
|
|
9
|
+
const hour = now.getHours();
|
|
10
|
+
return hour >= 22 || hour < 5;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Morning covers 05:00–09:00 — the "before the inbox wins" window.
|
|
14
|
+
// Disjoint from `isLateNight`, so the two rules never both fire.
|
|
15
|
+
export function isMorning(now: Date): boolean {
|
|
16
|
+
const hour = now.getHours();
|
|
17
|
+
return hour >= 5 && hour < 9;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Weekend fires only during waking hours so the late-night rule wins
|
|
21
|
+
// overnight on Fri→Sat (that's still "deep work", not "weekend vibes").
|
|
22
|
+
// Saturday = 6, Sunday = 0 per `Date.getDay()`.
|
|
23
|
+
export function isWeekend(now: Date): boolean {
|
|
24
|
+
const day = now.getDay();
|
|
25
|
+
const hour = now.getHours();
|
|
26
|
+
const onWeekend = day === 0 || day === 6;
|
|
27
|
+
const duringDay = hour >= 9 && hour < 22;
|
|
28
|
+
return onWeekend && duringDay;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Birthday match: context stores "MM-DD"; we compare to today in the
|
|
32
|
+
// caller's local clock. Returns false for any malformed input rather
|
|
33
|
+
// than throwing — the favicon must never crash on a typo.
|
|
34
|
+
export function isBirthday(now: Date, userBirthdayMMDD: string | null): boolean {
|
|
35
|
+
if (!userBirthdayMMDD) return false;
|
|
36
|
+
if (!/^\d{2}-\d{2}$/.test(userBirthdayMMDD)) return false;
|
|
37
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
38
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
39
|
+
return `${month}-${day}` === userBirthdayMMDD;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// New Year window — Jan 1–3 inclusive. Short enough that it stays
|
|
43
|
+
// special; 3 days covers the common "holiday still going" case
|
|
44
|
+
// (especially in Japan).
|
|
45
|
+
export function isNewYear(now: Date): boolean {
|
|
46
|
+
return now.getMonth() === 0 && now.getDate() >= 1 && now.getDate() <= 3;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Christmas window — Dec 24–25. Eve + day.
|
|
50
|
+
export function isChristmas(now: Date): boolean {
|
|
51
|
+
return now.getMonth() === 11 && (now.getDate() === 24 || now.getDate() === 25);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Overloaded: server 1-minute load average divided by logical core
|
|
55
|
+
// count. A ratio > 0.9 means "nearly all cores saturated for a full
|
|
56
|
+
// minute". Null input (no data yet, or Windows) → the rule is off.
|
|
57
|
+
export function isOverloaded(cpuLoadRatio: number | null): boolean {
|
|
58
|
+
if (cpuLoadRatio === null || !Number.isFinite(cpuLoadRatio)) return false;
|
|
59
|
+
return cpuLoadRatio > 0.9;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Many-unread threshold — tuned so a typical day with 1–3 sleepy
|
|
63
|
+
// tabs stays green, but a noisy ping storm bumps up to fuchsia.
|
|
64
|
+
export const MANY_UNREAD_THRESHOLD = 5;
|
|
65
|
+
export function isManyUnread(count: number): boolean {
|
|
66
|
+
return Number.isFinite(count) && count >= MANY_UNREAD_THRESHOLD;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Running-long threshold — anything past ~1 minute is a "real" run
|
|
70
|
+
// (code gen, long web searches) as opposed to a quick reply.
|
|
71
|
+
export const RUNNING_LONG_MS = 60 * 1000;
|
|
72
|
+
export function isRunningLong(runningSinceMs: number | null, now: Date): boolean {
|
|
73
|
+
if (runningSinceMs === null) return false;
|
|
74
|
+
const elapsed = now.getTime() - runningSinceMs;
|
|
75
|
+
return Number.isFinite(elapsed) && elapsed >= RUNNING_LONG_MS;
|
|
76
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Priority-ordered rule chain that picks the favicon's backing
|
|
2
|
+
// colour from the full runtime context. See
|
|
3
|
+
// `plans/done/feat-favicon-conditional-palette.md` for the reasoning
|
|
4
|
+
// behind the ordering and the chosen hex values.
|
|
5
|
+
//
|
|
6
|
+
// The function is pure: one input → one output. No clock, no DOM,
|
|
7
|
+
// no network. Every test pins the branch it wants by constructing
|
|
8
|
+
// the context verbatim.
|
|
9
|
+
|
|
10
|
+
import { FAVICON_REASONS, FAVICON_STATES, type FaviconContext, type FaviconPick } from "./types";
|
|
11
|
+
import { isBirthday, isChristmas, isLateNight, isManyUnread, isMorning, isNewYear, isOverloaded, isRunningLong, isWeekend } from "./conditions";
|
|
12
|
+
|
|
13
|
+
// Keeping the palette values adjacent to the resolver (rather than
|
|
14
|
+
// in a separate constants module) makes the "why does this colour
|
|
15
|
+
// fire?" audit a one-file read.
|
|
16
|
+
// "running" and "has-unread" no longer have background colors — the
|
|
17
|
+
// yellow dot (top-left) and red dot (top-right) communicate those
|
|
18
|
+
// states. The background is reserved for ambient context (error,
|
|
19
|
+
// load, calendar, time-of-day) plus the two *escalations* of the
|
|
20
|
+
// dot states (runningLong, manyUnread) that carry severity.
|
|
21
|
+
const COLORS = {
|
|
22
|
+
error: "#DC2626", // red-600
|
|
23
|
+
overloaded: "#EA580C", // orange-600 — machine is burning
|
|
24
|
+
manyUnread: "#D946EF", // fuchsia-500 — attention pile-up
|
|
25
|
+
runningLong: "#06B6D4", // cyan-500 — still thinking (> 60 s)
|
|
26
|
+
birthday: "#EAB308", // yellow-500
|
|
27
|
+
newYear: "#B91C1C", // red-700 — festive deeper red
|
|
28
|
+
christmas: "#15803D", // green-700 — festive deeper green
|
|
29
|
+
lateNight: "#6366F1", // indigo-500 — deep work
|
|
30
|
+
morning: "#F59E0B", // amber-500 — sunrise
|
|
31
|
+
weekend: "#14B8A6", // teal-500 — relaxed
|
|
32
|
+
idle: "#6B7280", // gray-500 — fallback
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
// Split the state-driven vs flavour branches so the priority
|
|
36
|
+
// argument is in one place and each helper stays under the
|
|
37
|
+
// cognitive-complexity threshold.
|
|
38
|
+
|
|
39
|
+
// Rules 1–4: error + load + escalations. These always fire when
|
|
40
|
+
// applicable, beating any flavour. The plain `running` and
|
|
41
|
+
// `hasUnread` rules are gone — the yellow / red corner dots own
|
|
42
|
+
// those signals now, so the background stays free for ambient
|
|
43
|
+
// context even while a session is running.
|
|
44
|
+
function resolveByState(ctx: FaviconContext): FaviconPick | null {
|
|
45
|
+
if (ctx.state === FAVICON_STATES.error) {
|
|
46
|
+
return { color: COLORS.error, reason: FAVICON_REASONS.error };
|
|
47
|
+
}
|
|
48
|
+
if (isOverloaded(ctx.cpuLoadRatio)) {
|
|
49
|
+
return { color: COLORS.overloaded, reason: FAVICON_REASONS.overloaded };
|
|
50
|
+
}
|
|
51
|
+
if (isManyUnread(ctx.sessionsUnreadCount)) {
|
|
52
|
+
return { color: COLORS.manyUnread, reason: FAVICON_REASONS.manyUnread };
|
|
53
|
+
}
|
|
54
|
+
if (ctx.state === FAVICON_STATES.running && isRunningLong(ctx.runningSinceMs, ctx.now)) {
|
|
55
|
+
return { color: COLORS.runningLong, reason: FAVICON_REASONS.runningLong };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Rules 7–12: flavour / easter eggs. Only consulted when no
|
|
61
|
+
// state-driven rule matched, so a running agent never gets a
|
|
62
|
+
// "cute" colour. Calendar beats clock; hour-based rules are
|
|
63
|
+
// disjoint so we pick whichever matches first.
|
|
64
|
+
function resolveByFlavour(ctx: FaviconContext): FaviconPick {
|
|
65
|
+
if (isBirthday(ctx.now, ctx.userBirthdayMMDD)) {
|
|
66
|
+
return { color: COLORS.birthday, reason: FAVICON_REASONS.birthday };
|
|
67
|
+
}
|
|
68
|
+
if (isNewYear(ctx.now)) {
|
|
69
|
+
return { color: COLORS.newYear, reason: FAVICON_REASONS.newYear };
|
|
70
|
+
}
|
|
71
|
+
if (isChristmas(ctx.now)) {
|
|
72
|
+
return { color: COLORS.christmas, reason: FAVICON_REASONS.christmas };
|
|
73
|
+
}
|
|
74
|
+
if (isLateNight(ctx.now)) {
|
|
75
|
+
return { color: COLORS.lateNight, reason: FAVICON_REASONS.lateNight };
|
|
76
|
+
}
|
|
77
|
+
if (isMorning(ctx.now)) {
|
|
78
|
+
return { color: COLORS.morning, reason: FAVICON_REASONS.morning };
|
|
79
|
+
}
|
|
80
|
+
if (isWeekend(ctx.now)) {
|
|
81
|
+
return { color: COLORS.weekend, reason: FAVICON_REASONS.weekend };
|
|
82
|
+
}
|
|
83
|
+
return { color: COLORS.idle, reason: FAVICON_REASONS.idle };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveFaviconColor(ctx: FaviconContext): FaviconPick {
|
|
87
|
+
return resolveByState(ctx) ?? resolveByFlavour(ctx);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Expose the palette for test assertions so the hex values aren't
|
|
91
|
+
// hard-coded in two places. Not exported from types.ts because it's
|
|
92
|
+
// resolver-implementation state, not part of the public contract.
|
|
93
|
+
export const FAVICON_COLORS = COLORS;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Shared types for the favicon palette resolver. Keeping them in a
|
|
2
|
+
// dedicated file lets `conditions.ts` and `resolveColor.ts` import
|
|
3
|
+
// them without cycling, and the unit tests pin the exact shape the
|
|
4
|
+
// context must satisfy.
|
|
5
|
+
|
|
6
|
+
export const FAVICON_STATES = {
|
|
7
|
+
idle: "idle",
|
|
8
|
+
running: "running",
|
|
9
|
+
done: "done",
|
|
10
|
+
error: "error",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type FaviconState = (typeof FAVICON_STATES)[keyof typeof FAVICON_STATES];
|
|
14
|
+
|
|
15
|
+
// Every possible path through `resolveColor` — the same enum used for
|
|
16
|
+
// the returned `reason` field so callers (and the log breadcrumb) can
|
|
17
|
+
// name the branch without memorising hex codes.
|
|
18
|
+
export const FAVICON_REASONS = {
|
|
19
|
+
error: "error",
|
|
20
|
+
overloaded: "overloaded",
|
|
21
|
+
manyUnread: "many-unread",
|
|
22
|
+
runningLong: "running-long",
|
|
23
|
+
birthday: "birthday",
|
|
24
|
+
newYear: "new-year",
|
|
25
|
+
christmas: "christmas",
|
|
26
|
+
lateNight: "late-night",
|
|
27
|
+
morning: "morning",
|
|
28
|
+
weekend: "weekend",
|
|
29
|
+
idle: "idle",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type FaviconReason = (typeof FAVICON_REASONS)[keyof typeof FAVICON_REASONS];
|
|
33
|
+
|
|
34
|
+
// The full runtime context fed to `resolveColor`. Everything is
|
|
35
|
+
// plumbed in so the function stays pure: no `new Date()` inside, no
|
|
36
|
+
// global fetches — test fixtures can pin any branch.
|
|
37
|
+
export interface FaviconContext {
|
|
38
|
+
state: FaviconState;
|
|
39
|
+
/** Unread across every session, not just the active one. */
|
|
40
|
+
sessionsUnreadCount: number;
|
|
41
|
+
/** Epoch ms when the current agent run started, or null if idle. */
|
|
42
|
+
runningSinceMs: number | null;
|
|
43
|
+
/** "now" — caller's clock. Tests pass a fixed Date. */
|
|
44
|
+
now: Date;
|
|
45
|
+
/**
|
|
46
|
+
* "MM-DD" if the user has a birthday stored in memory.md, else
|
|
47
|
+
* null. Null → the birthday rule is skipped.
|
|
48
|
+
*/
|
|
49
|
+
userBirthdayMMDD: string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Server's 1-minute load average divided by logical core count.
|
|
52
|
+
* `null` when the server hasn't reported yet or the platform
|
|
53
|
+
* doesn't support `loadavg` (Windows).
|
|
54
|
+
*/
|
|
55
|
+
cpuLoadRatio: number | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FaviconPick {
|
|
59
|
+
color: string;
|
|
60
|
+
reason: FaviconReason;
|
|
61
|
+
}
|