@frebreco/canvas 0.3.0-next.6 → 0.3.0-next.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/skills/.claude-plugin/plugin.json +5 -0
- package/skills/code-tour/SKILL.md +64 -0
- package/src/server/agents.ts +5 -0
- package/src/server/board-mcp.ts +4 -1
- package/src/server/server.ts +2 -0
- package/src/server/skills.ts +32 -0
- package/src/shared/board-tools.ts +14 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frebreco/canvas",
|
|
3
|
-
"version": "0.3.0-next.
|
|
3
|
+
"version": "0.3.0-next.8",
|
|
4
4
|
"description": "A multiplayer canvas for coding agents that run on your machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"bin",
|
|
20
20
|
"src",
|
|
21
|
+
"skills",
|
|
21
22
|
"README.md",
|
|
22
23
|
"LICENSE"
|
|
23
24
|
],
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-tour
|
|
3
|
+
description: Give a code tour on the canvas board — a guided walk through how part of the codebase works, as one files frame people click through. Use when asked for a tour or walkthrough of code, how a feature works across files, or to onboard someone to an area.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
A code tour is one files frame on the board whose list holds, in reading order: a guide (a
|
|
7
|
+
markdown scratch file), then the **stops** — project files at the lines that matter — and
|
|
8
|
+
optionally a diagram at the end. People click through it at their own pace; the thread only
|
|
9
|
+
points at it.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. **Find the stops.** Read the code until you can tell the story from entry point to effect.
|
|
14
|
+
Pick 4–10 stops in the order a newcomer needs them, usually where it starts → the core → the
|
|
15
|
+
edges. A stop is one file at a tight line range (roughly 5–40 lines): the function, the branch,
|
|
16
|
+
the type that carries the idea. Only files git tracks can be shown; a stop in an ignored file
|
|
17
|
+
gets described in the guide instead. Done when each stop's range is read from the file as it
|
|
18
|
+
is now, and the stops tell the story without gaps.
|
|
19
|
+
|
|
20
|
+
2. **Write the guide**, `tour-<topic>.md`: a one-paragraph overview, then one short section per
|
|
21
|
+
stop, numbered and titled like its list entry — what to look at in those lines, why it
|
|
22
|
+
matters, and how it hands over to the next stop. Name functions and types; quote lines
|
|
23
|
+
sparingly, the frame shows them. End with open questions or pitfalls if you found any.
|
|
24
|
+
|
|
25
|
+
3. **Draw it**, when the flow branches or spans several components: `tour-<topic>.html`, a
|
|
26
|
+
single self-contained page (inline CSS and SVG or script) with boxes named like the stops.
|
|
27
|
+
Skip it for a straight line of calls.
|
|
28
|
+
|
|
29
|
+
4. **Open the tour.** `view_board` first; retarget an existing tour frame on the same topic with
|
|
30
|
+
`update_frame` rather than opening a second one. Otherwise one `open_frame` of type `file`
|
|
31
|
+
whose `files` list is the whole tour, the guide first:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"type": "file",
|
|
36
|
+
"title": "Tour: login",
|
|
37
|
+
"files": [
|
|
38
|
+
{ "display": "0 Guide.md", "name": "tour-login.md", "content": "…" },
|
|
39
|
+
{ "display": "1 Route/login.ts", "path": "src/routes/login.ts", "start_line": 12, "end_line": 30 },
|
|
40
|
+
{ "display": "2 Session/session.ts", "path": "src/auth/session.ts", "start_line": 5, "end_line": 22 },
|
|
41
|
+
{ "display": "3 Diagram.html", "name": "tour-login.html", "content": "…" }
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Number the display names like the guide's sections, so people can match them; folders are
|
|
47
|
+
worth it only for chapters of a long tour. Done when every stop in the list has its section
|
|
48
|
+
in the guide and every section its stop.
|
|
49
|
+
|
|
50
|
+
5. **Reply briefly**: the tour's frame, how many stops, and the story in one or two sentences.
|
|
51
|
+
The guide carries the explanation; the thread stays short.
|
|
52
|
+
|
|
53
|
+
## Walking people through it
|
|
54
|
+
|
|
55
|
+
When people ask to be walked through the tour ("next", "show stop 3"), `update_frame` the tour
|
|
56
|
+
frame to that stop's `path`, `start_line` and `end_line`, and explain it in your reply. Changing
|
|
57
|
+
the frame makes you its occupant for the turn, so everyone following scrolls with you.
|
|
58
|
+
|
|
59
|
+
## Revising
|
|
60
|
+
|
|
61
|
+
Follow-up questions often deserve a stop or a section. `write_board_file` with the guide's path
|
|
62
|
+
rewrites it in place. `update_frame` with `files` replaces the whole list, so send every entry,
|
|
63
|
+
the new one in its place. Name the guide and diagram there by their `canvas:scratch/` paths:
|
|
64
|
+
`content` would create new copies.
|
package/src/server/agents.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
SessionSnapshot,
|
|
47
47
|
} from "../shared/protocol";
|
|
48
48
|
import { fromAcp, pendingChanges, settingsOf } from "./agent-config";
|
|
49
|
+
import { SKILLS_DIR } from "./skills";
|
|
49
50
|
import { trimEvent } from "./trim-event";
|
|
50
51
|
|
|
51
52
|
interface AgentDefinition extends AgentInfo {
|
|
@@ -78,6 +79,8 @@ export function detectAgents(): ReadonlyArray<AgentDefinition> {
|
|
|
78
79
|
claudeCode: {
|
|
79
80
|
options: {
|
|
80
81
|
allowedTools: BOARD_TOOL_NAMES.map((name) => `mcp__${BOARD_SERVER_NAME}__${name}`),
|
|
82
|
+
// canvas's own skills; the host's stay available beside them.
|
|
83
|
+
plugins: [{ type: "local", path: SKILLS_DIR, skipMcpDiscovery: true }],
|
|
81
84
|
},
|
|
82
85
|
},
|
|
83
86
|
},
|
|
@@ -93,6 +96,8 @@ export function detectAgents(): ReadonlyArray<AgentDefinition> {
|
|
|
93
96
|
model: opencodeModel,
|
|
94
97
|
// Shell commands ask, so a person approves anything the agent runs.
|
|
95
98
|
permission: { bash: "ask" },
|
|
99
|
+
// canvas's own skills; the host's stay available beside them.
|
|
100
|
+
skills: { paths: [SKILLS_DIR] },
|
|
96
101
|
}),
|
|
97
102
|
},
|
|
98
103
|
},
|
package/src/server/board-mcp.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { McpServer } from "@agentclientprotocol/sdk";
|
|
|
24
24
|
import { BOARD_SERVER_NAME, BOARD_TOOLS, boardInstructions } from "../shared/board-tools";
|
|
25
25
|
import type { FileContent } from "../shared/protocol";
|
|
26
26
|
import type { Scratch } from "./scratch";
|
|
27
|
+
import type { Skill } from "./skills";
|
|
27
28
|
|
|
28
29
|
export interface BoardCall {
|
|
29
30
|
readonly callId: string;
|
|
@@ -38,6 +39,8 @@ export interface BoardMcpOptions {
|
|
|
38
39
|
readonly scratch: Pick<Scratch, "create" | "write" | "read" | "list" | "remove">;
|
|
39
40
|
/** Send a call to the host's browser; false if none is connected. */
|
|
40
41
|
readonly relay: (call: BoardCall) => boolean;
|
|
42
|
+
/** canvas's skills, named in the priming. */
|
|
43
|
+
readonly skills?: ReadonlyArray<Skill>;
|
|
41
44
|
readonly timeoutMs?: number;
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -133,7 +136,7 @@ export class BoardMcp {
|
|
|
133
136
|
: LATEST_PROTOCOL,
|
|
134
137
|
capabilities: { tools: { listChanged: false } },
|
|
135
138
|
serverInfo: { name: BOARD_SERVER_NAME, version: "0.0.0" },
|
|
136
|
-
instructions: boardInstructions(sessionId),
|
|
139
|
+
instructions: boardInstructions(sessionId, this.options.skills),
|
|
137
140
|
});
|
|
138
141
|
case "ping":
|
|
139
142
|
return reply({});
|
package/src/server/server.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { AgentManager, detectAgents } from "./agents";
|
|
|
13
13
|
import { BoardMcp } from "./board-mcp";
|
|
14
14
|
import { Files } from "./files";
|
|
15
15
|
import { Scratch } from "./scratch";
|
|
16
|
+
import { canvasSkills } from "./skills";
|
|
16
17
|
import { Store } from "./store";
|
|
17
18
|
import { Terminals } from "./terminals";
|
|
18
19
|
|
|
@@ -51,6 +52,7 @@ export async function serve(options: ServeOptions) {
|
|
|
51
52
|
host.send(JSON.stringify({ t: "board-call", ...call } satisfies ServerToClient));
|
|
52
53
|
return true;
|
|
53
54
|
},
|
|
55
|
+
skills: canvasSkills(),
|
|
54
56
|
});
|
|
55
57
|
process.on("exit", () => boardMcp.stop());
|
|
56
58
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skills canvas gives every agent session, shipped in `skills/` beside
|
|
3
|
+
* `src/` (finding 12). The folder is a Claude Code plugin (its manifest points
|
|
4
|
+
* `skills` at itself) and an opencode skills path at once; `agents.ts` hands
|
|
5
|
+
* it to each agent, which lists the skills beside the host's own.
|
|
6
|
+
*
|
|
7
|
+
* Claude drops skill descriptions from its listing once the host has many
|
|
8
|
+
* skills, so the board priming names them too (`boardInstructions`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
export const SKILLS_DIR = resolve(import.meta.dir, "../../skills");
|
|
15
|
+
|
|
16
|
+
export interface Skill {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly description: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Every skill in `dir`, from its `SKILL.md` frontmatter. */
|
|
22
|
+
export function canvasSkills(dir: string = SKILLS_DIR): ReadonlyArray<Skill> {
|
|
23
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
24
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
25
|
+
.map((entry) => {
|
|
26
|
+
const text = readFileSync(join(dir, entry.name, "SKILL.md"), "utf8");
|
|
27
|
+
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(text)?.[1] ?? "";
|
|
28
|
+
const field = (key: string) => new RegExp(`^${key}: (.+)$`, "m").exec(frontmatter)?.[1];
|
|
29
|
+
return { name: field("name") ?? entry.name, description: field("description") ?? "" };
|
|
30
|
+
})
|
|
31
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
32
|
+
}
|
|
@@ -267,7 +267,10 @@ export const BOARD_TOOLS: ReadonlyArray<{
|
|
|
267
267
|
];
|
|
268
268
|
|
|
269
269
|
/** The agent's standing context, sent as MCP server instructions. */
|
|
270
|
-
export function boardInstructions(
|
|
270
|
+
export function boardInstructions(
|
|
271
|
+
frameId: string,
|
|
272
|
+
skills: ReadonlyArray<{ readonly name: string; readonly description: string }> = [],
|
|
273
|
+
): string {
|
|
271
274
|
return `You are running inside canvas: a shared, multiplayer board that people are looking at together, live. Its frames are coding-agent sessions, files of this project, browser previews and terminals. You are the agent in frame ${frameId}; people write prompts into it and read your replies there. Several people may prompt you.
|
|
272
275
|
|
|
273
276
|
Frames that sit close together form a cluster: people keep related work together that way, and your own cluster is your workspace. Within a cluster frames sit in rows; frames in a row share a height.
|
|
@@ -284,5 +287,14 @@ The ${BOARD_SERVER_NAME} tools let you see and change the board: ${BOARD_TOOL_NA
|
|
|
284
287
|
- Scratch files hold what exists only to be shown on this board: a write-up, a diagram, an HTML visualisation. Pass the text as \`content\` (with a \`name\`) to open_frame or update_frame, or use write_board_file; canvas keeps them outside the project, as canvas:scratch/<name>. Don't write such files into the project for the board. Anything else — a temp file for your own work, a script, test data — goes wherever it would without canvas.
|
|
285
288
|
- Any agent may read (read_board_file) and overwrite (write_board_file) any scratch file; view_board lists them.
|
|
286
289
|
- To show several files for one topic, prefer one file frame with a list (files) over a frame per file: people click through it at their own pace. You decide the tree: display paths, folders, order, line ranges. A write-up (a scratch file) at the top and a visualisation at the bottom fit in the same list.
|
|
287
|
-
- Markdown and HTML files render. HTML runs its scripts, but relative links and assets (CSS, images, other scripts) don't load, so inline them
|
|
290
|
+
- Markdown and HTML files render. HTML runs its scripts, but relative links and assets (CSS, images, other scripts) don't load, so inline them.${skillsSection(skills)}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** canvas's own skills: the agent lists them, but may have dropped their descriptions. */
|
|
294
|
+
function skillsSection(
|
|
295
|
+
skills: ReadonlyArray<{ readonly name: string; readonly description: string }>,
|
|
296
|
+
): string {
|
|
297
|
+
if (skills.length === 0) return "";
|
|
298
|
+
const list = skills.map((skill) => `- ${skill.name}: ${skill.description}`).join("\n");
|
|
299
|
+
return `\n\ncanvas also gives you skills for work on the board. When one fits the request, load it with your skill tool before you start:\n${list}`;
|
|
288
300
|
}
|