mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
fix(chat): give project and workspace tool results their page links (#20324)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - People ask a chat agent to take them to a project or to the files of a task > - The agent finds the right project or workspace, but it cannot link the page > - The `list_projects` and `list_workspaces` tool results have no page route, and the chat link guidance names no project or workspace format > - The agent then writes a bare path or a shortened directory that the person cannot open > - This pull request adds the page `href` to these tool results and adds the project link format to the chat guidance > - The benefit is that chat replies give a clickable link to the real project or workspace page ## Linked Issues or Issue Description **What happened** In chat, a person asked "Take me to the Launch project and show its open tasks." The agent found the project. Its reasoning said "There's no project page route documented explicitly". The reply wrote the project as a bare path `/COM/projects/<id>`, which is not clickable. In a second chat, a person asked where the files of a task are. The agent named the correct workspace but gave a shortened directory and no link. **Expected behavior** The reply links the project page (`/<prefix>/projects/<id>`) and the task's workspace page (`/<prefix>/execution-workspaces/<id>`) as Markdown links, so the person can open them. **Steps to reproduce** 1. Create a company with a project and a task that has a saved execution workspace. Enable isolated workspaces. 2. In chat, ask the agent to take you to the project, or to say where the task's files are. 3. Look at the reply. It has no project or workspace link. Related: Refs #17985 (an earlier open PR that adds project and goal link wording for newly created projects only). This PR covers project discovery and workspaces and adds tool-result hrefs. ## What Changed - `list_projects` rows now carry `href: /<prefix>/projects/<id>`. Both the native runner tool authority and the MCP project-tools route pass the company prefix. - `get_workspace` and `list_workspaces` now carry `href: /<prefix>/execution-workspaces/<id>`, only while isolated workspaces are enabled. The workspace route redirects to the dashboard when the feature is off, so no href is offered then. - The chat link guidance (`AGENT_CHAT_DIRECTIVE`) adds the project link format. It tells the agent to link a returned `href`, and to name a workspace that has no `href` instead of linking it. - Regression tests: guidance text against the registered UI route, `callProjectTool` href, and an embedded-Postgres authority test for project hrefs and the workspace flag on/off. ## Verification - Red then green: the three new tests fail on `origin/master` source and pass with this change. - `npx vitest run` in `server/` for: `agent-conversations`, `task-workspace-api`, `project-tools-pagination`, `task-workspace-tools`, `chat-project-tools`, `dot-runner`, `heartbeat-list` (prompt-size limits), `task-markdown`, `muse-native-visibility`, `paperclip-runner-tool-authority`, `runner-api`, `codex-local-execute`, `board-access-denial`. All pass. - `npx tsc --noEmit -p server` passes. - Live chat eval: see the comment on this PR for the paid runner E2E run of `navigation-preferences.find-project-view` and `workspaces.locate-files` with this change. ## Risks - Low risk. The change adds one optional field to read-only tool results. The output schemas allow extra fields. - The guidance grows by two sentences. The prompt-size test in `heartbeat-list.test.ts` passes. - `list_projects` and the workspace tools now do one extra small read (company prefix, and the instance experimental settings for workspaces). ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with tool use and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f84df91da
commit
365c771030
9 files changed
+99
-9
No files matched your search
@@ -1084,6 +1084,16 @@ describe("chat prompt policy", () => {
|
||||
expect(prompt).toContain("/<prefix>/agents/<agentId>/runs/<runId>");
|
||||
});
|
||||
|
||||
it("tells chat how to link projects and to link only the workspace pages a tool result gives", () => {
|
||||
const app = readFileSync(new URL("../../../ui/src/App.tsx", import.meta.url), "utf8");
|
||||
expect(app).toMatch(/<Route\s+path="projects\/:projectId"/);
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("agent profiles, projects) uses that same prefix");
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("For a project, link /<prefix>/projects/<projectId>.");
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("When a tool result gives a record's href, such as a project or a workspace, link that href; a workspace without an href has no page on this instance");
|
||||
const prompt = buildPaperclipTaskMarkdown({ issue: { id: "chat", identifier: null, title: "Chat", workMode: "ask", conversationAgentId: "agent" } });
|
||||
expect(prompt).toContain("/<prefix>/projects/<projectId>");
|
||||
});
|
||||
|
||||
it("says the task-chat lab exists only under pnpm dev in the person's own checkout", () => {
|
||||
expect(AGENT_CHAT_DIRECTIVE).toContain("Some developer labs, including the task-chat interaction lab at /<prefix>/dev/task-chat-lab, are registered only in a development build of the UI");
|
||||
expect(AGENT_CHAT_DIRECTIVE).not.toContain("Developer labs, such as");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createLocalAgentJwt } from "../agent-auth-jwt.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { agents, issues, heartbeatRuns, projects, companyMemberships, executionWorkspaces } from "@paperclipai/db";
|
||||
import { agents, companies, issues, heartbeatRuns, projects, companyMemberships, executionWorkspaces } from "@paperclipai/db";
|
||||
import { startRunnerApiTestServer } from "./helpers/runner-api-server.js";
|
||||
import { issueService } from "../services/issues.js";
|
||||
import { documentService } from "../services/documents.js";
|
||||
@@ -20,6 +20,24 @@ import { recoveryPostgresSupport as support } from "./helpers/recovery-postgres.
|
||||
afterAll(async () => { await server?.close(); if (originalSecret === undefined) delete process.env.PAPERCLIP_AGENT_JWT_SECRET; else process.env.PAPERCLIP_AGENT_JWT_SECRET = originalSecret; });
|
||||
const call = (fixture: Awaited<ReturnType<typeof server.fixture>>, tool: string, args: Record<string, unknown>) => fixture.authority.execute({ tool, arguments: args, callId: randomUUID() });
|
||||
|
||||
it("gives MCP list_projects rows the company-scoped project page href", async () => {
|
||||
const f = await server.fixture({ conversation: true, disableWakeOnDemand: true });
|
||||
const [{ issuePrefix }] = await server.db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, f.companyId));
|
||||
const token = createLocalAgentJwt(f.agentId, f.companyId, "paperclip_runner", f.runId, f.responsibleUserId)!;
|
||||
const originalApiUrl = process.env.PAPERCLIP_API_URL;
|
||||
process.env.PAPERCLIP_API_URL = server.apiUrl;
|
||||
try {
|
||||
const response = await fetch(`${server.apiUrl}/api/mcp/project-tools`, { method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "list_projects", arguments: {} } }) });
|
||||
const { result } = await response.json() as { result: { isError?: boolean; structuredContent?: { projects: Array<{ id: string; href?: string }> } } };
|
||||
expect(result.isError).toBeUndefined();
|
||||
expect(result.structuredContent?.projects.find(row => row.id === f.projectId)?.href).toBe(`/${issuePrefix}/projects/${f.projectId}`);
|
||||
} finally {
|
||||
if (originalApiUrl === undefined) delete process.env.PAPERCLIP_API_URL; else process.env.PAPERCLIP_API_URL = originalApiUrl;
|
||||
}
|
||||
});
|
||||
|
||||
it("pages only visible projects and prefilters low-trust agent and run scopes", async () => {
|
||||
const f = await server.fixture({ disableWakeOnDemand: true });
|
||||
const ids = Array.from({ length: 53 }, (_, i) => `abcdefab-0000-4000-8000-${String(i).padStart(12, "0")}`);
|
||||
|
||||
@@ -21,6 +21,13 @@ describe("project discovery result bounds", () => {
|
||||
.toMatchObject({ properties: { cursor: expect.any(Object), limit: expect.any(Object) } });
|
||||
});
|
||||
|
||||
it("adds each project's company-scoped page href when the company prefix is known", async () => {
|
||||
const page = { projects: [{ id: "abcdefab-0000-4000-8000-000000000001", name: "Launch", status: "backlog" }], nextCursor: null };
|
||||
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => page })));
|
||||
expect(await callProjectTool({ ...input, companyPrefix: "COM" })).toEqual({ ...page,
|
||||
projects: [{ ...page.projects[0], href: "/COM/projects/abcdefab-0000-4000-8000-000000000001" }] });
|
||||
});
|
||||
|
||||
it.each([{ limit: 0 }, { limit: 51 }, { cursor: "invalid" }])("rejects invalid paging arguments %j before fetching", async (arguments_) => {
|
||||
const fetch = vi.fn();
|
||||
vi.stubGlobal("fetch", fetch);
|
||||
|
||||
@@ -3,11 +3,12 @@ import { mkdtemp } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { agents, authUsers, companyMemberships, executionWorkspaceRepositories, executionWorkspaces, issues, heartbeatRuns, projects, projectWorkspaces } from "@paperclipai/db";
|
||||
import { agents, authUsers, companies, companyMemberships, executionWorkspaceRepositories, executionWorkspaces, issues, heartbeatRuns, projects, projectWorkspaces } from "@paperclipai/db";
|
||||
import { createLocalAgentJwt } from "../agent-auth-jwt.js";
|
||||
import { startRunnerApiTestServer } from "./helpers/runner-api-server.js";
|
||||
import { getEmbeddedPostgresTestSupport } from "./helpers/embedded-postgres.js";
|
||||
import { PaperclipRunnerToolAuthority } from "../services/native-runtime/paperclip-runner-tool-authority.js";
|
||||
import { instanceSettingsService } from "../services/instance-settings.js";
|
||||
|
||||
const support = await getEmbeddedPostgresTestSupport();
|
||||
(support.supported ? describe : describe.skip)("task workspace API and native semantic authority", () => {
|
||||
@@ -261,4 +262,25 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
expect((await server.db.select().from(executionWorkspaces).where(eq(executionWorkspaces.id, unrelated.id)))[0]?.cwd).toBe(f.workspace);
|
||||
expect((await server.db.select().from(issues).where(eq(issues.id, f.issueId)))[0]?.projectId).toBeNull();
|
||||
});
|
||||
it("returns real page hrefs for projects and, only while their pages are routed, for workspaces", async () => {
|
||||
const f = await server.fixture({ conversation: true, disableWakeOnDemand: true });
|
||||
const [{ issuePrefix }] = await server.db.select({ issuePrefix: companies.issuePrefix }).from(companies).where(eq(companies.id, f.companyId));
|
||||
const [workspace] = await server.db.insert(executionWorkspaces).values({ companyId: f.companyId, sourceIssueId: f.issueId, projectId: null,
|
||||
name: "Website files", cwd: f.workspace, mode: "shared_workspace", strategyType: "task_directory" }).returning();
|
||||
await server.db.update(issues).set({ projectId: null, projectWorkspaceId: null, executionWorkspaceId: workspace.id }).where(eq(issues.id, f.issueId));
|
||||
const call = (tool: string) => f.authority.execute({ tool, arguments: {}, callId: randomUUID() }) as Promise<any>;
|
||||
const projects = (await call("list_projects")).projects as Array<{ id: string; href?: string }>;
|
||||
expect(projects.find(row => row.id === f.projectId)?.href).toBe(`/${issuePrefix}/projects/${f.projectId}`);
|
||||
const settings = instanceSettingsService(server.db);
|
||||
const workspaceHrefs = async () => [(await call("get_workspace")).workspace?.href,
|
||||
((await call("list_workspaces")).workspaces as Array<{ id: string; href?: string }>).find(row => row.id === workspace.id)?.href];
|
||||
try {
|
||||
await settings.updateExperimental({ enableIsolatedWorkspaces: false });
|
||||
// The detail route redirects to the dashboard while the feature is off, so no href is offered.
|
||||
expect(await workspaceHrefs()).toEqual([undefined, undefined]);
|
||||
await settings.updateExperimental({ enableIsolatedWorkspaces: true });
|
||||
const href = `/${issuePrefix}/execution-workspaces/${workspace.id}`;
|
||||
expect(await workspaceHrefs()).toEqual([href, href]);
|
||||
} finally { await settings.updateExperimental({ enableIsolatedWorkspaces: false }); }
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { companies, type Db } from "@paperclipai/db";
|
||||
import { projectToolContext } from "../services/project-tool-context.js";
|
||||
import { callProjectTool, projectToolDefinitions } from "../services/project-tools.js";
|
||||
import { assertCompanyAccess } from "./authz.js";
|
||||
@@ -27,6 +28,8 @@ export function projectToolRoutes(db: Db) {
|
||||
token: req.header("authorization")!.replace(/^Bearer\s+/i, ""),
|
||||
companyId: context.run.companyId, issueId: context.issue.id, agentId: context.run.agentId,
|
||||
conversation: Boolean(context.issue.conversationAgentId),
|
||||
companyPrefix: params.name === "list_projects" ? (await db.select({ issuePrefix: companies.issuePrefix }).from(companies)
|
||||
.where(eq(companies.id, context.run.companyId)))[0]?.issuePrefix : null,
|
||||
});
|
||||
return send({ content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result });
|
||||
} catch (error) {
|
||||
|
||||
@@ -116,7 +116,7 @@ Create ordinary assigned tasks; this conversation is never a parent. Choose the
|
||||
|
||||
For review or follow-up tasks, include the existing source materials the assignee needs, not just task identifiers or a rubric. Read the source task's saved documents and comments, then copy the relevant existing content into the handoff or attach resources the recipient can actually read. A new task's tools may only expose its own documents and history. Supplying existing source material is coordination, not creating the assigned deliverable.
|
||||
|
||||
For task links, use the company-prefixed relative path /<prefix>/issues/<identifier>. The company prefix is the part of a task identifier before its final hyphen and number: for task RUN-1 the prefix is RUN. This conversation's own task identifier gives it, and every company link below (tasks, approvals, runs, agent profiles) uses that same prefix. Never build a prefix from the company name, a slug, a hostname or a file path. Write every company link as a Markdown link, such as [Review the approval](/<prefix>/approvals/<approvalId>): a bare path or a path in inline code is not clickable in this conversation. Start each link target with exactly one slash: a target beginning with //, such as //<prefix>/issues/<identifier>, points to a different website. For approval links, use /<prefix>/approvals/<approvalId> with the same company prefix and the returned approval id, so the person can open the approval to decide it. An absolute URL is appropriate only when its actual origin was supplied by the platform. Never invent a hostname for a Paperclip link. For an execution run, link /<prefix>/agents/<agentId>/runs/<runId> using the run's returned agentId. Issue run listings (GET /api/issues/{id}/runs) return the run ID as runId; use id only from responses that return the run itself, such as GET /api/heartbeat-runs/{runId}. Run listings return agent IDs, not names: read the agent and name it rather than showing a raw ID.
|
||||
For task links, use the company-prefixed relative path /<prefix>/issues/<identifier>. The company prefix is the part of a task identifier before its final hyphen and number: for task RUN-1 the prefix is RUN. This conversation's own task identifier gives it, and every company link below (tasks, approvals, runs, agent profiles, projects) uses that same prefix. Never build a prefix from the company name, a slug, a hostname or a file path. Write every company link as a Markdown link, such as [Review the approval](/<prefix>/approvals/<approvalId>): a bare path or a path in inline code is not clickable in this conversation. Start each link target with exactly one slash: a target beginning with //, such as //<prefix>/issues/<identifier>, points to a different website. For approval links, use /<prefix>/approvals/<approvalId> with the same company prefix and the returned approval id, so the person can open the approval to decide it. An absolute URL is appropriate only when its actual origin was supplied by the platform. Never invent a hostname for a Paperclip link. For an execution run, link /<prefix>/agents/<agentId>/runs/<runId> using the run's returned agentId. Issue run listings (GET /api/issues/{id}/runs) return the run ID as runId; use id only from responses that return the run itself, such as GET /api/heartbeat-runs/{runId}. Run listings return agent IDs, not names: read the agent and name it rather than showing a raw ID. For a project, link /<prefix>/projects/<projectId>. When a tool result gives a record's href, such as a project or a workspace, link that href; a workspace without an href has no page on this instance, so name it instead of linking it.
|
||||
|
||||
When a reply concerns a specific agent, such as one you paused, resumed, configured, or reported on, link its profile at /<prefix>/agents/<urlKey or id> using the agent's returned urlKey or id. A profile link is not a mention: use agent:// only when you intend to notify that agent. When you send a person to a control on an agent's profile, link the page that holds it instead of the bare profile: /<prefix>/agents/<urlKey or id>/api-keys for API keys, /identity for identity and instructions, /runtime for harness settings, /secrets for secrets and variables, /permissions for permissions. When you cannot hire an agent yourself, send the person to the hire form at /<prefix>/agents/new, where they can hire it directly, and to your own /permissions page, where they can turn on hiring for you. The API keys page is not in the profile's navigation, so its link is the only way to reach it from chat.
|
||||
|
||||
|
||||
@@ -851,7 +851,8 @@ export class PaperclipRunnerToolAuthority {
|
||||
const apiUrl = this.binding.apiUrl ?? process.env.PAPERCLIP_API_URL;
|
||||
const token = this.#apiToken(context);
|
||||
if (!apiUrl || !token) throw new Error("Workspace tool authentication is unavailable");
|
||||
return callTaskWorkspaceTool({ name: call.tool, arguments: input, apiUrl, token, companyId: this.binding.companyId, issueId: this.binding.issueId, workspaceRoot: this.binding.workspaceRoot });
|
||||
return callTaskWorkspaceTool({ name: call.tool, arguments: input, apiUrl, token, companyId: this.binding.companyId, issueId: this.binding.issueId, workspaceRoot: this.binding.workspaceRoot,
|
||||
workspacePagePrefix: ["get_workspace", "list_workspaces"].includes(call.tool) ? await this.#workspacePagePrefix() : null });
|
||||
}
|
||||
case "create_project":
|
||||
case "list_project_repositories":
|
||||
@@ -861,7 +862,8 @@ export class PaperclipRunnerToolAuthority {
|
||||
if (!apiUrl || !token) throw new Error("Project tool authentication is unavailable");
|
||||
return callProjectTool({ name: call.tool, arguments: input, apiUrl, token,
|
||||
companyId: this.binding.companyId, issueId: this.binding.issueId, agentId: this.binding.agentId,
|
||||
conversation: Boolean(context.issue.conversationAgentId) });
|
||||
conversation: Boolean(context.issue.conversationAgentId),
|
||||
companyPrefix: call.tool === "list_projects" ? await this.#companyPagePrefix() : null });
|
||||
}
|
||||
case "search_api": return searchRunnerApi(call.arguments);
|
||||
case "hire_agent": {
|
||||
@@ -1079,6 +1081,19 @@ export class PaperclipRunnerToolAuthority {
|
||||
issueId: this.binding.issueId, workMode: context.issue.workMode });
|
||||
}
|
||||
|
||||
/** Company prefix for page hrefs in tool results, so chat replies link real pages. */
|
||||
async #companyPagePrefix() {
|
||||
const [row] = await this.db.select({ issuePrefix: companies.issuePrefix }).from(companies)
|
||||
.where(eq(companies.id, this.binding.companyId));
|
||||
return row?.issuePrefix ?? null;
|
||||
}
|
||||
|
||||
/** Workspace pages are routed only while isolated workspaces are enabled. */
|
||||
async #workspacePagePrefix() {
|
||||
return (await instanceSettingsService(this.db).getExperimental()).enableIsolatedWorkspaces
|
||||
? this.#companyPagePrefix() : null;
|
||||
}
|
||||
|
||||
#apiToken(context: {
|
||||
actor: { adapterType: string };
|
||||
issue: { workMode: string };
|
||||
|
||||
@@ -19,6 +19,8 @@ export function projectToolDefinitions(workMode: string, includeTask = false) {
|
||||
export async function callProjectTool(input: {
|
||||
name: string; arguments: Record<string, unknown>; apiUrl: string; token: string;
|
||||
companyId: string; issueId: string; agentId: string; conversation: boolean; receiptOnly?: boolean;
|
||||
/** The company's issue prefix; list_projects rows then carry their page href. */
|
||||
companyPrefix?: string | null;
|
||||
}) {
|
||||
if (input.receiptOnly && input.name !== "create_project") throw badRequest("Receipt recovery is only supported for project creation");
|
||||
const args = input.arguments;
|
||||
@@ -71,5 +73,11 @@ export async function callProjectTool(input: {
|
||||
if (input.receiptOnly && response.status === 204) return undefined;
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `Project tool failed (${response.status})`);
|
||||
// Chat replies link a project only when they know its real page; without an
|
||||
// href the agent wrote the route as an unclickable path or omitted it.
|
||||
if (page && input.companyPrefix && Array.isArray(result.projects)) {
|
||||
return { ...result, projects: result.projects.map((project: { id: string }) => ({ ...project,
|
||||
href: `/${encodeURIComponent(input.companyPrefix!)}/projects/${encodeURIComponent(project.id)}` })) };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -31,6 +31,12 @@ function repositoryView(repo: Record<string, unknown>) {
|
||||
state: repo.state, pinnedCommit: repo.pinnedCommit, branchName: repo.branchName };
|
||||
}
|
||||
|
||||
/** The workspace detail page exists only while isolated workspaces are enabled,
|
||||
* so callers pass the company prefix only then; a missing href means no page. */
|
||||
function workspaceHref(prefix: string | null | undefined, id: unknown) {
|
||||
return prefix && typeof id === "string" ? { href: `/${encodeURIComponent(prefix)}/execution-workspaces/${encodeURIComponent(id)}` } : {};
|
||||
}
|
||||
|
||||
function workspacePage(rows: Array<Record<string, unknown>>, limit: number) {
|
||||
const workspaces: Array<Record<string, unknown>> = [];
|
||||
const result = () => ({ workspaces, limit, returnedCount: workspaces.length,
|
||||
@@ -57,7 +63,7 @@ function workspacePage(rows: Array<Record<string, unknown>>, limit: number) {
|
||||
return result();
|
||||
}
|
||||
|
||||
export async function callTaskWorkspaceTool(input: { name: string; arguments: Record<string, unknown>; apiUrl: string; token: string; companyId: string; issueId: string; workspaceRoot?: string }) {
|
||||
export async function callTaskWorkspaceTool(input: { name: string; arguments: Record<string, unknown>; apiUrl: string; token: string; companyId: string; issueId: string; workspaceRoot?: string; workspacePagePrefix?: string | null }) {
|
||||
let route = `/issues/${input.issueId}/workspace`;
|
||||
let method = "GET";
|
||||
let body: unknown;
|
||||
@@ -78,7 +84,7 @@ export async function callTaskWorkspaceTool(input: { name: string; arguments: Re
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `Workspace tool failed (${response.status})`);
|
||||
if (page) {
|
||||
return workspacePage(result, page.limit);
|
||||
return workspacePage(result.map((row: Record<string, unknown>) => ({ ...row, ...workspaceHref(input.workspacePagePrefix, row.id) })), page.limit);
|
||||
}
|
||||
if (input.name === "prepare_repository") {
|
||||
const { operationId, ...rest } = result;
|
||||
@@ -90,7 +96,8 @@ export async function callTaskWorkspaceTool(input: { name: string; arguments: Re
|
||||
const workspace = result.workspace;
|
||||
return { issueId: result.issueId, bindingRevision: result.bindingRevision, selection: result.selection,
|
||||
pendingSelection: result.pendingSelection, cwd: input.workspaceRoot ?? null,
|
||||
workspace: workspace ? { id: workspace.id, projectId: workspace.projectId, mode: workspace.mode, status: workspace.status } : null,
|
||||
workspace: workspace ? { id: workspace.id, projectId: workspace.projectId, mode: workspace.mode, status: workspace.status,
|
||||
...workspaceHref(input.workspacePagePrefix, workspace.id) } : null,
|
||||
repositories: (result.repositories ?? []).map(repositoryView),
|
||||
capabilities: result.capabilities, fileLocations: result.fileLocations };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user