From 8b9533c22a47bbf6f916667e2c25d8e8e419da37 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 1 Jun 2026 01:46:13 +0900 Subject: feat(tabs): tab-to-tab agent communication via short handles Add send_to_tab / read_tab tools so an agent can message or read another tab by a git-style short handle (shortest unique prefix of the tab UUID, min 4 chars), shown in the tab bar. - core/db/tabs: resolveTabPrefix + shortestUniquePrefix (open tabs only, LIKE-sanitized prefix matching) - new tools read-tab.ts / send-to-tab.ts (+ tests) decoupled from the DB TabRow via a minimal ResolvedTabRef projection - agent-manager: unified deliverMessage routing (busy -> queue, idle -> new turn) shared by POST /chat and send_to_tab; agent->agent auto-wake budget (MAX_AGENT_AUTO_WAKES) to bound ping-pong loops - summon/loader: send_to_tab + read_tab as grantable tools - frontend: shortHandleFor + handle badge in TabBar; perm toggles - notes: tab-comm / user-agents / todo-redesign plans - chore: biome format fixes (debug-logger, summon.test) Refs notes/plan-tab-comm.md --- packages/api/src/app.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) (limited to 'packages/api/src/app.ts') diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index 73d3de5..19cc193 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -56,28 +56,33 @@ app.post("/chat", async (c) => { return c.json({ error: "message must be a non-empty string" }, 400); } - if (agentManager.getTabStatus(tabId) === "running") { - const queueId = typeof body.queueId === "string" ? body.queueId : undefined; - const { messageId } = agentManager.queueMessage(tabId, message, queueId); - return c.json({ status: "queued", messageId }); - } - const keyId = typeof body.keyId === "string" ? body.keyId : undefined; const modelId = typeof body.modelId === "string" ? body.modelId : undefined; const agentModels = Array.isArray(body.agentModels) ? body.agentModels : undefined; const workingDirectory = typeof body.workingDirectory === "string" ? body.workingDirectory : undefined; + const queueId = typeof body.queueId === "string" ? body.queueId : undefined; const validEfforts = ["none", "low", "medium", "high", "max"]; const reasoningEffort = typeof body.reasoningEffort === "string" && validEfforts.includes(body.reasoningEffort) ? (body.reasoningEffort as "none" | "low" | "medium" | "high" | "max") : undefined; - // Non-blocking — let the agent run in the background - agentManager - .processMessage(tabId, message, keyId, modelId, reasoningEffort, workingDirectory, agentModels) - .catch(console.error); + // Single routing decision (queue if busy, new turn if idle) shared with the + // `send_to_tab` tool via `AgentManager.deliverMessage`. Non-blocking — a + // started turn runs in the background. + const outcome = agentManager.deliverMessage(tabId, message, { + ...(keyId ? { keyId } : {}), + ...(modelId ? { modelId } : {}), + ...(agentModels ? { agentModels } : {}), + ...(reasoningEffort ? { reasoningEffort } : {}), + ...(workingDirectory !== undefined ? { workingDirectory } : {}), + ...(queueId ? { queueId } : {}), + }); + if (outcome.status === "queued") { + return c.json({ status: "queued", messageId: outcome.messageId }); + } return c.json({ status: "ok" }); }); -- cgit v1.2.3