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,125 @@
|
|
|
1
|
+
// Pure helpers behind the /skills page sidebar. Lifted out of View.vue
|
|
2
|
+
// so the section-collapse state and the provenance rule (mc- prefix
|
|
3
|
+
// split, user/project source mapping) live in exactly one place and can
|
|
4
|
+
// be unit-tested in node:test without a DOM or a Vue runtime.
|
|
5
|
+
|
|
6
|
+
import type { SkillSummary } from "./index";
|
|
7
|
+
|
|
8
|
+
// categorizeSkill / pickInitialSelection only care about name + source,
|
|
9
|
+
// not description. Exposing a narrower input type lets unit tests build
|
|
10
|
+
// fixtures without padding placeholder descriptions everywhere.
|
|
11
|
+
export type SkillIdentity = Pick<SkillSummary, "name" | "source">;
|
|
12
|
+
|
|
13
|
+
// `mc-` is the launcher-managed namespace (see
|
|
14
|
+
// server/workspace/skills-preset.ts). Skills under this prefix ship
|
|
15
|
+
// with mulmoclaude and are overwritten on every boot, so the UI treats
|
|
16
|
+
// them as the read-only "system" provenance and gates editing
|
|
17
|
+
// accordingly. This is NOT the sidebar grouping axis — provenance only
|
|
18
|
+
// drives the per-row badge tooltip and the edit/delete gate. The
|
|
19
|
+
// sidebar groups by section (active vs catalog), see SKILL_SECTION_KEYS.
|
|
20
|
+
export const SYSTEM_SKILL_PREFIX = "mc-";
|
|
21
|
+
export type SkillProvenance = "system" | "project" | "user";
|
|
22
|
+
|
|
23
|
+
/** Map a skill to its provenance bucket (badge + edit-gate, not layout). */
|
|
24
|
+
export function categorizeSkill(skill: SkillIdentity): SkillProvenance {
|
|
25
|
+
if (skill.source === "user") return "user";
|
|
26
|
+
if (skill.name.startsWith(SYSTEM_SKILL_PREFIX)) return "system";
|
|
27
|
+
return "project";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Sidebar collapsible sections, aligned with the #1335 catalog/active
|
|
31
|
+
// model: "active" = skills in `.claude/skills/` (discovered by Claude
|
|
32
|
+
// Code, loaded into the system prompt); "catalog" = launcher-managed
|
|
33
|
+
// presets the user can browse / ★ star / ▶ run once without bloating
|
|
34
|
+
// the prompt. Provenance (system/project/user) is shown as a per-row
|
|
35
|
+
// badge inside the Active section, not as its own collapsible group.
|
|
36
|
+
export const SKILL_SECTION_KEYS = ["active", "catalog"] as const;
|
|
37
|
+
export type SkillSectionKey = (typeof SKILL_SECTION_KEYS)[number];
|
|
38
|
+
|
|
39
|
+
export const SECTION_LABEL_KEYS: Record<SkillSectionKey, string> = {
|
|
40
|
+
active: "pluginManageSkills.sectionActive",
|
|
41
|
+
catalog: "pluginManageSkills.sectionCatalog",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Both sections open by default — #1335 shows Active and Catalog
|
|
45
|
+
// expanded; the user collapses whichever they don't want to see.
|
|
46
|
+
export const DEFAULT_CLOSED_SECTIONS: readonly SkillSectionKey[] = [];
|
|
47
|
+
export const COLLAPSED_SECTIONS_STORAGE_KEY = "skills:sectionCollapsed";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @internal exported only so the unit tests can target the type guard
|
|
51
|
+
* directly. Call sites should reach it via loadCollapsedSections.
|
|
52
|
+
*/
|
|
53
|
+
export function isSkillSectionKey(value: unknown): value is SkillSectionKey {
|
|
54
|
+
return typeof value === "string" && (SKILL_SECTION_KEYS as readonly string[]).includes(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read the persisted collapse state, falling back to defaults on any error. */
|
|
58
|
+
export function loadCollapsedSections(): Set<SkillSectionKey> {
|
|
59
|
+
const defaults = new Set<SkillSectionKey>(DEFAULT_CLOSED_SECTIONS);
|
|
60
|
+
if (typeof window === "undefined") return defaults;
|
|
61
|
+
try {
|
|
62
|
+
const raw = window.localStorage.getItem(COLLAPSED_SECTIONS_STORAGE_KEY);
|
|
63
|
+
if (raw === null) return defaults;
|
|
64
|
+
const parsed: unknown = JSON.parse(raw);
|
|
65
|
+
if (!Array.isArray(parsed)) return defaults;
|
|
66
|
+
return new Set<SkillSectionKey>(parsed.filter(isSkillSectionKey));
|
|
67
|
+
} catch {
|
|
68
|
+
return defaults;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Persist the collapse state. Failures (e.g. localStorage disabled) are swallowed. */
|
|
73
|
+
export function persistCollapsedSections(state: ReadonlySet<SkillSectionKey>): void {
|
|
74
|
+
if (typeof window === "undefined") return;
|
|
75
|
+
try {
|
|
76
|
+
window.localStorage.setItem(COLLAPSED_SECTIONS_STORAGE_KEY, JSON.stringify([...state]));
|
|
77
|
+
} catch {
|
|
78
|
+
// localStorage may be unavailable (private mode) — swallow silently.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Per-external-repo collapse state (#1383 PR-C2). Distinct storage key
|
|
83
|
+
// from the section-level state above: the section axis is a fixed
|
|
84
|
+
// 2-value union (active/catalog), whereas repo ids are open-ended
|
|
85
|
+
// (one per installed external repo), so this set is validated as
|
|
86
|
+
// plain strings rather than against a key union. Default: every repo
|
|
87
|
+
// EXPANDED (absent = open) — a freshly installed repo should show its
|
|
88
|
+
// skills without a click.
|
|
89
|
+
export const REPO_COLLAPSED_STORAGE_KEY = "skills:repoCollapsed";
|
|
90
|
+
|
|
91
|
+
/** Read the persisted per-repo collapse set, defaulting to empty
|
|
92
|
+
* (all repos expanded) on any error. */
|
|
93
|
+
export function loadRepoCollapsed(): Set<string> {
|
|
94
|
+
if (typeof window === "undefined") return new Set();
|
|
95
|
+
try {
|
|
96
|
+
const raw = window.localStorage.getItem(REPO_COLLAPSED_STORAGE_KEY);
|
|
97
|
+
if (raw === null) return new Set();
|
|
98
|
+
const parsed: unknown = JSON.parse(raw);
|
|
99
|
+
if (!Array.isArray(parsed)) return new Set();
|
|
100
|
+
return new Set(parsed.filter((entry): entry is string => typeof entry === "string"));
|
|
101
|
+
} catch {
|
|
102
|
+
return new Set();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Persist the per-repo collapse set. Failures are swallowed. */
|
|
107
|
+
export function persistRepoCollapsed(state: ReadonlySet<string>): void {
|
|
108
|
+
if (typeof window === "undefined") return;
|
|
109
|
+
try {
|
|
110
|
+
window.localStorage.setItem(REPO_COLLAPSED_STORAGE_KEY, JSON.stringify([...state]));
|
|
111
|
+
} catch {
|
|
112
|
+
// localStorage may be unavailable (private mode) — swallow silently.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Auto-select the first active skill so the right pane isn't empty on
|
|
118
|
+
* open. Returns null when the Active section is collapsed (don't select
|
|
119
|
+
* a row the user can't see) or when there are no active skills.
|
|
120
|
+
*/
|
|
121
|
+
export function pickInitialSelection(skillList: readonly SkillIdentity[], collapsed: ReadonlySet<SkillSectionKey>): string | null {
|
|
122
|
+
if (skillList.length === 0) return null;
|
|
123
|
+
if (collapsed.has("active")) return null;
|
|
124
|
+
return skillList[0].name;
|
|
125
|
+
}
|
|
@@ -16,6 +16,36 @@ export const META = definePluginMeta({
|
|
|
16
16
|
update: { method: "PUT", path: "/:name" },
|
|
17
17
|
/** DELETE /api/skills/:name — delete a project-scope skill. */
|
|
18
18
|
remove: { method: "DELETE", path: "/:name" },
|
|
19
|
+
/** GET /api/skills/catalog — list catalog entries (preset for
|
|
20
|
+
* now; anthropic / community land in #1335 PR-C). Catalog
|
|
21
|
+
* entries are NOT in `.claude/skills/` and don't enter the
|
|
22
|
+
* Claude Code system prompt until the user ★ Stars them. */
|
|
23
|
+
catalogList: { method: "GET", path: "/catalog" },
|
|
24
|
+
/** POST /api/skills/catalog/star — body `{ source, slug }`.
|
|
25
|
+
* Copies the catalog entry into `.claude/skills/<slug>/` so
|
|
26
|
+
* Claude Code's slash-command resolver picks it up. */
|
|
27
|
+
catalogStar: { method: "POST", path: "/catalog/star" },
|
|
28
|
+
/** GET /api/skills/catalog/preview?source=&slug= — returns one
|
|
29
|
+
* catalog entry's description + body. Used by the 📖 Preview
|
|
30
|
+
* modal and the ▶ Run once action (which feeds `body` into a
|
|
31
|
+
* fresh chat as user input). For external source, pass `repoId`
|
|
32
|
+
* + `skillFolder` instead of `slug`. */
|
|
33
|
+
catalogPreview: { method: "GET", path: "/catalog/preview" },
|
|
34
|
+
/** GET /api/skills/external/suggestions — bundled list of repo
|
|
35
|
+
* URLs the launcher recommends as starting points (Anthropic
|
|
36
|
+
* skills + community picks). */
|
|
37
|
+
externalSuggestions: { method: "GET", path: "/external/suggestions" },
|
|
38
|
+
/** GET /api/skills/external/repos — list installed external
|
|
39
|
+
* repos with their recorded URL / subpath / SHA. */
|
|
40
|
+
externalReposList: { method: "GET", path: "/external/repos" },
|
|
41
|
+
/** POST /api/skills/external/repos — body `{ url, subpath?, ref? }`.
|
|
42
|
+
* Clone (or refresh) the repo, copy each discovered SKILL.md
|
|
43
|
+
* into the catalog under `<external>/<repoId>/`. */
|
|
44
|
+
externalReposInstall: { method: "POST", path: "/external/repos" },
|
|
45
|
+
/** DELETE /api/skills/external/repos/:repoId — remove the
|
|
46
|
+
* catalog dir + scratch clone for one external repo. Active
|
|
47
|
+
* copies under `.claude/skills/` are NOT touched (star = fork). */
|
|
48
|
+
externalReposRemove: { method: "DELETE", path: "/external/repos/:repoId" },
|
|
19
49
|
},
|
|
20
50
|
mcpDispatch: "create",
|
|
21
51
|
});
|
|
@@ -17,9 +17,9 @@ export interface MarkdownToolData {
|
|
|
17
17
|
/** True when the `markdown` field is a workspace-relative file path
|
|
18
18
|
* rather than inline content. Accepts only the canonical
|
|
19
19
|
* `artifacts/documents/*.md` prefix now that server-side
|
|
20
|
-
* `isMarkdownPath` agrees.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* `isMarkdownPath` agrees. Legacy `markdowns/*.md` references in
|
|
21
|
+
* old session JSONL are no longer auto-resolved — those sessions
|
|
22
|
+
* render their markdown content as plain text. */
|
|
23
23
|
export function isFilePath(value: string): boolean {
|
|
24
24
|
if (!value.endsWith(".md")) return false;
|
|
25
25
|
return value.startsWith("artifacts/documents/");
|
|
@@ -80,6 +80,11 @@ export interface PluginMeta {
|
|
|
80
80
|
* live as separate named exports in the plugin's `meta.ts`
|
|
81
81
|
* because their signatures are plugin-specific. */
|
|
82
82
|
readonly staticChannels?: Readonly<Record<string, string>>;
|
|
83
|
+
/** Optional host binaries this plugin's features need (ids from
|
|
84
|
+
* the optional-deps registry, e.g. `["ffmpeg"]`). When any is
|
|
85
|
+
* missing the host warns the user once and the plugin's
|
|
86
|
+
* dependency-bound features degrade gracefully — see #1385. */
|
|
87
|
+
readonly requires?: readonly string[];
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/** Substitute `:param` placeholders in a route URL with caller-
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="p-2 text-sm">
|
|
3
|
-
<div class="font-medium text-gray-700 truncate mb-1">
|
|
2
|
+
<div class="p-2 text-sm" data-testid="mulmo-script-preview">
|
|
3
|
+
<div class="font-medium text-gray-700 truncate mb-1" data-testid="mulmo-script-preview-title">
|
|
4
4
|
{{ title }}
|
|
5
5
|
</div>
|
|
6
|
-
<div v-if="description" class="text-xs text-gray-500 leading-relaxed">
|
|
6
|
+
<div v-if="description" class="text-xs text-gray-500 leading-relaxed" data-testid="mulmo-script-preview-description">
|
|
7
7
|
{{ description }}
|
|
8
8
|
</div>
|
|
9
9
|
</div>
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
<!-- Header -->
|
|
4
4
|
<div class="flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0">
|
|
5
5
|
<div class="min-w-0 flex-1">
|
|
6
|
-
<h2 class="text-lg font-semibold text-gray-800 truncate">
|
|
6
|
+
<h2 class="text-lg font-semibold text-gray-800 truncate" data-testid="mulmo-script-title">
|
|
7
7
|
{{ script.title || "Untitled Script" }}
|
|
8
8
|
</h2>
|
|
9
|
-
<p v-if="script.description" class="text-sm text-gray-500 mt-0.5 truncate">
|
|
9
|
+
<p v-if="script.description" class="text-sm text-gray-500 mt-0.5 truncate" data-testid="mulmo-script-description">
|
|
10
10
|
{{ script.description }}
|
|
11
11
|
</p>
|
|
12
12
|
<div class="flex items-center gap-3 mt-1 text-xs text-gray-400">
|
|
@@ -89,6 +89,33 @@
|
|
|
89
89
|
</div>
|
|
90
90
|
</div>
|
|
91
91
|
|
|
92
|
+
<!--
|
|
93
|
+
Inline error chip for movie-generation failures (#1197).
|
|
94
|
+
Previously the catch arm of `generateMovie` raised an `alert()` —
|
|
95
|
+
blocking, no retry path, and many users just dismissed the modal
|
|
96
|
+
and saw a stalled spinner with no explanation. The chip stays
|
|
97
|
+
visible until the next generate attempt clears it.
|
|
98
|
+
-->
|
|
99
|
+
<div
|
|
100
|
+
v-if="movieError"
|
|
101
|
+
data-testid="mulmo-script-movie-error-chip"
|
|
102
|
+
class="bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2 mx-4 mt-3 mb-1 rounded flex items-start gap-2"
|
|
103
|
+
>
|
|
104
|
+
<span class="material-icons text-base shrink-0 mt-px">error_outline</span>
|
|
105
|
+
<div class="flex-1 min-w-0">
|
|
106
|
+
<div class="font-medium">{{ t("pluginMulmoScript.movieGenerationFailed") }}</div>
|
|
107
|
+
<div class="break-words whitespace-pre-wrap mt-0.5">{{ movieError }}</div>
|
|
108
|
+
</div>
|
|
109
|
+
<button
|
|
110
|
+
class="shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50"
|
|
111
|
+
:disabled="movieGenerating"
|
|
112
|
+
data-testid="mulmo-script-movie-retry-button"
|
|
113
|
+
@click="generateMovie"
|
|
114
|
+
>
|
|
115
|
+
{{ t("pluginMulmoScript.retry") }}
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
92
119
|
<!-- Characters section -->
|
|
93
120
|
<div v-if="characterKeys.length > 0" class="border-b border-gray-100 shrink-0 px-4 py-3">
|
|
94
121
|
<div class="flex items-center justify-between mb-2">
|
|
@@ -459,6 +486,11 @@ interface Beat {
|
|
|
459
486
|
id?: string;
|
|
460
487
|
imagePrompt?: string;
|
|
461
488
|
image?: { type: string; [key: string]: unknown };
|
|
489
|
+
/** Beat duration in seconds. The mulmocast schema notes this is
|
|
490
|
+
* "Used only when the text is empty" — when there's no TTS audio
|
|
491
|
+
* to drive playback, the Play loop uses this as the auto-advance
|
|
492
|
+
* timer (#1073). */
|
|
493
|
+
duration?: number;
|
|
462
494
|
}
|
|
463
495
|
|
|
464
496
|
interface ImageEntry {
|
|
@@ -510,11 +542,27 @@ const localOverrides = reactive<Record<number, Beat>>({});
|
|
|
510
542
|
const movieGenerating = ref(false);
|
|
511
543
|
const movieDownloading = ref(false);
|
|
512
544
|
const moviePath = ref<string | null>(null);
|
|
545
|
+
// Persists the most-recent movie-generation failure so the spinner
|
|
546
|
+
// area can surface it inline with a retry button (#1197). Cleared
|
|
547
|
+
// at the start of every generate / regenerate attempt.
|
|
548
|
+
const movieError = ref<string | null>(null);
|
|
513
549
|
const beatAudios = reactive<Record<number, string>>({});
|
|
514
550
|
const audioState = reactive<Record<number, "generating" | "done" | "error">>({});
|
|
515
551
|
const audioErrors = reactive<Record<number, string>>({});
|
|
516
552
|
const playingAudio = ref<{ index: number; audio: HTMLAudioElement } | null>(null);
|
|
553
|
+
// Tracks the auto-advance timer running on a silent beat
|
|
554
|
+
// (`beat.text === ""`). Beats without text generate no audio, so the
|
|
555
|
+
// Play loop falls back to a `setTimeout(beat.duration)` for cues —
|
|
556
|
+
// without this, Play would stall on the first silent beat (#1073).
|
|
557
|
+
const silentPlaybackTimer = ref<{ index: number; timer: ReturnType<typeof setTimeout> } | null>(null);
|
|
517
558
|
const audioProgress = ref(0);
|
|
559
|
+
|
|
560
|
+
// Default duration (seconds) for a silent beat whose script doesn't
|
|
561
|
+
// set `duration` either. Picked to roughly match the time it takes a
|
|
562
|
+
// reader to scan a `textSlide` — long enough to read, short enough
|
|
563
|
+
// not to feel stuck. The script's own `duration` always wins.
|
|
564
|
+
const SILENT_BEAT_DEFAULT_SEC = 3;
|
|
565
|
+
const MS_PER_SECOND = 1000;
|
|
518
566
|
const beatListEl = ref<HTMLElement | null>(null);
|
|
519
567
|
const lightbox = ref<{
|
|
520
568
|
src: string;
|
|
@@ -564,10 +612,11 @@ function characterPrompt(key: string): string {
|
|
|
564
612
|
}
|
|
565
613
|
|
|
566
614
|
function stopPlayingAudio() {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
615
|
+
// Single helper that clears both the audio path and the silent
|
|
616
|
+
// auto-advance timer — callers (lightbox open / arrow nav / Stop
|
|
617
|
+
// button) get consistent behaviour without remembering which
|
|
618
|
+
// playback mode the current beat was using (#1073).
|
|
619
|
+
stopAllPlayback();
|
|
571
620
|
}
|
|
572
621
|
|
|
573
622
|
function openLightbox(index: number) {
|
|
@@ -614,7 +663,76 @@ const isPlayReady = computed<boolean>(() => {
|
|
|
614
663
|
function playPresentation() {
|
|
615
664
|
if (!isPlayReady.value) return;
|
|
616
665
|
openLightbox(0);
|
|
617
|
-
|
|
666
|
+
playBeat(0);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Stop whichever playback handle is active. Idempotent. Called by
|
|
670
|
+
// openLightbox, manual stop / pause buttons, and by `playBeat`
|
|
671
|
+
// before kicking off a new beat so we never double-schedule. (#1073)
|
|
672
|
+
function stopAllPlayback(): void {
|
|
673
|
+
if (playingAudio.value) {
|
|
674
|
+
playingAudio.value.audio.pause();
|
|
675
|
+
playingAudio.value = null;
|
|
676
|
+
audioProgress.value = 0;
|
|
677
|
+
}
|
|
678
|
+
if (silentPlaybackTimer.value) {
|
|
679
|
+
clearTimeout(silentPlaybackTimer.value.timer);
|
|
680
|
+
silentPlaybackTimer.value = null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Single entry point for "start playback at beat <index>". Routes
|
|
685
|
+
// on what the script DECLARED, not on what's currently hydrated:
|
|
686
|
+
//
|
|
687
|
+
// - `text` empty → silent path (`scheduleSilentAdvance`). The
|
|
688
|
+
// schema says no audio is generated for empty-text beats, so
|
|
689
|
+
// `duration` drives auto-advance.
|
|
690
|
+
// - `text` present + audio loaded → audio path. `audio.ended`
|
|
691
|
+
// chains via `advanceFromBeat`.
|
|
692
|
+
// - `text` present + audio NOT loaded → stop. The Play button's
|
|
693
|
+
// `isPlayReady` gate prevented this for beat 0, but mid-stream
|
|
694
|
+
// a transient fetch miss must not silently skip the narration
|
|
695
|
+
// by falling through to the silent timer (Codex review on
|
|
696
|
+
// #1073 — gating on `beatAudios[index]` would do exactly that).
|
|
697
|
+
//
|
|
698
|
+
// Either path chains to the next beat via `advanceFromBeat`, so a
|
|
699
|
+
// run of silent beats — or audio / silent / audio sequences —
|
|
700
|
+
// plays through without manual interaction.
|
|
701
|
+
function playBeat(index: number): void {
|
|
702
|
+
stopAllPlayback();
|
|
703
|
+
const hasText = Boolean(effectiveBeat(index).text);
|
|
704
|
+
if (!hasText) {
|
|
705
|
+
scheduleSilentAdvance(index);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (beatAudios[index]) {
|
|
709
|
+
playAudio(index);
|
|
710
|
+
}
|
|
711
|
+
// Text beat with no audio yet → stop. The user can re-click Play
|
|
712
|
+
// once the audio finishes hydrating.
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function scheduleSilentAdvance(index: number): void {
|
|
716
|
+
// Defensively narrow the script-supplied duration. A bad value
|
|
717
|
+
// (zero, negative, NaN, non-number) would otherwise collapse to
|
|
718
|
+
// an immediate timeout and the Play loop would race through every
|
|
719
|
+
// silent beat in a single tick (Codex review iter-5 on #1365).
|
|
720
|
+
// Falling back to the default keeps the presentation watchable.
|
|
721
|
+
const raw = effectiveBeat(index).duration;
|
|
722
|
+
const seconds = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : SILENT_BEAT_DEFAULT_SEC;
|
|
723
|
+
const timer = setTimeout(() => {
|
|
724
|
+
if (silentPlaybackTimer.value?.index !== index) return;
|
|
725
|
+
silentPlaybackTimer.value = null;
|
|
726
|
+
if (lightbox.value?.index === index) advanceFromBeat(index);
|
|
727
|
+
}, seconds * MS_PER_SECOND);
|
|
728
|
+
silentPlaybackTimer.value = { index, timer };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function advanceFromBeat(fromIndex: number): void {
|
|
732
|
+
lightboxMove(1);
|
|
733
|
+
const nextIndex = lightbox.value?.index;
|
|
734
|
+
if (nextIndex === undefined || nextIndex === fromIndex) return;
|
|
735
|
+
playBeat(nextIndex);
|
|
618
736
|
}
|
|
619
737
|
|
|
620
738
|
const hasPrev = computed(() => {
|
|
@@ -637,11 +755,12 @@ function jumpToBeat(index: number) {
|
|
|
637
755
|
if (!lightbox.value) return;
|
|
638
756
|
if (index === lightbox.value.index) return;
|
|
639
757
|
if (!renderedImages[index]) return;
|
|
640
|
-
|
|
758
|
+
// Carry the playback mode forward (audio OR silent timer) so a
|
|
759
|
+
// user clicking the beat-strip thumbnail mid-playback keeps the
|
|
760
|
+
// presentation rolling (#1073).
|
|
761
|
+
const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;
|
|
641
762
|
openLightbox(index);
|
|
642
|
-
if (wasPlaying
|
|
643
|
-
playAudio(index);
|
|
644
|
-
}
|
|
763
|
+
if (wasPlaying) playBeat(index);
|
|
645
764
|
}
|
|
646
765
|
|
|
647
766
|
function beatTooltip(index: number): string {
|
|
@@ -652,20 +771,19 @@ function beatTooltip(index: number): string {
|
|
|
652
771
|
function lightboxMove(delta: number) {
|
|
653
772
|
if (!lightbox.value) return;
|
|
654
773
|
const total = beats.value.length;
|
|
655
|
-
// If
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
// branch won't
|
|
661
|
-
|
|
774
|
+
// If a playback was in progress when the user clicked the arrow,
|
|
775
|
+
// carry it forward to whichever beat we land on — `playBeat`
|
|
776
|
+
// picks audio vs silent automatically. `openLightbox` stops the
|
|
777
|
+
// current playback, so capture the flag BEFORE that and chain
|
|
778
|
+
// AFTER. The on-ended / silent-advance paths already null their
|
|
779
|
+
// own state before calling `lightboxMove`, so this branch won't
|
|
780
|
+
// double-fire there.
|
|
781
|
+
const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;
|
|
662
782
|
let i = lightbox.value.index + delta;
|
|
663
783
|
while (i >= 0 && i < total) {
|
|
664
784
|
if (renderedImages[i]) {
|
|
665
785
|
openLightbox(i);
|
|
666
|
-
if (wasPlaying
|
|
667
|
-
playAudio(i);
|
|
668
|
-
}
|
|
786
|
+
if (wasPlaying) playBeat(i);
|
|
669
787
|
return;
|
|
670
788
|
}
|
|
671
789
|
i += delta;
|
|
@@ -911,13 +1029,7 @@ function playAudio(index: number) {
|
|
|
911
1029
|
if (playingAudio.value?.index !== index) return;
|
|
912
1030
|
playingAudio.value = null;
|
|
913
1031
|
audioProgress.value = 0;
|
|
914
|
-
if (lightbox.value?.index === index)
|
|
915
|
-
lightboxMove(1);
|
|
916
|
-
const nextIndex = lightbox.value?.index;
|
|
917
|
-
if (nextIndex !== undefined && nextIndex !== index && beatAudios[nextIndex]) {
|
|
918
|
-
playAudio(nextIndex);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
1032
|
+
if (lightbox.value?.index === index) advanceFromBeat(index);
|
|
921
1033
|
});
|
|
922
1034
|
audio.play();
|
|
923
1035
|
}
|
|
@@ -1019,10 +1131,9 @@ async function onCharDrop(event: DragEvent, key: string) {
|
|
|
1019
1131
|
}
|
|
1020
1132
|
|
|
1021
1133
|
function openCharacterLightbox(key: string) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1134
|
+
// Stop both audio and silent timer — character lightbox is
|
|
1135
|
+
// outside the play loop (#1073).
|
|
1136
|
+
stopAllPlayback();
|
|
1026
1137
|
lightbox.value = {
|
|
1027
1138
|
src: charImages[key],
|
|
1028
1139
|
text: key,
|
|
@@ -1135,6 +1246,15 @@ async function refreshScriptFromDisk(): Promise<void> {
|
|
|
1135
1246
|
}
|
|
1136
1247
|
|
|
1137
1248
|
async function initializeScript() {
|
|
1249
|
+
// Stop any in-flight playback BEFORE we tear down per-script state
|
|
1250
|
+
// — a pending `silentPlaybackTimer` or running audio from the
|
|
1251
|
+
// previous script would otherwise fire `advanceFromBeat()` against
|
|
1252
|
+
// the new script's lightbox / beat list and either crash or
|
|
1253
|
+
// silently jump the new presentation forward. Also close any open
|
|
1254
|
+
// lightbox so the user lands on the clean View for the new result
|
|
1255
|
+
// (Codex review iter-4 on #1365).
|
|
1256
|
+
stopAllPlayback();
|
|
1257
|
+
lightbox.value = null;
|
|
1138
1258
|
// Reset scroll position so new results start at the top
|
|
1139
1259
|
if (beatListEl.value) beatListEl.value.scrollTop = 0;
|
|
1140
1260
|
// Reset per-script state
|
|
@@ -1275,6 +1395,7 @@ async function refreshMoviePath(): Promise<void> {
|
|
|
1275
1395
|
|
|
1276
1396
|
async function generateMovie() {
|
|
1277
1397
|
movieGenerating.value = true;
|
|
1398
|
+
movieError.value = null;
|
|
1278
1399
|
try {
|
|
1279
1400
|
const res = await apiFetchRaw(endpoints.generateMovie.url, {
|
|
1280
1401
|
method: "POST",
|
|
@@ -1296,7 +1417,10 @@ async function generateMovie() {
|
|
|
1296
1417
|
},
|
|
1297
1418
|
});
|
|
1298
1419
|
} catch (err) {
|
|
1299
|
-
alert(
|
|
1420
|
+
// Surface inline (instead of `alert()` which blocks + has no
|
|
1421
|
+
// retry affordance). The error chip with a retry button lives
|
|
1422
|
+
// next to the generate button in the template (#1197).
|
|
1423
|
+
movieError.value = extractErrorMessage(err);
|
|
1300
1424
|
} finally {
|
|
1301
1425
|
movieGenerating.value = false;
|
|
1302
1426
|
}
|
|
@@ -49,4 +49,8 @@ export const META = definePluginMeta({
|
|
|
49
49
|
downloadMovie: { method: "GET", path: "/download-movie" },
|
|
50
50
|
},
|
|
51
51
|
mcpDispatch: "save",
|
|
52
|
+
// mulmocast shells out to ffmpeg for movie/beat rendering. Without
|
|
53
|
+
// it the editor still works but render/generate-movie degrades —
|
|
54
|
+
// the host warns once at boot (#1385).
|
|
55
|
+
requires: ["ffmpeg"],
|
|
52
56
|
});
|
|
@@ -127,12 +127,13 @@
|
|
|
127
127
|
<div
|
|
128
128
|
v-for="item in itemsForDay(day)"
|
|
129
129
|
:key="item.id"
|
|
130
|
-
class="text-xs px-1.5 py-0.5
|
|
131
|
-
:class="selectedId === item.id ? 'bg-blue-500 text-white' :
|
|
132
|
-
:title="item
|
|
130
|
+
class="text-xs px-1.5 py-0.5 cursor-pointer truncate"
|
|
131
|
+
:class="[segmentClasses(item, day), selectedId === item.id ? 'bg-blue-500 text-white' : chipColorClasses(item)]"
|
|
132
|
+
:title="chipTitle(item)"
|
|
133
133
|
@click="selectItem(item)"
|
|
134
134
|
>
|
|
135
|
-
<span v-if="itemTime(item)" class="font-medium">{{ itemTime(item) }} </span
|
|
135
|
+
<span v-if="isBrokenChip(item)" class="font-medium">⚠ </span><span v-else-if="itemTime(item)" class="font-medium">{{ itemTime(item) }} </span
|
|
136
|
+
>{{ item.title }}
|
|
136
137
|
</div>
|
|
137
138
|
</div>
|
|
138
139
|
</div>
|
|
@@ -173,12 +174,12 @@
|
|
|
173
174
|
<div
|
|
174
175
|
v-for="item in itemsForDay(day).slice(0, MAX_MONTH_ITEMS)"
|
|
175
176
|
:key="item.id"
|
|
176
|
-
class="text-[10px] leading-tight px-1 py-0.5
|
|
177
|
-
:class="selectedId === item.id ? 'bg-blue-500 text-white' :
|
|
178
|
-
:title="item
|
|
177
|
+
class="text-[10px] leading-tight px-1 py-0.5 cursor-pointer truncate"
|
|
178
|
+
:class="[segmentClasses(item, day), selectedId === item.id ? 'bg-blue-500 text-white' : chipColorClasses(item)]"
|
|
179
|
+
:title="chipTitle(item)"
|
|
179
180
|
@click="selectItem(item)"
|
|
180
181
|
>
|
|
181
|
-
{{ item.title }}
|
|
182
|
+
<span v-if="isBrokenChip(item)" class="font-medium">⚠ </span>{{ item.title }}
|
|
182
183
|
</div>
|
|
183
184
|
<div v-if="itemsForDay(day).length > MAX_MONTH_ITEMS" class="text-[10px] text-gray-400 px-1">
|
|
184
185
|
{{ t("pluginScheduler.moreCount", { count: itemsForDay(day).length - MAX_MONTH_ITEMS }) }}
|
|
@@ -261,6 +262,7 @@ import TasksTab from "./TasksTab.vue";
|
|
|
261
262
|
import { isToday, formatShortDate, formatMonthYear } from "../../utils/format/date";
|
|
262
263
|
import { errorMessage } from "../../utils/errors";
|
|
263
264
|
import { SCHEDULER_VIEW, SCHEDULER_VIEW_MODES as VIEW_MODES, SCHEDULER_TAB, type SchedulerViewMode as ViewMode, type SchedulerTab } from "./viewModes";
|
|
265
|
+
import { coversDay, eventColorClasses, isMalformedRange, segmentPosition, type SegmentPosition } from "./multiDayHelpers";
|
|
264
266
|
|
|
265
267
|
const { t } = useI18n();
|
|
266
268
|
|
|
@@ -367,7 +369,41 @@ function toDateString(date: Date): string {
|
|
|
367
369
|
|
|
368
370
|
function itemsForDay(day: Date): ScheduledItem[] {
|
|
369
371
|
const dateStr = toDateString(day);
|
|
370
|
-
return items.value.filter((item) =>
|
|
372
|
+
return items.value.filter((item) => coversDay(item, dateStr));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const SEGMENT_BASE: Record<SegmentPosition, string> = {
|
|
376
|
+
only: "rounded",
|
|
377
|
+
start: "rounded-l",
|
|
378
|
+
middle: "",
|
|
379
|
+
end: "rounded-r",
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
function segmentClasses(item: ScheduledItem, day: Date): string {
|
|
383
|
+
const pos = segmentPosition(item, toDateString(day));
|
|
384
|
+
return pos ? SEGMENT_BASE[pos] : "rounded";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Red dashed outline + warning-amber background screams "this is
|
|
388
|
+
// wrong, click and fix it." Returns a class string when the event
|
|
389
|
+
// has a broken range; empty when the event is well-formed and the
|
|
390
|
+
// per-event palette colour should apply.
|
|
391
|
+
const BROKEN_CLASSES = "bg-red-50 text-red-900 hover:bg-red-100 border border-dashed border-red-400";
|
|
392
|
+
|
|
393
|
+
function chipColorClasses(item: ScheduledItem): string {
|
|
394
|
+
if (isMalformedRange(item)) return BROKEN_CLASSES;
|
|
395
|
+
return eventColorClasses(item.id);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function chipTitle(item: ScheduledItem): string {
|
|
399
|
+
if (isMalformedRange(item)) {
|
|
400
|
+
return `⚠ ${t("pluginScheduler.invalidRange", { endDate: String(item.props.endDate) })} — ${item.title}`;
|
|
401
|
+
}
|
|
402
|
+
return item.title;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isBrokenChip(item: ScheduledItem): boolean {
|
|
406
|
+
return isMalformedRange(item);
|
|
371
407
|
}
|
|
372
408
|
|
|
373
409
|
const unscheduledItems = computed(() => items.value.filter((item) => !item.props.date));
|
|
@@ -11,9 +11,10 @@ const toolDefinition: ToolDefinition = {
|
|
|
11
11
|
prompt:
|
|
12
12
|
"When users mention calendar events, appointments, meetings, or one-off reminders that have a date/time, use manageCalendar. " +
|
|
13
13
|
"Use show to display the calendar, add to create an event, update to edit one, delete to remove one. " +
|
|
14
|
+
"Multi-day events (trips, conferences, vacations) set both `date` (start, inclusive) and `endDate` (end, inclusive) in `props`, both as `YYYY-MM-DD`. " +
|
|
14
15
|
"For recurring automated tasks driven by a schedule (e.g. 'every morning at 8 fetch news'), use manageAutomations instead.",
|
|
15
16
|
description:
|
|
16
|
-
"Manage the user's calendar — show / add / update / delete dated calendar items. Calendar items have a title and free-form properties (date, time, location, …).",
|
|
17
|
+
"Manage the user's calendar — show / add / update / delete dated calendar items. Calendar items have a title and free-form properties (date, time, location, …); multi-day events also set endDate.",
|
|
17
18
|
parameters: {
|
|
18
19
|
type: "object",
|
|
19
20
|
properties: {
|
|
@@ -32,7 +33,10 @@ const toolDefinition: ToolDefinition = {
|
|
|
32
33
|
},
|
|
33
34
|
props: {
|
|
34
35
|
type: "object",
|
|
35
|
-
description:
|
|
36
|
+
description:
|
|
37
|
+
"For 'add': initial properties (e.g. { date, time, location, endDate }). " +
|
|
38
|
+
"`date` and `endDate` are ISO `YYYY-MM-DD`; `endDate` is the inclusive last day of a multi-day event (omit for single-day events). " +
|
|
39
|
+
"For 'update': properties to merge in; set a key to null to remove it.",
|
|
36
40
|
additionalProperties: true,
|
|
37
41
|
},
|
|
38
42
|
},
|