FN-8409: enable coding tools for dashboard chat agents
Enable interactive dashboard chat sessions to use the project coding workspace safely. - Share the coding tool mode across direct, room, and planner chat sessions. - Document branch-sticky workspace behavior and durable-agent policy gates. - Cover coding tool access and prompt guidance with chat tests. - Add a minor changeset for the published Fusion package. Files changed: .changeset/fn-8409-chat-coding-tools.md | 7 +++++ docs/agents.md | 6 +++++ docs/dashboard-guide.md | 2 ++ .../dashboard/src/__tests__/chat-manager.test.ts | 31 +++++++++++++++------- .../src/__tests__/chat-system-prompt.test.ts | 11 ++++++++ packages/dashboard/src/chat.ts | 12 ++++++--- 6 files changed, 56 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-8409 Fusion-Task-Lineage: ca88794d-a5cf-4852-a931-90f61e3751df Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8409-chat-coding-tools.md
Normal file
7
.changeset/fn-8409-chat-coding-tools.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Dashboard chat agents can edit files and run bash with coding workspace tools.
|
||||
category: feature
|
||||
dev: Chat sessions keep tools:"coding"; system prompt + tests document write/edit/bash. Permanent-agent gates still apply when bound.
|
||||
@@ -921,6 +921,12 @@ Messaging is available in dashboard mailbox UI and CLI. In dashboard Mailbox →
|
||||
|
||||
Agent-backed dashboard chat sessions (including plugin-runtime agents such as Hermes/OpenClaw/Paperclip) also expose mailbox tools (`fn_send_message`, `fn_read_messages`) when a `MessageStore` is wired for that project. Model-only chats without an attached agent do not expose these tools.
|
||||
|
||||
### Dashboard Chat workspace tools
|
||||
|
||||
Dashboard Chat, Chat Room responders, and task-detail Planner Chat run at the interactive project checkout with coding workspace tools: `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls`. Use them for user-directed file changes and shell investigation. When a durable agent is bound, its permanent-agent permission policy still governs file writes/deletes and command execution; unbound model Chat has no durable-principal policy gate. Chat must keep the checkout branch sticky: inspect Git freely, but do not use `git checkout` or `git switch` unless the operator explicitly requests it.
|
||||
|
||||
Task-detail Planner Chat is included because it is a `task-planner:<taskId>` ChatManager session. This does not change the readonly planning/mission interview lanes or WhatsApp plugin chat. Chat verification remains limited to its existing allowlisted profiles rather than accepting arbitrary shell commands.
|
||||
|
||||
```bash
|
||||
fn message inbox
|
||||
fn message outbox
|
||||
|
||||
@@ -592,6 +592,8 @@ The full **New Task** dialog includes a compact **GitHub issue or PR** picker ne
|
||||
|
||||
Chat view provides project-scoped conversations with agents.
|
||||
|
||||
- Direct Chat, Chat Room responders, and task-detail Planner Chat have coding workspace tools at the interactive project checkout: `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls`. They can make user-directed edits and run shell investigation; a bound durable agent remains subject to its permanent-agent file-write and command-execution permission policy. These Chat sessions keep the checkout branch sticky unless you explicitly ask to switch it. Planning/mission interviews and WhatsApp plugin chat remain readonly.
|
||||
|
||||
<!-- FNXC:NativeStructureEmbed 2026-07-19-20:00: Roadmap-item references now resolve through the roadmap plugin's PostgreSQL-safe read adapter and open the restored hosted Roadmaps destination. -->
|
||||
- Chat recognizes native structure references in both assistant and user messages using the explicit `fusion://<kind>/<id>` form. Supported kinds are `mission`, `milestone`, `roadmap-item`, `research-finding`, `eval-result`, and `goal`. Use a bare token such as `fusion://mission/M-001` in either message type, or an assistant Markdown link such as `[Mission](fusion://mission/M-001)`. `roadmap-item` previews the roadmap feature title and description when available; a missing feature or unavailable roadmap data layer renders the shared unavailable card.
|
||||
- Recognized references render an inline preview card before you leave the conversation. Select **Open** on an available card to navigate to its owning dashboard view; missing, archived, or otherwise unavailable structures show a safe unavailable placeholder instead. Plain-text mode deliberately leaves reference text raw.
|
||||
|
||||
@@ -414,15 +414,19 @@ describe("ChatManager.sendMessage", () => {
|
||||
});
|
||||
|
||||
it("records task-detail planner chat tokens separately from task execution usage", async () => {
|
||||
__setCreateResolvedAgentSession(async () => ({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
model: { provider: "anthropic", id: "claude-sonnet-4-5" },
|
||||
getSessionStats: () => ({ tokens: { input: 10, output: 4, cacheRead: 0, cacheWrite: 0 } }),
|
||||
state: { messages: [{ role: "assistant", content: "Planner response" }] },
|
||||
},
|
||||
}));
|
||||
let createOptions: any;
|
||||
__setCreateResolvedAgentSession(async (options: any) => {
|
||||
createOptions = options;
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
model: { provider: "anthropic", id: "claude-sonnet-4-5" },
|
||||
getSessionStats: () => ({ tokens: { input: 10, output: 4, cacheRead: 0, cacheWrite: 0 } }),
|
||||
state: { messages: [{ role: "assistant", content: "Planner response" }] },
|
||||
},
|
||||
};
|
||||
});
|
||||
mockChatStore.getSession.mockReturnValue({
|
||||
id: "chat-planner",
|
||||
agentId: "task-planner:FN-7449",
|
||||
@@ -451,6 +455,8 @@ describe("ChatManager.sendMessage", () => {
|
||||
agentId: "task-planner:FN-7449",
|
||||
totalTokens: 14,
|
||||
}));
|
||||
expect(createOptions.tools).toBe("coding");
|
||||
expect(createOptions).not.toHaveProperty("toolsAllowlist");
|
||||
});
|
||||
|
||||
it("does not record chat token usage when session stats are unavailable or zero", async () => {
|
||||
@@ -1155,7 +1161,7 @@ describe("ChatManager.sendMessage", () => {
|
||||
|
||||
it("creates chat agents with the full coding toolset", async () => {
|
||||
let createOptions: any;
|
||||
__setCreateFnAgent(async (options: any) => {
|
||||
__setCreateResolvedAgentSession(async (options: any) => {
|
||||
createOptions = options;
|
||||
return {
|
||||
session: {
|
||||
@@ -1172,6 +1178,7 @@ describe("ChatManager.sendMessage", () => {
|
||||
await chatManager.sendMessage("chat-001", "Hello");
|
||||
|
||||
expect(createOptions.tools).toBe("coding");
|
||||
expect(createOptions).not.toHaveProperty("toolsAllowlist");
|
||||
});
|
||||
|
||||
it("requests bound agent and enabled plugin skills for regular chat", async () => {
|
||||
@@ -3669,7 +3676,9 @@ describe("ChatManager generation isolation", () => {
|
||||
mockAgentStore.getAgent.mockResolvedValue({ id: "agent-001", name: "Avery", role: "executor", state: "idle" });
|
||||
|
||||
let capturedTools: Array<{ name: string }> = [];
|
||||
let createOptions: any;
|
||||
__setCreateResolvedAgentSession(async (options: any) => {
|
||||
createOptions = options;
|
||||
capturedTools = options.customTools ?? [];
|
||||
return {
|
||||
session: {
|
||||
@@ -3685,6 +3694,8 @@ describe("ChatManager generation isolation", () => {
|
||||
await chatManager.sendRoomMessage("room-1", "How many tokens did FN-7310 use?");
|
||||
|
||||
const names = capturedTools.map((tool) => tool.name);
|
||||
expect(createOptions.tools).toBe("coding");
|
||||
expect(createOptions).not.toHaveProperty("toolsAllowlist");
|
||||
expect(names).not.toContain("fn_task_planner_get_task_metrics");
|
||||
for (const required of [
|
||||
"fn_task_list",
|
||||
|
||||
@@ -13,6 +13,17 @@ describe("chat system prompt guidance", () => {
|
||||
expect(CHAT_SYSTEM_PROMPT).toContain('to_id: "dashboard"');
|
||||
});
|
||||
|
||||
it("authorizes the full coding workspace toolset for user-directed changes", () => {
|
||||
const lower = CHAT_SYSTEM_PROMPT.toLowerCase();
|
||||
|
||||
for (const tool of ["read", "write", "edit", "bash", "grep", "find", "ls"]) {
|
||||
expect(lower).toContain(`\`${tool}\``);
|
||||
}
|
||||
expect(lower).toContain("user-requested code changes");
|
||||
expect(lower).toContain("do not claim that you only have read access");
|
||||
expect(lower).toContain("pending-approval");
|
||||
});
|
||||
|
||||
it("keeps the checked-out branch sticky unless explicitly requested", () => {
|
||||
const lower = CHAT_SYSTEM_PROMPT.toLowerCase();
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ async function ensureEngineReady(): Promise<void> {
|
||||
*/
|
||||
export const CHAT_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS}
|
||||
|
||||
You are a helpful AI assistant integrated into the fn task board system. You help users with questions about their project, code, architecture, and tasks. You have access to project files and can read them to provide informed responses, including referencing specific file paths and line numbers when possible. Do not change the branch the working directory is checked out on: do not run \`git checkout <branch>\` or \`git switch <branch>\` to a different branch unless the user explicitly asks. Read-only Git and branch inspection, such as \`git status\`, \`git branch\`, and \`git log\`, is allowed. Response length policy: default to a short, crisp reply (a few sentences or a short bulleted list) that directly answers the user; avoid preamble, restating the question, and filler. If a thorough answer genuinely needs long-form content (for example multi-step plans, design proposals, deep analyses, or long file excerpts), keep the chat reply brief with a one- or two-sentence summary and then send the full write-up via \`fn_send_message\` using \`type: "agent-to-user"\` and \`to_id: "dashboard"\`. That mailbox follow-up must add new substantive detail and must not duplicate the chat reply.`;
|
||||
You are a helpful AI assistant integrated into the fn task board system. You help users with questions about their project, code, architecture, and tasks. You have coding workspace tools on the project checkout: \`read\`, \`write\`, \`edit\`, \`bash\`, \`grep\`, \`find\`, and \`ls\`. Use \`write\`, \`edit\`, and \`bash\` for user-requested code changes, file edits, or shell investigation; prefer minimal, user-directed mutations and respect any pending-approval or blocked tool result. Do not claim that you only have read access. Do not change the branch the working directory is checked out on: do not run \`git checkout <branch>\` or \`git switch <branch>\` to a different branch unless the user explicitly asks. Read-only Git and branch inspection, such as \`git status\`, \`git branch\`, and \`git log\`, is allowed. Response length policy: default to a short, crisp reply (a few sentences or a short bulleted list) that directly answers the user; avoid preamble, restating the question, and filler. If a thorough answer genuinely needs long-form content (for example multi-step plans, design proposals, deep analyses, or long file excerpts), keep the chat reply brief with a one- or two-sentence summary and then send the full write-up via \`fn_send_message\` using \`type: "agent-to-user"\` and \`to_id: "dashboard"\`. That mailbox follow-up must add new substantive detail and must not duplicate the chat reply.`;
|
||||
|
||||
export const CHAT_AGENT_MESSAGE_ROUTING_GUIDANCE = `## Messaging Semantics\n\nYour chat reply is the primary response to the user. Do not also call \`fn_send_message\` with the same content just to mirror your chat response into mailbox.\n\nUse \`fn_send_message\` only when either (a) the user explicitly asks for mailbox/inbox/notification delivery (for example: "send me this in mail", "ntfy me when…", or "leave me a note in my inbox"), or (b) you are sending a genuinely longer follow-up that did not fit in a short chat reply. In either case, send with \`type: "agent-to-user"\` and target the dashboard user alias (\`to_id: "dashboard"\` is preferred), and ensure the mailbox message is additive rather than a duplicate of the chat reply. Never route that as a user/CLI → agent message.`;
|
||||
|
||||
@@ -268,6 +268,12 @@ const MAX_MESSAGES_PER_IP_PER_MINUTE = 30;
|
||||
/** Maximum file size for # mentions (50KB). Files larger than this are skipped. */
|
||||
const MAX_REFERENCED_FILE_SIZE = 50 * 1024;
|
||||
export const TASK_PLANNER_CHAT_AGENT_ID_PREFIX = "task-planner:";
|
||||
|
||||
/*
|
||||
FNXC:ChatCodingTools 2026-07-19-00:00:
|
||||
Dashboard Chat sessions intentionally use the project-root coding workspace builtins so direct, room, and task-detail planner Chat can read, write, edit, and investigate with bash. Keep this shared mode unfiltered: permanent-agent action gates still enforce file-write and command-execution policy when a durable agent is bound, while task-planner Chat reaches the same direct-chat session path.
|
||||
*/
|
||||
const CHAT_CODING_TOOLS = "coding" as const;
|
||||
const ROOM_AMBIENT_MAX_RESPONDERS = 5;
|
||||
|
||||
type ChatSessionStatsLike = { tokens?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; total?: number } };
|
||||
@@ -2036,7 +2042,7 @@ export class ChatManager {
|
||||
...(mergedRoomSkillSelection ? { skillSelection: mergedRoomSkillSelection } : {}),
|
||||
cwd: this.rootDir,
|
||||
systemPrompt,
|
||||
tools: "coding",
|
||||
tools: CHAT_CODING_TOOLS,
|
||||
...(workflowTools.length + chatFusionTools.length > 0
|
||||
? { customTools: dedupeChatTools([...workflowTools, ...chatFusionTools]) }
|
||||
: {}),
|
||||
@@ -2597,7 +2603,7 @@ export class ChatManager {
|
||||
const sessionOptions = {
|
||||
cwd: this.rootDir,
|
||||
systemPrompt,
|
||||
tools: "coding" as const,
|
||||
tools: CHAT_CODING_TOOLS,
|
||||
...(customTools.length > 0 ? { customTools } : {}),
|
||||
sessionManager,
|
||||
...(effectiveModelProvider && effectiveModelId
|
||||
|
||||
Reference in New Issue
Block a user