From 8e05565e841005d13862d7a90ee1f7a16d9acaea Mon Sep 17 00:00:00 2001 From: Gab Date: Fri, 27 Mar 2026 09:11:17 +1100 Subject: [PATCH] feat: new integration --- packages/tf-sync/src/tf_sync/config.py | 1 + packages/tf-sync/src/tf_sync/tools.py | 50 ++++++++++++++++++- packages/tfcode/src/agent/agent.ts | 31 ++++++++++++ packages/tfcode/src/cli/cmd/tools.ts | 48 ++++++++++++++++-- .../cmd/tui/component/prompt/autocomplete.tsx | 44 +++++++++++++++- .../tfcode/src/cli/cmd/tui/context/sync.tsx | 23 +++++++++ packages/tfcode/src/session/llm.ts | 2 +- 7 files changed, 192 insertions(+), 7 deletions(-) diff --git a/packages/tf-sync/src/tf_sync/config.py b/packages/tf-sync/src/tf_sync/config.py index 4b3214663..fc4a384b6 100644 --- a/packages/tf-sync/src/tf_sync/config.py +++ b/packages/tf-sync/src/tf_sync/config.py @@ -25,6 +25,7 @@ class ToolType(str, Enum): CODER_AGENT = "coder_agent" DATABASE_SCRIPT = "database_script" API_FUNCTION = "api_function" + PROMPT = "prompt" class FunctionRequestType(str, Enum): diff --git a/packages/tf-sync/src/tf_sync/tools.py b/packages/tf-sync/src/tf_sync/tools.py index dfbf9f7f2..7e14c5ecc 100644 --- a/packages/tf-sync/src/tf_sync/tools.py +++ b/packages/tf-sync/src/tf_sync/tools.py @@ -6,9 +6,10 @@ SDK Structure: - agent_functions: API Functions (with request_type) - connections: Provider connections (openai, anthropic, etc.) - agents: TF workspace agents +- prompts: Prompt templates (with available_to_agents mapping) """ -from typing import Any, Optional +from typing import Any, Optional, List from pydantic import BaseModel from toothfairyai.types import AgentFunction @@ -45,11 +46,23 @@ class SyncedTool(BaseModel): llm_provider: Optional[str] = None +class SyncedPrompt(BaseModel): + """A prompt template synced from ToothFairyAI workspace.""" + + id: str + label: str + interpolation_string: str + prompt_type: Optional[str] = None + available_to_agents: Optional[List[str]] = None + description: Optional[str] = None + + class ToolSyncResult(BaseModel): """Result of tool sync operation.""" success: bool tools: list[SyncedTool] = [] + prompts: list[SyncedPrompt] = [] by_type: dict[str, int] = {} error: Optional[str] = None @@ -149,6 +162,26 @@ def parse_agent(agent) -> SyncedTool: ) +def parse_prompt(prompt) -> SyncedPrompt: + """ + Parse Prompt from SDK into SyncedPrompt. + + Args: + prompt: Prompt from TF SDK + + Returns: + SyncedPrompt instance + """ + return SyncedPrompt( + id=prompt.id, + label=prompt.label, + interpolation_string=prompt.interpolation_string, + prompt_type=getattr(prompt, 'prompt_type', None), + available_to_agents=getattr(prompt, 'available_to_agents', None), + description=getattr(prompt, 'description', None), + ) + + def sync_tools(config: TFConfig) -> ToolSyncResult: """ Sync all tools from ToothFairyAI workspace using SDK. @@ -157,12 +190,13 @@ def sync_tools(config: TFConfig) -> ToolSyncResult: - Agent Functions (API Functions with request_type) - Agent Skills (functions with is_agent_skill=True) - Coder Agents (agents with mode='coder') + - Prompts (prompt templates with available_to_agents mapping) Args: config: TFConfig instance Returns: - ToolSyncResult with synced tools + ToolSyncResult with synced tools and prompts """ try: client = config.get_client() @@ -180,14 +214,26 @@ def sync_tools(config: TFConfig) -> ToolSyncResult: except Exception as e: pass + # Sync prompts + prompts = [] + try: + prompts_result = client.prompts.list() + prompts = [parse_prompt(p) for p in prompts_result.items] + except Exception as e: + pass + by_type = {} for tool in tools: type_name = tool.tool_type.value by_type[type_name] = by_type.get(type_name, 0) + 1 + if prompts: + by_type['prompt'] = len(prompts) + return ToolSyncResult( success=True, tools=tools, + prompts=prompts, by_type=by_type, ) diff --git a/packages/tfcode/src/agent/agent.ts b/packages/tfcode/src/agent/agent.ts index 3d2b93d46..906e6469b 100644 --- a/packages/tfcode/src/agent/agent.ts +++ b/packages/tfcode/src/agent/agent.ts @@ -306,6 +306,37 @@ export namespace Agent { } } + export interface TFPrompt { + id: string + label: string + interpolation_string: string + available_to_agents?: string[] + } + + async function loadTFPrompts(): Promise { + const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json") + try { + const content = await Bun.file(toolsPath).text() + const data = JSON.parse(content) + if (!data.success || !data.prompts) return [] + return data.prompts as TFPrompt[] + } catch { + return [] + } + } + + export async function getPromptForAgent(agentId: string): Promise { + const prompts = await loadTFPrompts() + return prompts.find((p) => p.available_to_agents?.includes(agentId)) ?? null + } + + export async function getPromptForAgentName(agentName: string): Promise { + const agents = await loadTFCoderAgents() + const agent = agents.find((a) => a.name === agentName) + if (!agent?.options?.tf_agent_id) return null + return getPromptForAgent(agent.options.tf_agent_id) + } + export async function list() { const cfg = await Config.get() const localAgents = await state() diff --git a/packages/tfcode/src/cli/cmd/tools.ts b/packages/tfcode/src/cli/cmd/tools.ts index 7f3aeaf6a..2b35ff475 100644 --- a/packages/tfcode/src/cli/cmd/tools.ts +++ b/packages/tfcode/src/cli/cmd/tools.ts @@ -14,7 +14,7 @@ const printError = (msg: string) => UI.error(msg) const success = (msg: string) => UI.println(UI.Style.TEXT_SUCCESS_BOLD + msg + UI.Style.TEXT_NORMAL) const info = (msg: string) => UI.println(UI.Style.TEXT_NORMAL + msg) -type ToolType = "mcp_server" | "agent_skill" | "database_script" | "api_function" +type ToolType = "mcp_server" | "agent_skill" | "database_script" | "api_function" | "coder_agent" | "prompt" interface SyncedTool { id: string @@ -28,11 +28,27 @@ interface SyncedTool { url?: string tools: string[] auth_via: string + interpolation_string?: string + goals?: string + temperature?: number + max_tokens?: number + llm_base_model?: string + llm_provider?: string +} + +interface SyncedPrompt { + id: string + label: string + interpolation_string: string + prompt_type?: string + available_to_agents?: string[] + description?: string } interface ToolSyncResult { success: boolean tools: SyncedTool[] + prompts: SyncedPrompt[] by_type: Record error?: string } @@ -101,12 +117,30 @@ try: "request_type": tool.request_type.value if tool.request_type else None, "url": tool.url, "tools": tool.tools, - "auth_via": tool.auth_via + "auth_via": tool.auth_via, + "interpolation_string": tool.interpolation_string, + "goals": tool.goals, + "temperature": tool.temperature, + "max_tokens": tool.max_tokens, + "llm_base_model": tool.llm_base_model, + "llm_provider": tool.llm_provider + }) + + prompts_data = [] + for prompt in result.prompts: + prompts_data.append({ + "id": prompt.id, + "label": prompt.label, + "interpolation_string": prompt.interpolation_string, + "prompt_type": prompt.prompt_type, + "available_to_agents": prompt.available_to_agents, + "description": prompt.description }) print(json.dumps({ "success": result.success, "tools": tools_data, + "prompts": prompts_data, "by_type": result.by_type, "error": result.error })) @@ -138,7 +172,13 @@ try: "request_type": tool.request_type.value if tool.request_type else None, "url": tool.url, "tools": tool.tools, - "auth_via": tool.auth_via + "auth_via": tool.auth_via, + "interpolation_string": tool.interpolation_string, + "goals": tool.goals, + "temperature": tool.temperature, + "max_tokens": tool.max_tokens, + "llm_base_model": tool.llm_base_model, + "llm_provider": tool.llm_provider }) print(json.dumps({ @@ -353,6 +393,8 @@ const ToolsListCommand = cmd({ agent_skill: "Skill", database_script: "DB", api_function: "API", + coder_agent: "Coder Agent", + prompt: "Prompt", }[tool.tool_type] info(` ${tool.name}`) diff --git a/packages/tfcode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/tfcode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 3240afab3..e482ff355 100644 --- a/packages/tfcode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/tfcode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -13,6 +13,8 @@ import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "@/util/locale" import type { PromptInfo } from "./history" import { useFrecency } from "./frecency" +import { Agent } from "@/agent/agent" +import { useLocal } from "@tui/context/local" function removeLineRange(input: string) { const hashIndex = input.lastIndexOf("#") @@ -77,6 +79,7 @@ export function Autocomplete(props: { }) { const sdk = useSDK() const sync = useSync() + const local = useLocal() const command = useCommandDialog() const { theme } = useTheme() const dimensions = useTerminalDimensions() @@ -353,6 +356,42 @@ export function Autocomplete(props: { ) }) + const tfPrompts = createMemo(() => { + if (!store.visible || store.visible === "/") return [] + + const currentAgent = local.agent.current() + const agentId = currentAgent.options?.tf_agent_id as string | undefined + if (!agentId) return [] + + const options: AutocompleteOption[] = [] + const width = props.anchor().width - 4 + + const prompts = sync.data.prompts || [] + + for (const prompt of prompts) { + const isAvailable = + !prompt.available_to_agents || + prompt.available_to_agents.length === 0 || + prompt.available_to_agents.includes(agentId) + + if (isAvailable) { + options.push({ + display: Locale.truncateMiddle("@" + prompt.label, width), + value: prompt.label, + description: "Prompt template", + onSelect: () => { + const cursor = props.input().logicalCursor + props.input().deleteRange(0, 0, cursor.row, cursor.col) + props.input().insertText(prompt.interpolation_string) + props.input().cursorOffset = Bun.stringWidth(prompt.interpolation_string) + }, + }) + } + } + + return options + }) + const commands = createMemo((): AutocompleteOption[] => { const results: AutocompleteOption[] = [...command.slashes()] @@ -386,9 +425,12 @@ export function Autocomplete(props: { const filesValue = files() const agentsValue = agents() const commandsValue = commands() + const promptsValue = tfPrompts() const mixed: AutocompleteOption[] = - store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue] + store.visible === "@" + ? [...agentsValue, ...promptsValue, ...(filesValue || []), ...mcpResources()] + : [...commandsValue] const searchValue = search() diff --git a/packages/tfcode/src/cli/cmd/tui/context/sync.tsx b/packages/tfcode/src/cli/cmd/tui/context/sync.tsx index 3b296a927..885307b8a 100644 --- a/packages/tfcode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/tfcode/src/cli/cmd/tui/context/sync.tsx @@ -29,6 +29,8 @@ import { batch, onMount } from "solid-js" import { Log } from "@/util/log" import type { Path } from "@opencode-ai/sdk" import type { Workspace } from "@opencode-ai/sdk/v2" +import path from "path" +import os from "os" export const { use: useSync, provider: SyncProvider } = createSimpleContext({ name: "Sync", @@ -75,6 +77,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ vcs: VcsInfo | undefined path: Path workspaceList: Workspace[] + prompts: Array<{ + id: string + label: string + interpolation_string: string + available_to_agents?: string[] + }> }>({ provider_next: { all: [], @@ -103,6 +111,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ vcs: undefined, path: { state: "", config: "", worktree: "", directory: "" }, workspaceList: [], + prompts: [], }) const sdk = useSDK() @@ -113,6 +122,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ setStore("workspaceList", reconcile(result.data)) } + async function loadTFPrompts() { + const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json") + try { + const content = await Bun.file(toolsPath).text() + const data = JSON.parse(content) + if (data.success && data.prompts) { + setStore("prompts", reconcile(data.prompts)) + } + } catch { + // File doesn't exist or is invalid, that's OK + } + } + sdk.event.listen((e) => { const event = e.details switch (event.type) { @@ -423,6 +445,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))), sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))), syncWorkspaces(), + loadTFPrompts(), ]).then(() => { setStore("status", "complete") }) diff --git a/packages/tfcode/src/session/llm.ts b/packages/tfcode/src/session/llm.ts index 3f4fc3ce1..851fd70b2 100644 --- a/packages/tfcode/src/session/llm.ts +++ b/packages/tfcode/src/session/llm.ts @@ -70,7 +70,7 @@ export namespace LLM { const system: string[] = [] // Build highlighted agent instructions for ToothFairyAI agents - const tfHighlightedInstructions = buildTFAgentInstructions(input.agent) + const tfHighlightedInstructions = await buildTFAgentInstructions(input.agent) system.push( [