mulmoclaude 0.6.4 → 0.7.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 +1 -1
- package/client/assets/{html2canvas-CDGcmOD3-XVrO-eyz.js → html2canvas-CDGcmOD3-CKJ6vKPo.js} +1 -1
- package/client/assets/{index-zZIqEbNX.js → index-BG_JJcKI.js} +193 -197
- package/client/assets/index-DCoo3kpR.css +2 -0
- package/client/assets/{index.es-DqtpmBm8-DHT6q10o.js → index.es-DqtpmBm8-DFXjJgCa.js} +1 -1
- package/client/assets/lib-Dpph7PBN.js +114 -0
- package/client/assets/marp-cCGismx0.js +3452 -0
- package/client/assets/schemas-DuYzyHQc.js +64 -0
- package/client/index.html +5 -4
- package/package.json +7 -5
- package/server/agent/backend/claude-code.ts +44 -10
- package/server/agent/backend/fake-echo.ts +5 -1
- package/server/agent/config.ts +49 -12
- package/server/agent/mcp-server.ts +13 -2
- package/server/agent/mcp-tools/handlePermission.ts +115 -0
- package/server/agent/mcp-tools/index.ts +2 -1
- package/server/agent/mcp-tools/x.ts +18 -2
- package/server/agent/prompt.ts +3 -45
- package/server/api/csrfGuard.ts +122 -32
- package/server/api/routes/collections.ts +320 -0
- package/server/api/routes/config.ts +45 -0
- package/server/api/routes/feeds.ts +70 -0
- package/server/api/routes/files.ts +167 -0
- package/server/api/routes/marp-themes.ts +15 -0
- package/server/api/routes/mulmo-script.ts +139 -0
- package/server/api/routes/pdf.ts +108 -0
- package/server/api/routes/plugins.ts +10 -0
- package/server/api/routes/shortcuts.ts +49 -0
- package/server/api/routes/wiki.ts +35 -0
- package/server/build/dispatcher.mjs +40 -22
- package/server/events/notifications.ts +14 -9
- package/server/index.ts +49 -14
- package/server/plugins/preset-list.ts +18 -10
- package/server/plugins/preset-loader.ts +11 -4
- package/server/prompts/index.ts +1 -3
- package/server/prompts/system/system.md +7 -2
- package/server/system/env.ts +14 -0
- package/server/utils/clientDir.ts +7 -0
- package/server/utils/files/index.ts +0 -2
- package/server/utils/files/shortcuts-io.ts +63 -0
- package/server/utils/slug.ts +3 -4
- package/server/workspace/billing-migration.ts +69 -0
- package/server/workspace/collections/delete.ts +186 -0
- package/server/workspace/collections/discovery.ts +730 -0
- package/server/workspace/collections/index.ts +23 -0
- package/server/workspace/collections/io.ts +287 -0
- package/server/workspace/collections/notifications.ts +404 -0
- package/server/workspace/collections/paths.ts +125 -0
- package/server/workspace/collections/spawn.ts +213 -0
- package/server/workspace/collections/templatePath.ts +36 -0
- package/server/workspace/collections/types.ts +334 -0
- package/server/workspace/collections/watcher.ts +398 -0
- package/server/workspace/feeds/engine.ts +135 -0
- package/server/workspace/feeds/fetch/httpClient.ts +127 -0
- package/server/workspace/feeds/fetch/rssParser.ts +117 -0
- package/server/workspace/feeds/index.ts +8 -0
- package/server/workspace/feeds/ingestTypes.ts +66 -0
- package/server/workspace/feeds/pathResolver.ts +74 -0
- package/server/workspace/feeds/paths.ts +30 -0
- package/server/workspace/feeds/projectItem.ts +92 -0
- package/server/workspace/feeds/registry.ts +43 -0
- package/server/workspace/feeds/retrievers/httpJson.ts +19 -0
- package/server/workspace/feeds/retrievers/index.ts +27 -0
- package/server/workspace/feeds/retrievers/registerAll.ts +5 -0
- package/server/workspace/feeds/retrievers/rss.ts +24 -0
- package/server/workspace/feeds/state.ts +55 -0
- package/server/workspace/helps/billing-clients-worklog.md +215 -0
- package/server/workspace/helps/billing-invoice.md +457 -0
- package/server/workspace/helps/collection-skills.md +664 -0
- package/server/workspace/helps/feeds.md +110 -0
- package/server/workspace/helps/index.md +9 -3
- package/server/workspace/helps/portfolio-tracker.md +211 -0
- package/server/workspace/helps/presentation-deck.md +828 -0
- package/server/workspace/helps/todo-collection.md +140 -0
- package/server/workspace/helps/vocabulary.md +106 -0
- package/server/workspace/hooks/handlers/skillBridge.ts +101 -40
- package/server/workspace/marp-themes.ts +46 -0
- package/server/workspace/paths.ts +46 -11
- package/server/workspace/skills-preset/mc-manage-skills/SKILL.md +13 -0
- package/server/workspace/skills-preset/mc-wiki-deep-lint/SKILL.md +108 -0
- package/server/workspace/skills-preset/mc-wiki-health-check/SKILL.md +61 -0
- package/server/workspace/skills-preset/mc-wiki-ingest/SKILL.md +182 -0
- package/server/workspace/skills-preset/mc-wiki-promote/SKILL.md +175 -0
- package/server/workspace/skills-preset.ts +376 -2
- package/server/workspace/wiki-pages/io.ts +34 -2
- package/server/workspace/workspace.ts +20 -1
- package/src/App.vue +70 -41
- package/src/components/BackendOfflineBanner.vue +56 -0
- package/src/components/ChatInput.vue +72 -6
- package/src/components/CollectionCalendarView.vue +243 -0
- package/src/components/CollectionDashboardView.vue +181 -0
- package/src/components/CollectionDayView.vue +308 -0
- package/src/components/CollectionEmbedView.vue +69 -0
- package/src/components/CollectionKanbanView.vue +196 -0
- package/src/components/CollectionRecordModal.vue +93 -0
- package/src/components/CollectionRecordPanel.vue +567 -0
- package/src/components/CollectionView.vue +1748 -0
- package/src/components/CollectionsIndexView.vue +152 -0
- package/src/components/ConfirmModal.vue +344 -0
- package/src/components/FeedsView.vue +225 -0
- package/src/components/FileContentRenderer.vue +122 -30
- package/src/components/FileTree.vue +218 -2
- package/src/components/FileTreePane.vue +2 -0
- package/src/components/FilesView.vue +95 -17
- package/src/components/PinToggle.vue +52 -0
- package/src/components/PluginLauncher.vue +97 -37
- package/src/components/RightSidebar.vue +74 -3
- package/src/components/RolesView.vue +1 -1
- package/src/components/SettingsModal.vue +146 -72
- package/src/components/SlashCommandMenu.vue +56 -0
- package/src/components/StackView.vue +128 -48
- package/src/components/collectionEmbed.ts +29 -0
- package/src/components/collectionTypes.ts +177 -0
- package/src/composables/collections/useCollectionRendering.ts +350 -0
- package/src/composables/useConfirm.ts +70 -0
- package/src/composables/useFileTree.ts +35 -3
- package/src/composables/useImeAwareEnter.ts +14 -0
- package/src/composables/usePdfDownload.ts +6 -1
- package/src/composables/useShortcuts.ts +163 -0
- package/src/composables/useSlashCommandMenu.ts +138 -0
- package/src/config/apiRoutes.ts +46 -13
- package/src/config/createFilePolicy.ts +82 -0
- package/src/config/roles.ts +46 -47
- package/src/config/systemFileDescriptors.ts +0 -30
- package/src/config/toolNames.ts +1 -5
- package/src/config/workspacePaths.ts +4 -9
- package/src/lang/de.ts +154 -221
- package/src/lang/en.ts +153 -218
- package/src/lang/es.ts +154 -219
- package/src/lang/fr.ts +155 -221
- package/src/lang/index.ts +55 -0
- package/src/lang/ja.ts +153 -219
- package/src/lang/ko.ts +153 -218
- package/src/lang/pt-BR.ts +154 -219
- package/src/lang/zh.ts +152 -218
- package/src/lib/vue-i18n.ts +15 -45
- package/src/lib/wiki-page/graph.ts +108 -0
- package/src/main.ts +2 -5
- package/src/plugins/_generated/metas.ts +2 -8
- package/src/plugins/_generated/registrations.ts +2 -4
- package/src/plugins/_generated/server-bindings.ts +5 -12
- package/src/plugins/manageSkills/View.vue +36 -68
- package/src/plugins/manageSkills/presetDetection.ts +25 -0
- package/src/plugins/markdown/MarpView.vue +301 -0
- package/src/plugins/markdown/Preview.vue +26 -3
- package/src/plugins/markdown/View.vue +230 -1
- package/src/plugins/markdown/definition.ts +21 -1
- package/src/plugins/presentCollection/Preview.vue +30 -0
- package/src/plugins/presentCollection/View.vue +78 -0
- package/src/plugins/presentCollection/definition.ts +30 -0
- package/src/plugins/presentCollection/index.ts +25 -0
- package/src/plugins/presentCollection/meta.ts +15 -0
- package/src/plugins/presentCollection/plugin.ts +39 -0
- package/src/plugins/presentCollection/types.ts +13 -0
- package/src/plugins/presentForm/View.vue +56 -6
- package/src/plugins/presentForm/types.ts +3 -3
- package/src/plugins/presentMulmoScript/View.vue +252 -5
- package/src/plugins/presentMulmoScript/helpers.ts +18 -0
- package/src/plugins/presentMulmoScript/meta.ts +11 -0
- package/src/plugins/scheduler/AutomationsView.vue +13 -11
- package/src/plugins/scheduler/automationsDefinition.ts +7 -10
- package/src/plugins/scheduler/automationsMeta.ts +27 -0
- package/src/plugins/scheduler/index.ts +19 -38
- package/src/plugins/wiki/View.vue +120 -27
- package/src/plugins/wiki/components/WikiGraphView.vue +75 -0
- package/src/plugins/wiki/components/WikiPageBody.vue +18 -0
- package/src/plugins/wiki/index.ts +6 -0
- package/src/plugins/wiki/route.ts +8 -1
- package/src/router/index.ts +31 -34
- package/src/router/pageRoutes.ts +2 -7
- package/src/tools/types.ts +4 -3
- package/src/types/notification.ts +5 -9
- package/src/types/session.ts +11 -0
- package/src/types/shortcuts.ts +37 -0
- package/src/utils/agent/eventDispatch.ts +4 -0
- package/src/utils/api.ts +47 -1
- package/src/utils/canvas/stackGrouping.ts +96 -0
- package/src/utils/chat/permalink.ts +20 -0
- package/src/utils/collections/actionVisible.ts +55 -0
- package/src/utils/collections/calendarGrid.ts +328 -0
- package/src/utils/collections/collectionViewMode.ts +42 -0
- package/src/utils/collections/derivedFormula.ts +364 -0
- package/src/utils/collections/draft.ts +160 -0
- package/src/utils/collections/enumColors.ts +130 -0
- package/src/utils/collections/itemLabel.ts +42 -0
- package/src/utils/confirmDelete.ts +13 -0
- package/src/utils/markdown/marpAspect.ts +28 -0
- package/src/utils/markdown/marpCustomSize.ts +120 -0
- package/src/utils/markdown/marpDetect.ts +15 -0
- package/src/utils/markdown/marpTheme.ts +109 -0
- package/src/utils/markdown/wikiEmbedHandlers.ts +1 -1
- package/src/utils/path/workspaceLinkRouter.ts +126 -9
- package/src/utils/session/sessionEntries.ts +1 -0
- package/src/utils/session/sessionFactory.ts +1 -0
- package/src/utils/session/sessionHelpers.ts +6 -1
- package/client/assets/index-CyBr8Mkr.css +0 -2
- package/server/api/routes/encore.ts +0 -55
- package/server/api/routes/news.ts +0 -133
- package/server/api/routes/sources.ts +0 -550
- package/server/encore/INVARIANTS.md +0 -272
- package/server/encore/boot.ts +0 -39
- package/server/encore/closure.ts +0 -36
- package/server/encore/cycle.ts +0 -276
- package/server/encore/dispatch.ts +0 -103
- package/server/encore/handlers/amend.ts +0 -99
- package/server/encore/handlers/appendNote.ts +0 -74
- package/server/encore/handlers/defineEncore.ts +0 -42
- package/server/encore/handlers/listTickets.ts +0 -107
- package/server/encore/handlers/markStepDone.ts +0 -41
- package/server/encore/handlers/markTargetSkipped.ts +0 -33
- package/server/encore/handlers/query.ts +0 -138
- package/server/encore/handlers/recordValues.ts +0 -44
- package/server/encore/handlers/resolveNotification.ts +0 -121
- package/server/encore/handlers/setup.ts +0 -81
- package/server/encore/handlers/shared.ts +0 -137
- package/server/encore/handlers/snooze.ts +0 -87
- package/server/encore/handlers/startObligationChat.ts +0 -64
- package/server/encore/handlers/startSetupChat.ts +0 -50
- package/server/encore/lock.ts +0 -61
- package/server/encore/notifier.ts +0 -123
- package/server/encore/obligation.ts +0 -25
- package/server/encore/paths.ts +0 -78
- package/server/encore/reconcile.ts +0 -661
- package/server/encore/tick.ts +0 -191
- package/server/encore/yaml-fm.ts +0 -63
- package/server/prompts/system/news-concierge.md +0 -24
- package/server/prompts/system/sources-context.md +0 -16
- package/server/utils/files/encore-io.ts +0 -111
- package/server/workspace/helps/encore-dsl.md +0 -482
- package/server/workspace/helps/sources.md +0 -42
- package/server/workspace/news/reader.ts +0 -247
- package/server/workspace/skills-preset/mc-manage-sources/SKILL.md +0 -106
- package/server/workspace/sources/arxivDiscovery.ts +0 -182
- package/server/workspace/sources/classifier.ts +0 -268
- package/server/workspace/sources/fetchers/arxiv.ts +0 -170
- package/server/workspace/sources/fetchers/github.ts +0 -106
- package/server/workspace/sources/fetchers/githubIssues.ts +0 -210
- package/server/workspace/sources/fetchers/githubReleases.ts +0 -186
- package/server/workspace/sources/fetchers/index.ts +0 -71
- package/server/workspace/sources/fetchers/registerAll.ts +0 -15
- package/server/workspace/sources/fetchers/rss.ts +0 -141
- package/server/workspace/sources/fetchers/rssParser.ts +0 -295
- package/server/workspace/sources/httpFetcher.ts +0 -230
- package/server/workspace/sources/interests.ts +0 -120
- package/server/workspace/sources/paths.ts +0 -110
- package/server/workspace/sources/pipeline/dedup.ts +0 -60
- package/server/workspace/sources/pipeline/fetch.ts +0 -182
- package/server/workspace/sources/pipeline/index.ts +0 -301
- package/server/workspace/sources/pipeline/notify.ts +0 -80
- package/server/workspace/sources/pipeline/plan.ts +0 -68
- package/server/workspace/sources/pipeline/summarize.ts +0 -189
- package/server/workspace/sources/pipeline/write.ts +0 -185
- package/server/workspace/sources/rateLimiter.ts +0 -148
- package/server/workspace/sources/registry.ts +0 -304
- package/server/workspace/sources/robots.ts +0 -271
- package/server/workspace/sources/sourceState.ts +0 -142
- package/server/workspace/sources/taxonomy.ts +0 -74
- package/server/workspace/sources/types.ts +0 -153
- package/server/workspace/sources/urls.ts +0 -112
- package/src/components/NewsView.vue +0 -267
- package/src/components/SourcesManager.vue +0 -915
- package/src/components/SourcesView.vue +0 -45
- package/src/components/TodoExplorer.vue +0 -423
- package/src/components/todo/TodoAddDialog.vue +0 -135
- package/src/components/todo/TodoEditDialog.vue +0 -51
- package/src/components/todo/TodoEditPanel.vue +0 -117
- package/src/components/todo/TodoKanbanView.vue +0 -290
- package/src/components/todo/TodoListView.vue +0 -88
- package/src/components/todo/TodoTableView.vue +0 -210
- package/src/composables/useNewsItems.ts +0 -38
- package/src/composables/useNewsReadState.ts +0 -75
- package/src/plugins/encore/EncoreDashboard.vue +0 -504
- package/src/plugins/encore/EncoreRedirect.vue +0 -116
- package/src/plugins/encore/View.vue +0 -36
- package/src/plugins/encore/defineEncoreDefinition.ts +0 -74
- package/src/plugins/encore/defineEncoreMeta.ts +0 -13
- package/src/plugins/encore/index.ts +0 -93
- package/src/plugins/encore/manageEncoreDefinition.ts +0 -100
- package/src/plugins/encore/manageEncoreMeta.ts +0 -36
- package/src/plugins/manageSource/Preview.vue +0 -33
- package/src/plugins/manageSource/View.vue +0 -13
- package/src/plugins/manageSource/definition.ts +0 -66
- package/src/plugins/manageSource/index.ts +0 -75
- package/src/plugins/manageSource/meta.ts +0 -21
- package/src/plugins/scheduler/CalendarView.vue +0 -23
- package/src/plugins/scheduler/Preview.vue +0 -73
- package/src/plugins/scheduler/View.vue +0 -608
- package/src/plugins/scheduler/calendarDefinition.ts +0 -47
- package/src/plugins/scheduler/calendarMeta.ts +0 -28
- package/src/plugins/scheduler/multiDayHelpers.ts +0 -95
- package/src/plugins/scheduler/viewModes.ts +0 -26
- package/src/types/encore-dsl/at-expression.ts +0 -120
- package/src/types/encore-dsl/at-resolver.ts +0 -32
- package/src/types/encore-dsl/cadence.ts +0 -289
- package/src/types/encore-dsl/schema.ts +0 -288
- package/src/utils/filesPreview/schedulerPreview.ts +0 -44
- package/src/utils/filesPreview/todoPreview.ts +0 -51
- package/src/utils/sources/filter.ts +0 -69
- /package/client/assets/{JsonEditor-D6WBWLoa.js → JsonEditor-C_RDoefj.js} +0 -0
- /package/client/assets/{chunk-D8eiyYIV-LcKZGJv5.js → chunk-D8eiyYIV-BY16KEZc.js} +0 -0
- /package/client/assets/{purify.es-Fx1Nqyry-Dwtk-9WZ.js → purify.es-Fx1Nqyry-BufT4RJl.js} +0 -0
- /package/client/assets/{typeof-DBp4T-Ny-CSr8wx1e.js → typeof-DBp4T-Ny-z2wCIsir.js} +0 -0
|
@@ -1,504 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// Encore dashboard — read-only landing for /encore.
|
|
3
|
-
//
|
|
4
|
-
// What this surface IS:
|
|
5
|
-
// - A browser over `obligations/<id>/index.md` + every
|
|
6
|
-
// `obligations/<id>/<cycleId>.md` cycle file on disk.
|
|
7
|
-
// - One row per obligation with cadence / targets / current-cycle
|
|
8
|
-
// status. Expanding the row reveals that obligation's cycle
|
|
9
|
-
// history (reverse-chron).
|
|
10
|
-
//
|
|
11
|
-
// What this surface IS NOT:
|
|
12
|
-
// - Not editable. There is no "mark done" / "snooze" / "create"
|
|
13
|
-
// button — those are LLM-only verbs. The user reaches them by
|
|
14
|
-
// asking in chat (the bell flow handles notification clicks).
|
|
15
|
-
// - Not a notification surface. The bell stays the channel for
|
|
16
|
-
// "the LLM wants your attention"; this page just lists what
|
|
17
|
-
// Encore is tracking and what already happened.
|
|
18
|
-
//
|
|
19
|
-
// Data source: a single `kind: "query"` dispatch with `range: "all"`,
|
|
20
|
-
// which returns every obligation + every cycle in one round trip.
|
|
21
|
-
// No new server endpoint was added for the page.
|
|
22
|
-
|
|
23
|
-
import { computed, onMounted, ref } from "vue";
|
|
24
|
-
import { useI18n } from "vue-i18n";
|
|
25
|
-
import { pluginEndpoints } from "../api";
|
|
26
|
-
import { apiCall } from "../../utils/api";
|
|
27
|
-
import { META } from "./manageEncoreMeta";
|
|
28
|
-
import type { EncoreEndpoints } from "./manageEncoreDefinition";
|
|
29
|
-
import type { EncoreDsl, StepDef } from "../../types/encore-dsl/schema";
|
|
30
|
-
import type { Cadence } from "../../types/encore-dsl/cadence";
|
|
31
|
-
|
|
32
|
-
// Wire shape mirrors `server/encore/handlers/query.ts`. The
|
|
33
|
-
// per-target record matches `TargetRecord` from `server/encore/cycle.ts`
|
|
34
|
-
// — duplicated here because that module lives outside `src/` and
|
|
35
|
-
// can't be imported from a Vue component.
|
|
36
|
-
interface TargetRecord {
|
|
37
|
-
values?: Record<string, unknown>;
|
|
38
|
-
skipped?: string;
|
|
39
|
-
completedSteps?: Record<string, string>;
|
|
40
|
-
snoozedSteps?: Record<string, string>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface CycleState {
|
|
44
|
-
cycleId: string;
|
|
45
|
-
cycleStart: string;
|
|
46
|
-
cycleDeadline: string;
|
|
47
|
-
records: Record<string, TargetRecord>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface QueryCycleResult {
|
|
51
|
-
cycleId: string;
|
|
52
|
-
path: string;
|
|
53
|
-
state: CycleState;
|
|
54
|
-
body: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface QueryObligationResult {
|
|
58
|
-
obligationId: string;
|
|
59
|
-
indexPath: string;
|
|
60
|
-
dsl: EncoreDsl;
|
|
61
|
-
body: string;
|
|
62
|
-
cycles: QueryCycleResult[];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface QueryResponse {
|
|
66
|
-
ok: boolean;
|
|
67
|
-
message: string;
|
|
68
|
-
obligations?: QueryObligationResult[];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface TicketSummary {
|
|
72
|
-
pendingId: string;
|
|
73
|
-
obligationId: string;
|
|
74
|
-
cycleId: string;
|
|
75
|
-
notificationId: string;
|
|
76
|
-
stepId: string;
|
|
77
|
-
createdAt: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface ListTicketsResponse {
|
|
81
|
-
ok: boolean;
|
|
82
|
-
message: string;
|
|
83
|
-
tickets?: TicketSummary[];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const { t } = useI18n();
|
|
87
|
-
|
|
88
|
-
const loading = ref(true);
|
|
89
|
-
const errorMessage = ref<string | null>(null);
|
|
90
|
-
const obligations = ref<QueryObligationResult[]>([]);
|
|
91
|
-
const tickets = ref<TicketSummary[]>([]);
|
|
92
|
-
const expanded = ref<Record<string, boolean>>({});
|
|
93
|
-
|
|
94
|
-
async function loadObligations(): Promise<void> {
|
|
95
|
-
loading.value = true;
|
|
96
|
-
errorMessage.value = null;
|
|
97
|
-
try {
|
|
98
|
-
const endpoints = pluginEndpoints<EncoreEndpoints>(META.apiNamespace);
|
|
99
|
-
const { method, url } = endpoints.dispatch;
|
|
100
|
-
// Query + listTickets in parallel — they're independent reads
|
|
101
|
-
// and we want them both before the row chips/bell render.
|
|
102
|
-
const [queryResponse, ticketsResponse] = await Promise.all([
|
|
103
|
-
apiCall<QueryResponse>(url, { method, body: { kind: "query", range: "all" } }),
|
|
104
|
-
apiCall<ListTicketsResponse>(url, { method, body: { kind: "listTickets" } }),
|
|
105
|
-
]);
|
|
106
|
-
if (!queryResponse.ok) {
|
|
107
|
-
errorMessage.value = queryResponse.error;
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
obligations.value = queryResponse.data.obligations ?? [];
|
|
111
|
-
// A ticket-fetch failure shouldn't block the dashboard — degrade
|
|
112
|
-
// gracefully (bells just won't render). Surface the error in the
|
|
113
|
-
// console for debug visibility.
|
|
114
|
-
if (ticketsResponse.ok) {
|
|
115
|
-
tickets.value = ticketsResponse.data.tickets ?? [];
|
|
116
|
-
} else {
|
|
117
|
-
tickets.value = [];
|
|
118
|
-
console.warn("encore-dashboard: listTickets failed —", ticketsResponse.error);
|
|
119
|
-
}
|
|
120
|
-
} catch (err) {
|
|
121
|
-
errorMessage.value = err instanceof Error ? err.message : String(err);
|
|
122
|
-
} finally {
|
|
123
|
-
loading.value = false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
onMounted(() => {
|
|
128
|
-
void loadObligations();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// (obligationId, cycleId) → tickets[] for the per-cycle bell badge.
|
|
132
|
-
// Computed so it stays in sync when either the query or the ticket
|
|
133
|
-
// list reloads. The key is a composite string because Records can't
|
|
134
|
-
// be indexed by a tuple.
|
|
135
|
-
const ticketsByCycle = computed<Record<string, TicketSummary[]>>(() => {
|
|
136
|
-
const map: Record<string, TicketSummary[]> = {};
|
|
137
|
-
for (const ticket of tickets.value) {
|
|
138
|
-
const key = `${ticket.obligationId}/${ticket.cycleId}`;
|
|
139
|
-
if (!map[key]) map[key] = [];
|
|
140
|
-
map[key].push(ticket);
|
|
141
|
-
}
|
|
142
|
-
return map;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
function cycleTickets(obligationId: string, cycleId: string): TicketSummary[] {
|
|
146
|
-
return ticketsByCycle.value[`${obligationId}/${cycleId}`] ?? [];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Bell click: navigate to the same URL the host's NotificationBell
|
|
150
|
-
// would build for this ticket (/encore?pendingId=…¬ificationId=…),
|
|
151
|
-
// then EncoreRedirect handles the resolveNotification dispatch and
|
|
152
|
-
// onward redirect to /chat/<chatId>. If the obligation has multiple
|
|
153
|
-
// live tickets we route to the first — the dashboard isn't the
|
|
154
|
-
// place to triage; resolving one usually unblocks the row.
|
|
155
|
-
function openTicket(ticket: TicketSummary): void {
|
|
156
|
-
const params = new URLSearchParams({
|
|
157
|
-
pendingId: ticket.pendingId,
|
|
158
|
-
notificationId: ticket.notificationId,
|
|
159
|
-
});
|
|
160
|
-
window.location.href = `/encore?${params.toString()}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function toggle(obligationId: string): void {
|
|
164
|
-
expanded.value[obligationId] = !expanded.value[obligationId];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Per-obligation chat button → server-side `startObligationChat`
|
|
168
|
-
// kind seeds a fresh chat with the obligation in context, then we
|
|
169
|
-
// full-navigate to /chat/<chatId> (same trick the bell-click flow
|
|
170
|
-
// uses so the seeded turn renders on first paint). The button
|
|
171
|
-
// stops propagation so clicking it doesn't also toggle the row.
|
|
172
|
-
const chatStarting = ref<Record<string, boolean>>({});
|
|
173
|
-
|
|
174
|
-
interface StartObligationChatResult {
|
|
175
|
-
ok: boolean;
|
|
176
|
-
chatId?: string;
|
|
177
|
-
navigateTo?: string;
|
|
178
|
-
error?: string;
|
|
179
|
-
message?: string;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function startChatForObligation(obligationId: string): Promise<void> {
|
|
183
|
-
if (chatStarting.value[obligationId]) return;
|
|
184
|
-
chatStarting.value[obligationId] = true;
|
|
185
|
-
try {
|
|
186
|
-
const endpoints = pluginEndpoints<EncoreEndpoints>(META.apiNamespace);
|
|
187
|
-
const { method, url } = endpoints.dispatch;
|
|
188
|
-
const response = await apiCall<StartObligationChatResult>(url, {
|
|
189
|
-
method,
|
|
190
|
-
body: { kind: "startObligationChat", obligationId },
|
|
191
|
-
});
|
|
192
|
-
if (!response.ok) {
|
|
193
|
-
errorMessage.value = response.error;
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
const result = response.data;
|
|
197
|
-
if (!result.ok || !result.chatId) {
|
|
198
|
-
errorMessage.value = result.error ?? t("encoreDashboard.unexpectedResponse");
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
window.location.href = result.navigateTo ?? `/chat/${result.chatId}`;
|
|
202
|
-
} catch (err) {
|
|
203
|
-
errorMessage.value = err instanceof Error ? err.message : String(err);
|
|
204
|
-
} finally {
|
|
205
|
-
chatStarting.value[obligationId] = false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// "+ Add" toolbar button → server-side `startSetupChat` kind seeds
|
|
210
|
-
// a new chat asking the LLM to walk the user through creating a
|
|
211
|
-
// fresh obligation, then full-navigates. Mirrors
|
|
212
|
-
// `startChatForObligation` but without an obligationId.
|
|
213
|
-
const setupStarting = ref(false);
|
|
214
|
-
|
|
215
|
-
async function startSetupChat(): Promise<void> {
|
|
216
|
-
if (setupStarting.value) return;
|
|
217
|
-
setupStarting.value = true;
|
|
218
|
-
try {
|
|
219
|
-
const endpoints = pluginEndpoints<EncoreEndpoints>(META.apiNamespace);
|
|
220
|
-
const { method, url } = endpoints.dispatch;
|
|
221
|
-
const response = await apiCall<StartObligationChatResult>(url, {
|
|
222
|
-
method,
|
|
223
|
-
body: { kind: "startSetupChat" },
|
|
224
|
-
});
|
|
225
|
-
if (!response.ok) {
|
|
226
|
-
errorMessage.value = response.error;
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
const result = response.data;
|
|
230
|
-
if (!result.ok || !result.chatId) {
|
|
231
|
-
errorMessage.value = result.error ?? t("encoreDashboard.unexpectedResponse");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
window.location.href = result.navigateTo ?? `/chat/${result.chatId}`;
|
|
235
|
-
} catch (err) {
|
|
236
|
-
errorMessage.value = err instanceof Error ? err.message : String(err);
|
|
237
|
-
} finally {
|
|
238
|
-
setupStarting.value = false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ── derived closure (mirrors server/encore/closure.ts) ──
|
|
243
|
-
|
|
244
|
-
function isStepClosed(record: TargetRecord | undefined, step: StepDef): boolean {
|
|
245
|
-
if (!record) return false;
|
|
246
|
-
if (record.skipped) return true;
|
|
247
|
-
return Boolean(record.completedSteps?.[step.id]);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function isTargetClosed(record: TargetRecord | undefined, dsl: EncoreDsl): boolean {
|
|
251
|
-
if (!record) return false;
|
|
252
|
-
if (record.skipped) return true;
|
|
253
|
-
return dsl.steps.every((step) => isStepClosed(record, step));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function isCycleClosed(state: CycleState, dsl: EncoreDsl): boolean {
|
|
257
|
-
return dsl.targets.every((target) => isTargetClosed(state.records[target.id], dsl));
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ── display helpers ──
|
|
261
|
-
|
|
262
|
-
function cadenceLabel(cadence: Cadence): string {
|
|
263
|
-
return t(`encoreDashboard.cadence.${cadence.type}`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function statusLabel(status: EncoreDsl["status"]): string {
|
|
267
|
-
return t(`encoreDashboard.status.${status}`);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function statusClasses(status: EncoreDsl["status"]): string {
|
|
271
|
-
if (status === "active") return "bg-green-100 text-green-700";
|
|
272
|
-
if (status === "paused") return "bg-yellow-100 text-yellow-700";
|
|
273
|
-
return "bg-gray-200 text-gray-600";
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function targetCountLabel(dsl: EncoreDsl): string {
|
|
277
|
-
return t("encoreDashboard.targetCount", { count: dsl.targets.length });
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function formatDate(iso: string): string {
|
|
281
|
-
// Cycle window timestamps are ISO; show date-only for readability.
|
|
282
|
-
return iso.slice(0, 10);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
type StepStatus = "done" | "skipped" | "open";
|
|
286
|
-
|
|
287
|
-
function cycleStepStatus(state: CycleState, target: { id: string }, step: StepDef): StepStatus {
|
|
288
|
-
const record = state.records[target.id];
|
|
289
|
-
if (record?.skipped) return "skipped";
|
|
290
|
-
if (record?.completedSteps?.[step.id]) return "done";
|
|
291
|
-
return "open";
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function stepStatusIcon(status: StepStatus): string {
|
|
295
|
-
if (status === "done") return "check_circle";
|
|
296
|
-
if (status === "skipped") return "remove_circle_outline";
|
|
297
|
-
return "radio_button_unchecked";
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function stepStatusClasses(status: StepStatus): string {
|
|
301
|
-
// Done is settled history — grey, same theme as the "Closed" chip.
|
|
302
|
-
// Skipped sits a touch lighter; open is the live state, so it
|
|
303
|
-
// keeps the blue accent.
|
|
304
|
-
if (status === "done") return "text-gray-500";
|
|
305
|
-
if (status === "skipped") return "text-gray-400";
|
|
306
|
-
return "text-blue-500";
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Cycles to display under an obligation header, reverse-chron.
|
|
310
|
-
// - When the row is expanded: full history.
|
|
311
|
-
// - When the row is collapsed: only cycles that have a live ticket.
|
|
312
|
-
// This is the always-visible "pending" surface — the user sees
|
|
313
|
-
// what needs attention without clicking expand.
|
|
314
|
-
// Server returns cycles ascending; we reverse for newest-first.
|
|
315
|
-
function visibleCycles(item: QueryObligationResult): QueryCycleResult[] {
|
|
316
|
-
const all = [...item.cycles].reverse();
|
|
317
|
-
if (expanded.value[item.obligationId]) return all;
|
|
318
|
-
return all.filter((cycle) => cycleTickets(item.obligationId, cycle.cycleId).length > 0);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Sort: active first, then paused, then retired. Within each
|
|
322
|
-
// status, alphabetical by displayName.
|
|
323
|
-
const sortedObligations = computed<QueryObligationResult[]>(() => {
|
|
324
|
-
const statusRank: Record<EncoreDsl["status"], number> = { active: 0, paused: 1, retired: 2 };
|
|
325
|
-
return [...obligations.value].sort((lhs, rhs) => {
|
|
326
|
-
const statusDiff = statusRank[lhs.dsl.status] - statusRank[rhs.dsl.status];
|
|
327
|
-
if (statusDiff !== 0) return statusDiff;
|
|
328
|
-
return lhs.dsl.displayName.localeCompare(rhs.dsl.displayName);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
interface RecordedValue {
|
|
333
|
-
key: string;
|
|
334
|
-
value: string;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function recordedValuesForTarget(state: CycleState, targetId: string): RecordedValue[] {
|
|
338
|
-
const values = state.records[targetId]?.values;
|
|
339
|
-
if (!values) return [];
|
|
340
|
-
return Object.entries(values)
|
|
341
|
-
.filter(([, value]) => value !== null && value !== undefined && value !== "")
|
|
342
|
-
.map(([key, value]) => ({
|
|
343
|
-
key,
|
|
344
|
-
value: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : JSON.stringify(value),
|
|
345
|
-
}));
|
|
346
|
-
}
|
|
347
|
-
</script>
|
|
348
|
-
|
|
349
|
-
<template>
|
|
350
|
-
<div class="h-full overflow-y-auto" data-testid="encore-dashboard">
|
|
351
|
-
<div class="max-w-4xl mx-auto px-4 py-6">
|
|
352
|
-
<div class="flex items-center justify-between mb-1">
|
|
353
|
-
<h1 class="text-xl font-semibold text-gray-800">{{ t("encoreDashboard.title") }}</h1>
|
|
354
|
-
<button
|
|
355
|
-
type="button"
|
|
356
|
-
class="h-8 px-2.5 flex items-center gap-1 rounded bg-blue-600 text-white text-sm hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-wait"
|
|
357
|
-
:disabled="setupStarting"
|
|
358
|
-
data-testid="encore-add-button"
|
|
359
|
-
@click="startSetupChat()"
|
|
360
|
-
>
|
|
361
|
-
<span class="material-icons text-base">{{ setupStarting ? "hourglass_empty" : "add" }}</span>
|
|
362
|
-
<span>{{ t("encoreDashboard.addButtonLabel") }}</span>
|
|
363
|
-
</button>
|
|
364
|
-
</div>
|
|
365
|
-
<p class="text-sm text-gray-500 mb-6">{{ t("encoreDashboard.subtitle") }}</p>
|
|
366
|
-
|
|
367
|
-
<div v-if="loading" class="text-sm text-gray-500">{{ t("encoreDashboard.loading") }}</div>
|
|
368
|
-
|
|
369
|
-
<div v-else-if="errorMessage" class="text-sm text-red-600">{{ t("encoreDashboard.errorPrefix") }}{{ errorMessage }}</div>
|
|
370
|
-
|
|
371
|
-
<div v-else-if="sortedObligations.length === 0" class="text-sm text-gray-500 border border-dashed border-gray-300 rounded p-6 text-center">
|
|
372
|
-
{{ t("encoreDashboard.empty") }}
|
|
373
|
-
</div>
|
|
374
|
-
|
|
375
|
-
<ul v-else class="space-y-2">
|
|
376
|
-
<li
|
|
377
|
-
v-for="item in sortedObligations"
|
|
378
|
-
:key="item.obligationId"
|
|
379
|
-
class="border border-gray-200 rounded bg-white"
|
|
380
|
-
:data-testid="`encore-obligation-${item.obligationId}`"
|
|
381
|
-
>
|
|
382
|
-
<div class="flex items-stretch hover:bg-gray-50 transition-colors">
|
|
383
|
-
<button
|
|
384
|
-
type="button"
|
|
385
|
-
class="flex-1 min-w-0 px-4 py-3 flex items-center gap-3 text-left"
|
|
386
|
-
:aria-expanded="!!expanded[item.obligationId]"
|
|
387
|
-
@click="toggle(item.obligationId)"
|
|
388
|
-
>
|
|
389
|
-
<span class="material-icons text-gray-400 text-base">{{ expanded[item.obligationId] ? "expand_more" : "chevron_right" }}</span>
|
|
390
|
-
<span class="flex-1 min-w-0">
|
|
391
|
-
<span class="block text-sm font-medium text-gray-800 truncate">{{ item.dsl.displayName }}</span>
|
|
392
|
-
<span class="block text-xs text-gray-500 mt-0.5">
|
|
393
|
-
{{ cadenceLabel(item.dsl.cadence) }} · {{ targetCountLabel(item.dsl) }} · {{ item.cycles.length }} {{ t("encoreDashboard.cyclesSuffix") }}
|
|
394
|
-
</span>
|
|
395
|
-
</span>
|
|
396
|
-
<!-- "Active" is the default and would just be noise on
|
|
397
|
-
every row — show the chip only for the off-normal
|
|
398
|
-
states (paused / retired). -->
|
|
399
|
-
<span
|
|
400
|
-
v-if="item.dsl.status !== 'active'"
|
|
401
|
-
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
|
402
|
-
:class="statusClasses(item.dsl.status)"
|
|
403
|
-
>
|
|
404
|
-
{{ statusLabel(item.dsl.status) }}
|
|
405
|
-
</span>
|
|
406
|
-
</button>
|
|
407
|
-
<button
|
|
408
|
-
type="button"
|
|
409
|
-
class="px-3 flex items-center justify-center text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors disabled:opacity-50 disabled:cursor-wait"
|
|
410
|
-
:title="t('encoreDashboard.chatButtonTitle')"
|
|
411
|
-
:aria-label="t('encoreDashboard.chatButtonTitle')"
|
|
412
|
-
:disabled="!!chatStarting[item.obligationId]"
|
|
413
|
-
:data-testid="`encore-obligation-chat-${item.obligationId}`"
|
|
414
|
-
@click="startChatForObligation(item.obligationId)"
|
|
415
|
-
>
|
|
416
|
-
<span class="material-icons text-base">{{ chatStarting[item.obligationId] ? "hourglass_empty" : "chat_bubble_outline" }}</span>
|
|
417
|
-
</button>
|
|
418
|
-
</div>
|
|
419
|
-
|
|
420
|
-
<div v-if="visibleCycles(item).length > 0" class="border-t border-gray-100 px-4 py-3 bg-gray-50">
|
|
421
|
-
<ul class="space-y-3">
|
|
422
|
-
<li
|
|
423
|
-
v-for="cycle in visibleCycles(item)"
|
|
424
|
-
:key="cycle.cycleId"
|
|
425
|
-
class="bg-white border border-gray-200 rounded p-3"
|
|
426
|
-
:data-testid="`encore-cycle-${item.obligationId}-${cycle.cycleId}`"
|
|
427
|
-
>
|
|
428
|
-
<div class="flex items-center gap-2 mb-2">
|
|
429
|
-
<span class="text-sm font-mono text-gray-700">{{ cycle.cycleId }}</span>
|
|
430
|
-
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -- decorative arrow between two date values, language-neutral -->
|
|
431
|
-
<span class="text-xs text-gray-400">{{ formatDate(cycle.state.cycleStart) }} → {{ formatDate(cycle.state.cycleDeadline) }}</span>
|
|
432
|
-
<!-- Right-aligned group so the layout still pins to
|
|
433
|
-
the right when the "closed" chip is hidden
|
|
434
|
-
(open cycles render no chip, only the per-step
|
|
435
|
-
icons inside the row signal what's left). -->
|
|
436
|
-
<span class="ml-auto inline-flex items-center gap-2">
|
|
437
|
-
<!-- Light grey signals "settled history, nothing to do." -->
|
|
438
|
-
<span
|
|
439
|
-
v-if="isCycleClosed(cycle.state, item.dsl)"
|
|
440
|
-
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500"
|
|
441
|
-
>
|
|
442
|
-
{{ t("encoreDashboard.cycleClosed") }}
|
|
443
|
-
</span>
|
|
444
|
-
<button
|
|
445
|
-
v-if="cycleTickets(item.obligationId, cycle.cycleId).length > 0"
|
|
446
|
-
type="button"
|
|
447
|
-
class="relative inline-flex items-center justify-center w-6 h-6 rounded text-amber-500 hover:text-amber-600 hover:bg-amber-50 transition-colors"
|
|
448
|
-
:title="t('encoreDashboard.bellButtonTitle')"
|
|
449
|
-
:aria-label="t('encoreDashboard.bellButtonTitle')"
|
|
450
|
-
:data-testid="`encore-cycle-bell-${item.obligationId}-${cycle.cycleId}`"
|
|
451
|
-
@click="openTicket(cycleTickets(item.obligationId, cycle.cycleId)[0])"
|
|
452
|
-
>
|
|
453
|
-
<span class="material-icons text-base">notifications_active</span>
|
|
454
|
-
<span
|
|
455
|
-
v-if="cycleTickets(item.obligationId, cycle.cycleId).length > 1"
|
|
456
|
-
class="absolute -top-1 -right-1 min-w-[14px] h-3.5 px-1 rounded-full bg-red-500 text-white text-[9px] leading-[14px] text-center font-semibold"
|
|
457
|
-
>
|
|
458
|
-
{{ cycleTickets(item.obligationId, cycle.cycleId).length }}
|
|
459
|
-
</span>
|
|
460
|
-
</button>
|
|
461
|
-
</span>
|
|
462
|
-
</div>
|
|
463
|
-
<div class="space-y-1">
|
|
464
|
-
<div v-for="target in item.dsl.targets" :key="target.id" class="text-xs">
|
|
465
|
-
<div class="flex items-center gap-2">
|
|
466
|
-
<span class="font-medium text-gray-600 min-w-[6rem] truncate">{{ target.displayName ?? target.id }}</span>
|
|
467
|
-
<span
|
|
468
|
-
v-for="step in item.dsl.steps"
|
|
469
|
-
:key="step.id"
|
|
470
|
-
class="inline-flex items-center gap-1 text-gray-500"
|
|
471
|
-
:title="step.displayName ?? step.id"
|
|
472
|
-
>
|
|
473
|
-
<span class="material-icons text-xs" :class="stepStatusClasses(cycleStepStatus(cycle.state, target, step))">{{
|
|
474
|
-
stepStatusIcon(cycleStepStatus(cycle.state, target, step))
|
|
475
|
-
}}</span>
|
|
476
|
-
<span class="truncate max-w-[10rem]">{{ step.displayName ?? step.id }}</span>
|
|
477
|
-
</span>
|
|
478
|
-
</div>
|
|
479
|
-
<div v-if="recordedValuesForTarget(cycle.state, target.id).length > 0" class="ml-[6.5rem] mt-1 flex flex-wrap gap-1">
|
|
480
|
-
<span
|
|
481
|
-
v-for="entry in recordedValuesForTarget(cycle.state, target.id)"
|
|
482
|
-
:key="entry.key"
|
|
483
|
-
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-gray-100 text-[11px] text-gray-600"
|
|
484
|
-
>
|
|
485
|
-
<span class="text-gray-400">{{ entry.key }}:</span>
|
|
486
|
-
<span class="font-mono">{{ entry.value }}</span>
|
|
487
|
-
</span>
|
|
488
|
-
</div>
|
|
489
|
-
</div>
|
|
490
|
-
</div>
|
|
491
|
-
</li>
|
|
492
|
-
</ul>
|
|
493
|
-
</div>
|
|
494
|
-
<!-- Expanded by the user, but the obligation has no cycles
|
|
495
|
-
on disk yet. Falls outside the visibleCycles section
|
|
496
|
-
because that section is hidden when the list is empty. -->
|
|
497
|
-
<div v-else-if="expanded[item.obligationId]" class="border-t border-gray-100 px-4 py-3 bg-gray-50 text-xs text-gray-500">
|
|
498
|
-
{{ t("encoreDashboard.noCycles") }}
|
|
499
|
-
</div>
|
|
500
|
-
</li>
|
|
501
|
-
</ul>
|
|
502
|
-
</div>
|
|
503
|
-
</div>
|
|
504
|
-
</template>
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// Chat-on-mount redirect — extracted from View.vue (#feat-encore-page).
|
|
3
|
-
//
|
|
4
|
-
// Why this branch exists at all:
|
|
5
|
-
// - The tick NEVER calls chat.start(). If it did, the chat would
|
|
6
|
-
// appear in the user's sidebar before they engaged with the
|
|
7
|
-
// notification (the "abandoned chat" problem).
|
|
8
|
-
// - Chat creation must be deferred until the user clicks the bell.
|
|
9
|
-
// The bell's `navigateTarget` is just a URL, so to intercept the
|
|
10
|
-
// click we own the destination route (/encore?pendingId=…) and
|
|
11
|
-
// run plugin code on mount.
|
|
12
|
-
// - On mount this component dispatches resolveNotification (which
|
|
13
|
-
// starts the chat server-side) and redirects to /chat/<chatId>
|
|
14
|
-
// via a FULL navigation. The full nav guarantees the
|
|
15
|
-
// `from encore` chip renders on first paint — same trick
|
|
16
|
-
// debug-plugin uses.
|
|
17
|
-
//
|
|
18
|
-
// Notification clearing is NOT done here — that's the LLM's job
|
|
19
|
-
// once it's talking to the user in the resulting chat (the LLM
|
|
20
|
-
// calls markStepDone / markTargetSkipped with the pendingId; the
|
|
21
|
-
// MCP handler reads the ticket and calls notifier.clear). The only
|
|
22
|
-
// clear-here case is an orphan ticket (already swept) — the server
|
|
23
|
-
// clears the bell entry by notificationId and returns
|
|
24
|
-
// { orphan: true }, and we render the "already resolved" line.
|
|
25
|
-
|
|
26
|
-
import { computed, onMounted, ref } from "vue";
|
|
27
|
-
import { pluginEndpoints } from "../api";
|
|
28
|
-
import { apiCall } from "../../utils/api";
|
|
29
|
-
import { META } from "./manageEncoreMeta";
|
|
30
|
-
import type { EncoreEndpoints } from "./manageEncoreDefinition";
|
|
31
|
-
|
|
32
|
-
const props = defineProps<{ pendingId: string }>();
|
|
33
|
-
|
|
34
|
-
const status = ref<"starting" | "redirecting" | "error" | "orphan">("starting");
|
|
35
|
-
const errorMessage = ref<string | null>(null);
|
|
36
|
-
|
|
37
|
-
// The host's NotificationBell.vue splices `notificationId=<entryId>`
|
|
38
|
-
// onto every navigateTarget at click time (see appendNotificationId
|
|
39
|
-
// in src/components/NotificationBell.vue). Reading it lets the
|
|
40
|
-
// server clear orphan bell entries whose ticket was already swept.
|
|
41
|
-
const notificationId = computed<string | null>(() => {
|
|
42
|
-
if (typeof window === "undefined") return null;
|
|
43
|
-
const params = new URLSearchParams(window.location.search);
|
|
44
|
-
return params.get("notificationId");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
interface ResolveResult {
|
|
48
|
-
ok: boolean;
|
|
49
|
-
chatId?: string;
|
|
50
|
-
navigateTo?: string;
|
|
51
|
-
orphan?: boolean;
|
|
52
|
-
error?: string;
|
|
53
|
-
message?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function resolveAndRedirect(): Promise<void> {
|
|
57
|
-
try {
|
|
58
|
-
const endpoints = pluginEndpoints<EncoreEndpoints>(META.apiNamespace);
|
|
59
|
-
const { method, url } = endpoints.dispatch;
|
|
60
|
-
const response = await apiCall<ResolveResult>(url, {
|
|
61
|
-
method,
|
|
62
|
-
body: {
|
|
63
|
-
kind: "resolveNotification",
|
|
64
|
-
pendingId: props.pendingId,
|
|
65
|
-
notificationId: notificationId.value ?? undefined,
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
if (!response.ok) {
|
|
69
|
-
status.value = "error";
|
|
70
|
-
errorMessage.value = response.error;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const result = response.data;
|
|
74
|
-
if (result.orphan) {
|
|
75
|
-
status.value = "orphan";
|
|
76
|
-
errorMessage.value = result.message ?? "This notification was already resolved.";
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (!result.ok || !result.chatId) {
|
|
80
|
-
status.value = "error";
|
|
81
|
-
errorMessage.value = result.error ?? "resolveNotification returned no chatId";
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
status.value = "redirecting";
|
|
85
|
-
// Full-page navigation (not Vue-router push) so the seeded
|
|
86
|
-
// user turn renders with the `from encore` chip on first
|
|
87
|
-
// paint — same mechanism debug-plugin uses.
|
|
88
|
-
window.location.href = result.navigateTo ?? `/chat/${result.chatId}`;
|
|
89
|
-
} catch (err) {
|
|
90
|
-
status.value = "error";
|
|
91
|
-
errorMessage.value = err instanceof Error ? err.message : String(err);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
onMounted(() => {
|
|
96
|
-
void resolveAndRedirect();
|
|
97
|
-
});
|
|
98
|
-
</script>
|
|
99
|
-
|
|
100
|
-
<template>
|
|
101
|
-
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -- chat-on-mount page is a transient redirect, not a user-facing surface; strings stay out of the 8-locale bundle (matches debug-plugin's View). -->
|
|
102
|
-
<div class="h-full flex items-center justify-center text-sm text-gray-500">
|
|
103
|
-
<div v-if="status === 'starting'">Starting chat…</div>
|
|
104
|
-
<div v-else-if="status === 'redirecting'">Redirecting to chat…</div>
|
|
105
|
-
<div v-else-if="status === 'orphan'" class="text-center max-w-md px-4">
|
|
106
|
-
<div class="text-gray-700 mb-2">This notification has already been resolved.</div>
|
|
107
|
-
<a href="/chat" class="block text-blue-600 hover:underline text-sm">← back to chat</a>
|
|
108
|
-
</div>
|
|
109
|
-
<div v-else class="text-center max-w-md px-4">
|
|
110
|
-
<div class="text-red-600 mb-2">Couldn't open the resolution chat.</div>
|
|
111
|
-
<div class="text-xs text-gray-500">{{ errorMessage }}</div>
|
|
112
|
-
<a href="/chat" class="block mt-3 text-blue-600 hover:underline text-sm">← back to chat</a>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
|
|
116
|
-
</template>
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// Encore page entry — branches between two unrelated surfaces that
|
|
3
|
-
// happen to share the /encore route:
|
|
4
|
-
//
|
|
5
|
-
// 1. `?pendingId=<uuid>` present → `EncoreRedirect.vue`
|
|
6
|
-
// Notification-click landing. Dispatches `resolveNotification`
|
|
7
|
-
// on mount, then full-navigates to the resulting /chat/<chatId>.
|
|
8
|
-
// The user never actually sees this surface — transient (~300ms).
|
|
9
|
-
//
|
|
10
|
-
// 2. No `pendingId` → `EncoreDashboard.vue`
|
|
11
|
-
// Read-only browser over `obligations/` (active obligations +
|
|
12
|
-
// cycle history). Reached from the top-bar launcher; no
|
|
13
|
-
// mutating affordances — those are LLM-only verbs.
|
|
14
|
-
//
|
|
15
|
-
// The branch lives here (rather than in the router) because the
|
|
16
|
-
// route name and Vue surface stay one-to-one — App.vue mounts a
|
|
17
|
-
// single component per `currentPage`.
|
|
18
|
-
|
|
19
|
-
import { computed } from "vue";
|
|
20
|
-
import { useRoute } from "vue-router";
|
|
21
|
-
import EncoreRedirect from "./EncoreRedirect.vue";
|
|
22
|
-
import EncoreDashboard from "./EncoreDashboard.vue";
|
|
23
|
-
|
|
24
|
-
const route = useRoute();
|
|
25
|
-
|
|
26
|
-
const pendingId = computed<string | null>(() => {
|
|
27
|
-
const value = route.query.pendingId;
|
|
28
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
29
|
-
return null;
|
|
30
|
-
});
|
|
31
|
-
</script>
|
|
32
|
-
|
|
33
|
-
<template>
|
|
34
|
-
<EncoreRedirect v-if="pendingId" :pending-id="pendingId" />
|
|
35
|
-
<EncoreDashboard v-else />
|
|
36
|
-
</template>
|