Skip to content
Merged
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
88 changes: 80 additions & 8 deletions src/lib/ai/tools/workspace-tools.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { z } from "zod";
import { logger } from "@/lib/utils/logger";
import { workspaceWorker } from "@/lib/ai/workers";
import { loadWorkspaceState } from "@/lib/workspace/state-loader";
import { formatSelectedCardsContext } from "@/lib/utils/format-workspace-context";
import type { Item } from "@/lib/workspace-state/types";

export interface WorkspaceToolContext {
workspaceId: string | null;
Expand Down Expand Up @@ -192,7 +195,7 @@ export function createDeleteCardTool(ctx: WorkspaceToolContext) {
export function createSelectCardsTool(ctx: WorkspaceToolContext) {
return {
description:
"Select one or more cards by their TITLES and add them to the conversation context. This tool helps you surface specific cards when the user refers to them. The client-side UI will perform fuzzy matching to find the best matching cards for the titles you provide.",
"Select one or more cards by their TITLES and add them to the conversation context. This tool helps you surface specific cards when the user refers to them. The tool will perform fuzzy matching to find the best matching cards and return their full content immediately.",
inputSchema: z.object({
cardTitles: z.array(z.string()).describe("Array of card titles to search for and select"),
}),
Expand All @@ -203,16 +206,85 @@ export function createSelectCardsTool(ctx: WorkspaceToolContext) {
return {
success: false,
message: "cardTitles array must be provided and non-empty.",
context: "",
};
}

// No auth check needed - client already has workspace state and handles authorization
// Client-side UI handles fuzzy matching and ID resolution
return {
success: true,
message: `Requested selection of ${cardTitles.length} card${cardTitles.length === 1 ? "" : "s"}: ${cardTitles.join(", ")}. The client will perform fuzzy matching to find and select the matching cards.`,
cardTitles: cardTitles,
};
if (!ctx.workspaceId) {
return {
success: false,
message: "No workspace context available",
context: "",
};
}

try {
// Load workspace state to access all items
const state = await loadWorkspaceState(ctx.workspaceId);

if (!state || !state.items || state.items.length === 0) {
return {
success: true,
message: `No cards found in workspace. Requested selection of ${cardTitles.length} card${cardTitles.length === 1 ? "" : "s"}: ${cardTitles.join(", ")}.`,
addedCount: 0,
context: "",
};
}

// Perform fuzzy matching (matching client-side logic)
// 1. Exact match first
// 2. Contains match if no exact match
const selectedItems: Item[] = [];
const processedIds = new Set<string>();

for (const title of cardTitles) {
const searchTitle = title.toLowerCase().trim();

// Try exact match first
let match = state.items.find(
item => item.name.toLowerCase().trim() === searchTitle && !processedIds.has(item.id)
);

// If no exact match, try contains match
if (!match) {
match = state.items.find(
item => item.name.toLowerCase().includes(searchTitle) && !processedIds.has(item.id)
);
}

if (match) {
selectedItems.push(match);
processedIds.add(match.id);
}
}

// Format the selected cards context
const context = formatSelectedCardsContext(selectedItems, state.items);

const addedCount = selectedItems.length;
const notFoundCount = cardTitles.length - addedCount;

let message = `Selected ${addedCount} card${addedCount === 1 ? "" : "s"}`;
if (notFoundCount > 0) {
message += ` (${notFoundCount} not found)`;
}
message += `: ${selectedItems.map(item => item.name).join(", ")}`;

@cubic-dev-ai cubic-dev-ai Bot Jan 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Awkward message formatting when no cards are found - the message will end with a trailing colon and nothing after it (e.g., Selected 0 cards (3 not found): ). Consider conditionally appending the colon and names only when addedCount > 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/ai/tools/workspace-tools.ts, line 271:

<comment>Awkward message formatting when no cards are found - the message will end with a trailing colon and nothing after it (e.g., `Selected 0 cards (3 not found): `). Consider conditionally appending the colon and names only when `addedCount > 0`.</comment>

<file context>
@@ -203,16 +206,85 @@ export function createSelectCardsTool(ctx: WorkspaceToolContext) {
+                if (notFoundCount > 0) {
+                    message += ` (${notFoundCount} not found)`;
+                }
+                message += `: ${selectedItems.map(item => item.name).join(", ")}`;
+
+                return {
</file context>
Suggested change
message += `: ${selectedItems.map(item => item.name).join(", ")}`;
if (addedCount > 0) {
message += `: ${selectedItems.map(item => item.name).join(", ")}`;
}
Fix with Cubic


return {
success: true,
message,
addedCount,
context, // Return formatted context so AI has immediate access
cardTitles: cardTitles,
};
Comment on lines +267 to +279

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Edge case: message formatting when no cards are found.

When addedCount is 0 (no matches), line 271 produces: "Selected 0 cards (X not found): " with a trailing colon and empty string from the join.

Consider handling the zero-match case explicitly:

🐛 Proposed fix
                 let message = `Selected ${addedCount} card${addedCount === 1 ? "" : "s"}`;
                 if (notFoundCount > 0) {
                     message += ` (${notFoundCount} not found)`;
                 }
-                message += `: ${selectedItems.map(item => item.name).join(", ")}`;
+                if (addedCount > 0) {
+                    message += `: ${selectedItems.map(item => item.name).join(", ")}`;
+                }

                 return {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let message = `Selected ${addedCount} card${addedCount === 1 ? "" : "s"}`;
if (notFoundCount > 0) {
message += ` (${notFoundCount} not found)`;
}
message += `: ${selectedItems.map(item => item.name).join(", ")}`;
return {
success: true,
message,
addedCount,
context, // Return formatted context so AI has immediate access
cardTitles: cardTitles,
};
let message = `Selected ${addedCount} card${addedCount === 1 ? "" : "s"}`;
if (notFoundCount > 0) {
message += ` (${notFoundCount} not found)`;
}
if (addedCount > 0) {
message += `: ${selectedItems.map(item => item.name).join(", ")}`;
}
return {
success: true,
message,
addedCount,
context, // Return formatted context so AI has immediate access
cardTitles: cardTitles,
};
🤖 Prompt for AI Agents
In `@src/lib/ai/tools/workspace-tools.ts` around lines 267 - 279, The current
message construction (using addedCount, notFoundCount, and selectedItems)
produces a trailing colon and empty list when addedCount is 0; update the logic
in the block that builds message so that if addedCount === 0 you set message to
a clear no-results string (e.g. "No cards selected" plus " (X not found)" when
notFoundCount>0) and skip appending the colon and join of selectedItems,
otherwise keep the existing pluralized "Selected N card(s): names" behavior;
modify the code around the message variable and the return object (which
includes message, addedCount, context, cardTitles) accordingly.

} catch (error: any) {
logger.error("❌ [SELECT-CARDS] Error loading workspace state:", error);
return {
success: false,
message: `Failed to load workspace state: ${error?.message || String(error)}`,
context: "",
};
}
},
};
}