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,417 @@
|
|
|
1
|
+
// Install / uninstall of an external-skill repo into the catalog
|
|
2
|
+
// (#1383 / #1335 PR-C).
|
|
3
|
+
//
|
|
4
|
+
// Pipeline for install:
|
|
5
|
+
// 1. validate URL → derive repoId
|
|
6
|
+
// 2. clone (or refresh) the scratch dir via `cloneOrUpdate`
|
|
7
|
+
// 3. discover SKILL.md files inside the (optionally subpath-
|
|
8
|
+
// scoped) tree
|
|
9
|
+
// 4. copy each discovered skill folder into
|
|
10
|
+
// `<workspace>/data/skills/catalog/external/<repoId>/`
|
|
11
|
+
// 5. write `.source.json` recording url / subpath / ref / sha / time
|
|
12
|
+
//
|
|
13
|
+
// Uninstall is the inverse: blow away the catalog dir for `repoId`
|
|
14
|
+
// and the corresponding scratch clone. Active copies (anything the
|
|
15
|
+
// user already Starred into `.claude/skills/`) are NOT touched —
|
|
16
|
+
// star = fork, so users keep what they activated even after the
|
|
17
|
+
// upstream repo is removed.
|
|
18
|
+
|
|
19
|
+
import { copyFile, lstat, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
|
|
22
|
+
import { workspacePath } from "../../workspace.js";
|
|
23
|
+
import { WORKSPACE_DIRS } from "../../paths.js";
|
|
24
|
+
import { parseSkillFrontmatter } from "../parser.js";
|
|
25
|
+
import { log } from "../../../system/logger/index.js";
|
|
26
|
+
import { errorMessage } from "../../../utils/errors.js";
|
|
27
|
+
import { writeFileAtomic } from "../../../utils/files/index.js";
|
|
28
|
+
import { cloneOrUpdate, defaultCacheRoot, type CloneDeps, type CloneResult } from "./clone.js";
|
|
29
|
+
import { canonicalRepoUrl, deriveRepoId, safeRepoId, safeSkillFolder, sanitiseSubpath, urlCacheKey } from "./id.js";
|
|
30
|
+
|
|
31
|
+
const SOURCE_METADATA_FILE = ".source.json";
|
|
32
|
+
|
|
33
|
+
export interface InstallRepoOptions {
|
|
34
|
+
url: string;
|
|
35
|
+
subpath?: string;
|
|
36
|
+
ref?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface InstallResult {
|
|
40
|
+
repoId: string;
|
|
41
|
+
url: string;
|
|
42
|
+
subpath?: string;
|
|
43
|
+
ref?: string;
|
|
44
|
+
sha: string;
|
|
45
|
+
installedAt: string;
|
|
46
|
+
/** Number of SKILL.md files discovered + copied into the catalog
|
|
47
|
+
* dir. Zero is technically valid (an empty repo) but suspicious;
|
|
48
|
+
* callers may surface it as a warning. */
|
|
49
|
+
skillCount: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ExternalInstallOptions extends CloneDeps {
|
|
53
|
+
/** Override the workspace root. Default: live `workspacePath`. */
|
|
54
|
+
workspaceRoot?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface DiscoveredSkill {
|
|
58
|
+
/** Folder name as it appears in the catalog dir. `"."` indicates
|
|
59
|
+
* the SKILL.md lives at the repo root (single-skill repo). */
|
|
60
|
+
folder: string;
|
|
61
|
+
/** Absolute source path of the skill dir inside the scratch
|
|
62
|
+
* clone. */
|
|
63
|
+
sourceDir: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function isDirectory(absPath: string): Promise<boolean> {
|
|
67
|
+
try {
|
|
68
|
+
return (await stat(absPath)).isDirectory();
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// `lstat`-based checks for the UNTRUSTED scratch-clone tree. A
|
|
75
|
+
// malicious external repo can ship symlinks (`SKILL.md` →
|
|
76
|
+
// `/etc/passwd`, a skill dir → `/`); `stat` follows them, so
|
|
77
|
+
// discovery/copy would read or duplicate host files. These reject
|
|
78
|
+
// symlinks (and anything that isn't a plain dir / plain file). Our
|
|
79
|
+
// own catalog dirs still use the `stat`-based helpers above.
|
|
80
|
+
async function isRealDirectory(absPath: string): Promise<boolean> {
|
|
81
|
+
try {
|
|
82
|
+
return (await lstat(absPath)).isDirectory();
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function isRealFile(absPath: string): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
return (await lstat(absPath)).isFile();
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function hasParseableSkill(skillDir: string): Promise<boolean> {
|
|
97
|
+
const skillMd = path.join(skillDir, "SKILL.md");
|
|
98
|
+
if (!(await isRealFile(skillMd))) return false;
|
|
99
|
+
try {
|
|
100
|
+
const raw = await readFile(skillMd, "utf-8");
|
|
101
|
+
return parseSkillFrontmatter(raw) !== null;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Discovery rules:
|
|
108
|
+
* - `subpath` given → glob `<scratch>/<subpath>/‍*‍/SKILL.md`,
|
|
109
|
+
* one level deep.
|
|
110
|
+
* - Else → first try `<scratch>/SKILL.md` (single-skill-at-root).
|
|
111
|
+
* If absent, glob `<scratch>/‍*‍/SKILL.md` (multi-skill
|
|
112
|
+
* without a `subpath` declaration). */
|
|
113
|
+
async function discoverSkills(cacheDir: string, subpath: string | undefined): Promise<DiscoveredSkill[]> {
|
|
114
|
+
if (subpath) {
|
|
115
|
+
const scanRoot = path.join(cacheDir, subpath);
|
|
116
|
+
if (!(await isRealDirectory(scanRoot))) return [];
|
|
117
|
+
return await scanOneLevel(scanRoot);
|
|
118
|
+
}
|
|
119
|
+
if (await hasParseableSkill(cacheDir)) {
|
|
120
|
+
return [{ folder: ".", sourceDir: cacheDir }];
|
|
121
|
+
}
|
|
122
|
+
return await scanOneLevel(cacheDir);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function scanOneLevel(scanRoot: string): Promise<DiscoveredSkill[]> {
|
|
126
|
+
let entries: string[];
|
|
127
|
+
try {
|
|
128
|
+
entries = await readdir(scanRoot);
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const out: DiscoveredSkill[] = [];
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
// Same folder-name rule the read/star side uses (`safeSkillFolder`)
|
|
135
|
+
// so anything we accept here stays listable + addressable. Skips
|
|
136
|
+
// hidden / `.` / `..` / separator-bearing names too.
|
|
137
|
+
const folder = safeSkillFolder(entry);
|
|
138
|
+
if (folder === null) continue;
|
|
139
|
+
const candidate = path.join(scanRoot, folder);
|
|
140
|
+
if (!(await isRealDirectory(candidate))) continue;
|
|
141
|
+
if (!(await hasParseableSkill(candidate))) continue;
|
|
142
|
+
out.push({ folder, sourceDir: candidate });
|
|
143
|
+
}
|
|
144
|
+
// Stable order for deterministic test output.
|
|
145
|
+
out.sort((left, right) => left.folder.localeCompare(right.folder));
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function catalogExternalRoot(workspaceRoot: string): string {
|
|
150
|
+
return path.join(workspaceRoot, WORKSPACE_DIRS.skillsCatalog, "external");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface ExtRepoPaths {
|
|
154
|
+
repoDir: string;
|
|
155
|
+
metadataPath: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function pathsForRepo(workspaceRoot: string, repoId: string): ExtRepoPaths {
|
|
159
|
+
const repoDir = path.join(catalogExternalRoot(workspaceRoot), repoId);
|
|
160
|
+
return { repoDir, metadataPath: path.join(repoDir, SOURCE_METADATA_FILE) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function writeMetadata(metadataPath: string, payload: Omit<InstallResult, "repoId" | "skillCount">): Promise<void> {
|
|
164
|
+
await writeFileAtomic(metadataPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Copy the discovered skill folder into the catalog under
|
|
168
|
+
* `<external>/<repoId>/<targetFolder>/`. For single-skill-at-root
|
|
169
|
+
* installs (`folder === "."`), the SKILL.md + siblings are copied
|
|
170
|
+
* directly under the repo dir; nothing nests. */
|
|
171
|
+
async function copyIntoCatalog(skill: DiscoveredSkill, repoDir: string): Promise<void> {
|
|
172
|
+
if (skill.folder === ".") {
|
|
173
|
+
await copyTreeNoSymlinks(skill.sourceDir, repoDir, [".git", SOURCE_METADATA_FILE]);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
await copyTreeNoSymlinks(skill.sourceDir, path.join(repoDir, skill.folder), [".git", SOURCE_METADATA_FILE]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Recursively copy `srcDir` → `destDir`, copying ONLY plain dirs and
|
|
180
|
+
* plain files. `Dirent` predicates use `lstat` semantics, so a
|
|
181
|
+
* symlink entry is neither `isDirectory()` nor `isFile()` and is
|
|
182
|
+
* silently skipped — an external repo can't smuggle a `SKILL.md` →
|
|
183
|
+
* `/etc/passwd` (or a dir symlink) into the catalog / active layer.
|
|
184
|
+
* `node:fs cp` with `recursive` would dereference such links. */
|
|
185
|
+
async function copyTreeNoSymlinks(srcDir: string, destDir: string, skip: readonly string[]): Promise<void> {
|
|
186
|
+
await mkdir(destDir, { recursive: true });
|
|
187
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
if (skip.includes(entry.name)) continue;
|
|
190
|
+
const src = path.join(srcDir, entry.name);
|
|
191
|
+
const dst = path.join(destDir, entry.name);
|
|
192
|
+
if (entry.isDirectory()) {
|
|
193
|
+
await copyTreeNoSymlinks(src, dst, skip);
|
|
194
|
+
} else if (entry.isFile()) {
|
|
195
|
+
await copyFile(src, dst);
|
|
196
|
+
}
|
|
197
|
+
// Symlinks / sockets / FIFOs intentionally skipped.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type InstallExternalRepoResult =
|
|
202
|
+
| { kind: "installed"; detail: InstallResult }
|
|
203
|
+
| { kind: "invalid-url"; url: string }
|
|
204
|
+
| { kind: "invalid-subpath"; subpath: string }
|
|
205
|
+
| { kind: "no-skills"; repoId: string; sha: string }
|
|
206
|
+
| { kind: "id-collision"; repoId: string; existingUrl: string }
|
|
207
|
+
| { kind: "error"; reason: string };
|
|
208
|
+
|
|
209
|
+
/** When `repoDir` already holds an install, return its recorded
|
|
210
|
+
* canonical URL. `repoId` is a lossy punctuation-normalised slug, so
|
|
211
|
+
* two different repos (`foo/a.b`, `foo/a-b`) can map to the same dir;
|
|
212
|
+
* the canonical URL in `.source.json` is what actually identifies
|
|
213
|
+
* the occupant. */
|
|
214
|
+
async function existingCanonicalUrl(metadataPath: string): Promise<string | null> {
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(await readFile(metadataPath, "utf-8")) as { url?: unknown };
|
|
217
|
+
return typeof parsed.url === "string" ? canonicalRepoUrl(parsed.url) : null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Install an external skill repo into the catalog. */
|
|
224
|
+
export async function installExternalRepo(opts: InstallRepoOptions, deps: ExternalInstallOptions = {}): Promise<InstallExternalRepoResult> {
|
|
225
|
+
const repoId = deriveRepoId(opts.url);
|
|
226
|
+
if (!repoId) return { kind: "invalid-url", url: opts.url };
|
|
227
|
+
|
|
228
|
+
// Sanitise the subpath BEFORE it reaches `path.join` / the git
|
|
229
|
+
// sparse-checkout pattern. `path.join` collapses `..`, so an
|
|
230
|
+
// un-validated subpath could escape the scratch cache dir.
|
|
231
|
+
let subpath: string | undefined;
|
|
232
|
+
if (opts.subpath !== undefined) {
|
|
233
|
+
const clean = sanitiseSubpath(opts.subpath);
|
|
234
|
+
if (clean === null) return { kind: "invalid-subpath", subpath: opts.subpath };
|
|
235
|
+
subpath = clean;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const workspaceRoot = deps.workspaceRoot ?? workspacePath;
|
|
239
|
+
let clone: CloneResult;
|
|
240
|
+
try {
|
|
241
|
+
clone = await cloneOrUpdate({ url: opts.url, subpath, ref: opts.ref }, deps);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
log.warn("skills-external", "git clone failed", { url: opts.url, error: errorMessage(err) });
|
|
244
|
+
return { kind: "error", reason: errorMessage(err, "git clone failed") };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const skills = await discoverSkills(clone.cacheDir, subpath);
|
|
248
|
+
|
|
249
|
+
// Bail BEFORE touching the catalog. Wiping + recreating + writing
|
|
250
|
+
// `.source.json` first meant a zero-skill fetch returned 422 while
|
|
251
|
+
// `GET /external/repos` still listed the repo and the previous
|
|
252
|
+
// catalog contents were already destroyed. Leaving a prior good
|
|
253
|
+
// install intact on a transient empty fetch is the safer outcome.
|
|
254
|
+
if (skills.length === 0) {
|
|
255
|
+
log.warn("skills-external", "install found no skills; catalog left untouched", { repoId, url: opts.url, subpath });
|
|
256
|
+
return { kind: "no-skills", repoId, sha: clone.sha };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const { repoDir, metadataPath } = pathsForRepo(workspaceRoot, repoId);
|
|
260
|
+
|
|
261
|
+
// Collision guard: `repoId` is a lossy slug. If this dir is already
|
|
262
|
+
// occupied by a DIFFERENT source repo (canonical URL mismatch),
|
|
263
|
+
// wiping it would silently destroy that repo's catalog + metadata.
|
|
264
|
+
// Refuse instead.
|
|
265
|
+
const occupant = await existingCanonicalUrl(metadataPath);
|
|
266
|
+
const incoming = canonicalRepoUrl(opts.url);
|
|
267
|
+
if (occupant && incoming && occupant !== incoming) {
|
|
268
|
+
log.warn("skills-external", "repoId collision; refusing to overwrite", { repoId, occupant, incoming });
|
|
269
|
+
return { kind: "id-collision", repoId, existingUrl: occupant };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Wipe the previous catalog tree for this repoId so a re-install
|
|
273
|
+
// starts clean (removed skills don't linger). Cache dir is kept
|
|
274
|
+
// because we still need the checked-out tree to copy from.
|
|
275
|
+
await rm(repoDir, { recursive: true, force: true });
|
|
276
|
+
await mkdir(repoDir, { recursive: true });
|
|
277
|
+
|
|
278
|
+
for (const skill of skills) {
|
|
279
|
+
await copyIntoCatalog(skill, repoDir);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const installedAt = new Date().toISOString();
|
|
283
|
+
const detail: InstallResult = {
|
|
284
|
+
repoId,
|
|
285
|
+
url: opts.url,
|
|
286
|
+
subpath,
|
|
287
|
+
ref: opts.ref,
|
|
288
|
+
sha: clone.sha,
|
|
289
|
+
installedAt,
|
|
290
|
+
skillCount: skills.length,
|
|
291
|
+
};
|
|
292
|
+
await writeMetadata(metadataPath, {
|
|
293
|
+
url: opts.url,
|
|
294
|
+
subpath,
|
|
295
|
+
ref: opts.ref,
|
|
296
|
+
sha: clone.sha,
|
|
297
|
+
installedAt,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
log.info("skills-external", "installed repo", { repoId, sha: clone.sha, skillCount: skills.length });
|
|
301
|
+
return { kind: "installed", detail };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export type UninstallResult = { kind: "uninstalled"; repoId: string } | { kind: "not-found"; repoId: string } | { kind: "invalid-repo-id"; repoId: string };
|
|
305
|
+
|
|
306
|
+
/** Uninstall a repo: remove `data/skills/catalog/external/<repoId>/`
|
|
307
|
+
* and the matching scratch clone keyed by the recorded URL. Active
|
|
308
|
+
* copies under `.claude/skills/` are left in place. */
|
|
309
|
+
export async function uninstallExternalRepo(repoIdRaw: string, deps: ExternalInstallOptions = {}): Promise<UninstallResult> {
|
|
310
|
+
// `repoIdRaw` is user-controlled (DELETE /external/repos/:repoId).
|
|
311
|
+
// Launder via the `path.basename` round-trip BEFORE composing any
|
|
312
|
+
// path — CodeQL only clears `js/path-injection` taint through that
|
|
313
|
+
// sanitiser, not a bare regex `.test()`.
|
|
314
|
+
const repoId = safeRepoId(repoIdRaw);
|
|
315
|
+
if (repoId === null) return { kind: "invalid-repo-id", repoId: repoIdRaw };
|
|
316
|
+
const workspaceRoot = deps.workspaceRoot ?? workspacePath;
|
|
317
|
+
const { repoDir, metadataPath } = pathsForRepo(workspaceRoot, repoId);
|
|
318
|
+
if (!(await isDirectory(repoDir))) return { kind: "not-found", repoId };
|
|
319
|
+
|
|
320
|
+
// Read URL from metadata BEFORE removing so we can also drop the
|
|
321
|
+
// scratch clone. Missing metadata isn't fatal — uninstall the
|
|
322
|
+
// catalog dir regardless.
|
|
323
|
+
let url: string | null = null;
|
|
324
|
+
try {
|
|
325
|
+
const raw = await readFile(metadataPath, "utf-8");
|
|
326
|
+
const parsed = JSON.parse(raw) as { url?: unknown };
|
|
327
|
+
const { url: parsedUrl } = parsed;
|
|
328
|
+
if (typeof parsedUrl === "string") url = parsedUrl;
|
|
329
|
+
} catch {
|
|
330
|
+
/* missing or corrupt metadata — proceed with catalog cleanup */
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await rm(repoDir, { recursive: true, force: true });
|
|
334
|
+
if (url) {
|
|
335
|
+
const cacheRoot = deps.cacheRoot ?? defaultCacheRoot();
|
|
336
|
+
const cacheDir = path.join(cacheRoot, urlCacheKey(url));
|
|
337
|
+
await rm(cacheDir, { recursive: true, force: true }).catch((err) => {
|
|
338
|
+
log.warn("skills-external", "scratch clone cleanup failed", { repoId, cacheDir, error: errorMessage(err) });
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
log.info("skills-external", "uninstalled repo", { repoId });
|
|
342
|
+
return { kind: "uninstalled", repoId };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface InstalledRepo {
|
|
346
|
+
repoId: string;
|
|
347
|
+
url: string;
|
|
348
|
+
subpath?: string;
|
|
349
|
+
ref?: string;
|
|
350
|
+
sha: string;
|
|
351
|
+
installedAt: string;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
interface RepoMetadataShape {
|
|
355
|
+
url: string;
|
|
356
|
+
subpath?: string;
|
|
357
|
+
ref?: string;
|
|
358
|
+
sha: string;
|
|
359
|
+
installedAt: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isRepoMetadata(value: unknown): value is RepoMetadataShape {
|
|
363
|
+
if (!value || typeof value !== "object") return false;
|
|
364
|
+
const record = value as Record<string, unknown>;
|
|
365
|
+
if (typeof record.url !== "string") return false;
|
|
366
|
+
if (typeof record.sha !== "string") return false;
|
|
367
|
+
if (typeof record.installedAt !== "string") return false;
|
|
368
|
+
if (record.subpath !== undefined && typeof record.subpath !== "string") return false;
|
|
369
|
+
if (record.ref !== undefined && typeof record.ref !== "string") return false;
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Enumerate installed external repos by reading each `.source.json`
|
|
374
|
+
* under `data/skills/catalog/external/`. Repos whose metadata fails
|
|
375
|
+
* shape validation are skipped with a `log.warn`. */
|
|
376
|
+
export async function listInstalledRepos(deps: ExternalInstallOptions = {}): Promise<InstalledRepo[]> {
|
|
377
|
+
const workspaceRoot = deps.workspaceRoot ?? workspacePath;
|
|
378
|
+
const root = catalogExternalRoot(workspaceRoot);
|
|
379
|
+
let entries: string[];
|
|
380
|
+
try {
|
|
381
|
+
entries = await readdir(root);
|
|
382
|
+
} catch {
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
const out: InstalledRepo[] = [];
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
if (entry.startsWith(".")) continue;
|
|
388
|
+
// `entry` is `readdir`-sourced (tainted). Launder through the
|
|
389
|
+
// `path.basename` round-trip before any join.
|
|
390
|
+
const repoId = safeRepoId(entry);
|
|
391
|
+
if (repoId === null) continue;
|
|
392
|
+
const repoDir = path.join(root, repoId);
|
|
393
|
+
if (!(await isDirectory(repoDir))) continue;
|
|
394
|
+
const metadataPath = path.join(repoDir, SOURCE_METADATA_FILE);
|
|
395
|
+
let parsed: unknown;
|
|
396
|
+
try {
|
|
397
|
+
parsed = JSON.parse(await readFile(metadataPath, "utf-8"));
|
|
398
|
+
} catch {
|
|
399
|
+
log.warn("skills-external", "metadata missing or unreadable; skipping repo", { repoId });
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (!isRepoMetadata(parsed)) {
|
|
403
|
+
log.warn("skills-external", "metadata failed shape check; skipping repo", { repoId });
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
out.push({
|
|
407
|
+
repoId,
|
|
408
|
+
url: parsed.url,
|
|
409
|
+
subpath: parsed.subpath,
|
|
410
|
+
ref: parsed.ref,
|
|
411
|
+
sha: parsed.sha,
|
|
412
|
+
installedAt: parsed.installedAt,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
out.sort((left, right) => left.repoId.localeCompare(right.repoId));
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Bundled external-skill suggestions surfaced by
|
|
2
|
+
// `GET /api/skills/external/suggestions` (#1383 / #1335 PR-C).
|
|
3
|
+
//
|
|
4
|
+
// These are hand-curated repos we recommend to new users. The list
|
|
5
|
+
// ships in the launcher binary — there's no workspace-side state to
|
|
6
|
+
// migrate when entries change across releases. Users discover them
|
|
7
|
+
// through the "+ Add skill repository" modal (UI lands in PR-C2).
|
|
8
|
+
//
|
|
9
|
+
// Adding an entry here is a deliberate editorial act: each new repo
|
|
10
|
+
// expands what new users see by default. Stay conservative — verify
|
|
11
|
+
// the repo's license is permissive, the skill quality is high, and
|
|
12
|
+
// the repo is actively maintained.
|
|
13
|
+
|
|
14
|
+
export interface ExternalPresetSuggestion {
|
|
15
|
+
url: string;
|
|
16
|
+
/** Optional subpath if the repo bundles multiple skills under a
|
|
17
|
+
* common dir (Anthropic's `skills/<name>/` layout). Omitted for
|
|
18
|
+
* single-skill repos that ship `SKILL.md` at the root. */
|
|
19
|
+
subpath?: string;
|
|
20
|
+
/** User-facing repo name. Free-form; UI may show it as the
|
|
21
|
+
* collapsible section heading. */
|
|
22
|
+
displayName: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/** SPDX-ish license identifier from the repo's LICENSE file.
|
|
25
|
+
* Display-only; not enforced. */
|
|
26
|
+
license?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const EXTERNAL_PRESETS: readonly ExternalPresetSuggestion[] = [
|
|
30
|
+
{
|
|
31
|
+
url: "https://github.com/anthropics/skills",
|
|
32
|
+
subpath: "skills",
|
|
33
|
+
displayName: "Anthropic skills",
|
|
34
|
+
description: "Anthropic's official skill collection — PDF / Excel / DOCX builders, MCP server scaffolder, and more.",
|
|
35
|
+
license: "MIT",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
// Engineering-workflow skills that also pay off inside
|
|
39
|
+
// MulmoClaude's task agent: brainstorming, writing/executing
|
|
40
|
+
// plans, systematic-debugging, code review. Layout is one-level
|
|
41
|
+
// (`skills/<name>/SKILL.md`), so the existing one-level
|
|
42
|
+
// discovery handles it without a scanner change. Verified
|
|
43
|
+
// 2026-05: MIT, actively maintained, high-quality skills.
|
|
44
|
+
url: "https://github.com/obra/superpowers",
|
|
45
|
+
subpath: "skills",
|
|
46
|
+
displayName: "Superpowers",
|
|
47
|
+
description: "Battle-tested workflow skills — brainstorming, planning, systematic debugging, TDD, and code review.",
|
|
48
|
+
license: "MIT",
|
|
49
|
+
},
|
|
50
|
+
] as const;
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
// Preset skills bundled with mulmoclaude
|
|
2
|
-
// `helps/`: this file owns the boot-time copy from
|
|
3
|
-
// `server/workspace/skills-preset/<slug>/SKILL.md` (shipped with the
|
|
4
|
-
// launcher tarball) into `<workspaceRoot>/.claude/skills/<slug>/SKILL.md`,
|
|
5
|
-
// where Claude Code's slash-command resolver picks them up.
|
|
1
|
+
// Preset skills bundled with mulmoclaude.
|
|
6
2
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
3
|
+
// History: introduced in #1210 to ship launcher-managed "factory"
|
|
4
|
+
// skills like `mc-library`. Originally synced straight into
|
|
5
|
+
// `<workspaceRoot>/.claude/skills/<slug>/`, which made every preset
|
|
6
|
+
// auto-active and inflated the Claude system prompt as new presets
|
|
7
|
+
// landed.
|
|
8
|
+
//
|
|
9
|
+
// #1335 PR-A flipped the destination to the catalog
|
|
10
|
+
// (`<workspaceRoot>/data/skills/catalog/preset/<slug>/`). Catalog
|
|
11
|
+
// entries are visible to UI / tooling but NOT discovered by Claude
|
|
12
|
+
// Code's slash-command resolver — they don't enter the system
|
|
13
|
+
// prompt unless the user (or a later UI in PR-B) explicitly copies
|
|
14
|
+
// one into `.claude/skills/`.
|
|
15
|
+
//
|
|
16
|
+
// The launcher overwrites catalog entries unconditionally on every
|
|
17
|
+
// boot — they're factory defaults, not user state. Anything in
|
|
18
|
+
// `.claude/skills/` (active layer) is left untouched.
|
|
13
19
|
//
|
|
14
20
|
// `syncPresetSkills(...)` is exported as a pure-ish helper (takes
|
|
15
21
|
// paths + a logger sink, returns a summary) so tests can drive it
|
|
@@ -25,7 +31,10 @@ const SKILL_FILENAME = "SKILL.md";
|
|
|
25
31
|
export interface SyncPresetSkillsOptions {
|
|
26
32
|
/** Source directory: `<launcher>/server/workspace/skills-preset/`. */
|
|
27
33
|
sourceDir: string;
|
|
28
|
-
/** Destination directory:
|
|
34
|
+
/** Destination directory:
|
|
35
|
+
* `<workspaceRoot>/data/skills/catalog/preset/`. The catalog
|
|
36
|
+
* half of the catalog-vs-active split — entries here are visible
|
|
37
|
+
* to UI but NOT to Claude Code's prompt-time skill resolver. */
|
|
29
38
|
destDir: string;
|
|
30
39
|
/** Logger callbacks — kept injectable so tests don't need to
|
|
31
40
|
* spin up the structured logger. The boot-side wrapper threads
|
|
@@ -142,11 +151,14 @@ function removeRetiredPresets(destDir: string, synced: ReadonlySet<string>, opts
|
|
|
142
151
|
}
|
|
143
152
|
}
|
|
144
153
|
|
|
145
|
-
/** Copy every preset slug from `sourceDir` into `destDir
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
154
|
+
/** Copy every preset slug from `sourceDir` into `destDir` (the
|
|
155
|
+
* preset slot under the catalog root), then remove any `mc-*`
|
|
156
|
+
* entries in `destDir` that no longer have a source. The catalog
|
|
157
|
+
* preset subdir is fully launcher-owned, so the `mc-*` prefix
|
|
158
|
+
* check at destination is defence-in-depth: a stray non-preset
|
|
159
|
+
* slug landing in `catalog/preset/` is unexpected, and we'd
|
|
160
|
+
* rather skip it than silently delete a directory we don't
|
|
161
|
+
* recognise. */
|
|
150
162
|
export function syncPresetSkills(opts: SyncPresetSkillsOptions): SyncPresetSkillsResult {
|
|
151
163
|
const result: SyncPresetSkillsResult = { copied: [], removed: [], skipped: [] };
|
|
152
164
|
if (!existsSync(opts.sourceDir)) {
|
|
@@ -41,13 +41,18 @@ export function initWorkspace(): string {
|
|
|
41
41
|
copyFileSync(path.join(TEMPLATES_DIR, file), path.join(WORKSPACE_PATHS.helps, file));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Sync preset skills
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
44
|
+
// Sync preset skills from `server/workspace/skills-preset/` into
|
|
45
|
+
// the catalog (#1335 PR-A). Catalog entries are visible to UI /
|
|
46
|
+
// tooling but NOT in `.claude/skills/`, so they don't enter
|
|
47
|
+
// Claude Code's system prompt by default. Activation (catalog →
|
|
48
|
+
// `.claude/skills/`) lands with the UI in #1335 PR-B; until then
|
|
49
|
+
// the catalog is reachable by file path only. The dest directory
|
|
50
|
+
// is created here because it's outside `EAGER_WORKSPACE_DIRS`
|
|
51
|
+
// (it lives several levels deep and is preset-specific).
|
|
52
|
+
mkdirSync(WORKSPACE_PATHS.skillsCatalogPreset, { recursive: true });
|
|
48
53
|
syncPresetSkills({
|
|
49
54
|
sourceDir: SKILLS_PRESET_SOURCE_DIR,
|
|
50
|
-
destDir: WORKSPACE_PATHS.
|
|
55
|
+
destDir: WORKSPACE_PATHS.skillsCatalogPreset,
|
|
51
56
|
onInfo: (message, data) => log.info("skills-preset", message, data),
|
|
52
57
|
onWarn: (message, data) => log.warn("skills-preset", message, data),
|
|
53
58
|
});
|
package/src/App.vue
CHANGED
|
@@ -158,8 +158,20 @@
|
|
|
158
158
|
<div v-else-if="selectedResult" class="h-full overflow-auto p-6">
|
|
159
159
|
<pre class="text-sm text-gray-700 whitespace-pre-wrap">{{ JSON.stringify(selectedResult, null, 2) }}</pre>
|
|
160
160
|
</div>
|
|
161
|
-
<div v-else class="flex items-center justify-center h-full text-
|
|
162
|
-
<
|
|
161
|
+
<div v-else class="flex flex-col items-center justify-center h-full px-6 text-center">
|
|
162
|
+
<span class="material-icons text-5xl text-gray-400 mb-2" aria-hidden="true">{{ sessionRoleIcon }}</span>
|
|
163
|
+
<p class="text-lg font-medium text-gray-700 mb-4">{{ sessionRoleName }}</p>
|
|
164
|
+
<div v-if="sessionRoleQueries.length > 0" class="flex flex-wrap gap-2 justify-center max-w-xl">
|
|
165
|
+
<button
|
|
166
|
+
v-for="(query, queryIdx) in sessionRoleQueries"
|
|
167
|
+
:key="`${queryIdx}-${query}`"
|
|
168
|
+
class="text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-full px-4 py-2 border border-gray-300 transition-colors"
|
|
169
|
+
@click="sendMessage(query)"
|
|
170
|
+
>
|
|
171
|
+
{{ query }}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
<p v-else class="text-sm text-gray-500">{{ t("app.startConversation") }}</p>
|
|
163
175
|
</div>
|
|
164
176
|
</template>
|
|
165
177
|
<StackView
|
|
@@ -223,6 +235,19 @@
|
|
|
223
235
|
Debug plugin is not loaded. Make sure @mulmoclaude/debug-plugin is built and registered as a preset.
|
|
224
236
|
</div>
|
|
225
237
|
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
|
|
238
|
+
<!-- Encore page. The View branches on `?pendingId`:
|
|
239
|
+
- With pendingId: chat-on-mount redirect (transient,
|
|
240
|
+
~300ms) for notification clicks — the tick publishes
|
|
241
|
+
`/encore?pendingId=<uuid>` URLs and the View
|
|
242
|
+
dispatches resolveNotification then full-navigates
|
|
243
|
+
to /chat/<chatId>.
|
|
244
|
+
- Without pendingId: read-only dashboard of active
|
|
245
|
+
obligations and their cycle history, reachable from
|
|
246
|
+
the top-bar launcher. -->
|
|
247
|
+
<component :is="encoreViewComponent" v-else-if="currentPage === 'encore' && encoreViewComponent" />
|
|
248
|
+
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -- encore chat-on-mount page is a transient redirect, not a user-facing surface. -->
|
|
249
|
+
<div v-else-if="currentPage === 'encore'" class="h-full flex items-center justify-center text-sm text-gray-500">Encore is not loaded.</div>
|
|
250
|
+
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
|
|
226
251
|
</div>
|
|
227
252
|
|
|
228
253
|
<!-- Bottom bar (Stack chat only — plugin views have no
|
|
@@ -328,7 +353,6 @@ import { useGlobalImageErrorRepair } from "./composables/useImageErrorRepair";
|
|
|
328
353
|
import { useMergedSessions } from "./composables/useMergedSessions";
|
|
329
354
|
import { useLayoutMode } from "./composables/useLayoutMode";
|
|
330
355
|
import { useSidePanelVisible } from "./composables/useSidePanelVisible";
|
|
331
|
-
import { useSelectedResult } from "./composables/useSelectedResult";
|
|
332
356
|
import { useMcpTools } from "./composables/useMcpTools";
|
|
333
357
|
import { useRoles } from "./composables/useRoles";
|
|
334
358
|
import { useCurrentRole } from "./composables/useCurrentRole";
|
|
@@ -443,10 +467,11 @@ const { geminiAvailable, sandboxEnabled, cpuLoadRatio, fetchHealth } = useHealth
|
|
|
443
467
|
const { activeSession, toolResults, sidebarResults, isRunning, activeSessionRunning, statusMessage, toolCallHistory, activeSessionCount, unreadCount } =
|
|
444
468
|
useSessionDerived({ sessionMap, currentSessionId, sessions });
|
|
445
469
|
|
|
446
|
-
const
|
|
447
|
-
activeSession,
|
|
448
|
-
|
|
449
|
-
|
|
470
|
+
const selectedResultUuid = computed<string | null>({
|
|
471
|
+
get: () => activeSession.value?.selectedResultUuid ?? null,
|
|
472
|
+
set: (val) => {
|
|
473
|
+
if (activeSession.value) activeSession.value.selectedResultUuid = val;
|
|
474
|
+
},
|
|
450
475
|
});
|
|
451
476
|
|
|
452
477
|
// Display name and icon of the role the active session was created
|
|
@@ -661,6 +686,11 @@ const selectedResult = computed(() => toolResults.value.find((result) => result.
|
|
|
661
686
|
// the /debug branch in the template lights up without a refresh.
|
|
662
687
|
const debugViewComponent = computed(() => getPlugin("manageDebug")?.viewComponent ?? null);
|
|
663
688
|
|
|
689
|
+
// Encore plugin View — built-in plugin under src/plugins/encore/.
|
|
690
|
+
// Mounted at /encore — the chat-on-mount page after a notification-bell click
|
|
691
|
+
// (View.vue dispatches resolveNotification on mount and redirects).
|
|
692
|
+
const encoreViewComponent = computed(() => getPlugin(TOOL_NAMES.manageEncore)?.viewComponent ?? null);
|
|
693
|
+
|
|
664
694
|
// Google Maps API key from `AppSettings.googleMapsApiKey`. Fetched
|
|
665
695
|
// once on mount and refreshed whenever Settings reports a save.
|
|
666
696
|
//
|
|
@@ -855,7 +885,6 @@ async function loadSession(sessionId: string) {
|
|
|
855
885
|
id: sessionId,
|
|
856
886
|
entries: response.data,
|
|
857
887
|
defaultRoleId: roles.value[0]?.id ?? "",
|
|
858
|
-
urlResult: typeof route.query.result === "string" ? route.query.result : null,
|
|
859
888
|
serverSummary: sessions.value.find((summary) => summary.id === sessionId),
|
|
860
889
|
nowIso: new Date().toISOString(),
|
|
861
890
|
});
|