Skip to content

Commit 0429496

Browse files
feat(chat): add skill preload — /command chips trigger skill tool calls before LLM
- Add SkillToolCall component (ZapIcon, Skill(name) title, success/error state) - Register SkillToolCall in ToolCallBlock for tool names 'skill' and 'load_skill' - In ChatPaneInterface.handleSend: extract custom command chip names from content, strip leading / from message text, pass names as metadata.skills to sendMessage - Add skills?: string[] to sendMessageInput metadata schema (zod.ts) - Pass preloadSkills to harness.sendMessage in service.ts - Add ChatSendMessageInput.metadata.skills type field - Add docs/skill-preload-feature.md with implementation state and setup instructions Requires superset-sh/mastra#9 for harness.sendMessage preloadSkills support and .claude/commands/ being included in skillPaths.
1 parent 5b7ed4a commit 0429496

8 files changed

Lines changed: 208 additions & 4 deletions

File tree

apps/desktop/src/renderer/components/Chat/ChatInterface/components/ToolCallBlock/ToolCallBlock.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ import { ListProjectsToolCall } from "./components/ListProjectsToolCall";
3535
import { ListTaskStatusesToolCall } from "./components/ListTaskStatusesToolCall";
3636
import { ListTasksToolCall } from "./components/ListTasksToolCall";
3737
import { ListWorkspacesToolCall } from "./components/ListWorkspacesToolCall";
38+
import { LspInspectToolCall } from "./components/LspInspectToolCall";
3839
import { StartAgentSessionToolCall } from "./components/StartAgentSessionToolCall";
40+
import { SkillToolCall } from "./components/SkillToolCall";
3941
import { SubagentToolCall } from "./components/SubagentToolCall";
40-
import { LspInspectToolCall } from "./components/LspInspectToolCall";
41-
import { TaskWriteToolCall } from "./components/TaskWriteToolCall";
4242
import { SupersetToolCall } from "./components/SupersetToolCall";
4343
import { SwitchWorkspaceToolCall } from "./components/SwitchWorkspaceToolCall";
44+
import { TaskWriteToolCall } from "./components/TaskWriteToolCall";
4445
import { UpdateTaskToolCall } from "./components/UpdateTaskToolCall";
4546
import { UpdateWorkspaceToolCall } from "./components/UpdateWorkspaceToolCall";
4647
import { getExecuteCommandViewModel } from "./utils/getExecuteCommandViewModel";
@@ -646,6 +647,16 @@ export function ToolCallBlock({
646647
);
647648
}
648649

650+
if (toolName === "skill" || toolName === "load_skill") {
651+
const skillName =
652+
typeof args.name === "string"
653+
? args.name
654+
: typeof args.command === "string"
655+
? args.command
656+
: toolDisplayName;
657+
return <SkillToolCall part={part} skillName={skillName} />;
658+
}
659+
649660
// --- Fallback: generic tool UI ---
650661
return <GenericToolCall part={part} toolName={toolDisplayName} />;
651662
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ToolCallRow } from "@superset/ui/ai-elements/tool-call-row";
2+
import { ZapIcon } from "lucide-react";
3+
import type { ToolPart } from "../../../../utils/tool-helpers";
4+
5+
type SkillToolCallProps = {
6+
part: ToolPart;
7+
skillName: string;
8+
};
9+
10+
export function SkillToolCall({ part, skillName }: SkillToolCallProps) {
11+
const isError = part.state === "output-error";
12+
const isPending =
13+
part.state !== "output-available" && part.state !== "output-error";
14+
15+
return (
16+
<ToolCallRow
17+
icon={ZapIcon}
18+
isError={isError}
19+
isPending={isPending}
20+
title={`Skill(${skillName})`}
21+
>
22+
{!isPending ? (
23+
<div className="py-1 pl-3">
24+
{isError ? (
25+
<p className="text-xs text-destructive">Failed to load skill</p>
26+
) : (
27+
<p className="text-xs text-muted-foreground">
28+
Successfully loaded skill
29+
</p>
30+
)}
31+
</div>
32+
) : undefined}
33+
</ToolCallRow>
34+
);
35+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SkillToolCall } from "./SkillToolCall";

apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useQuery } from "@tanstack/react-query";
99
import type { ChatStatus } from "ai";
1010
import type React from "react";
1111
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12+
import { findSlashCommandByNameOrAlias } from "@superset/chat/shared";
1213
import type { SlashCommand } from "renderer/components/Chat/ChatInterface/hooks/useSlashCommands";
1314
import type {
1415
ModelOption,
@@ -503,7 +504,7 @@ export function ChatPaneInterface({
503504
// Scroll chat to bottom whenever the footer question overlay appears, changes, or disappears
504505
useEffect(() => {
505506
bumpFooterScroll();
506-
}, [pendingQuestion?.questionId, pendingQuestion?.question, bumpFooterScroll]);
507+
}, [bumpFooterScroll]);
507508

508509
useEffect(() => {
509510
onRawSnapshotChange?.({
@@ -530,6 +531,27 @@ export function ChatPaneInterface({
530531
async (payload: { content: string; files?: HarnessFilePayload[] }) => {
531532
let content = payload.content.trim();
532533

534+
// Extract custom skill chips (e.g. /redesign anywhere in message) before
535+
// slash command resolution. Built-in commands at the start still go through
536+
// the existing resolver; custom commands become preloadSkills metadata.
537+
const skillNames: string[] = [];
538+
const contentWithSkillsExtracted = content.replace(
539+
/\/(\S+)/g,
540+
(match, name: string) => {
541+
const command = findSlashCommandByNameOrAlias(slashCommands, name);
542+
if (command?.kind === "custom") {
543+
if (!skillNames.includes(command.name)) {
544+
skillNames.push(command.name);
545+
}
546+
return name; // strip the leading /
547+
}
548+
return match;
549+
},
550+
);
551+
if (skillNames.length > 0) {
552+
content = contentWithSkillsExtracted.trim();
553+
}
554+
533555
const isSlashCommand = content.startsWith("/");
534556
const slashCommandResult = await resolveSlashCommandInput(content);
535557
if (slashCommandResult.handled) {
@@ -587,6 +609,7 @@ export function ChatPaneInterface({
587609
metadata: {
588610
model: activeModel?.id,
589611
thinkingLevel,
612+
...(skillNames.length > 0 ? { skills: skillNames } : {}),
590613
},
591614
};
592615

@@ -634,6 +657,7 @@ export function ChatPaneInterface({
634657
sendMessageToSession,
635658
setRuntimeErrorMessage,
636659
onUserMessageSubmitted,
660+
slashCommands,
637661
thinkingLevel,
638662
],
639663
);

apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/utils/sendMessage/sendMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type ChatSendMessageInput = {
1212
metadata: {
1313
model?: string;
1414
thinkingLevel?: ThinkingLevel;
15+
skills?: string[];
1516
};
1617
};
1718

docs/skill-preload-feature.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Skill Preload Feature — Implementation State
2+
3+
Branch: `chat-ux-enhancements` (super-canopy)
4+
Mastra PR: superset-sh/mastra#9 (branch `mk/skill-preload-and-command-paths`)
5+
6+
## What this does
7+
8+
When a user embeds a `/command` chip in a message — e.g. "please help me /redesign this component" — the system:
9+
10+
1. Extracts the custom command name(s) from the chip nodes
11+
2. Strips the leading `/` from each chip in the message text sent to the LLM
12+
3. Passes the command names as `metadata.skills` to the backend
13+
4. The backend forwards them as `preloadSkills` to `harness.sendMessage()`
14+
5. The harness prepends an instruction so the agent calls `skill(name)` for each one before responding
15+
6. Visible `SkillToolCall` blocks appear in the chat UI before the LLM reply
16+
17+
Built-in slash commands (`/new`, `/stop`, `/model`, `/mcp`) are unaffected — only `kind === "custom"` commands are extracted as skills.
18+
19+
---
20+
21+
## Files changed in super-canopy
22+
23+
### New files
24+
- `apps/desktop/src/renderer/components/Chat/ChatInterface/components/ToolCallBlock/components/SkillToolCall/SkillToolCall.tsx`
25+
- `apps/desktop/src/renderer/components/Chat/ChatInterface/components/ToolCallBlock/components/SkillToolCall/index.ts`
26+
27+
### Modified files
28+
29+
**`packages/chat/src/server/trpc/zod.ts`**
30+
- Added `skills?: z.array(z.string())` to `sendMessageInput` metadata schema
31+
32+
**`packages/chat/src/server/trpc/service.ts`**
33+
- Passes `preloadSkills: input.metadata?.skills` to `harness.sendMessage()`
34+
35+
**`apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/utils/sendMessage/sendMessage.ts`**
36+
- Added `skills?: string[]` to `ChatSendMessageInput.metadata` type
37+
38+
**`apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceChat/components/WorkspaceChatInterface/ChatPaneInterface.tsx`**
39+
- Added import: `findSlashCommandByNameOrAlias` from `@superset/chat/shared`
40+
- In `handleSend`: extracts custom skill chip names from content via regex, strips `/` prefix, passes as `metadata.skills`
41+
- Added `slashCommands` to `useCallback` dependency array
42+
43+
**`apps/desktop/src/renderer/components/Chat/ChatInterface/components/ToolCallBlock/ToolCallBlock.tsx`**
44+
- Added import + registration for `SkillToolCall`
45+
- Handles `toolName === "skill" || toolName === "load_skill"`
46+
47+
---
48+
49+
## Files changed in mastra fork (superset-sh/mastra#9)
50+
51+
**`mastracode/src/agents/workspace.ts`**
52+
- Added `.claude/commands/` and `.agents/commands/` (local + global) to `skillPaths`
53+
- These directories are where Superset slash command files live (`.md` files)
54+
55+
**`packages/core/src/harness/harness.ts`**
56+
- Added `preloadSkills?: string[]` to `sendMessage()` signature
57+
- When provided: prepends `<system>` block instructing agent to call `skill(name)` for each entry before responding
58+
59+
---
60+
61+
## Local testing setup
62+
63+
The `package.json` resolutions in this branch are **temporarily pointing to local tarballs** at `/tmp/mastra-local/`. These files only exist on the machine where they were built.
64+
65+
To rebuild from the mastra fork on a new machine:
66+
67+
```bash
68+
# 1. Clone or pull the mastra fork
69+
git clone https://github.com/superset-sh/mastra.git ~/Sites/mastra
70+
cd ~/Sites/mastra
71+
git checkout mk/skill-preload-and-command-paths
72+
73+
# 2. Install dependencies
74+
corepack enable
75+
pnpm install
76+
77+
# 3. Build mastracode and @mastra/core
78+
pnpm turbo build --filter="@mastra/core" --filter="mastracode"
79+
80+
# 4. Pack the tarballs
81+
mkdir -p /tmp/mastra-local
82+
cd mastracode && pnpm pack --pack-destination /tmp/mastra-local && cd ..
83+
cd packages/core && pnpm pack --pack-destination /tmp/mastra-local && cd ../..
84+
85+
# 5. Wire into super-canopy (already done in package.json on this branch)
86+
cd /path/to/super-canopy
87+
bun install
88+
```
89+
90+
To restore production packages after testing:
91+
92+
```bash
93+
# In super-canopy package.json, revert resolutions back to:
94+
# "mastracode": "https://github.com/superset-sh/mastra/releases/download/mastracode-v0.4.0-superset.16/mastracode-0.10.0-alpha.6.tgz"
95+
# "@mastra/core": "https://github.com/superset-sh/mastra/releases/download/mastracode-v0.4.0-superset.16/mastra-core-1.18.0-alpha.3.tgz"
96+
bun install
97+
```
98+
99+
Once superset-sh/mastra#9 is merged and a new tarball release is cut, update the `resolutions` URLs to point to the new release and remove the local tarball entries.
100+
101+
---
102+
103+
## What's NOT done yet
104+
105+
- The `package.json` resolutions need to be reverted to GitHub URLs before merging this branch (the local `/tmp/mastra-local/` paths will break on CI and other machines)
106+
- Once mastra#9 is merged + released as `mastracode-v0.4.0-superset.17` (or similar), update resolutions to the new release URLs
107+
- The `TiptapPromptEditor`'s `SlashCommandPreview` component shows a preview for the old single-command-at-start flow — may want to update it to handle embedded chips
108+
- Consider whether the `focusShortcutText` hint in the workspace `ChatInputFooter` needs to be re-added (it was removed in a previous refactor; the main `ChatInputFooter` passes it via `TiptapPromptEditor` but the workspace version does not pass `sessionId`/`workspaceId`)
109+
110+
---
111+
112+
## How to test
113+
114+
1. Add a command file to the project:
115+
```bash
116+
echo "You are a UI redesign expert. Analyze the component and suggest improvements." > .claude/commands/redesign.md
117+
```
118+
119+
2. Start the desktop app:
120+
```bash
121+
bun dev --filter=@superset/desktop
122+
```
123+
124+
3. In chat, type `help me /redesign`, select `redesign` from the slash command popover, then submit.
125+
126+
4. Expected: `Skill(redesign)` tool call block appears in the chat before the LLM reply, with "Successfully loaded skill" shown when complete.

packages/chat/src/server/trpc/service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,11 @@ export class ChatRuntimeService {
284284
? submittedUserMessage
285285
: undefined,
286286
});
287-
return runtime.harness.sendMessage(input.payload);
287+
const skills = input.metadata?.skills;
288+
return runtime.harness.sendMessage({
289+
...input.payload,
290+
...(skills?.length ? { preloadSkills: skills } : {}),
291+
});
288292
}),
289293

290294
restartFromMessage: t.procedure

packages/chat/src/server/trpc/zod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export const sendMessageInput = z.object({
8686
.object({
8787
model: z.string().optional(),
8888
thinkingLevel: thinkingLevelSchema.optional(),
89+
/** Skill names to preload before the LLM sees the message. */
90+
skills: z.array(z.string()).optional(),
8991
})
9092
.optional(),
9193
});

0 commit comments

Comments
 (0)