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.
Files changed (182) hide show
  1. package/README.md +26 -0
  2. package/bin/mulmoclaude.js +11 -1
  3. package/client/assets/JsonEditor-D6WBWLoa.js +10 -0
  4. package/client/assets/JsonEditor-Di5xGeZY.css +1 -0
  5. package/client/assets/_plugin-vue_export-helper-BOai-rQB.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-LcKZGJv5.js +1 -0
  7. package/client/assets/{html2canvas-CDGcmOD3-Bkf2uOth.js → html2canvas-CDGcmOD3-XVrO-eyz.js} +1 -1
  8. package/client/assets/index-CyBr8Mkr.css +2 -0
  9. package/client/assets/index-zZIqEbNX.js +5106 -0
  10. package/client/assets/{index.es-DqtpmBm8-D9mAh_KQ.js → index.es-DqtpmBm8-DHT6q10o.js} +1 -1
  11. package/client/assets/material-symbols-outlined-DtIK7AQn.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-D6kcV0wa.js +1 -0
  13. package/client/assets/{runtime-vue-BVUzgYGA.js → runtime-vue-fFYhnNg3.js} +1 -1
  14. package/client/assets/{vue-C8UuIO9J.js → vue-D4w8THF_.js} +1 -1
  15. package/client/assets/vue-i18n-CQbxVmNs.js +3 -0
  16. package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +4 -0
  17. package/client/index.html +10 -10
  18. package/package.json +9 -8
  19. package/server/agent/backend/claude-code.ts +34 -0
  20. package/server/agent/backend/fake-echo.ts +370 -0
  21. package/server/agent/backend/index.ts +16 -1
  22. package/server/agent/config.ts +74 -24
  23. package/server/agent/index.ts +104 -80
  24. package/server/agent/mcpFailureMonitor.ts +167 -0
  25. package/server/agent/mcpPreflight.ts +185 -0
  26. package/server/agent/prompt.ts +50 -359
  27. package/server/agent/stdioHttpShim.ts +171 -0
  28. package/server/agent/stream.ts +12 -1
  29. package/server/api/routes/encore.ts +55 -0
  30. package/server/api/routes/files.ts +22 -0
  31. package/server/api/routes/mulmo-script.ts +19 -1
  32. package/server/api/routes/schedulerHandlers.ts +52 -4
  33. package/server/api/routes/sessions.ts +15 -0
  34. package/server/api/routes/skills.ts +263 -0
  35. package/server/build/dispatcher.mjs +299 -0
  36. package/server/encore/INVARIANTS.md +272 -0
  37. package/server/encore/boot.ts +39 -0
  38. package/server/encore/closure.ts +36 -0
  39. package/server/encore/cycle.ts +276 -0
  40. package/server/encore/dispatch.ts +103 -0
  41. package/server/encore/handlers/amend.ts +99 -0
  42. package/server/encore/handlers/appendNote.ts +74 -0
  43. package/server/encore/handlers/defineEncore.ts +42 -0
  44. package/server/encore/handlers/listTickets.ts +107 -0
  45. package/server/encore/handlers/markStepDone.ts +41 -0
  46. package/server/encore/handlers/markTargetSkipped.ts +33 -0
  47. package/server/encore/handlers/query.ts +138 -0
  48. package/server/encore/handlers/recordValues.ts +44 -0
  49. package/server/encore/handlers/resolveNotification.ts +121 -0
  50. package/server/encore/handlers/setup.ts +81 -0
  51. package/server/encore/handlers/shared.ts +137 -0
  52. package/server/encore/handlers/snooze.ts +87 -0
  53. package/server/encore/handlers/startObligationChat.ts +64 -0
  54. package/server/encore/handlers/startSetupChat.ts +50 -0
  55. package/server/encore/lock.ts +61 -0
  56. package/server/encore/notifier.ts +123 -0
  57. package/server/encore/obligation.ts +25 -0
  58. package/server/encore/paths.ts +78 -0
  59. package/server/encore/reconcile.ts +661 -0
  60. package/server/encore/tick.ts +191 -0
  61. package/server/encore/yaml-fm.ts +63 -0
  62. package/server/events/notifications.ts +19 -91
  63. package/server/index.ts +94 -9
  64. package/server/notifier/engine.ts +102 -1
  65. package/server/notifier/macosReminderAdapter.ts +30 -0
  66. package/server/notifier/runtime-api.ts +41 -1
  67. package/server/notifier/types.ts +15 -2
  68. package/server/plugins/runtime.ts +11 -2
  69. package/server/prompts/index.ts +39 -0
  70. package/server/prompts/system/journal-pointer.md +12 -0
  71. package/server/prompts/system/memory-management-atomic.md +33 -0
  72. package/server/prompts/system/memory-management-topic.md +60 -0
  73. package/server/prompts/system/news-concierge.md +24 -0
  74. package/server/prompts/system/sandbox-tools.md +10 -0
  75. package/server/prompts/system/sources-context.md +16 -0
  76. package/server/prompts/system/system.md +91 -0
  77. package/server/system/announceOptionalDeps.ts +57 -0
  78. package/server/system/appVersion.ts +34 -0
  79. package/server/system/config.ts +17 -1
  80. package/server/system/docker.ts +14 -6
  81. package/server/system/env.ts +18 -5
  82. package/server/system/optionalDeps.ts +129 -0
  83. package/server/utils/cli-flags.d.mts +14 -0
  84. package/server/utils/cli-flags.mjs +53 -0
  85. package/server/utils/files/encore-io.ts +111 -0
  86. package/server/utils/time.ts +6 -0
  87. package/server/workspace/helps/business.md +2 -2
  88. package/server/workspace/helps/encore-dsl.md +482 -0
  89. package/server/workspace/helps/index.md +15 -13
  90. package/server/workspace/helps/mulmoscript.md +3 -3
  91. package/server/workspace/helps/sandbox.md +2 -2
  92. package/server/workspace/hooks/dispatcher.ts +7 -5
  93. package/server/workspace/hooks/provision.ts +6 -3
  94. package/server/workspace/paths.ts +13 -4
  95. package/server/workspace/skills/catalog.ts +355 -0
  96. package/server/workspace/skills/external/catalog.ts +283 -0
  97. package/server/workspace/skills/external/clone.ts +129 -0
  98. package/server/workspace/skills/external/id.ts +194 -0
  99. package/server/workspace/skills/external/install.ts +417 -0
  100. package/server/workspace/skills/external/presets.ts +50 -0
  101. package/server/workspace/skills-preset.ts +29 -17
  102. package/server/workspace/workspace.ts +10 -5
  103. package/src/App.vue +37 -8
  104. package/src/components/FileContentRenderer.vue +102 -9
  105. package/src/components/JsonEditor.vue +160 -0
  106. package/src/components/NotificationBell.vue +35 -3
  107. package/src/components/PluginLauncher.vue +20 -41
  108. package/src/components/RightSidebar.vue +19 -0
  109. package/src/components/SettingsMcpTab.vue +58 -11
  110. package/src/components/SettingsModal.vue +22 -1
  111. package/src/components/StackView.vue +10 -1
  112. package/src/components/TodoExplorer.vue +16 -0
  113. package/src/components/todo/TodoKanbanView.vue +34 -6
  114. package/src/composables/useNotifications.ts +21 -1
  115. package/src/config/apiRoutes.ts +0 -6
  116. package/src/config/mcpCatalog.ts +12 -7
  117. package/src/config/mcpTypes.ts +5 -0
  118. package/src/config/roles.ts +52 -15
  119. package/src/config/systemFileDescriptors.ts +12 -0
  120. package/src/lang/de.ts +108 -12
  121. package/src/lang/en.ts +105 -11
  122. package/src/lang/es.ts +106 -11
  123. package/src/lang/fr.ts +106 -11
  124. package/src/lang/ja.ts +104 -11
  125. package/src/lang/ko.ts +105 -11
  126. package/src/lang/pt-BR.ts +106 -11
  127. package/src/lang/zh.ts +103 -11
  128. package/src/main.ts +1 -0
  129. package/src/plugins/_generated/metas.ts +4 -0
  130. package/src/plugins/_generated/registrations.ts +2 -0
  131. package/src/plugins/_generated/server-bindings.ts +5 -0
  132. package/src/plugins/encore/EncoreDashboard.vue +504 -0
  133. package/src/plugins/encore/EncoreRedirect.vue +116 -0
  134. package/src/plugins/encore/View.vue +36 -0
  135. package/src/plugins/encore/defineEncoreDefinition.ts +74 -0
  136. package/src/plugins/encore/defineEncoreMeta.ts +13 -0
  137. package/src/plugins/encore/index.ts +93 -0
  138. package/src/plugins/encore/manageEncoreDefinition.ts +100 -0
  139. package/src/plugins/encore/manageEncoreMeta.ts +36 -0
  140. package/src/plugins/manageSkills/View.vue +832 -30
  141. package/src/plugins/manageSkills/categories.ts +125 -0
  142. package/src/plugins/manageSkills/meta.ts +30 -0
  143. package/src/plugins/markdown/definition.ts +3 -3
  144. package/src/plugins/meta-types.ts +5 -0
  145. package/src/plugins/presentMulmoScript/Preview.vue +3 -3
  146. package/src/plugins/presentMulmoScript/View.vue +157 -33
  147. package/src/plugins/presentMulmoScript/meta.ts +4 -0
  148. package/src/plugins/scheduler/View.vue +45 -9
  149. package/src/plugins/scheduler/calendarDefinition.ts +6 -2
  150. package/src/plugins/scheduler/multiDayHelpers.ts +95 -0
  151. package/src/plugins/skill/View.vue +1 -5
  152. package/src/plugins/spreadsheet/View.vue +3 -3
  153. package/src/plugins/spreadsheet/definition.ts +1 -1
  154. package/src/plugins/textResponse/Preview.vue +14 -1
  155. package/src/plugins/textResponse/View.vue +39 -24
  156. package/src/plugins/wiki/components/WikiPageBody.vue +4 -0
  157. package/src/router/index.ts +11 -0
  158. package/src/router/pageRoutes.ts +1 -0
  159. package/src/types/encore-dsl/at-expression.ts +120 -0
  160. package/src/types/encore-dsl/at-resolver.ts +32 -0
  161. package/src/types/encore-dsl/cadence.ts +289 -0
  162. package/src/types/encore-dsl/schema.ts +288 -0
  163. package/src/types/notification.ts +2 -1
  164. package/src/types/session.ts +6 -0
  165. package/src/types/sse.ts +5 -0
  166. package/src/types/toolCallHistory.ts +7 -0
  167. package/src/utils/agent/eventDispatch.ts +26 -5
  168. package/src/utils/agent/mcpHint.ts +50 -0
  169. package/src/utils/image/htmlSrcAttrs.ts +117 -13
  170. package/src/utils/session/sessionEntries.ts +8 -32
  171. package/client/assets/PluginScopedRoot-YjvQq0Nn.js +0 -3
  172. package/client/assets/chunk-CernVdwh.js +0 -1
  173. package/client/assets/chunk-D8eiyYIV-CAXpUwLd.js +0 -1
  174. package/client/assets/index-BwrlMMHr.js +0 -5005
  175. package/client/assets/index-CvvNuegU.css +0 -2
  176. package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
  177. package/client/assets/runtime-protocol-vue-C1To4M3t.js +0 -1
  178. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +0 -4
  179. package/server/api/routes/notifications.ts +0 -195
  180. package/server/notifier/legacy-adapters.ts +0 -76
  181. package/server/workspace/hooks/dispatcher.mjs +0 -300
  182. 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
- <!-- `grow shrink basis-0` instead of `flex-1` is intentional: StackView's
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. Any legacy `spreadsheets/*.json`
205
- // references in old session JSONL must be migrated via
206
- // `scripts/migrate-legacy-artifact-paths.ts` (#773).
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
- <div class="p-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
- <div class="h-full flex flex-col">
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="flex items-center gap-2">
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;
@@ -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
 
@@ -19,6 +19,7 @@ export const PAGE_ROUTES = {
19
19
  sources: "sources",
20
20
  news: "news",
21
21
  debug: "debug",
22
+ encore: "encore",
22
23
  } as const;
23
24
 
24
25
  export type PageRouteName = (typeof PAGE_ROUTES)[keyof typeof PAGE_ROUTES];
@@ -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
+ }