feat(agent): generate Skill drafts from conversations and canvases

This commit is contained in:
yu
2026-08-05 02:45:59 +08:00
parent 08deda5b3f
commit f01cb889ef
7 changed files with 948 additions and 29 deletions
+60 -1
View File
@@ -23,6 +23,25 @@ test("MCP 读取当前激活网页的画布", async (t) => {
assert.equal(field(await session.callTool("canvas_get_state", {}), "projectId"), "canvas-second");
});
test("按精确 clientId 读取画布快照,不受当前焦点影响", (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.updateState(snapshot("canvas-first"), "first");
session.updateState(snapshot("canvas-second"), "second");
session.activateClient("second");
assert.equal(field(session.canvasStateForClient("first"), "projectId"), "canvas-first");
assert.equal(field(session.canvasStateForClient("second"), "projectId"), "canvas-second");
assert.equal(session.canvasStateForClient("missing"), null);
first.close();
assert.equal(session.canvasStateForClient("first"), null);
});
test("画布写操作只发送给当前激活网页", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
@@ -202,7 +221,7 @@ test("new clients receive the current Codex state and later updates", (t) => {
t.after(() => client.close());
const hello = client.event("hello");
assert.equal(field(hello, "protocolVersion"), 4);
assert.equal(field(hello, "protocolVersion"), 5);
assert.deepEqual(field(hello, "workspace"), { activeThreadId: "thread-2" });
assert.deepEqual(field(hello, "codex"), { busy: true, threadId: "thread-2", turnId: "turn-1" });
assert.deepEqual(field(hello, "pendingApprovals"), [{ requestId: "approval-1", threadId: "thread-2" }]);
@@ -228,6 +247,46 @@ test("Codex 写操作在多窗口之间互斥且不能与运行 turn 并发", ()
assert.equal(session.beginCodexMutation(), false);
});
test("Skill draft generation broadcasts shared busy state and restores the previous thread", (t) => {
const session = new CanvasSession();
const first = connect(session, "first");
const second = connect(session, "second");
t.after(() => {
first.close();
second.close();
});
session.setCodexState({ threadId: "thread-1", turnId: "turn-previous" });
const previous = session.codexStateSnapshot;
assert.equal(session.beginCodexMutation(), true);
session.setCodexState({ busy: true, threadId: previous.threadId, turnId: "" }, { preserveReplay: true });
assert.deepEqual(first.events("codex_state").at(-1), { busy: true, threadId: "thread-1", turnId: "" });
assert.deepEqual(second.events("codex_state").at(-1), { busy: true, threadId: "thread-1", turnId: "" });
assert.equal(session.beginCodexMutation(), false);
session.setCodexState(previous, { preserveReplay: true });
session.endCodexMutation();
assert.deepEqual(first.events("codex_state").at(-1), previous);
assert.deepEqual(second.events("codex_state").at(-1), previous);
assert.equal(session.beginCodexMutation(), true);
session.endCodexMutation();
});
test("Skill draft busy state preserves the previous turn replay until history acknowledges it", (t) => {
const session = new CanvasSession();
session.setCodexState({ busy: true, threadId: "thread-1", turnId: "turn-1" });
session.emitThread("agent_event", "thread-1", { turnId: "turn-1", type: "item.updated", item: { id: "assistant-1", type: "agent_message", text: "回答" } });
session.setCodexState({ busy: false });
const previous = session.codexStateSnapshot;
session.setCodexState({ busy: true, threadId: "thread-1", turnId: "" }, { preserveReplay: true });
session.setCodexState(previous, { preserveReplay: true });
const client = connect(session, "first", "thread-1");
t.after(() => client.close());
assert.equal(client.events("agent_event").length, 1);
});
test("a bound client remains the tool target while focus changes", async (t) => {
const session = new CanvasSession();
const first = connect(session, "first");