Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/chat/tools/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe("chat tool registration", () => {
expect(tools).toHaveProperty("find_related_meetings");
expect(tools).toHaveProperty("edit_memo");
expect(tools).toHaveProperty("edit_summary");
expect(tools).toHaveProperty("move_meeting_contents");
expect(tools).not.toHaveProperty("search_sessions");
expect(tools).not.toHaveProperty("grep_notes");
expect(tools).not.toHaveProperty("read_note");
Expand Down
26 changes: 26 additions & 0 deletions apps/desktop/src/chat/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
buildGetRecurringMeetingHistoryTool,
buildListMeetingsTool,
} from "./meetings";
import { buildMoveMeetingContentsTool } from "./move-meeting-contents";
import {
buildFindRelatedMeetingsTool,
buildSearchMeetingContentTool,
Expand Down Expand Up @@ -107,6 +108,10 @@ export const buildChatTools = (deps: ToolDependencies) => ({
"apply_session_correction",
buildApplySessionCorrectionTool(deps),
),
move_meeting_contents: withToolLogging(
"move_meeting_contents",
buildMoveMeetingContentsTool(deps),
),
});

type LocalTools = {
Expand Down Expand Up @@ -254,6 +259,27 @@ type LocalTools = {
};
};
};
move_meeting_contents: {
input: {
sourceMeetingId?: string;
targetMeetingId: string;
};
output: {
status: string;
message?: string;
sourceMeetingId?: string;
targetMeetingId?: string;
sourceTitle?: string;
targetTitle?: string;
moved?: {
recording: boolean;
transcripts: number;
summaries: number;
notes: boolean;
actionItems: number;
};
};
};
};

export type Tools = LocalTools;
Expand Down
61 changes: 61 additions & 0 deletions apps/desktop/src/chat/tools/move-meeting-contents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
moveSessionContents: vi.fn(),
}));

vi.mock("~/session/move-contents", () => ({
moveSessionContents: mocks.moveSessionContents,
}));

import { buildMoveMeetingContentsTool } from "./move-meeting-contents";

describe("move meeting contents chat tool", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.moveSessionContents.mockResolvedValue({
status: "moved",
sourceMeetingId: "source",
targetMeetingId: "target",
sourceTitle: "Standup",
targetTitle: "Board",
moved: {
recording: true,
transcripts: 1,
summaries: 1,
notes: true,
actionItems: 0,
},
});
});

it("defaults the source meeting to the current session", async () => {
const tool = buildMoveMeetingContentsTool({
getSessionId: () => "source",
});

await expect(
(tool as any).execute({ targetMeetingId: "target" }),
).resolves.toMatchObject({ status: "moved" });

expect(mocks.moveSessionContents).toHaveBeenCalledWith({
sourceSessionId: "source",
targetSessionId: "target",
});
});

it("requires an explicit source when no meeting is open", async () => {
const tool = buildMoveMeetingContentsTool({
getSessionId: () => undefined,
});

await expect(
(tool as any).execute({ targetMeetingId: "target" }),
).resolves.toEqual({
status: "error",
message:
"No source meeting selected. Provide sourceMeetingId explicitly when calling move_meeting_contents.",
});
expect(mocks.moveSessionContents).not.toHaveBeenCalled();
});
});
47 changes: 47 additions & 0 deletions apps/desktop/src/chat/tools/move-meeting-contents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { tool } from "ai";
import { z } from "zod";

import type { ToolDependencies } from "./types";

import { moveSessionContents } from "~/session/move-contents";

export const buildMoveMeetingContentsTool = (
deps: Pick<ToolDependencies, "getSessionId">,
) =>
tool({
description:
"Move a finished recording, transcript, generated summaries, notes, and action items from one meeting onto another existing meeting. Use this when the user says a recording or notes landed on the wrong meeting. Resolve both meeting IDs with list_meetings or search_meetings first and never guess IDs. The target meeting must not already have a recording or transcript.",
inputSchema: z.object({
sourceMeetingId: z
.string()
.optional()
.describe(
"Meeting that currently has the recording or notes. Defaults to the current meeting.",
),
targetMeetingId: z
.string()
.describe(
"Existing meeting that should receive the recording and notes.",
),
}),
execute: async (params: {
sourceMeetingId?: string;
targetMeetingId: string;
}) => {
const sourceMeetingId = params.sourceMeetingId ?? deps.getSessionId();
const targetMeetingId = params.targetMeetingId;

if (!sourceMeetingId) {
return {
status: "error",
message:
"No source meeting selected. Provide sourceMeetingId explicitly when calling move_meeting_contents.",
};
}

return moveSessionContents({
sourceSessionId: sourceMeetingId,
targetSessionId: targetMeetingId,
});
},
});
2 changes: 2 additions & 0 deletions apps/desktop/src/chat/transport/use-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ describe("chat transport prompt guidance", () => {
expect(prompt).toContain(
"Use apply_session_correction for narrow exact old-to-new corrections and edit_summary for broader summary rewrites",
);
expect(prompt).toContain("call move_meeting_contents");
expect(prompt).toContain("Do not guess IDs");
expect(prompt).toContain(
"Use edit_summary only for existing generated post-meeting summaries",
);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/chat/transport/use-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Context and local meeting tool guidance:
- When the user asks to rewrite, revise, refocus, shorten, or restructure an existing summary, call edit_summary with the complete replacement markdown so they can review and apply it. Do not return the rewrite only as a fenced markdown block.
- Use edit_summary only for existing generated post-meeting summaries. Use apply_session_correction for narrow exact old-to-new corrections and edit_summary for broader summary rewrites. Only return a draft without calling edit_memo or edit_summary when the user explicitly asks not to change the meeting content or no target session can be resolved.
- When the user corrects note content with wording like "it's not X but Y", use apply_session_correction to update the current session summary and transcript unless they explicitly ask for one target only. Add uncommon names, companies, products, acronyms, or jargon from the correction to dictionaryTerms so future transcription can prefer them; skip common names. If the tool reports partial, use get_meeting or retry with the exact remaining text instead of claiming both were updated.
- When the user asks to move a recording, transcript, or notes onto a different existing meeting, resolve both meeting IDs with list_meetings or search_meetings, then call move_meeting_contents. Default the source to the current meeting when they are looking at the misplaced recording. Do not guess IDs. If the target already has a recording or transcript, explain that and stop.
- Do not ask the user to open or share a meeting until list_meetings, search_meetings, search_meeting_content, and get_meeting cannot find enough local context.
- Use typed meeting tools instead of constructing shell commands, crawling files, or accessing SQLite directly.
- Do not assume meeting contents from chat history when a typed tool can read the current source of truth.
Expand Down
Loading
Loading