import { getAppBaseUrl } from './common.ts' // ============================================================================ // Types // ============================================================================ /** Tabs on a canvas. These are real routes, so they can be linked directly. */ export type CanvasTab = 'craft' | 'test' | 'usage' | 'workstation' /** Tabs on a workflow. Also real routes. */ export type WorkflowTab = 'craft' | 'test' | 'usage' export interface WorkflowUrlOptions { tab?: WorkflowTab /** Pin the page to one version instead of the latest. */ promptVersionId?: string /** Open a specific run. */ executionId?: string /** Select a specific node — the link to send when one step is misbehaving. */ nodeId?: string } export interface TelaLink { /** What the user gets by opening it, in their terms. */ label: string url: string } // ============================================================================ // Entity links // ============================================================================ function withQuery(url: string, params: Record): string { const query = new URLSearchParams() for (const [key, value] of Object.entries(params)) { if (value) query.set(key, value) } const search = query.toString() return search ? `${url}?${search}` : url } /** * A canvas, on the tab that matches what the user is doing. * * Canvas tabs are routes, so `test` and `workstation` are directly linkable. There is no * per-test-case or per-version query parameter on this route — the tab is as deep as it goes. */ export function getCanvasTabUrl(canvasId: string, tab: CanvasTab = 'craft'): string { return `${getAppBaseUrl()}/prompt/${canvasId}/${tab}` } /** * A workflow, optionally pinned to a version, a run, or a single node. * * `nodeId` is the one to reach for when a specific step is the problem: it opens the editor with * that node selected, which is the difference between "here is your workflow" and "here is the * step that failed". */ export function getWorkflowTabUrl(promptId: string, options: WorkflowUrlOptions = {}): string { const base = `${getAppBaseUrl()}/workflows/${promptId}${options.tab ? `/${options.tab}` : ''}` return withQuery(base, { promptVersionId: options.promptVersionId, executionId: options.executionId, nodeId: options.nodeId, }) } /** * An agent. `branch` is the only deep-link parameter it accepts. * * The agent's craft/test/usage tabs are component state rather than routes, so unlike a canvas or a * workflow they cannot be linked to directly — say "open the Test tab" alongside the link. */ export function getAgentPageUrl(agentId: string, options: { branch?: string } = {}): string { return withQuery(`${getAppBaseUrl()}/agent/${agentId}`, { branch: options.branch }) } /** One agent run, with its full session transcript. */ export function getAgentSessionUrl(sessionId: string): string { return `${getAppBaseUrl()}/agent/run/${sessionId}` } /** The template gallery. */ export function getTemplatesUrl(): string { return `${getAppBaseUrl()}/templates` } // ============================================================================ // "See and debug it here" // ============================================================================ /** * The links worth handing a user for a canvas, given what they were doing. * * Always includes the craft view; adds the test view once test cases exist and the Workstation * once an application does. */ export function getCanvasLinks( canvasId: string, options: { hasTestCases?: boolean, hasWorkstation?: boolean } = {}, ): TelaLink[] { const links: TelaLink[] = [ { label: 'edit the prompt', url: getCanvasTabUrl(canvasId, 'craft') }, ] if (options.hasTestCases) links.push({ label: 'run and review test cases', url: getCanvasTabUrl(canvasId, 'test') }) if (options.hasWorkstation) links.push({ label: 'run it as tasks', url: getCanvasTabUrl(canvasId, 'workstation') }) return links } /** * The links worth handing a user for a workflow. Pass what you know: a failing node or a specific * run turns a generic link into one that lands on the problem. */ export function getWorkflowLinks( promptId: string, options: { promptVersionId?: string, executionId?: string, nodeId?: string, hasTestCases?: boolean } = {}, ): TelaLink[] { const { promptVersionId, executionId, nodeId } = options const links: TelaLink[] = [ { label: nodeId ? 'open the workflow with that step selected' : 'edit the workflow', url: getWorkflowTabUrl(promptId, { tab: 'craft', promptVersionId, nodeId }), }, ] if (executionId) { links.push({ label: 'inspect that run', url: getWorkflowTabUrl(promptId, { tab: 'test', promptVersionId, executionId }), }) } else if (options.hasTestCases) { links.push({ label: 'run and review test cases', url: getWorkflowTabUrl(promptId, { tab: 'test', promptVersionId }), }) } return links } /** * The links worth handing a user for an agent. */ export function getAgentLinks( agentId: string, options: { sessionId?: string, branch?: string } = {}, ): TelaLink[] { const links: TelaLink[] = [ { label: 'edit the agent', url: getAgentPageUrl(agentId, { branch: options.branch }) }, ] if (options.sessionId) links.push({ label: 'read that run', url: getAgentSessionUrl(options.sessionId) }) return links } /** * Render links as the closing line of an answer. * * ``` * formatLinks([{ label: 'edit the prompt', url: '…' }]) * // → 'You can see and debug it here: edit the prompt — https://…' * ``` */ export function formatLinks(links: TelaLink[], lead = 'You can see and debug it here'): string { if (!links.length) return '' if (links.length === 1) return `${lead}: ${links[0]!.url}` return [`${lead}:`, ...links.map(link => `- ${link.label}: ${link.url}`)].join('\n') }