mulmoclaude 0.6.2 → 0.6.4
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/JsonEditor-D6WBWLoa.js +10 -0
- package/client/assets/JsonEditor-Di5xGeZY.css +1 -0
- package/client/assets/_plugin-vue_export-helper-BOai-rQB.js +1 -0
- package/client/assets/chunk-D8eiyYIV-LcKZGJv5.js +1 -0
- package/client/assets/{html2canvas-CDGcmOD3-Bkf2uOth.js → html2canvas-CDGcmOD3-XVrO-eyz.js} +1 -1
- package/client/assets/index-CyBr8Mkr.css +2 -0
- package/client/assets/index-zZIqEbNX.js +5106 -0
- package/client/assets/{index.es-DqtpmBm8-D9mAh_KQ.js → index.es-DqtpmBm8-DHT6q10o.js} +1 -1
- package/client/assets/material-symbols-outlined-DtIK7AQn.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-D6kcV0wa.js +1 -0
- package/client/assets/{runtime-vue-BVUzgYGA.js → runtime-vue-fFYhnNg3.js} +1 -1
- package/client/assets/{vue-C8UuIO9J.js → vue-D4w8THF_.js} +1 -1
- package/client/assets/vue-i18n-CQbxVmNs.js +3 -0
- package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +4 -0
- package/client/index.html +10 -10
- package/package.json +9 -8
- 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 +74 -24
- package/server/agent/index.ts +104 -80
- package/server/agent/mcpFailureMonitor.ts +167 -0
- package/server/agent/mcpPreflight.ts +185 -0
- package/server/agent/prompt.ts +50 -359
- package/server/agent/stdioHttpShim.ts +171 -0
- package/server/agent/stream.ts +12 -1
- package/server/api/routes/encore.ts +55 -0
- package/server/api/routes/files.ts +22 -0
- 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/build/dispatcher.mjs +299 -0
- package/server/encore/INVARIANTS.md +272 -0
- package/server/encore/boot.ts +39 -0
- package/server/encore/closure.ts +36 -0
- package/server/encore/cycle.ts +276 -0
- package/server/encore/dispatch.ts +103 -0
- package/server/encore/handlers/amend.ts +99 -0
- package/server/encore/handlers/appendNote.ts +74 -0
- package/server/encore/handlers/defineEncore.ts +42 -0
- package/server/encore/handlers/listTickets.ts +107 -0
- package/server/encore/handlers/markStepDone.ts +41 -0
- package/server/encore/handlers/markTargetSkipped.ts +33 -0
- package/server/encore/handlers/query.ts +138 -0
- package/server/encore/handlers/recordValues.ts +44 -0
- package/server/encore/handlers/resolveNotification.ts +121 -0
- package/server/encore/handlers/setup.ts +81 -0
- package/server/encore/handlers/shared.ts +137 -0
- package/server/encore/handlers/snooze.ts +87 -0
- package/server/encore/handlers/startObligationChat.ts +64 -0
- package/server/encore/handlers/startSetupChat.ts +50 -0
- package/server/encore/lock.ts +61 -0
- package/server/encore/notifier.ts +123 -0
- package/server/encore/obligation.ts +25 -0
- package/server/encore/paths.ts +78 -0
- package/server/encore/reconcile.ts +661 -0
- package/server/encore/tick.ts +191 -0
- package/server/encore/yaml-fm.ts +63 -0
- package/server/events/notifications.ts +19 -91
- package/server/index.ts +94 -9
- package/server/notifier/engine.ts +102 -1
- package/server/notifier/macosReminderAdapter.ts +30 -0
- package/server/notifier/runtime-api.ts +41 -1
- package/server/notifier/types.ts +15 -2
- package/server/plugins/runtime.ts +11 -2
- package/server/prompts/index.ts +39 -0
- package/server/prompts/system/journal-pointer.md +12 -0
- package/server/prompts/system/memory-management-atomic.md +33 -0
- package/server/prompts/system/memory-management-topic.md +60 -0
- package/server/prompts/system/news-concierge.md +24 -0
- package/server/prompts/system/sandbox-tools.md +10 -0
- package/server/prompts/system/sources-context.md +16 -0
- package/server/prompts/system/system.md +91 -0
- package/server/system/announceOptionalDeps.ts +57 -0
- package/server/system/appVersion.ts +34 -0
- package/server/system/config.ts +17 -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/files/encore-io.ts +111 -0
- package/server/utils/time.ts +6 -0
- package/server/workspace/helps/business.md +2 -2
- package/server/workspace/helps/encore-dsl.md +482 -0
- package/server/workspace/helps/index.md +15 -13
- package/server/workspace/helps/mulmoscript.md +3 -3
- package/server/workspace/helps/sandbox.md +2 -2
- package/server/workspace/hooks/dispatcher.ts +7 -5
- package/server/workspace/hooks/provision.ts +6 -3
- 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 +37 -8
- package/src/components/FileContentRenderer.vue +102 -9
- package/src/components/JsonEditor.vue +160 -0
- package/src/components/NotificationBell.vue +35 -3
- package/src/components/PluginLauncher.vue +20 -41
- package/src/components/RightSidebar.vue +19 -0
- package/src/components/SettingsMcpTab.vue +58 -11
- package/src/components/SettingsModal.vue +22 -1
- package/src/components/StackView.vue +10 -1
- package/src/components/TodoExplorer.vue +16 -0
- package/src/components/todo/TodoKanbanView.vue +34 -6
- package/src/composables/useNotifications.ts +21 -1
- package/src/config/apiRoutes.ts +0 -6
- package/src/config/mcpCatalog.ts +12 -7
- package/src/config/mcpTypes.ts +5 -0
- package/src/config/roles.ts +52 -15
- package/src/config/systemFileDescriptors.ts +12 -0
- package/src/lang/de.ts +108 -12
- package/src/lang/en.ts +105 -11
- package/src/lang/es.ts +106 -11
- package/src/lang/fr.ts +106 -11
- package/src/lang/ja.ts +104 -11
- package/src/lang/ko.ts +105 -11
- package/src/lang/pt-BR.ts +106 -11
- package/src/lang/zh.ts +103 -11
- package/src/main.ts +1 -0
- package/src/plugins/_generated/metas.ts +4 -0
- package/src/plugins/_generated/registrations.ts +2 -0
- package/src/plugins/_generated/server-bindings.ts +5 -0
- package/src/plugins/encore/EncoreDashboard.vue +504 -0
- package/src/plugins/encore/EncoreRedirect.vue +116 -0
- package/src/plugins/encore/View.vue +36 -0
- package/src/plugins/encore/defineEncoreDefinition.ts +74 -0
- package/src/plugins/encore/defineEncoreMeta.ts +13 -0
- package/src/plugins/encore/index.ts +93 -0
- package/src/plugins/encore/manageEncoreDefinition.ts +100 -0
- package/src/plugins/encore/manageEncoreMeta.ts +36 -0
- package/src/plugins/manageSkills/View.vue +832 -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/skill/View.vue +1 -5
- package/src/plugins/spreadsheet/View.vue +3 -3
- package/src/plugins/spreadsheet/definition.ts +1 -1
- package/src/plugins/textResponse/Preview.vue +14 -1
- package/src/plugins/textResponse/View.vue +39 -24
- package/src/plugins/wiki/components/WikiPageBody.vue +4 -0
- package/src/router/index.ts +11 -0
- package/src/router/pageRoutes.ts +1 -0
- package/src/types/encore-dsl/at-expression.ts +120 -0
- package/src/types/encore-dsl/at-resolver.ts +32 -0
- package/src/types/encore-dsl/cadence.ts +289 -0
- package/src/types/encore-dsl/schema.ts +288 -0
- package/src/types/notification.ts +2 -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/image/htmlSrcAttrs.ts +117 -13
- 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/server/workspace/hooks/dispatcher.mjs +0 -300
- package/src/composables/useSelectedResult.ts +0 -49
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ScheduledItem } from "./index";
|
|
2
|
+
|
|
3
|
+
export type SegmentPosition = "only" | "start" | "middle" | "end";
|
|
4
|
+
|
|
5
|
+
export interface EventRange {
|
|
6
|
+
start: string;
|
|
7
|
+
end: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
11
|
+
|
|
12
|
+
function asIsoDate(value: unknown): string | null {
|
|
13
|
+
return typeof value === "string" && ISO_DATE.test(value) ? value : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function eventRange(item: ScheduledItem): EventRange | null {
|
|
17
|
+
const start = asIsoDate(item.props.date);
|
|
18
|
+
if (!start) return null;
|
|
19
|
+
const endRaw = asIsoDate(item.props.endDate);
|
|
20
|
+
if (!endRaw) return { start, end: start };
|
|
21
|
+
if (endRaw < start) return { start, end: start };
|
|
22
|
+
return { start, end: endRaw };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// True when the event has an `endDate` set but it doesn't form
|
|
26
|
+
// a valid forward range: malformed ISO string, end-before-start,
|
|
27
|
+
// or no usable `date` to anchor against. Drives the "broken"
|
|
28
|
+
// chip style so the user notices and can fix the typo instead
|
|
29
|
+
// of silently losing the multi-day intent.
|
|
30
|
+
export function isMalformedRange(item: ScheduledItem): boolean {
|
|
31
|
+
if (item.props.endDate === undefined || item.props.endDate === null) return false;
|
|
32
|
+
if (typeof item.props.endDate !== "string" || item.props.endDate.length === 0) return false;
|
|
33
|
+
const start = asIsoDate(item.props.date);
|
|
34
|
+
const end = asIsoDate(item.props.endDate);
|
|
35
|
+
if (!start || !end) return true;
|
|
36
|
+
return end < start;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function coversDay(item: ScheduledItem, dateStr: string): boolean {
|
|
40
|
+
const range = eventRange(item);
|
|
41
|
+
if (!range) return false;
|
|
42
|
+
return range.start <= dateStr && dateStr <= range.end;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function segmentPosition(item: ScheduledItem, dateStr: string): SegmentPosition | null {
|
|
46
|
+
const range = eventRange(item);
|
|
47
|
+
if (!range) return null;
|
|
48
|
+
if (dateStr < range.start || dateStr > range.end) return null;
|
|
49
|
+
if (range.start === range.end) return "only";
|
|
50
|
+
if (dateStr === range.start) return "start";
|
|
51
|
+
if (dateStr === range.end) return "end";
|
|
52
|
+
return "middle";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Per-event color so adjacent multi-day events read as distinct
|
|
56
|
+
// bars instead of one indistinguishable blue block. Full class
|
|
57
|
+
// strings (not template-built) so Tailwind's content scanner can
|
|
58
|
+
// find every variant.
|
|
59
|
+
//
|
|
60
|
+
// Covers all 17 of Tailwind's chromatic hues at bg-100/text-900
|
|
61
|
+
// (legible on white) with hover:bg-200. The wide palette keeps
|
|
62
|
+
// collisions rare even when 10+ events stack near each other.
|
|
63
|
+
const EVENT_PALETTE = [
|
|
64
|
+
"bg-red-100 text-red-900 hover:bg-red-200",
|
|
65
|
+
"bg-orange-100 text-orange-900 hover:bg-orange-200",
|
|
66
|
+
"bg-amber-100 text-amber-900 hover:bg-amber-200",
|
|
67
|
+
"bg-yellow-100 text-yellow-900 hover:bg-yellow-200",
|
|
68
|
+
"bg-lime-100 text-lime-900 hover:bg-lime-200",
|
|
69
|
+
"bg-green-100 text-green-900 hover:bg-green-200",
|
|
70
|
+
"bg-emerald-100 text-emerald-900 hover:bg-emerald-200",
|
|
71
|
+
"bg-teal-100 text-teal-900 hover:bg-teal-200",
|
|
72
|
+
"bg-cyan-100 text-cyan-900 hover:bg-cyan-200",
|
|
73
|
+
"bg-sky-100 text-sky-900 hover:bg-sky-200",
|
|
74
|
+
"bg-blue-100 text-blue-900 hover:bg-blue-200",
|
|
75
|
+
"bg-indigo-100 text-indigo-900 hover:bg-indigo-200",
|
|
76
|
+
"bg-violet-100 text-violet-900 hover:bg-violet-200",
|
|
77
|
+
"bg-purple-100 text-purple-900 hover:bg-purple-200",
|
|
78
|
+
"bg-fuchsia-100 text-fuchsia-900 hover:bg-fuchsia-200",
|
|
79
|
+
"bg-pink-100 text-pink-900 hover:bg-pink-200",
|
|
80
|
+
"bg-rose-100 text-rose-900 hover:bg-rose-200",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export function eventColorClasses(eventId: string): string {
|
|
84
|
+
// Defensive on non-string input: handlers sanitize ids at write,
|
|
85
|
+
// but pre-existing on-disk items may pre-date that guard and
|
|
86
|
+
// shouldn't crash the render.
|
|
87
|
+
const safe = typeof eventId === "string" ? eventId : "";
|
|
88
|
+
let hash = 0;
|
|
89
|
+
for (let i = 0; i < safe.length; i++) {
|
|
90
|
+
hash = (hash * 31 + safe.charCodeAt(i)) | 0;
|
|
91
|
+
}
|
|
92
|
+
return EVENT_PALETTE[Math.abs(hash) % EVENT_PALETTE.length] ?? EVENT_PALETTE[0] ?? "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const EVENT_PALETTE_SIZE = EVENT_PALETTE.length;
|
|
@@ -13,11 +13,7 @@
|
|
|
13
13
|
<details class="group">
|
|
14
14
|
<summary class="cursor-pointer list-none p-4 flex items-start gap-3 hover:bg-purple-100/40 rounded-lg" :data-testid="'skill-summary-' + skillName">
|
|
15
15
|
<span class="material-icons text-purple-600 text-base mt-0.5 shrink-0">extension</span>
|
|
16
|
-
|
|
17
|
-
`.stack-natural :deep(.flex-1) { flex: 0 0 auto !important }` rule (intended
|
|
18
|
-
for vertical-flex content height) would otherwise pin this horizontal flex
|
|
19
|
-
child to its content width, breaking long-description wrapping in stack mode. -->
|
|
20
|
-
<div class="grow shrink basis-0 min-w-0">
|
|
16
|
+
<div class="flex-1 min-w-0">
|
|
21
17
|
<div class="flex items-baseline gap-2 flex-wrap">
|
|
22
18
|
<span class="font-medium text-purple-900">{{ skillName }}</span>
|
|
23
19
|
<span v-if="skillScope !== 'unknown'" class="text-[10px] uppercase tracking-wide text-purple-500 px-1.5 py-0.5 rounded-full bg-purple-100">
|
|
@@ -201,9 +201,9 @@ const loading = ref(false);
|
|
|
201
201
|
const errorMessage = ref("");
|
|
202
202
|
const resolvedSheets = ref<SpreadsheetSheet[]>([]);
|
|
203
203
|
|
|
204
|
-
// Accepts only the canonical prefix.
|
|
205
|
-
// references in old session JSONL
|
|
206
|
-
//
|
|
204
|
+
// Accepts only the canonical prefix. Legacy `spreadsheets/*.json`
|
|
205
|
+
// references in old session JSONL are no longer auto-resolved —
|
|
206
|
+
// those sessions render the file path as plain text.
|
|
207
207
|
function isFilePath(value: unknown): value is string {
|
|
208
208
|
return typeof value === "string" && value.startsWith("artifacts/spreadsheets/") && value.endsWith(".json");
|
|
209
209
|
}
|
|
@@ -28,7 +28,7 @@ const toolDefinition: ToolDefinition = {
|
|
|
28
28
|
type: "function",
|
|
29
29
|
name: TOOL_NAME,
|
|
30
30
|
description: "Display an Excel-like spreadsheet with formulas and calculations.",
|
|
31
|
-
prompt: `Use ${TOOL_NAME} when the user asks for a spreadsheet, table with calculations, or what-if analysis. Use formulas and cell references instead of pre-calculated values so the spreadsheet stays interactive. For cell format details and available functions, read \`helps/spreadsheet.md\` in the workspace.`,
|
|
31
|
+
prompt: `Use ${TOOL_NAME} when the user asks for a spreadsheet, table with calculations, or what-if analysis. Use formulas and cell references instead of pre-calculated values so the spreadsheet stays interactive. For cell format details and available functions, read \`config/helps/spreadsheet.md\` in the workspace.`,
|
|
32
32
|
parameters: {
|
|
33
33
|
type: "object",
|
|
34
34
|
properties: {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<!-- Plugin-seeded first user turn (e.g. Encore): render the same
|
|
3
|
+
one-line extension-icon + label preview the skill plugin uses,
|
|
4
|
+
so the chat-history sidebar reads it as "seeded by a plugin"
|
|
5
|
+
rather than a wall-of-text user message. -->
|
|
6
|
+
<div v-if="isSeededUserTurn" class="flex items-center gap-1.5 text-sm text-gray-700 p-2" data-testid="text-response-preview-seeded">
|
|
7
|
+
<span class="material-icons text-purple-500 text-sm shrink-0">extension</span>
|
|
8
|
+
<span class="truncate font-medium">{{ t("pluginTextResponse.seededByPlugin", { pkg: seededByPlugin }) }}</span>
|
|
9
|
+
</div>
|
|
10
|
+
<div v-else class="p-2">
|
|
3
11
|
<div class="preview-text text-sm leading-snug" :class="textColorClass">{{ previewText }}</div>
|
|
4
12
|
<div v-if="attachments.length > 0" class="flex flex-wrap gap-1 mt-1.5" data-testid="text-response-preview-attachments">
|
|
5
13
|
<SentAttachmentChip v-for="path in attachments" :key="path" :path="path" variant="thumb" />
|
|
@@ -9,16 +17,21 @@
|
|
|
9
17
|
|
|
10
18
|
<script setup lang="ts">
|
|
11
19
|
import { computed } from "vue";
|
|
20
|
+
import { useI18n } from "vue-i18n";
|
|
12
21
|
import { marked } from "marked";
|
|
13
22
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
14
23
|
import type { TextResponseData } from "./types";
|
|
15
24
|
import SentAttachmentChip from "../../components/SentAttachmentChip.vue";
|
|
16
25
|
|
|
26
|
+
const { t } = useI18n();
|
|
27
|
+
|
|
17
28
|
const props = defineProps<{
|
|
18
29
|
result: ToolResultComplete<TextResponseData>;
|
|
19
30
|
}>();
|
|
20
31
|
|
|
21
32
|
const messageRole = computed(() => props.result.data?.role ?? "assistant");
|
|
33
|
+
const seededByPlugin = computed<string>(() => props.result.data?.seededByPlugin ?? "");
|
|
34
|
+
const isSeededUserTurn = computed(() => Boolean(seededByPlugin.value) && messageRole.value === "user");
|
|
22
35
|
|
|
23
36
|
const textColorClass = computed(() => {
|
|
24
37
|
switch (messageRole.value) {
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<!-- Plugin-seeded first user turn (e.g. Encore): mirror the skill
|
|
3
|
+
plugin's collapsed-card layout. Skips the assistant chrome
|
|
4
|
+
(PDF / edit / copy) since the message wasn't authored by the
|
|
5
|
+
user and editing it post-hoc has no meaning. -->
|
|
6
|
+
<div v-if="isSeededUserTurn" class="h-full flex flex-col overflow-y-auto p-6" data-testid="text-response-seeded-card">
|
|
7
|
+
<div class="max-w-3xl mx-auto w-full">
|
|
8
|
+
<div class="rounded-lg border border-purple-200 bg-purple-50 shadow-sm">
|
|
9
|
+
<details class="group">
|
|
10
|
+
<summary class="cursor-pointer list-none p-4 flex items-start gap-3 hover:bg-purple-100/40 rounded-lg" data-testid="text-response-seeded-summary">
|
|
11
|
+
<span class="material-icons text-purple-600 text-base mt-0.5 shrink-0">extension</span>
|
|
12
|
+
<div class="flex-1 min-w-0">
|
|
13
|
+
<div class="flex items-baseline gap-2 flex-wrap">
|
|
14
|
+
<span class="font-medium text-purple-900">{{ t("pluginTextResponse.seededByPlugin", { pkg: seededByPlugin }) }}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="text-sm text-gray-700 mt-1">{{ t("pluginTextResponse.seededByPluginTooltip", { pkg: seededByPlugin }) }}</div>
|
|
17
|
+
</div>
|
|
18
|
+
<span class="material-icons text-gray-400 text-base shrink-0 group-open:rotate-180 transition-transform">expand_more</span>
|
|
19
|
+
</summary>
|
|
20
|
+
<div class="border-t border-purple-200 p-4 bg-white rounded-b-lg">
|
|
21
|
+
<!-- eslint-disable vue/no-v-html -- marked.parse output of the plugin-seeded prompt; trusted in-process render matching the standard textResponse path. Multi-line element so disable/enable pair (CLAUDE.md UI rule). -->
|
|
22
|
+
<div class="markdown-content prose prose-slate max-w-none" @click="openLinksInNewTab" v-html="renderedHtml"></div>
|
|
23
|
+
<!-- eslint-enable vue/no-v-html -->
|
|
24
|
+
<div v-if="messageAttachments.length > 0" class="space-y-3 mt-3" data-testid="text-response-seeded-attachments">
|
|
25
|
+
<SentAttachmentChip v-for="path in messageAttachments" :key="path" :path="path" variant="block" />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</details>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div v-else class="h-full flex flex-col">
|
|
3
33
|
<div v-if="isAssistant" class="flex items-center justify-end gap-2 px-3 py-2 border-b border-gray-100 shrink-0">
|
|
4
34
|
<button
|
|
5
35
|
class="h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
|
|
@@ -17,23 +47,9 @@
|
|
|
17
47
|
<div class="text-response-content-wrapper">
|
|
18
48
|
<div class="p-6">
|
|
19
49
|
<div class="max-w-3xl mx-auto space-y-4">
|
|
20
|
-
<div
|
|
21
|
-
class="rounded-lg border border-gray-300 bg-white shadow-sm p-5"
|
|
22
|
-
:class="roleTheme"
|
|
23
|
-
:data-testid="seededByPlugin ? 'text-response-plugin-seeded' : undefined"
|
|
24
|
-
>
|
|
50
|
+
<div class="rounded-lg border border-gray-300 bg-white shadow-sm p-5" :class="roleTheme">
|
|
25
51
|
<div class="flex justify-between items-start mb-2 text-sm text-gray-500">
|
|
26
|
-
<span class="
|
|
27
|
-
<span class="font-medium text-gray-700">{{ speakerLabel }}</span>
|
|
28
|
-
<span
|
|
29
|
-
v-if="seededByPlugin"
|
|
30
|
-
class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] uppercase font-medium bg-purple-100 text-purple-700 ring-1 ring-purple-200"
|
|
31
|
-
:title="t('pluginTextResponse.seededByPluginTooltip', { pkg: seededByPlugin })"
|
|
32
|
-
data-testid="text-response-seeded-by-plugin"
|
|
33
|
-
>
|
|
34
|
-
{{ t("pluginTextResponse.seededByPlugin", { pkg: seededByPlugin }) }}
|
|
35
|
-
</span>
|
|
36
|
-
</span>
|
|
52
|
+
<span class="font-medium text-gray-700">{{ speakerLabel }}</span>
|
|
37
53
|
<span v-if="transportKind" class="italic">{{ transportKind }}</span>
|
|
38
54
|
</div>
|
|
39
55
|
<!-- eslint-disable vue/no-v-html -- marked.parse output of app-owned assistant response text; trusted in-process render. Multi-line element so disable/enable pair (CLAUDE.md UI rule) instead of -next-line. -->
|
|
@@ -126,6 +142,12 @@ const messageAttachments = computed<string[]>(() => props.selectedResult.data?.a
|
|
|
126
142
|
// muted background variant so the user can tell the message came
|
|
127
143
|
// from a plugin and not themselves.
|
|
128
144
|
const seededByPlugin = computed<string>(() => props.selectedResult.data?.seededByPlugin ?? "");
|
|
145
|
+
// First-user-turn-seeded-by-plugin signal (#1218-adjacent): render
|
|
146
|
+
// the skill-style collapsed card path instead of the default user
|
|
147
|
+
// bubble. `parseSessionEntries` only stamps `seededByPlugin` on the
|
|
148
|
+
// very first user turn of a plugin-origin session, so this branch is
|
|
149
|
+
// inherently scoped to the opening message.
|
|
150
|
+
const isSeededUserTurn = computed(() => Boolean(seededByPlugin.value) && messageRole.value === "user");
|
|
129
151
|
|
|
130
152
|
const renderedHtml = computed(() => {
|
|
131
153
|
if (!messageText.value) return "";
|
|
@@ -164,13 +186,6 @@ const speakerLabel = computed(() => {
|
|
|
164
186
|
});
|
|
165
187
|
|
|
166
188
|
const roleTheme = computed(() => {
|
|
167
|
-
// Plugin-seeded user turns get a muted gray background instead of
|
|
168
|
-
// the standard "user" green so the row reads as "this came from a
|
|
169
|
-
// plugin, not you." The chip beside the speaker label carries the
|
|
170
|
-
// pkg name; the background change is the at-a-glance signal.
|
|
171
|
-
if (seededByPlugin.value && messageRole.value === "user") {
|
|
172
|
-
return "bg-gray-50 border-gray-200";
|
|
173
|
-
}
|
|
174
189
|
switch (messageRole.value) {
|
|
175
190
|
case "system":
|
|
176
191
|
return "bg-blue-50 border-blue-200";
|
|
@@ -61,6 +61,10 @@ function onClick(event: MouseEvent) {
|
|
|
61
61
|
.wiki-content :deep(.wiki-link:hover) {
|
|
62
62
|
text-decoration-style: solid;
|
|
63
63
|
}
|
|
64
|
+
.wiki-content :deep(a) {
|
|
65
|
+
color: #2563eb;
|
|
66
|
+
text-decoration: underline;
|
|
67
|
+
}
|
|
64
68
|
.wiki-content :deep(h1) {
|
|
65
69
|
font-size: 1.5rem;
|
|
66
70
|
font-weight: 700;
|
package/src/router/index.ts
CHANGED
|
@@ -71,6 +71,17 @@ const routes: RouteRecordRaw[] = [
|
|
|
71
71
|
// experimental plugin features (notifier engine, etc.). Rendered by
|
|
72
72
|
// the @mulmoclaude/debug-plugin runtime plugin.
|
|
73
73
|
{ path: "/debug", name: PAGE_ROUTES.debug, component: Stub },
|
|
74
|
+
// Encore page. Two surfaces share the route, picked in
|
|
75
|
+
// `src/plugins/encore/View.vue`:
|
|
76
|
+
// - `/encore?pendingId=<uuid>` — chat-on-mount redirect.
|
|
77
|
+
// The tick NEVER calls chat.start; instead it publishes
|
|
78
|
+
// notifications pointing here. On click the View dispatches
|
|
79
|
+
// `resolveNotification` (which calls chat.start server-side)
|
|
80
|
+
// and full-navigates to /chat/<chatId>. Transient (~300ms).
|
|
81
|
+
// - `/encore` (no pendingId) — read-only dashboard listing
|
|
82
|
+
// active obligations + cycle history. Reached from the
|
|
83
|
+
// top-bar launcher. See plans/feat-encore-as-builtin.md.
|
|
84
|
+
{ path: "/encore", name: PAGE_ROUTES.encore, component: Stub },
|
|
74
85
|
{ path: "/:pathMatch(.*)*", redirect: "/chat" },
|
|
75
86
|
];
|
|
76
87
|
|
package/src/router/pageRoutes.ts
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Parser for the `at` mini-DSL used inside `firingPlan[].at` and
|
|
2
|
+
// `steps[].deadline`.
|
|
3
|
+
//
|
|
4
|
+
// Grammar:
|
|
5
|
+
// at-expr := anchor [ offset ]
|
|
6
|
+
// anchor := "cycle-start" | "cycle-deadline" | "step-deadline" | "schedule:" iso-date
|
|
7
|
+
// offset := ("+" | "-") integer "d"
|
|
8
|
+
// iso-date := YYYY "-" MM "-" DD
|
|
9
|
+
//
|
|
10
|
+
// Examples:
|
|
11
|
+
// cycle-start
|
|
12
|
+
// cycle-start+30d
|
|
13
|
+
// cycle-deadline-21d
|
|
14
|
+
// cycle-deadline
|
|
15
|
+
// cycle-deadline+1d
|
|
16
|
+
// step-deadline-3d
|
|
17
|
+
// schedule:2026-02-01
|
|
18
|
+
//
|
|
19
|
+
// `step-deadline` is only valid INSIDE a step's `firingPlan`; the
|
|
20
|
+
// caller supplies an `allowStepDeadline` flag and the parser rejects
|
|
21
|
+
// it everywhere else (notably inside a step's `deadline` field
|
|
22
|
+
// itself).
|
|
23
|
+
//
|
|
24
|
+
// Pure module: no fs, no clock; resolution happens in
|
|
25
|
+
// `./at-resolver.ts` against cycle anchors.
|
|
26
|
+
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
|
|
29
|
+
export type AtAnchor = "cycle-start" | "cycle-deadline" | "step-deadline" | "schedule";
|
|
30
|
+
|
|
31
|
+
export interface AtExpression {
|
|
32
|
+
/** Which anchor the expression resolves against. */
|
|
33
|
+
anchor: AtAnchor;
|
|
34
|
+
/** Day offset added to the anchor. 0 for bare anchors. */
|
|
35
|
+
offsetDays: number;
|
|
36
|
+
/** Absolute ISO date, only present when anchor === "schedule". */
|
|
37
|
+
date?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
41
|
+
|
|
42
|
+
/** Validate that `yyyy-mm-dd` represents a real calendar date.
|
|
43
|
+
* `ISO_DATE_RE` alone accepts "2026-02-30" or "2026-13-01" — Date
|
|
44
|
+
* parsing wraps those silently into March / next-year, which would
|
|
45
|
+
* shift firing schedules without surfacing as an error. */
|
|
46
|
+
function isRealCalendarDate(iso: string): boolean {
|
|
47
|
+
const [year, month, day] = iso.split("-").map(Number);
|
|
48
|
+
if (month < 1 || month > 12) return false;
|
|
49
|
+
if (day < 1 || day > 31) return false;
|
|
50
|
+
const candidate = new Date(Date.UTC(year, month - 1, day));
|
|
51
|
+
return candidate.getUTCFullYear() === year && candidate.getUTCMonth() === month - 1 && candidate.getUTCDate() === day;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Parse a raw at-expression string. Throws on malformed input. */
|
|
55
|
+
export function parseAtExpression(raw: string, opts: { allowStepDeadline: boolean }): AtExpression {
|
|
56
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
57
|
+
throw new Error("at-expression: must be a non-empty string");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// schedule:<iso-date> is its own shape — no offset allowed in v1.
|
|
61
|
+
if (raw.startsWith("schedule:")) {
|
|
62
|
+
const rest = raw.slice("schedule:".length);
|
|
63
|
+
if (!ISO_DATE_RE.test(rest)) {
|
|
64
|
+
throw new Error(`at-expression: "schedule:<YYYY-MM-DD>" expected, got ${JSON.stringify(raw)}`);
|
|
65
|
+
}
|
|
66
|
+
if (!isRealCalendarDate(rest)) {
|
|
67
|
+
throw new Error(`at-expression: ${JSON.stringify(raw)} is not a valid calendar date (Feb 30, Apr 31, month 13, etc. are rejected).`);
|
|
68
|
+
}
|
|
69
|
+
return { anchor: "schedule", offsetDays: 0, date: rest };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// anchor [+/-]Nd. The anchor names are a closed set so we list
|
|
73
|
+
// them explicitly rather than `[a-z-]+`. The alternation is
|
|
74
|
+
// bounded and the optional offset group has no nested
|
|
75
|
+
// quantifiers, so backtracking is constant-bounded — but the
|
|
76
|
+
// security linter doesn't infer that, so we disable it inline
|
|
77
|
+
// with this rationale.
|
|
78
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- alternation is a closed set of literals; optional `(\d+)d` group has no nested quantifiers, no ReDoS surface.
|
|
79
|
+
const offsetMatch = raw.match(/^(cycle-start|cycle-deadline|step-deadline)(?:([+-])(\d+)d)?$/);
|
|
80
|
+
if (!offsetMatch) {
|
|
81
|
+
throw new Error(`at-expression: ${JSON.stringify(raw)} does not match grammar (anchor [±Nd])`);
|
|
82
|
+
}
|
|
83
|
+
const [, anchorStr, sign, days] = offsetMatch;
|
|
84
|
+
let anchor: AtAnchor;
|
|
85
|
+
switch (anchorStr) {
|
|
86
|
+
case "cycle-start":
|
|
87
|
+
anchor = "cycle-start";
|
|
88
|
+
break;
|
|
89
|
+
case "cycle-deadline":
|
|
90
|
+
anchor = "cycle-deadline";
|
|
91
|
+
break;
|
|
92
|
+
case "step-deadline":
|
|
93
|
+
if (!opts.allowStepDeadline) {
|
|
94
|
+
throw new Error(`at-expression: "step-deadline" is only valid inside a step's firingPlan, not in ${JSON.stringify(raw)}`);
|
|
95
|
+
}
|
|
96
|
+
anchor = "step-deadline";
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`at-expression: unknown anchor ${JSON.stringify(anchorStr)} (expected one of cycle-start, cycle-deadline, step-deadline, schedule:DATE)`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const offsetDays = sign && days ? (sign === "+" ? 1 : -1) * Number.parseInt(days, 10) : 0;
|
|
103
|
+
return { anchor, offsetDays };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Zod refinement helper that validates a string against the
|
|
107
|
+
* grammar. Use via `.superRefine` since the `allowStepDeadline`
|
|
108
|
+
* flag depends on where in the DSL tree the expression appears. */
|
|
109
|
+
export function atExprSchema(opts: { allowStepDeadline: boolean }): z.ZodType<string> {
|
|
110
|
+
return z.string().superRefine((value, ctx) => {
|
|
111
|
+
try {
|
|
112
|
+
parseAtExpression(value, opts);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
ctx.addIssue({
|
|
115
|
+
code: z.ZodIssueCode.custom,
|
|
116
|
+
message: err instanceof Error ? err.message : String(err),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Resolve a parsed at-expression to an ISO date, given the
|
|
2
|
+
// cycle / step anchors. Pure function; the caller supplies the
|
|
3
|
+
// anchors (computed from the cadence + slot in `./cadence.ts`).
|
|
4
|
+
|
|
5
|
+
import { addDays } from "./cadence.js";
|
|
6
|
+
import type { AtExpression } from "./at-expression.js";
|
|
7
|
+
|
|
8
|
+
export interface AtAnchors {
|
|
9
|
+
/** ISO date — cycle start. Always available. */
|
|
10
|
+
cycleStart: string;
|
|
11
|
+
/** ISO date — cycle deadline. Always available. */
|
|
12
|
+
cycleDeadline: string;
|
|
13
|
+
/** ISO date — this step's deadline. Only available when resolving
|
|
14
|
+
* inside a step's firingPlan (the step's own deadline is itself
|
|
15
|
+
* an at-expr resolved against cycleStart / cycleDeadline). */
|
|
16
|
+
stepDeadline?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveAtExpression(expr: AtExpression, anchors: AtAnchors): string {
|
|
20
|
+
if (expr.anchor === "cycle-start") return addDays(anchors.cycleStart, expr.offsetDays);
|
|
21
|
+
if (expr.anchor === "cycle-deadline") return addDays(anchors.cycleDeadline, expr.offsetDays);
|
|
22
|
+
if (expr.anchor === "step-deadline") {
|
|
23
|
+
if (!anchors.stepDeadline) {
|
|
24
|
+
throw new Error("at-resolver: step-deadline anchor used but no stepDeadline anchor provided");
|
|
25
|
+
}
|
|
26
|
+
return addDays(anchors.stepDeadline, expr.offsetDays);
|
|
27
|
+
}
|
|
28
|
+
if (!expr.date) {
|
|
29
|
+
throw new Error("at-resolver: schedule anchor missing date");
|
|
30
|
+
}
|
|
31
|
+
return addDays(expr.date, expr.offsetDays);
|
|
32
|
+
}
|