mulmoclaude 0.1.2 → 0.3.0
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/bin/mulmoclaude.js +1 -1
- package/client/assets/{index-KNLBjwuh.css → index-Bm70FDU2.css} +1 -1
- package/client/assets/{index-D8rhwXLq.js → index-eHWB79u5.js} +3 -3
- package/client/index.html +2 -2
- package/package.json +1 -1
- package/server/agent/config.ts +12 -12
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/x.ts +5 -5
- package/server/agent/prompt.ts +9 -4
- package/server/agent/sandboxMounts.ts +7 -7
- package/server/agent/stream.ts +4 -4
- package/server/api/routes/files.ts +9 -9
- package/server/api/routes/scheduler.ts +8 -8
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +14 -14
- package/server/api/routes/sessions.ts +24 -24
- package/server/api/routes/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/wiki.ts +14 -14
- package/server/events/scheduler-adapter.ts +20 -20
- package/server/events/session-store/index.ts +10 -10
- package/server/events/task-manager/index.ts +7 -7
- package/server/index.ts +19 -19
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +9 -9
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +2 -2
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/roles-io.ts +10 -10
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +2 -2
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.ts +5 -5
- package/server/workspace/chat-index/indexer.ts +15 -15
- package/server/workspace/custom-dirs.ts +11 -11
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/indexFile.ts +29 -25
- package/server/workspace/reference-dirs.ts +18 -18
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/sources/arxivDiscovery.ts +8 -8
- package/server/workspace/sources/classifier.ts +7 -7
- package/server/workspace/sources/fetchers/arxiv.ts +7 -7
- package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
- package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
- package/server/workspace/sources/interests.ts +9 -9
- package/server/workspace/sources/pipeline/index.ts +6 -6
- package/server/workspace/sources/pipeline/plan.ts +5 -5
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +11 -9
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +26 -16
- package/server/workspace/wiki-backlinks/index.ts +8 -8
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
- package/src/App.vue +30 -30
- package/src/components/ChatInput.vue +7 -7
- package/src/components/LockStatusPopup.vue +2 -2
- package/src/components/NotificationToast.vue +2 -2
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +6 -6
- package/src/components/SettingsMcpTab.vue +7 -7
- package/src/components/SettingsModal.vue +3 -3
- package/src/components/SettingsReferenceDirsTab.vue +10 -10
- package/src/components/SettingsWorkspaceDirsTab.vue +5 -5
- package/src/components/SuggestionsPanel.vue +2 -2
- package/src/components/todo/TodoAddDialog.vue +2 -2
- package/src/components/todo/TodoEditPanel.vue +2 -2
- package/src/components/todo/TodoListView.vue +5 -5
- package/src/composables/useCanvasViewMode.ts +5 -5
- package/src/composables/useClickOutside.ts +2 -2
- package/src/composables/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- package/src/composables/useMcpTools.ts +2 -2
- package/src/composables/useNotifications.ts +3 -3
- package/src/composables/usePdfDownload.ts +4 -4
- package/src/composables/usePendingCalls.ts +1 -1
- package/src/composables/usePubSub.ts +10 -10
- package/src/composables/useRoles.ts +1 -1
- package/src/composables/useSandboxStatus.ts +1 -1
- package/src/composables/useSessionDerived.ts +3 -3
- package/src/composables/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +2 -2
- package/src/config/roles.ts +2 -2
- package/src/plugins/chart/Preview.vue +4 -4
- package/src/plugins/manageSkills/View.vue +3 -3
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +2 -2
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/View.vue +4 -4
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +6 -6
- package/src/plugins/scheduler/TasksTab.vue +4 -4
- package/src/plugins/textResponse/View.vue +2 -2
- package/src/plugins/todo/Preview.vue +2 -2
- package/src/plugins/todo/View.vue +11 -11
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/wiki/Preview.vue +5 -5
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/router/guards.ts +12 -12
- package/src/types/session.ts +4 -3
- package/src/utils/agent/request.ts +3 -3
- package/src/utils/dom/scrollable.ts +2 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/files/sortChildren.ts +6 -6
- package/src/utils/format/frontmatter.ts +6 -6
- package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
- package/src/utils/markdown/extractFirstH1.ts +2 -2
- package/src/utils/path/relativeLink.ts +15 -15
- package/src/utils/role/icon.ts +2 -2
- package/src/utils/role/merge.ts +2 -2
- package/src/utils/role/plugins.ts +1 -1
- package/src/utils/session/sessionFactory.ts +2 -2
- package/src/utils/session/sessionHelpers.ts +2 -2
- package/src/utils/tools/dedup.ts +4 -4
- package/src/utils/tools/result.ts +3 -3
- package/src/utils/types.ts +2 -2
|
@@ -59,7 +59,7 @@ export function usePendingCalls(opts: UsePendingCallsOptions) {
|
|
|
59
59
|
// unused.
|
|
60
60
|
const __tickDep = displayTick.value;
|
|
61
61
|
const now = Date.now();
|
|
62
|
-
return opts.toolCallHistory.value.filter((
|
|
62
|
+
return opts.toolCallHistory.value.filter((entry) => __tickDep >= 0 && isCallStillPending(entry, now));
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
function teardown(): void {
|
|
@@ -21,16 +21,16 @@ let socket: Socket | null = null;
|
|
|
21
21
|
|
|
22
22
|
const listeners = new Map<string, Set<Callback>>();
|
|
23
23
|
|
|
24
|
-
function resendSubscriptions(
|
|
24
|
+
function resendSubscriptions(sock: Socket): void {
|
|
25
25
|
for (const channel of listeners.keys()) {
|
|
26
|
-
|
|
26
|
+
sock.emit("subscribe", channel);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function connect(): Socket {
|
|
31
31
|
if (socket) return socket;
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const sock = io({
|
|
34
34
|
path: "/ws/pubsub",
|
|
35
35
|
// Match the server. Long-polling is fine as a fallback but
|
|
36
36
|
// the server refuses it, so don't negotiate it here either —
|
|
@@ -38,17 +38,17 @@ function connect(): Socket {
|
|
|
38
38
|
transports: ["websocket"],
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
sock.on("connect", () => resendSubscriptions(sock));
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
sock.on("data", (msg: PubSubMessage) => {
|
|
44
44
|
const cbs = listeners.get(msg.channel);
|
|
45
45
|
if (cbs) {
|
|
46
|
-
for (const
|
|
46
|
+
for (const handler of cbs) handler(msg.data);
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
socket =
|
|
51
|
-
return
|
|
50
|
+
socket = sock;
|
|
51
|
+
return sock;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function maybeDisconnect(): void {
|
|
@@ -63,8 +63,8 @@ export function usePubSub() {
|
|
|
63
63
|
if (!listeners.has(channel)) listeners.set(channel, new Set());
|
|
64
64
|
listeners.get(channel)!.add(callback);
|
|
65
65
|
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
66
|
+
const sock = connect();
|
|
67
|
+
if (sock.connected) sock.emit("subscribe", channel);
|
|
68
68
|
// If not yet connected, the "connect" handler replays every
|
|
69
69
|
// listener's subscription, so newly-added channels are
|
|
70
70
|
// covered without extra bookkeeping.
|
|
@@ -17,7 +17,7 @@ export function useRoles(): {
|
|
|
17
17
|
} {
|
|
18
18
|
const roles = ref<Role[]>(ROLES);
|
|
19
19
|
const currentRoleId = ref(ROLES[0].id);
|
|
20
|
-
const currentRole = computed(() => roles.value.find((
|
|
20
|
+
const currentRole = computed(() => roles.value.find((role) => role.id === currentRoleId.value) ?? roles.value[0]);
|
|
21
21
|
|
|
22
22
|
async function refreshRoles(): Promise<void> {
|
|
23
23
|
const result = await apiGet<Role[]>(API_ROUTES.roles.list);
|
|
@@ -29,7 +29,7 @@ function isSandboxStatus(raw: RawResponse): raw is {
|
|
|
29
29
|
} {
|
|
30
30
|
if (typeof raw.sshAgent !== "boolean") return false;
|
|
31
31
|
if (!Array.isArray(raw.mounts)) return false;
|
|
32
|
-
return raw.mounts.every((
|
|
32
|
+
return raw.mounts.every((mount) => typeof mount === "string");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface UseSandboxStatusHandle {
|
|
@@ -16,7 +16,7 @@ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>
|
|
|
16
16
|
|
|
17
17
|
const sidebarResults = computed(() => deduplicateResults(toolResults.value));
|
|
18
18
|
|
|
19
|
-
const currentSummary = computed(() => sessions.value.find((
|
|
19
|
+
const currentSummary = computed(() => sessions.value.find((summary) => summary.id === currentSessionId.value));
|
|
20
20
|
|
|
21
21
|
// The server-side summary already merges pendingGenerations into
|
|
22
22
|
// `isRunning` (see server/api/routes/sessions.ts), but pub/sub events
|
|
@@ -33,9 +33,9 @@ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>
|
|
|
33
33
|
|
|
34
34
|
const toolCallHistory = computed<ToolCallHistoryItem[]>(() => activeSession.value?.toolCallHistory ?? []);
|
|
35
35
|
|
|
36
|
-
const activeSessionCount = computed(() => sessions.value.filter((
|
|
36
|
+
const activeSessionCount = computed(() => sessions.value.filter((session) => session.isRunning).length);
|
|
37
37
|
|
|
38
|
-
const unreadCount = computed(() => sessions.value.filter((
|
|
38
|
+
const unreadCount = computed(() => sessions.value.filter((session) => session.hasUnread).length);
|
|
39
39
|
|
|
40
40
|
return {
|
|
41
41
|
activeSession,
|
|
@@ -29,20 +29,20 @@ export function useSessionSync(opts: {
|
|
|
29
29
|
console.warn("[session-sync] failed to fetch sessions:", err);
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
for (const
|
|
33
|
-
const live = sessionMap.get(
|
|
32
|
+
for (const summary of summaries) {
|
|
33
|
+
const live = sessionMap.get(summary.id);
|
|
34
34
|
if (!live) continue;
|
|
35
|
-
live.isRunning =
|
|
36
|
-
live.statusMessage =
|
|
37
|
-
const unread =
|
|
38
|
-
if (!(unread &&
|
|
35
|
+
live.isRunning = summary.isRunning ?? false;
|
|
36
|
+
live.statusMessage = summary.statusMessage ?? "";
|
|
37
|
+
const unread = summary.hasUnread ?? false;
|
|
38
|
+
if (!(unread && summary.id === currentSessionId.value)) {
|
|
39
39
|
live.hasUnread = unread;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function markSessionRead(
|
|
45
|
-
const result = await apiPost<{ ok: boolean }>(API_ROUTES.sessions.markRead.replace(":id", encodeURIComponent(
|
|
44
|
+
async function markSessionRead(sessionId: string): Promise<void> {
|
|
45
|
+
const result = await apiPost<{ ok: boolean }>(API_ROUTES.sessions.markRead.replace(":id", encodeURIComponent(sessionId)));
|
|
46
46
|
if (!result.ok || result.data.ok === false) {
|
|
47
47
|
await refreshSessionStates();
|
|
48
48
|
}
|
|
@@ -9,8 +9,8 @@ import { CANVAS_VIEW, type CanvasViewMode } from "../utils/canvas/viewMode";
|
|
|
9
9
|
const CHAT_VIEWS = [CANVAS_VIEW.single, CANVAS_VIEW.stack] as const;
|
|
10
10
|
type ChatViewMode = (typeof CHAT_VIEWS)[number];
|
|
11
11
|
|
|
12
|
-
function isChatView(
|
|
13
|
-
return (CHAT_VIEWS as readonly string[]).includes(
|
|
12
|
+
function isChatView(mode: string): mode is ChatViewMode {
|
|
13
|
+
return (CHAT_VIEWS as readonly string[]).includes(mode);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function useViewLayout(opts: {
|
package/src/config/roles.ts
CHANGED
|
@@ -330,6 +330,6 @@ export type BuiltInRoleId = (typeof BUILTIN_ROLE_IDS)[keyof typeof BUILTIN_ROLE_
|
|
|
330
330
|
|
|
331
331
|
export const DEFAULT_ROLE_ID: BuiltInRoleId = BUILTIN_ROLE_IDS.general;
|
|
332
332
|
|
|
333
|
-
export function getRole(
|
|
334
|
-
return ROLES.find((
|
|
333
|
+
export function getRole(roleId: string): Role {
|
|
334
|
+
return ROLES.find((role) => role.id === roleId) ?? ROLES[0];
|
|
335
335
|
}
|
|
@@ -25,8 +25,8 @@ const hint = computed(() => {
|
|
|
25
25
|
const charts = data.value?.document?.charts ?? [];
|
|
26
26
|
if (charts.length === 0) return "";
|
|
27
27
|
const types = charts
|
|
28
|
-
.map((
|
|
29
|
-
.filter((
|
|
28
|
+
.map((chart) => chart.type ?? inferTypeFromOption(chart.option))
|
|
29
|
+
.filter((chartType): chartType is string => Boolean(chartType))
|
|
30
30
|
.slice(0, 3);
|
|
31
31
|
const suffix = charts.length > types.length ? ", …" : "";
|
|
32
32
|
const typeList = types.join(", ");
|
|
@@ -41,8 +41,8 @@ function inferTypeFromOption(option: Record<string, unknown>): string | null {
|
|
|
41
41
|
const first = series[0] as { type?: unknown };
|
|
42
42
|
if (typeof first.type === "string") return first.type;
|
|
43
43
|
} else if (series && typeof series === "object") {
|
|
44
|
-
const
|
|
45
|
-
if (typeof
|
|
44
|
+
const seriesType = (series as { type?: unknown }).type;
|
|
45
|
+
if (typeof seriesType === "string") return seriesType;
|
|
46
46
|
}
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
@@ -173,7 +173,7 @@ const saving = ref(false);
|
|
|
173
173
|
const editDescription = ref("");
|
|
174
174
|
const editBody = ref("");
|
|
175
175
|
|
|
176
|
-
const selected = computed(() => skills.value.find((
|
|
176
|
+
const selected = computed(() => skills.value.find((skill) => skill.name === selectedName.value) ?? null);
|
|
177
177
|
|
|
178
178
|
const renderedBody = computed(() => {
|
|
179
179
|
const body = detail.value?.body;
|
|
@@ -272,7 +272,7 @@ async function saveEdit(): Promise<void> {
|
|
|
272
272
|
body: editBody.value,
|
|
273
273
|
};
|
|
274
274
|
// Update the sidebar summary too.
|
|
275
|
-
const idx = skills.value.findIndex((
|
|
275
|
+
const idx = skills.value.findIndex((skill) => skill.name === name);
|
|
276
276
|
if (idx >= 0) {
|
|
277
277
|
skills.value[idx] = {
|
|
278
278
|
...skills.value[idx],
|
|
@@ -311,7 +311,7 @@ async function deleteSkill(): Promise<void> {
|
|
|
311
311
|
return;
|
|
312
312
|
}
|
|
313
313
|
// Remove from the local list, advance selection, clear detail.
|
|
314
|
-
const idx = skills.value.findIndex((
|
|
314
|
+
const idx = skills.value.findIndex((skill) => skill.name === name);
|
|
315
315
|
if (idx >= 0) {
|
|
316
316
|
skills.value.splice(idx, 1);
|
|
317
317
|
}
|
|
@@ -24,7 +24,7 @@ const hint = computed(() => {
|
|
|
24
24
|
if (sources.length === 0) return "No sources registered yet.";
|
|
25
25
|
const names = sources
|
|
26
26
|
.slice(0, 3)
|
|
27
|
-
.map((
|
|
27
|
+
.map((source: Source) => source.slug)
|
|
28
28
|
.join(", ");
|
|
29
29
|
const tail = sources.length > 3 ? ", …" : "";
|
|
30
30
|
const plural = sources.length === 1 ? "" : "s";
|
|
@@ -156,8 +156,8 @@ const sourceDetails = ref<HTMLDetailsElement>();
|
|
|
156
156
|
const editing = ref(false);
|
|
157
157
|
const { copied, copy } = useClipboardCopy();
|
|
158
158
|
|
|
159
|
-
function onDetailsToggle(
|
|
160
|
-
const open = (
|
|
159
|
+
function onDetailsToggle(event: Event) {
|
|
160
|
+
const open = (event.target as HTMLDetailsElement).open;
|
|
161
161
|
editing.value = open;
|
|
162
162
|
if (!open) {
|
|
163
163
|
editableMarkdown.value = markdownContent.value;
|
|
@@ -25,8 +25,8 @@ export function stripHtmlToPreview(html: string, maxLength: number): string {
|
|
|
25
25
|
};
|
|
26
26
|
let i = 0;
|
|
27
27
|
while (i < html.length) {
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
28
|
+
const char = html[i];
|
|
29
|
+
if (char === "<") {
|
|
30
30
|
const close = html.indexOf(">", i + 1);
|
|
31
31
|
if (close !== -1) {
|
|
32
32
|
// Real tag span `<...>` — skip it, emit a separator.
|
|
@@ -36,7 +36,7 @@ export function stripHtmlToPreview(html: string, maxLength: number): string {
|
|
|
36
36
|
}
|
|
37
37
|
// No closing `>` anywhere after — treat as literal.
|
|
38
38
|
}
|
|
39
|
-
emitChar(state,
|
|
39
|
+
emitChar(state, char);
|
|
40
40
|
i++;
|
|
41
41
|
}
|
|
42
42
|
trimTrailingSpace(state.out);
|
|
@@ -48,12 +48,12 @@ interface WalkerState {
|
|
|
48
48
|
lastWasSpace: boolean;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function emitChar(state: WalkerState,
|
|
52
|
-
if (isWhitespace(
|
|
51
|
+
function emitChar(state: WalkerState, char: string): void {
|
|
52
|
+
if (isWhitespace(char)) {
|
|
53
53
|
emitSeparator(state);
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
state.out.push(
|
|
56
|
+
state.out.push(char);
|
|
57
57
|
state.lastWasSpace = false;
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -67,6 +67,6 @@ function trimTrailingSpace(out: string[]): void {
|
|
|
67
67
|
if (out.length > 0 && out[out.length - 1] === " ") out.pop();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function isWhitespace(
|
|
71
|
-
return
|
|
70
|
+
function isWhitespace(char: string): boolean {
|
|
71
|
+
return char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\v" || char === "\f";
|
|
72
72
|
}
|
|
@@ -431,7 +431,7 @@ const charErrors = reactive<Record<string, string>>({});
|
|
|
431
431
|
const charDragOver = reactive<Record<string, boolean>>({});
|
|
432
432
|
const beatDragOver = reactive<Record<number, boolean>>({});
|
|
433
433
|
|
|
434
|
-
const anyBeatRendering = computed(() => Object.values(renderState).some((
|
|
434
|
+
const anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === "rendering"));
|
|
435
435
|
|
|
436
436
|
const characterKeys = computed(() => {
|
|
437
437
|
const imgs = script.value.imageParams?.images ?? {};
|
|
@@ -447,10 +447,10 @@ const chatSessionId = computed(() => activeSessionRef?.value?.id);
|
|
|
447
447
|
const pendingForThisScript = computed(() => {
|
|
448
448
|
const out: Record<string, PendingGeneration> = {};
|
|
449
449
|
const pending = activeSessionRef?.value?.pendingGenerations ?? {};
|
|
450
|
-
const
|
|
451
|
-
if (!
|
|
450
|
+
const currentPath = filePath.value;
|
|
451
|
+
if (!currentPath) return out;
|
|
452
452
|
for (const [mapKey, entry] of Object.entries(pending)) {
|
|
453
|
-
if (entry.filePath ===
|
|
453
|
+
if (entry.filePath === currentPath) out[mapKey] = entry;
|
|
454
454
|
}
|
|
455
455
|
return out;
|
|
456
456
|
});
|
|
@@ -63,7 +63,7 @@ export function shouldAutoRenderBeat(beat: { image?: { type?: string } | undefin
|
|
|
63
63
|
* what's missing after a movie-generation event arrives.
|
|
64
64
|
*/
|
|
65
65
|
export function getMissingCharacterKeys(keys: readonly string[], images: Record<string, unknown>, renderState: Record<string, string | undefined>): string[] {
|
|
66
|
-
return keys.filter((
|
|
66
|
+
return keys.filter((charKey) => !images[charKey] && renderState[charKey] !== "rendering");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -26,8 +26,8 @@ const items = ref<ScheduledItem[]>(props.result.data?.items ?? []);
|
|
|
26
26
|
const { refresh } = useFreshPluginData<ScheduledItem[]>({
|
|
27
27
|
endpoint: () => API_ROUTES.scheduler.base,
|
|
28
28
|
extract: (json) => {
|
|
29
|
-
const
|
|
30
|
-
return Array.isArray(
|
|
29
|
+
const extracted = (json as { data?: { items?: ScheduledItem[] } }).data?.items;
|
|
30
|
+
return Array.isArray(extracted) ? extracted : null;
|
|
31
31
|
},
|
|
32
32
|
apply: (data) => {
|
|
33
33
|
items.value = data;
|
|
@@ -49,15 +49,15 @@ const upcomingItems = computed(() => {
|
|
|
49
49
|
const noDate: ScheduledItem[] = [];
|
|
50
50
|
|
|
51
51
|
for (const item of items.value) {
|
|
52
|
-
const
|
|
53
|
-
if (typeof
|
|
54
|
-
if (
|
|
52
|
+
const dateVal = item.props.date;
|
|
53
|
+
if (typeof dateVal === "string") {
|
|
54
|
+
if (dateVal >= today) withDate.push(item);
|
|
55
55
|
} else {
|
|
56
56
|
noDate.push(item);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
withDate.sort((
|
|
60
|
+
withDate.sort((itemA, itemB) => (String(itemA.props.date) < String(itemB.props.date) ? -1 : 1));
|
|
61
61
|
|
|
62
62
|
return [...withDate, ...noDate];
|
|
63
63
|
});
|
|
@@ -168,9 +168,9 @@ function formatSchedule(schedule: TaskSchedule): string {
|
|
|
168
168
|
return JSON.stringify(schedule);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
async function runTask(
|
|
171
|
+
async function runTask(taskId: string): Promise<void> {
|
|
172
172
|
mutationError.value = "";
|
|
173
|
-
const url = API_ROUTES.scheduler.taskRun.replace(":id",
|
|
173
|
+
const url = API_ROUTES.scheduler.taskRun.replace(":id", taskId);
|
|
174
174
|
const result = await apiPost(url, {});
|
|
175
175
|
if (!result.ok) {
|
|
176
176
|
mutationError.value = `Run failed: ${result.error}`;
|
|
@@ -190,9 +190,9 @@ async function toggleEnabled(task: SchedulerTask): Promise<void> {
|
|
|
190
190
|
await fetchTasks();
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
async function deleteTask(
|
|
193
|
+
async function deleteTask(taskId: string): Promise<void> {
|
|
194
194
|
mutationError.value = "";
|
|
195
|
-
const url = API_ROUTES.scheduler.task.replace(":id",
|
|
195
|
+
const url = API_ROUTES.scheduler.task.replace(":id", taskId);
|
|
196
196
|
const result = await apiDelete(url);
|
|
197
197
|
if (!result.ok) {
|
|
198
198
|
mutationError.value = `Delete failed: ${result.error}`;
|
|
@@ -168,8 +168,8 @@ const { pdfDownloading, pdfError, downloadPdf: rawDownloadPdf } = usePdfDownload
|
|
|
168
168
|
const detailsEl = ref<HTMLDetailsElement>();
|
|
169
169
|
const editing = ref(false);
|
|
170
170
|
|
|
171
|
-
function onDetailsToggle(
|
|
172
|
-
editing.value = (
|
|
171
|
+
function onDetailsToggle(event: Event) {
|
|
172
|
+
editing.value = (event.target as HTMLDetailsElement).open;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
onMounted(() => {
|
|
@@ -42,8 +42,8 @@ const items = ref<TodoItem[]>(props.result.data?.items ?? []);
|
|
|
42
42
|
const { refresh } = useFreshPluginData<TodoItem[]>({
|
|
43
43
|
endpoint: () => API_ROUTES.todos.list,
|
|
44
44
|
extract: (json) => {
|
|
45
|
-
const
|
|
46
|
-
return Array.isArray(
|
|
45
|
+
const extracted = (json as { data?: { items?: TodoItem[] } }).data?.items;
|
|
46
|
+
return Array.isArray(extracted) ? extracted : null;
|
|
47
47
|
},
|
|
48
48
|
apply: (data) => {
|
|
49
49
|
items.value = data;
|
|
@@ -111,8 +111,8 @@ const items = ref<TodoItem[]>(props.selectedResult.data?.items ?? []);
|
|
|
111
111
|
const { refresh } = useFreshPluginData<TodoItem[]>({
|
|
112
112
|
endpoint: () => API_ROUTES.todos.list,
|
|
113
113
|
extract: (json) => {
|
|
114
|
-
const
|
|
115
|
-
return Array.isArray(
|
|
114
|
+
const extracted = (json as { data?: { items?: TodoItem[] } }).data?.items;
|
|
115
|
+
return Array.isArray(extracted) ? extracted : null;
|
|
116
116
|
},
|
|
117
117
|
apply: (data) => {
|
|
118
118
|
items.value = data;
|
|
@@ -163,12 +163,12 @@ function clearFilters(): void {
|
|
|
163
163
|
|
|
164
164
|
// ── YAML helpers ─────────────────────────────────────────────────────────────
|
|
165
165
|
|
|
166
|
-
function yamlStringValue(
|
|
167
|
-
const needsQuotes =
|
|
166
|
+
function yamlStringValue(str: string): string {
|
|
167
|
+
const needsQuotes = str === "" || /[:#[\]{},&*?|<>=!%@`]/.test(str) || /^\s|\s$/.test(str) || /^(true|false|null|~)$/i.test(str) || /^\d/.test(str);
|
|
168
168
|
if (needsQuotes) {
|
|
169
|
-
return `"${
|
|
169
|
+
return `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
170
170
|
}
|
|
171
|
-
return
|
|
171
|
+
return str;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function serializeYaml(item: TodoItem): string {
|
|
@@ -191,19 +191,19 @@ function parseFlowSequence(raw: string): string[] {
|
|
|
191
191
|
let buffer = "";
|
|
192
192
|
let inQuotes = false;
|
|
193
193
|
for (let i = 0; i < inner.length; i++) {
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
194
|
+
const char = inner[i];
|
|
195
|
+
if (char === '"' && inner[i - 1] !== "\\") {
|
|
196
196
|
inQuotes = !inQuotes;
|
|
197
|
-
buffer +=
|
|
197
|
+
buffer += char;
|
|
198
198
|
continue;
|
|
199
199
|
}
|
|
200
|
-
if (
|
|
200
|
+
if (char === "," && !inQuotes) {
|
|
201
201
|
const piece = parseYamlValue(buffer.trim());
|
|
202
202
|
if (piece) result.push(piece);
|
|
203
203
|
buffer = "";
|
|
204
204
|
continue;
|
|
205
205
|
}
|
|
206
|
-
buffer +=
|
|
206
|
+
buffer += char;
|
|
207
207
|
}
|
|
208
208
|
const last = parseYamlValue(buffer.trim());
|
|
209
209
|
if (last) result.push(last);
|
|
@@ -166,12 +166,12 @@ export function useTodos(initialItems: TodoItem[] = [], initialColumns: StatusCo
|
|
|
166
166
|
error,
|
|
167
167
|
refresh,
|
|
168
168
|
createItem: (input) => call(API_ROUTES.todos.items, "POST", input),
|
|
169
|
-
patchItem: (
|
|
170
|
-
moveItem: (
|
|
171
|
-
deleteItem: (
|
|
169
|
+
patchItem: (itemId, input) => call(API_ROUTES.todos.item.replace(":id", encodeURIComponent(itemId)), "PATCH", input),
|
|
170
|
+
moveItem: (itemId, input) => call(API_ROUTES.todos.itemMove.replace(":id", encodeURIComponent(itemId)), "POST", input),
|
|
171
|
+
deleteItem: (itemId) => call(API_ROUTES.todos.item.replace(":id", encodeURIComponent(itemId)), "DELETE"),
|
|
172
172
|
addColumn: (input) => call(API_ROUTES.todos.columns, "POST", input),
|
|
173
|
-
patchColumn: (
|
|
174
|
-
deleteColumn: (
|
|
173
|
+
patchColumn: (colId, input) => call(API_ROUTES.todos.column.replace(":id", encodeURIComponent(colId)), "PATCH", input),
|
|
174
|
+
deleteColumn: (colId) => call(API_ROUTES.todos.column.replace(":id", encodeURIComponent(colId)), "DELETE"),
|
|
175
175
|
reorderColumns: (ids) => call(API_ROUTES.todos.columnsOrder, "PUT", { ids }),
|
|
176
176
|
};
|
|
177
177
|
}
|
|
@@ -43,11 +43,11 @@ const { refresh } = useFreshPluginData<WikiData>({
|
|
|
43
43
|
watch(
|
|
44
44
|
() => props.result.uuid,
|
|
45
45
|
() => {
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
action.value =
|
|
49
|
-
title.value =
|
|
50
|
-
pageEntries.value =
|
|
46
|
+
const wikiData = props.result.data;
|
|
47
|
+
if (wikiData) {
|
|
48
|
+
action.value = wikiData.action ?? "index";
|
|
49
|
+
title.value = wikiData.title ?? "Wiki";
|
|
50
|
+
pageEntries.value = wikiData.pageEntries ?? [];
|
|
51
51
|
}
|
|
52
52
|
void refresh();
|
|
53
53
|
},
|
|
@@ -43,11 +43,11 @@ export function renderWikiLinks(content: string): string {
|
|
|
43
43
|
* immediately after `from` (zero-length page name, which the old
|
|
44
44
|
* regex rejected via the `+` quantifier).
|
|
45
45
|
*/
|
|
46
|
-
function findNextCloseBrackets(
|
|
46
|
+
function findNextCloseBrackets(str: string, from: number): number {
|
|
47
47
|
let j = from;
|
|
48
|
-
while (j <
|
|
49
|
-
if (
|
|
50
|
-
if (
|
|
48
|
+
while (j < str.length) {
|
|
49
|
+
if (str[j] === "]") {
|
|
50
|
+
if (str[j + 1] === "]" && j > from) return j;
|
|
51
51
|
// Bare `]` inside the page-name span — old regex would not
|
|
52
52
|
// match here, so we bail and let the caller emit the `[[`
|
|
53
53
|
// as literal text.
|
package/src/router/guards.ts
CHANGED
|
@@ -18,43 +18,43 @@ import { VALID_VIEW_MODES } from "../utils/canvas/viewMode";
|
|
|
18
18
|
// doesn't exist on the server" gracefully.
|
|
19
19
|
const SESSION_ID_RE = /^[\w-]{1,128}$/;
|
|
20
20
|
|
|
21
|
-
function isValidSessionId(
|
|
22
|
-
return typeof
|
|
21
|
+
function isValidSessionId(value: unknown): boolean {
|
|
22
|
+
return typeof value === "string" && SESSION_ID_RE.test(value);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function installGuards(router: Router): void {
|
|
26
|
-
router.beforeEach((
|
|
26
|
+
router.beforeEach((dest) => {
|
|
27
27
|
// Only run guards on the chat route — other routes (redirect, etc.)
|
|
28
28
|
// don't carry parameters that need sanitizing.
|
|
29
|
-
if (
|
|
29
|
+
if (dest.name !== "chat") return;
|
|
30
30
|
|
|
31
31
|
// ── sessionId format check ───────────────────────────────────
|
|
32
|
-
const sessionId =
|
|
32
|
+
const sessionId = dest.params.sessionId;
|
|
33
33
|
if (typeof sessionId === "string" && sessionId.length > 0 && !isValidSessionId(sessionId)) {
|
|
34
34
|
// Garbage sessionId → strip it and go to /chat (new session).
|
|
35
35
|
return { name: "chat", params: {}, query: {}, replace: true };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// ── view mode whitelist ──────────────────────────────────────
|
|
39
|
-
const view =
|
|
39
|
+
const view = dest.query.view;
|
|
40
40
|
if (typeof view === "string" && !VALID_VIEW_MODES.has(view)) {
|
|
41
|
-
const cleaned = { ...
|
|
41
|
+
const cleaned = { ...dest.query };
|
|
42
42
|
delete cleaned.view;
|
|
43
|
-
return { ...
|
|
43
|
+
return { ...dest, query: cleaned, replace: true };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// ── file path traversal check ────────────────────────────────
|
|
47
|
-
const filePath =
|
|
47
|
+
const filePath = dest.query.path;
|
|
48
48
|
if (typeof filePath === "string") {
|
|
49
49
|
if (filePath.includes("..") || filePath.startsWith("/")) {
|
|
50
|
-
const cleaned = { ...
|
|
50
|
+
const cleaned = { ...dest.query };
|
|
51
51
|
delete cleaned.path;
|
|
52
|
-
return { ...
|
|
52
|
+
return { ...dest, query: cleaned, replace: true };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// ?path= without ?view=files → auto-add view=files so FilesView mounts.
|
|
56
56
|
if (view !== "files") {
|
|
57
|
-
return { ...
|
|
57
|
+
return { ...dest, query: { ...dest.query, view: "files" }, replace: true };
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
});
|
package/src/types/session.ts
CHANGED
|
@@ -70,10 +70,11 @@ export interface ToolResultEntry extends SessionEntry {
|
|
|
70
70
|
result: ToolResultComplete;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export const isTextEntry = (
|
|
74
|
-
(
|
|
73
|
+
export const isTextEntry = (entry: SessionEntry): entry is TextEntry =>
|
|
74
|
+
(entry.source === "user" || entry.source === "assistant") && entry.type === EVENT_TYPES.text && typeof entry.message === "string";
|
|
75
75
|
|
|
76
|
-
export const isToolResultEntry = (
|
|
76
|
+
export const isToolResultEntry = (entry: SessionEntry): entry is ToolResultEntry =>
|
|
77
|
+
entry.source === "tool" && entry.type === EVENT_TYPES.toolResult && entry.result !== undefined;
|
|
77
78
|
|
|
78
79
|
// In-memory session held in `sessionMap`. PR #88 introduced this so
|
|
79
80
|
// multiple chats can run concurrently — `id` matches the `chatSessionId`
|
|
@@ -45,11 +45,11 @@ export async function postAgentRun(body: AgentRequestBody): Promise<{ ok: true }
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
return { ok: true };
|
|
48
|
-
} catch (
|
|
49
|
-
console.error("[agent] fetch error:",
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error("[agent] fetch error:", err);
|
|
50
50
|
return {
|
|
51
51
|
ok: false,
|
|
52
|
-
error:
|
|
52
|
+
error: err instanceof Error ? err.message : "Connection error.",
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// real DOM.
|
|
12
12
|
export function findScrollableChild(container: HTMLElement): HTMLElement | null {
|
|
13
13
|
const children = container.querySelectorAll("*");
|
|
14
|
-
for (const
|
|
15
|
-
const html =
|
|
14
|
+
for (const elem of children) {
|
|
15
|
+
const html = elem as HTMLElement;
|
|
16
16
|
if (html.scrollHeight > html.clientHeight) {
|
|
17
17
|
const style = getComputedStyle(html);
|
|
18
18
|
if (style.overflowY === "auto" || style.overflowY === "scroll" || style.overflow === "auto" || style.overflow === "scroll") {
|
|
@@ -13,7 +13,7 @@ export function parseStoredExpandedDirs(raw: string | null): Set<string> {
|
|
|
13
13
|
try {
|
|
14
14
|
const parsed: unknown = JSON.parse(raw);
|
|
15
15
|
if (!Array.isArray(parsed)) return new Set(DEFAULT_EXPANDED);
|
|
16
|
-
const strings = parsed.filter((
|
|
16
|
+
const strings = parsed.filter((val): val is string => typeof val === "string");
|
|
17
17
|
return new Set(strings);
|
|
18
18
|
} catch {
|
|
19
19
|
return new Set(DEFAULT_EXPANDED);
|
|
@@ -7,14 +7,14 @@ import type { FileSortMode } from "../../composables/useFileSortMode";
|
|
|
7
7
|
// on name so the order is deterministic).
|
|
8
8
|
export function sortChildren(children: readonly TreeNode[], mode: FileSortMode): TreeNode[] {
|
|
9
9
|
const copy = children.slice();
|
|
10
|
-
copy.sort((
|
|
11
|
-
if (
|
|
10
|
+
copy.sort((nodeA, nodeB) => {
|
|
11
|
+
if (nodeA.type !== nodeB.type) return nodeA.type === "dir" ? -1 : 1;
|
|
12
12
|
if (mode === "recent") {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
13
|
+
const modTimeA = nodeA.modifiedMs ?? -Infinity;
|
|
14
|
+
const modTimeB = nodeB.modifiedMs ?? -Infinity;
|
|
15
|
+
if (modTimeA !== modTimeB) return modTimeB - modTimeA;
|
|
16
16
|
}
|
|
17
|
-
return
|
|
17
|
+
return nodeA.name.localeCompare(nodeB.name);
|
|
18
18
|
});
|
|
19
19
|
return copy;
|
|
20
20
|
}
|