mulmoclaude 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/bin/mulmoclaude.js +11 -1
- package/client/assets/chunk-D8eiyYIV-CW0rPbG2.js +1 -0
- package/client/assets/{html2canvas-CDGcmOD3-Bkf2uOth.js → html2canvas-CDGcmOD3-BjwfzAN8.js} +1 -1
- package/client/assets/index-Bp1owZ-i.js +5101 -0
- package/client/assets/index-c63H1pnd.css +2 -0
- package/client/assets/{index.es-DqtpmBm8-D9mAh_KQ.js → index.es-DqtpmBm8-DudYPW7R.js} +1 -1
- package/client/assets/material-symbols-outlined-C0dZ3SlO.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-BUk5WXSy.js +1 -0
- package/client/assets/{runtime-vue-BVUzgYGA.js → runtime-vue-fFYhnNg3.js} +1 -1
- package/client/assets/{vue-C8UuIO9J.js → vue-Kqzpl9Vx.js} +1 -1
- package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +4 -0
- package/client/index.html +9 -11
- package/package.json +5 -4
- package/server/agent/backend/claude-code.ts +34 -0
- package/server/agent/backend/fake-echo.ts +370 -0
- package/server/agent/backend/index.ts +16 -1
- package/server/agent/config.ts +8 -1
- package/server/agent/mcpFailureMonitor.ts +167 -0
- package/server/agent/mcpPreflight.ts +185 -0
- package/server/agent/stream.ts +12 -1
- package/server/api/routes/mulmo-script.ts +19 -1
- package/server/api/routes/schedulerHandlers.ts +52 -4
- package/server/api/routes/sessions.ts +15 -0
- package/server/api/routes/skills.ts +263 -0
- package/server/events/notifications.ts +19 -91
- package/server/index.ts +87 -9
- package/server/notifier/macosReminderAdapter.ts +30 -0
- package/server/system/announceOptionalDeps.ts +50 -0
- package/server/system/config.ts +8 -1
- package/server/system/docker.ts +14 -6
- package/server/system/env.ts +18 -5
- package/server/system/optionalDeps.ts +129 -0
- package/server/utils/cli-flags.d.mts +14 -0
- package/server/utils/cli-flags.mjs +53 -0
- package/server/utils/time.ts +6 -0
- package/server/workspace/helps/business.md +2 -2
- package/server/workspace/helps/mulmoscript.md +3 -3
- package/server/workspace/helps/sandbox.md +2 -2
- package/server/workspace/hooks/dispatcher.mjs +1 -1
- package/server/workspace/paths.ts +13 -4
- package/server/workspace/skills/catalog.ts +355 -0
- package/server/workspace/skills/external/catalog.ts +283 -0
- package/server/workspace/skills/external/clone.ts +129 -0
- package/server/workspace/skills/external/id.ts +194 -0
- package/server/workspace/skills/external/install.ts +417 -0
- package/server/workspace/skills/external/presets.ts +50 -0
- package/server/workspace/skills-preset.ts +29 -17
- package/server/workspace/workspace.ts +10 -5
- package/src/App.vue +19 -8
- package/src/components/RightSidebar.vue +19 -0
- package/src/components/StackView.vue +10 -1
- package/src/config/apiRoutes.ts +0 -6
- package/src/config/roles.ts +2 -0
- package/src/lang/de.ts +50 -1
- package/src/lang/en.ts +49 -1
- package/src/lang/es.ts +49 -1
- package/src/lang/fr.ts +49 -1
- package/src/lang/ja.ts +49 -1
- package/src/lang/ko.ts +49 -1
- package/src/lang/pt-BR.ts +49 -1
- package/src/lang/zh.ts +49 -1
- package/src/plugins/manageSkills/View.vue +795 -30
- package/src/plugins/manageSkills/categories.ts +125 -0
- package/src/plugins/manageSkills/meta.ts +30 -0
- package/src/plugins/markdown/definition.ts +3 -3
- package/src/plugins/meta-types.ts +5 -0
- package/src/plugins/presentMulmoScript/Preview.vue +3 -3
- package/src/plugins/presentMulmoScript/View.vue +157 -33
- package/src/plugins/presentMulmoScript/meta.ts +4 -0
- package/src/plugins/scheduler/View.vue +45 -9
- package/src/plugins/scheduler/calendarDefinition.ts +6 -2
- package/src/plugins/scheduler/multiDayHelpers.ts +95 -0
- package/src/plugins/spreadsheet/View.vue +3 -3
- package/src/types/notification.ts +1 -1
- package/src/types/session.ts +6 -0
- package/src/types/sse.ts +5 -0
- package/src/types/toolCallHistory.ts +7 -0
- package/src/utils/agent/eventDispatch.ts +26 -5
- package/src/utils/agent/mcpHint.ts +50 -0
- package/src/utils/session/sessionEntries.ts +8 -32
- package/client/assets/PluginScopedRoot-YjvQq0Nn.js +0 -3
- package/client/assets/chunk-CernVdwh.js +0 -1
- package/client/assets/chunk-D8eiyYIV-CAXpUwLd.js +0 -1
- package/client/assets/index-BwrlMMHr.js +0 -5005
- package/client/assets/index-CvvNuegU.css +0 -2
- package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-C1To4M3t.js +0 -1
- package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +0 -4
- package/server/api/routes/notifications.ts +0 -195
- package/server/notifier/legacy-adapters.ts +0 -76
- package/src/composables/useSelectedResult.ts +0 -49
- /package/client/assets/{purify.es-Fx1Nqyry-Dwtk-9WZ.js → purify.es-Fx1Nqyry-B3aL7Uvj.js} +0 -0
- /package/client/assets/{typeof-DBp4T-Ny-CSr8wx1e.js → typeof-DBp4T-Ny-Bef7RiR_.js} +0 -0
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
// PoC push endpoint — proves the server can fire a delayed message
|
|
2
|
-
// simultaneously to every open Web tab (pub-sub) and every bridge
|
|
3
|
-
// (chat-service `pushToBridge`). Stepping stone for the in-app
|
|
4
|
-
// notification center (#144) and external-channel notifications
|
|
5
|
-
// (#142); see plans/done/feat-notification-push-scaffold.md for the
|
|
6
|
-
// motivation and the production plan.
|
|
7
|
-
//
|
|
8
|
-
// Usage (basic):
|
|
9
|
-
// curl -X POST http://localhost:3001/api/notifications/test \
|
|
10
|
-
// -H "Authorization: Bearer $(cat ~/mulmoclaude/.session-token)" \
|
|
11
|
-
// -H "Content-Type: application/json" \
|
|
12
|
-
// -d '{"message":"hello","delaySeconds":5}'
|
|
13
|
-
//
|
|
14
|
-
// Usage (with permalink action — #762):
|
|
15
|
-
// curl -X POST http://localhost:3001/api/notifications/test \
|
|
16
|
-
// -H "Authorization: Bearer $TOKEN" \
|
|
17
|
-
// -H "Content-Type: application/json" \
|
|
18
|
-
// -d '{"message":"Scheduled task fired","kind":"scheduler",
|
|
19
|
-
// "action":{"type":"navigate",
|
|
20
|
-
// "target":{"view":"automations",
|
|
21
|
-
// "taskId":"finance-daily-briefing"}}}'
|
|
22
|
-
//
|
|
23
|
-
// For a one-shot "fire one of every target kind" run,
|
|
24
|
-
// scripts/dev/fire-sample-notifications.sh drives this endpoint.
|
|
25
|
-
//
|
|
26
|
-
// PR 4 of feat-encore made `publishNotification()` a thin wrapper
|
|
27
|
-
// over the notifier engine, so this route no longer needs injected
|
|
28
|
-
// pubsub / bridge deps — bridge push fans out via the legacy
|
|
29
|
-
// adapter subscribed to the engine, and `scheduleTestNotification`
|
|
30
|
-
// just calls the wrapper.
|
|
31
|
-
|
|
32
|
-
import { Router, type Request, type Response } from "express";
|
|
33
|
-
import { scheduleTestNotification, type ScheduleNotificationOptions } from "../../events/notifications.js";
|
|
34
|
-
import { log } from "../../system/logger/index.js";
|
|
35
|
-
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
36
|
-
import {
|
|
37
|
-
NOTIFICATION_ACTION_TYPES,
|
|
38
|
-
NOTIFICATION_KINDS,
|
|
39
|
-
NOTIFICATION_VIEWS,
|
|
40
|
-
type NotificationAction,
|
|
41
|
-
type NotificationKind,
|
|
42
|
-
type NotificationTarget,
|
|
43
|
-
} from "../../../src/types/notification.js";
|
|
44
|
-
import { isRecord } from "../../utils/types.js";
|
|
45
|
-
|
|
46
|
-
interface TestRequestBody {
|
|
47
|
-
message?: unknown;
|
|
48
|
-
body?: unknown;
|
|
49
|
-
delaySeconds?: unknown;
|
|
50
|
-
transportId?: unknown;
|
|
51
|
-
kind?: unknown;
|
|
52
|
-
action?: unknown;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface TestResponse {
|
|
56
|
-
firesAt: string;
|
|
57
|
-
delaySeconds: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const KIND_SET = new Set<string>(Object.values(NOTIFICATION_KINDS));
|
|
61
|
-
const VIEW_SET = new Set<string>(Object.values(NOTIFICATION_VIEWS));
|
|
62
|
-
|
|
63
|
-
function parseKind(value: unknown): NotificationKind | undefined {
|
|
64
|
-
if (typeof value !== "string") return undefined;
|
|
65
|
-
return KIND_SET.has(value) ? (value as NotificationKind) : undefined;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// `path` / `slug` / `anchor` / etc. arrive as `unknown` from the JSON
|
|
69
|
-
// body. The URL builders in `legacyActionToNavigateTarget` assume each
|
|
70
|
-
// field is `string | undefined`; passing a number through (e.g.
|
|
71
|
-
// `path: 123`) would crash later inside `setTimeout` — after the 202
|
|
72
|
-
// is sent — when `path.split("/")` runs. So validate per-view here:
|
|
73
|
-
// any required field that isn't a non-empty string, or any optional
|
|
74
|
-
// field that is present but isn't a string, drops the whole action.
|
|
75
|
-
// The notification still fires; it just lands without a click target.
|
|
76
|
-
function isOptionalString(value: unknown): boolean {
|
|
77
|
-
return value === undefined || typeof value === "string";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function asOptionalString(value: unknown): string | undefined {
|
|
81
|
-
return typeof value === "string" ? value : undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Per-view validators — split out so the dispatcher stays under the
|
|
85
|
-
// cognitive-complexity threshold. Each validator returns either the
|
|
86
|
-
// typed target or `undefined` for any field that doesn't match the
|
|
87
|
-
// `src/types/notification.ts` discriminated union.
|
|
88
|
-
|
|
89
|
-
function validateChatTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
90
|
-
// sessionId required: chat without it would bounce off the catch-all redirect.
|
|
91
|
-
if (typeof value.sessionId !== "string" || value.sessionId.length === 0) return undefined;
|
|
92
|
-
if (!isOptionalString(value.resultUuid)) return undefined;
|
|
93
|
-
return { view: NOTIFICATION_VIEWS.chat, sessionId: value.sessionId, resultUuid: asOptionalString(value.resultUuid) };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function validateTodosTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
97
|
-
if (!isOptionalString(value.itemId)) return undefined;
|
|
98
|
-
return { view: NOTIFICATION_VIEWS.todos, itemId: asOptionalString(value.itemId) };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function validateAutomationsTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
102
|
-
if (!isOptionalString(value.taskId)) return undefined;
|
|
103
|
-
return { view: NOTIFICATION_VIEWS.automations, taskId: asOptionalString(value.taskId) };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function validateSourcesTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
107
|
-
if (!isOptionalString(value.slug)) return undefined;
|
|
108
|
-
return { view: NOTIFICATION_VIEWS.sources, slug: asOptionalString(value.slug) };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function validateFilesTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
112
|
-
if (!isOptionalString(value.path)) return undefined;
|
|
113
|
-
return { view: NOTIFICATION_VIEWS.files, path: asOptionalString(value.path) };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function validateWikiTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
117
|
-
if (!isOptionalString(value.slug)) return undefined;
|
|
118
|
-
if (!isOptionalString(value.anchor)) return undefined;
|
|
119
|
-
return { view: NOTIFICATION_VIEWS.wiki, slug: asOptionalString(value.slug), anchor: asOptionalString(value.anchor) };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function validateNavigateTarget(value: unknown): NotificationTarget | undefined {
|
|
123
|
-
if (!isRecord(value)) return undefined;
|
|
124
|
-
if (typeof value.view !== "string" || !VIEW_SET.has(value.view)) return undefined;
|
|
125
|
-
switch (value.view) {
|
|
126
|
-
case NOTIFICATION_VIEWS.chat:
|
|
127
|
-
return validateChatTarget(value);
|
|
128
|
-
case NOTIFICATION_VIEWS.todos:
|
|
129
|
-
return validateTodosTarget(value);
|
|
130
|
-
case NOTIFICATION_VIEWS.calendar:
|
|
131
|
-
return { view: NOTIFICATION_VIEWS.calendar };
|
|
132
|
-
case NOTIFICATION_VIEWS.automations:
|
|
133
|
-
return validateAutomationsTarget(value);
|
|
134
|
-
case NOTIFICATION_VIEWS.sources:
|
|
135
|
-
return validateSourcesTarget(value);
|
|
136
|
-
case NOTIFICATION_VIEWS.files:
|
|
137
|
-
return validateFilesTarget(value);
|
|
138
|
-
case NOTIFICATION_VIEWS.wiki:
|
|
139
|
-
return validateWikiTarget(value);
|
|
140
|
-
default:
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Exported for unit tests. Production callers go through
|
|
146
|
-
* `createNotificationsRouter` and never see this function directly. */
|
|
147
|
-
export function parseAction(value: unknown): NotificationAction | undefined {
|
|
148
|
-
if (!isRecord(value)) return undefined;
|
|
149
|
-
if (value.type === NOTIFICATION_ACTION_TYPES.none) {
|
|
150
|
-
return { type: NOTIFICATION_ACTION_TYPES.none };
|
|
151
|
-
}
|
|
152
|
-
if (value.type !== NOTIFICATION_ACTION_TYPES.navigate) return undefined;
|
|
153
|
-
const target = validateNavigateTarget(value.target);
|
|
154
|
-
if (!target) return undefined;
|
|
155
|
-
return { type: NOTIFICATION_ACTION_TYPES.navigate, target };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseBody(body: TestRequestBody): ScheduleNotificationOptions {
|
|
159
|
-
const opts: ScheduleNotificationOptions = {};
|
|
160
|
-
if (typeof body.message === "string" && body.message.length > 0) {
|
|
161
|
-
opts.message = body.message;
|
|
162
|
-
}
|
|
163
|
-
if (typeof body.body === "string" && body.body.length > 0) {
|
|
164
|
-
opts.body = body.body;
|
|
165
|
-
}
|
|
166
|
-
if (typeof body.delaySeconds === "number") {
|
|
167
|
-
opts.delaySeconds = body.delaySeconds;
|
|
168
|
-
}
|
|
169
|
-
if (typeof body.transportId === "string" && body.transportId.length > 0) {
|
|
170
|
-
opts.transportId = body.transportId;
|
|
171
|
-
}
|
|
172
|
-
const kind = parseKind(body.kind);
|
|
173
|
-
if (kind) opts.kind = kind;
|
|
174
|
-
const action = parseAction(body.action);
|
|
175
|
-
if (action) opts.action = action;
|
|
176
|
-
return opts;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function createNotificationsRouter(): Router {
|
|
180
|
-
const router = Router();
|
|
181
|
-
router.post(API_ROUTES.notifications.test, (req: Request<object, unknown, TestRequestBody>, res: Response<TestResponse>) => {
|
|
182
|
-
const opts = parseBody(req.body ?? {});
|
|
183
|
-
const scheduled = scheduleTestNotification(opts);
|
|
184
|
-
log.info("notifications", "scheduled test push", {
|
|
185
|
-
delaySeconds: scheduled.delaySeconds,
|
|
186
|
-
firesAt: scheduled.firesAt,
|
|
187
|
-
transportId: opts.transportId,
|
|
188
|
-
});
|
|
189
|
-
res.status(202).json({
|
|
190
|
-
firesAt: scheduled.firesAt,
|
|
191
|
-
delaySeconds: scheduled.delaySeconds,
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
return router;
|
|
195
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// Bridge + macOS Reminder side-channel adapters for the notifier engine.
|
|
2
|
-
//
|
|
3
|
-
// These previously fired inline inside `publishNotification()` (legacy
|
|
4
|
-
// `server/events/notifications.ts`). PR 4 of feat-encore relocates them
|
|
5
|
-
// to in-process listeners on the engine so the wrapper stays concerned
|
|
6
|
-
// only with mapping → engine, and any future direct caller of
|
|
7
|
-
// `notifier.publish()` automatically gets the same fan-out by setting
|
|
8
|
-
// the fields the adapters read.
|
|
9
|
-
//
|
|
10
|
-
// **No severity-based routing in v1.** The macOS adapter fires for
|
|
11
|
-
// every `published` event (gated only by darwin + env flag inside
|
|
12
|
-
// `pushToMacosReminder` itself), and the bridge adapter fires only
|
|
13
|
-
// when the entry carries a legacy `transportId` in its `pluginData`.
|
|
14
|
-
// If a future use case wants severity-driven routing, the engine
|
|
15
|
-
// already stores `severity` on every entry — the adapters can grow
|
|
16
|
-
// the check without engine changes.
|
|
17
|
-
//
|
|
18
|
-
// Why in-process listeners (`engine.onEvent`) rather than a pubsub
|
|
19
|
-
// subscription: the host's `IPubSub` is fan-out-only with no
|
|
20
|
-
// server-side subscribe API. Going through socket.io for an
|
|
21
|
-
// in-process notification would mean shipping the event out and
|
|
22
|
-
// reading it back through a websocket round-trip, which is silly.
|
|
23
|
-
|
|
24
|
-
import { onEvent } from "./engine.js";
|
|
25
|
-
import { isLegacyNotifierPluginData } from "../events/notifications.js";
|
|
26
|
-
import { pushToMacosReminder } from "../system/macosNotify.js";
|
|
27
|
-
import { log } from "../system/logger/index.js";
|
|
28
|
-
import type { NotifierEntry } from "./types.js";
|
|
29
|
-
|
|
30
|
-
export type PushToBridge = (transportId: string, chatId: string, message: string) => void;
|
|
31
|
-
|
|
32
|
-
export interface LegacyAdapterDeps {
|
|
33
|
-
pushToBridge: PushToBridge;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Format the bridge message identically to the legacy inline path so
|
|
37
|
-
* Telegram / CLI subscribers see the same text shape they always did
|
|
38
|
-
* (icon + title + optional body). */
|
|
39
|
-
function formatBridgeMessage(entry: NotifierEntry): string {
|
|
40
|
-
const legacy = isLegacyNotifierPluginData(entry.pluginData) ? entry.pluginData : null;
|
|
41
|
-
// U+2705 (white heavy check mark) for the agent kind, U+1F514 (bell)
|
|
42
|
-
// for everything else — same fallback the legacy formatter used.
|
|
43
|
-
const icon = legacy?.kind === "agent" ? "✅" : "\u{1F514}";
|
|
44
|
-
const parts = [icon, entry.title];
|
|
45
|
-
if (entry.body) parts.push(entry.body);
|
|
46
|
-
return parts.join(" ");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Wire the adapters as in-process listeners on the notifier engine.
|
|
50
|
-
* Returns an unsubscribe function for tests / teardown. */
|
|
51
|
-
export function startLegacyAdapters(deps: LegacyAdapterDeps): () => void {
|
|
52
|
-
return onEvent((event) => {
|
|
53
|
-
if (event.type !== "published") return;
|
|
54
|
-
const { entry } = event;
|
|
55
|
-
// macOS sink is a no-op outside darwin / when
|
|
56
|
-
// DISABLE_MACOS_REMINDER_NOTIFICATIONS=1, so it's safe to fire
|
|
57
|
-
// unconditionally. `pushToMacosReminder` itself wraps every
|
|
58
|
-
// failure path in try / log.warn.
|
|
59
|
-
void pushToMacosReminder(entry.title, entry.body);
|
|
60
|
-
const legacy = isLegacyNotifierPluginData(entry.pluginData) ? entry.pluginData : null;
|
|
61
|
-
if (legacy?.transportId) {
|
|
62
|
-
try {
|
|
63
|
-
// chatId is hardcoded — the legacy `chatId` knob on the PoC
|
|
64
|
-
// endpoint was a one-caller artifact (only `scheduleTestNotification`
|
|
65
|
-
// ever set it) and was removed alongside the migration. If a
|
|
66
|
-
// real production caller later needs per-conversation routing,
|
|
67
|
-
// it deserves a designed surface, not a recreated PoC field.
|
|
68
|
-
deps.pushToBridge(legacy.transportId, "notifications", formatBridgeMessage(entry));
|
|
69
|
-
} catch (err) {
|
|
70
|
-
// Keep the legacy contract: a failing bridge sink must never
|
|
71
|
-
// bubble out of the notifier emit chain.
|
|
72
|
-
log.warn("notifier-legacy-adapters", "bridge push failed", { error: String(err) });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Writable computed that bridges activeSession.selectedResultUuid
|
|
2
|
-
// with the URL's ?result= query parameter.
|
|
3
|
-
|
|
4
|
-
import { computed, watch, type ComputedRef, type WritableComputedRef } from "vue";
|
|
5
|
-
import { useRoute, useRouter, isNavigationFailure } from "vue-router";
|
|
6
|
-
import type { ActiveSession } from "../types/session";
|
|
7
|
-
|
|
8
|
-
export function useSelectedResult(opts: {
|
|
9
|
-
activeSession: ComputedRef<ActiveSession | undefined>;
|
|
10
|
-
sessionMap: Map<string, ActiveSession>;
|
|
11
|
-
currentSessionId: { readonly value: string };
|
|
12
|
-
}): {
|
|
13
|
-
selectedResultUuid: WritableComputedRef<string | null>;
|
|
14
|
-
} {
|
|
15
|
-
const { activeSession } = opts;
|
|
16
|
-
const route = useRoute();
|
|
17
|
-
const router = useRouter();
|
|
18
|
-
|
|
19
|
-
const selectedResultUuid = computed({
|
|
20
|
-
get: () => activeSession.value?.selectedResultUuid ?? null,
|
|
21
|
-
set: (val: string | null) => {
|
|
22
|
-
if (activeSession.value) activeSession.value.selectedResultUuid = val;
|
|
23
|
-
const { result: __result, ...restQuery } = route.query;
|
|
24
|
-
const nextQuery = val ? { ...restQuery, result: val } : restQuery;
|
|
25
|
-
router.replace({ query: nextQuery }).catch((err: unknown) => {
|
|
26
|
-
if (!isNavigationFailure(err)) {
|
|
27
|
-
console.error("[selectedResultUuid] navigation failed:", err);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// External URL changes for ?result= → sync into the session.
|
|
34
|
-
watch(
|
|
35
|
-
() => route.query.result,
|
|
36
|
-
(newResult) => {
|
|
37
|
-
const session = opts.sessionMap.get(opts.currentSessionId.value);
|
|
38
|
-
if (!session) return;
|
|
39
|
-
// Ignore malformed (array) values rather than clobbering state.
|
|
40
|
-
if (Array.isArray(newResult)) return;
|
|
41
|
-
const resultId = typeof newResult === "string" ? newResult : null;
|
|
42
|
-
if (resultId !== session.selectedResultUuid) {
|
|
43
|
-
session.selectedResultUuid = resultId;
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
return { selectedResultUuid };
|
|
49
|
-
}
|
|
File without changes
|
|
File without changes
|