FN-6605: load chat skill slash commands
Load dashboard chat slash-command skill requests into model-loop sessions.
- Parse /skill:{name} tokens in main chat, QuickChat, and room responders.
- Merge typed skill requests with existing agent and plugin skill selection while preserving normal filters.
- Strip slash-command tokens from model prompts without mutating persisted chat history.
- Cover slash-command parsing, deduplication, prompt stripping, and room responder skill selection.
- Document dashboard chat skill command behavior and add a published package changeset.
Files changed:
.changeset/fn-6605-chat-skill-slash-command.md | 5 +
docs/agents.md | 1 +
docs/dashboard-guide.md | 1 +
.../dashboard/src/__tests__/chat-manager.test.ts | 158 +++++++++++++++++++++
.../dashboard/src/__tests__/chat.rooms.test.ts | 46 ++++++
packages/dashboard/src/chat.ts | 100 ++++++++++++-
6 files changed, 307 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-6605
Fusion-Task-Lineage: 4ad2e015-30a5-4608-b69c-e42e46166f0e
This commit is contained in:
5
.changeset/fn-6605-chat-skill-slash-command.md
Normal file
5
.changeset/fn-6605-chat-skill-slash-command.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Load dashboard chat skills requested with `/skill:{name}` and strip the command token from model prompts.
|
||||||
@@ -24,6 +24,7 @@ fn chat <agent-id> [message…] [--once] [--non-interactive] [--poll-ms <n>]
|
|||||||
- Each message is stored as a `user-to-agent` MessageStore message from `cli` with `metadata.wakeRecipient=true`.
|
- Each message is stored as a `user-to-agent` MessageStore message from `cli` with `metadata.wakeRecipient=true`.
|
||||||
- Agent replies are polled from your inbox and printed as they arrive.
|
- Agent replies are polled from your inbox and printed as they arrive.
|
||||||
- Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills.
|
- Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills.
|
||||||
|
- In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command.
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ Quick Chat is an optional floating panel for fast, project-scoped assistant conv
|
|||||||
|
|
||||||
- Controlled by the project setting `showQuickChatFAB`
|
- Controlled by the project setting `showQuickChatFAB`
|
||||||
- Supports agent mentions (`@agent`) and shared `#` task/file mentions
|
- Supports agent mentions (`@agent`) and shared `#` task/file mentions
|
||||||
|
- Supports `/skill:{name}` in model-loop chat to request a specific enabled skill for that session; the slash token is removed from the model prompt while the original user message remains in chat history
|
||||||
- Uses the same model/provider infrastructure as full Chat view
|
- Uses the same model/provider infrastructure as full Chat view
|
||||||
- On small screens, compact tool-call summaries in the floating panel intentionally stay single-line (count + tool names + status) to preserve message density
|
- On small screens, compact tool-call summaries in the floating panel intentionally stay single-line (count + tool names + status) to preserve message density
|
||||||
- The panel header uses a session-first flow: the main dropdown lists persisted sessions (preferring `session.title`, then falling back to deterministic `Session N` labels)
|
- The panel header uses a session-first flow: the main dropdown lists persisted sessions (preferring `session.title`, then falling back to deterministic `Session N` labels)
|
||||||
|
|||||||
@@ -706,6 +706,164 @@ describe("ChatManager.sendMessage", () => {
|
|||||||
expect(createOptions.skillSelection.requestedSkillNames).toEqual(["fusion", "ce-debug"]);
|
expect(createOptions.skillSelection.requestedSkillNames).toEqual(["fusion", "ce-debug"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads a single-segment /skill command and strips it from the chat prompt", async () => {
|
||||||
|
const promptSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
let createOptions: any;
|
||||||
|
__setCreateResolvedAgentSession(async (options: any) => {
|
||||||
|
createOptions = options;
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: promptSpy,
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: { messages: [{ role: "assistant", content: "Skill command ready" }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
mockAgentStore.getAgent.mockResolvedValue({
|
||||||
|
id: "agent-001",
|
||||||
|
name: "Avery",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {},
|
||||||
|
metadata: { skills: ["agent-debug"] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatManager = createChatManager();
|
||||||
|
await chatManager.sendMessage("chat-001", "/skill:ce-debug please debug this");
|
||||||
|
|
||||||
|
expect(createOptions.skillSelection.requestedSkillNames).toEqual(["agent-debug", "ce-debug"]);
|
||||||
|
expect(promptSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const promptContent = promptSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(promptContent).toBe("please debug this");
|
||||||
|
expect(promptContent).not.toContain("/skill:");
|
||||||
|
expect(mockChatStore.addMessage).toHaveBeenCalledWith("chat-001", expect.objectContaining({
|
||||||
|
role: "user",
|
||||||
|
content: "/skill:ce-debug please debug this",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads two-segment and multiple /skill commands in typed order", async () => {
|
||||||
|
const promptSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
let createOptions: any;
|
||||||
|
__setCreateResolvedAgentSession(async (options: any) => {
|
||||||
|
createOptions = options;
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: promptSpy,
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: { messages: [{ role: "assistant", content: "Multiple skills ready" }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
mockAgentStore.getAgent.mockResolvedValue({
|
||||||
|
id: "agent-001",
|
||||||
|
name: "Avery",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {},
|
||||||
|
metadata: { skills: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatManager = createChatManager();
|
||||||
|
await chatManager.sendMessage("chat-001", "start /skill:review/pr please /skill:gamma now");
|
||||||
|
|
||||||
|
expect(createOptions.skillSelection.requestedSkillNames).toEqual(["fusion", "review/pr", "gamma"]);
|
||||||
|
const promptContent = promptSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(promptContent).toBe("start please now");
|
||||||
|
expect(promptContent).not.toContain("/skill:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dedupes typed /skill commands against agent and plugin skills case-insensitively", async () => {
|
||||||
|
const promptSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
let createOptions: any;
|
||||||
|
__setCreateResolvedAgentSession(async (options: any) => {
|
||||||
|
createOptions = options;
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: promptSpy,
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: { messages: [{ role: "assistant", content: "Deduped" }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
mockAgentStore.getAgent.mockResolvedValue({
|
||||||
|
id: "agent-001",
|
||||||
|
name: "Avery",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {},
|
||||||
|
metadata: { skills: ["ce-debug"] },
|
||||||
|
});
|
||||||
|
const pluginRunner = {
|
||||||
|
getPluginSkills: vi.fn(() => [
|
||||||
|
{ pluginId: "fusion-plugin-compound-engineering", skill: { name: "review/pr", enabled: true } },
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const chatManager = createChatManager(pluginRunner);
|
||||||
|
await chatManager.sendMessage("chat-001", "/skill:CE-DEBUG /skill:review/pr/SKILL.md use both");
|
||||||
|
|
||||||
|
expect(createOptions.skillSelection.requestedSkillNames).toEqual(["ce-debug", "review/pr"]);
|
||||||
|
const names = createOptions.skillSelection.requestedSkillNames.filter((name: string) => name.toLowerCase() === "ce-debug");
|
||||||
|
expect(names).toHaveLength(1);
|
||||||
|
expect(promptSpy.mock.calls[0]?.[0]).toBe("use both");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates skill selection for model-only QuickChat when /skill is typed without plugin skills", async () => {
|
||||||
|
mockChatStore.getSession.mockReturnValue({
|
||||||
|
id: "chat-001",
|
||||||
|
agentId: null,
|
||||||
|
status: "active",
|
||||||
|
});
|
||||||
|
const promptSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
let createOptions: any;
|
||||||
|
__setCreateResolvedAgentSession(async (options: any) => {
|
||||||
|
createOptions = options;
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: promptSpy,
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: { messages: [{ role: "assistant", content: "Model-only skill ready" }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatManager = createChatManager();
|
||||||
|
await chatManager.sendMessage("chat-001", "/skill:foo answer directly");
|
||||||
|
|
||||||
|
expect(createOptions.skillSelection).toMatchObject({
|
||||||
|
projectRootDir: "/tmp/test",
|
||||||
|
sessionPurpose: "executor",
|
||||||
|
});
|
||||||
|
expect(createOptions.skillSelection.requestedSkillNames).toContain("foo");
|
||||||
|
expect(promptSpy.mock.calls[0]?.[0]).toBe("answer directly");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves skill selection and prompt content unchanged when no /skill command is present", async () => {
|
||||||
|
const promptSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
let createOptions: any;
|
||||||
|
__setCreateResolvedAgentSession(async (options: any) => {
|
||||||
|
createOptions = options;
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: promptSpy,
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: { messages: [{ role: "assistant", content: "Plain reply" }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
mockAgentStore.getAgent.mockResolvedValue({
|
||||||
|
id: "agent-001",
|
||||||
|
name: "Avery",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {},
|
||||||
|
metadata: { skills: ["agent-debug"] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatManager = createChatManager();
|
||||||
|
await chatManager.sendMessage("chat-001", "plain hello");
|
||||||
|
|
||||||
|
expect(createOptions.skillSelection.requestedSkillNames).toEqual(["agent-debug"]);
|
||||||
|
expect(promptSpy.mock.calls[0]?.[0]).toBe("plain hello");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps agent skills when the chat plugin runner lacks skill discovery", async () => {
|
it("keeps agent skills when the chat plugin runner lacks skill discovery", async () => {
|
||||||
let createOptions: any;
|
let createOptions: any;
|
||||||
__setCreateResolvedAgentSession(async (options: any) => {
|
__setCreateResolvedAgentSession(async (options: any) => {
|
||||||
|
|||||||
@@ -140,6 +140,52 @@ describe("Chat orchestration — rooms (FN-3805..FN-3811 contract)", () => {
|
|||||||
expect(createOptions.skillSelection.requestedSkillNames).not.toContain("disabled-debug");
|
expect(createOptions.skillSelection.requestedSkillNames).not.toContain("disabled-debug");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads typed /skill commands for room responders and strips them from the room prompt", async () => {
|
||||||
|
mockChatStore.listRoomMembers.mockReturnValue([
|
||||||
|
{ roomId: "room-1", agentId: "agent-a", role: "member", addedAt: "2026-01-01" },
|
||||||
|
]);
|
||||||
|
mockAgentStore.listAgents.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "agent-a",
|
||||||
|
name: "Alpha",
|
||||||
|
role: "executor",
|
||||||
|
runtimeConfig: {},
|
||||||
|
metadata: { skills: ["room-agent-debug"] },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const promptSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
let createOptions: any;
|
||||||
|
__setCreateResolvedAgentSession(async (options: any) => {
|
||||||
|
createOptions = options;
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: promptSpy,
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: {
|
||||||
|
messages: [{ role: "assistant", content: "Room reply" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
const manager = new ChatManager(mockChatStore as any, "/tmp", mockAgentStore as any);
|
||||||
|
await manager.sendRoomMessage("room-1", "/skill:ce-debug hello @Alpha");
|
||||||
|
|
||||||
|
expect(createOptions.skillSelection).toMatchObject({
|
||||||
|
projectRootDir: "/tmp",
|
||||||
|
sessionPurpose: "heartbeat",
|
||||||
|
});
|
||||||
|
expect(createOptions.skillSelection.requestedSkillNames).toEqual(["room-agent-debug", "ce-debug"]);
|
||||||
|
expect(promptSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const roomPrompt = promptSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(roomPrompt).toContain("Latest user message to answer:\n\nhello @Alpha");
|
||||||
|
expect(roomPrompt).not.toContain("/skill:");
|
||||||
|
expect(mockChatStore.addRoomMessage.mock.calls[0]?.[1]).toMatchObject({
|
||||||
|
role: "user",
|
||||||
|
content: "/skill:ce-debug hello @Alpha",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("suppresses trimmed skip sentinel replies while persisting normal co-responder replies", async () => {
|
it("suppresses trimmed skip sentinel replies while persisting normal co-responder replies", async () => {
|
||||||
mockChatStore.listRoomMembers.mockReturnValue([
|
mockChatStore.listRoomMembers.mockReturnValue([
|
||||||
{ roomId: "room-1", agentId: "agent-a", role: "member", addedAt: "2026-01-01" },
|
{ roomId: "room-1", agentId: "agent-a", role: "member", addedAt: "2026-01-01" },
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type {
|
|||||||
Settings,
|
Settings,
|
||||||
TaskStore,
|
TaskStore,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
|
import type { SkillSelectionContext } from "@fusion/engine";
|
||||||
import { summarizeTitle } from "@fusion/core";
|
import { summarizeTitle } from "@fusion/core";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
@@ -117,6 +118,79 @@ const diagnostics: DiagnosticsLogger = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SKILL_COMMAND_PATTERN = /(^|\s)\/skill:([^\s]+)/gi;
|
||||||
|
|
||||||
|
function bareChatSkillCommandName(name: string): string {
|
||||||
|
return name
|
||||||
|
.replace(/\/SKILL\.md$/i, "")
|
||||||
|
.replace(/[.,;!?)]*$/g, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushDedupedSkillName(names: string[], seen: Set<string>, name: string): void {
|
||||||
|
const bareName = bareChatSkillCommandName(name);
|
||||||
|
if (!bareName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = bareName.toLowerCase();
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
names.push(bareName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSkillCommands(content: string): { requestedSkillNames: string[]; strippedContent: string } {
|
||||||
|
const requestedSkillNames: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
let foundCommand = false;
|
||||||
|
|
||||||
|
const strippedContent = content.replace(SKILL_COMMAND_PATTERN, (match, leadingWhitespace: string, rawName: string) => {
|
||||||
|
foundCommand = true;
|
||||||
|
pushDedupedSkillName(requestedSkillNames, seen, rawName);
|
||||||
|
return leadingWhitespace ? " " : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!foundCommand) {
|
||||||
|
return { requestedSkillNames, strippedContent: content };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestedSkillNames,
|
||||||
|
strippedContent: strippedContent.replace(/\s+/g, " ").trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeTypedSkillCommands(
|
||||||
|
baseSkillSelection: SkillSelectionContext | undefined,
|
||||||
|
typedSkillNames: string[],
|
||||||
|
projectRootDir: string,
|
||||||
|
sessionPurpose: string,
|
||||||
|
): SkillSelectionContext | undefined {
|
||||||
|
if (typedSkillNames.length === 0) {
|
||||||
|
return baseSkillSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedSkillNames: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const name of baseSkillSelection?.requestedSkillNames ?? []) {
|
||||||
|
pushDedupedSkillName(requestedSkillNames, seen, name);
|
||||||
|
}
|
||||||
|
for (const name of typedSkillNames) {
|
||||||
|
pushDedupedSkillName(requestedSkillNames, seen, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ChatSkills 2026-06-17-18:16:
|
||||||
|
The advertised chat `/skill:{name}` command must request that skill for the model-loop session while keeping execution settings authoritative; this merge only adds requested names to the existing skill-selection context so the resolver still filters disabled or excluded skills.
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
projectRootDir: baseSkillSelection?.projectRootDir ?? projectRootDir,
|
||||||
|
requestedSkillNames,
|
||||||
|
sessionPurpose: baseSkillSelection?.sessionPurpose ?? sessionPurpose,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureEngineReady(): Promise<void> {
|
async function ensureEngineReady(): Promise<void> {
|
||||||
if (buildAgentChatPromptFn) {
|
if (buildAgentChatPromptFn) {
|
||||||
return;
|
return;
|
||||||
@@ -1318,6 +1392,7 @@ export class ChatManager {
|
|||||||
diagnostics,
|
diagnostics,
|
||||||
);
|
);
|
||||||
const attachmentContentBlock = formatChatAttachmentContents(attachmentContents);
|
const attachmentContentBlock = formatChatAttachmentContents(attachmentContents);
|
||||||
|
const parsedSkillCommands = parseSkillCommands(input.content);
|
||||||
const roomPromptParts = [
|
const roomPromptParts = [
|
||||||
`You are replying as ${input.responder.name} in room #${input.roomName}.`,
|
`You are replying as ${input.responder.name} in room #${input.roomName}.`,
|
||||||
"Reply to the latest user room message in the context of this shared room thread.",
|
"Reply to the latest user room message in the context of this shared room thread.",
|
||||||
@@ -1327,7 +1402,7 @@ export class ChatManager {
|
|||||||
summaryMaxChars: roomCompactionSettings.summaryMaxChars,
|
summaryMaxChars: roomCompactionSettings.summaryMaxChars,
|
||||||
}),
|
}),
|
||||||
"Latest user message to answer:",
|
"Latest user message to answer:",
|
||||||
input.content,
|
parsedSkillCommands.strippedContent,
|
||||||
];
|
];
|
||||||
if (attachmentContentBlock) {
|
if (attachmentContentBlock) {
|
||||||
roomPromptParts.push(attachmentContentBlock);
|
roomPromptParts.push(attachmentContentBlock);
|
||||||
@@ -1347,6 +1422,12 @@ export class ChatManager {
|
|||||||
this.rootDir,
|
this.rootDir,
|
||||||
this.getPluginRunnerForSkillSelection(),
|
this.getPluginRunnerForSkillSelection(),
|
||||||
);
|
);
|
||||||
|
const mergedRoomSkillSelection = mergeTypedSkillCommands(
|
||||||
|
roomSkillContext.skillSelectionContext,
|
||||||
|
parsedSkillCommands.requestedSkillNames,
|
||||||
|
this.rootDir,
|
||||||
|
"heartbeat",
|
||||||
|
);
|
||||||
|
|
||||||
const resolvedSession = await createResolvedAgentSession({
|
const resolvedSession = await createResolvedAgentSession({
|
||||||
sessionPurpose: "heartbeat",
|
sessionPurpose: "heartbeat",
|
||||||
@@ -1355,8 +1436,11 @@ export class ChatManager {
|
|||||||
/*
|
/*
|
||||||
FNXC:ChatSkills 2026-06-16-19:13:
|
FNXC:ChatSkills 2026-06-16-19:13:
|
||||||
Chat-room responder sessions must request the responder agent skills plus enabled plugin skills so chat-only agent replies can use skills such as ce-debug just like heartbeat/executor lanes.
|
Chat-room responder sessions must request the responder agent skills plus enabled plugin skills so chat-only agent replies can use skills such as ce-debug just like heartbeat/executor lanes.
|
||||||
|
|
||||||
|
FNXC:ChatSkills 2026-06-17-18:16:
|
||||||
|
Room responders share the chat slash-command contract: `/skill:{name}` is removed from the prompt text and merged into heartbeat skill selection without changing persisted room-message text.
|
||||||
*/
|
*/
|
||||||
...(roomSkillContext.skillSelectionContext ? { skillSelection: roomSkillContext.skillSelectionContext } : {}),
|
...(mergedRoomSkillSelection ? { skillSelection: mergedRoomSkillSelection } : {}),
|
||||||
cwd: this.rootDir,
|
cwd: this.rootDir,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
tools: "coding",
|
tools: "coding",
|
||||||
@@ -1559,6 +1643,8 @@ export class ChatManager {
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const parsedSkillCommands = parseSkillCommands(content);
|
||||||
|
|
||||||
const hasMentionCandidates = /@[\w-]+/.test(content);
|
const hasMentionCandidates = /@[\w-]+/.test(content);
|
||||||
const mentionAgents = hasMentionCandidates ? await this.listAgentsForMentions() : [];
|
const mentionAgents = hasMentionCandidates ? await this.listAgentsForMentions() : [];
|
||||||
const mentions = hasMentionCandidates ? await this.parseMentions(content, mentionAgents) : [];
|
const mentions = hasMentionCandidates ? await this.parseMentions(content, mentionAgents) : [];
|
||||||
@@ -1670,7 +1756,7 @@ export class ChatManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve #file references in the current message before sending to AI
|
// Resolve #file references in the current message before sending to AI
|
||||||
const resolvedContent = await resolveFileReferences(content, this.rootDir);
|
const resolvedContent = await resolveFileReferences(parsedSkillCommands.strippedContent, this.rootDir);
|
||||||
|
|
||||||
const attachmentSummary = attachments && attachments.length > 0
|
const attachmentSummary = attachments && attachments.length > 0
|
||||||
? `[User attached: ${attachments
|
? `[User attached: ${attachments
|
||||||
@@ -1815,6 +1901,12 @@ export class ChatManager {
|
|||||||
this.rootDir,
|
this.rootDir,
|
||||||
this.getPluginRunnerForSkillSelection(),
|
this.getPluginRunnerForSkillSelection(),
|
||||||
);
|
);
|
||||||
|
const mergedChatSkillSelection = mergeTypedSkillCommands(
|
||||||
|
chatSkillContext.skillSelectionContext,
|
||||||
|
parsedSkillCommands.requestedSkillNames,
|
||||||
|
this.rootDir,
|
||||||
|
"executor",
|
||||||
|
);
|
||||||
agentResult = await createResolvedAgentSession({
|
agentResult = await createResolvedAgentSession({
|
||||||
sessionPurpose: "executor",
|
sessionPurpose: "executor",
|
||||||
...(agentRuntimeHint ? { runtimeHint: agentRuntimeHint } : {}),
|
...(agentRuntimeHint ? { runtimeHint: agentRuntimeHint } : {}),
|
||||||
@@ -1823,7 +1915,7 @@ export class ChatManager {
|
|||||||
FNXC:ChatSkills 2026-06-16-19:13:
|
FNXC:ChatSkills 2026-06-16-19:13:
|
||||||
Regular chat and QuickChat must request bound-agent skills plus enabled plugin skills so dashboard chat loads capabilities such as ce-debug instead of creating skill-less sessions.
|
Regular chat and QuickChat must request bound-agent skills plus enabled plugin skills so dashboard chat loads capabilities such as ce-debug instead of creating skill-less sessions.
|
||||||
*/
|
*/
|
||||||
...(chatSkillContext.skillSelectionContext ? { skillSelection: chatSkillContext.skillSelectionContext } : {}),
|
...(mergedChatSkillSelection ? { skillSelection: mergedChatSkillSelection } : {}),
|
||||||
...sessionOptions,
|
...sessionOptions,
|
||||||
});
|
});
|
||||||
this.activeGenerations.set(sessionId, { abortController, agentResult, generationId });
|
this.activeGenerations.set(sessionId, { abortController, agentResult, generationId });
|
||||||
|
|||||||
Reference in New Issue
Block a user