From c095414857eeedf3a7d7ceee0e2b416a81d988be Mon Sep 17 00:00:00 2001 From: Gab Date: Thu, 26 Mar 2026 13:42:50 +1100 Subject: [PATCH] feat: better management of prompts --- .../cli/cmd/tui/component/dialog-tf-mcp.tsx | 204 ++++++++++++++++++ .../cli/cmd/tui/component/prompt/index.tsx | 12 ++ packages/tfcode/src/provider/models.ts | 25 +-- packages/tfcode/src/provider/provider.ts | 2 +- 4 files changed, 224 insertions(+), 19 deletions(-) create mode 100644 packages/tfcode/src/cli/cmd/tui/component/dialog-tf-mcp.tsx diff --git a/packages/tfcode/src/cli/cmd/tui/component/dialog-tf-mcp.tsx b/packages/tfcode/src/cli/cmd/tui/component/dialog-tf-mcp.tsx new file mode 100644 index 000000000..8f171b71d --- /dev/null +++ b/packages/tfcode/src/cli/cmd/tui/component/dialog-tf-mcp.tsx @@ -0,0 +1,204 @@ +import { createMemo, createSignal, createResource, batch } from "solid-js" +import { useSync } from "@tui/context/sync" +import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select" +import { useTheme } from "../context/theme" +import { Keybind } from "@/util/keybind" +import { TextAttributes } from "@opentui/core" +import { useSDK } from "@tui/context/sdk" +import { Global } from "@/global" +import path from "path" +import { Filesystem } from "@/util/filesystem" +import { useToast } from "../ui/toast" +import { reconcile } from "solid-js/store" + +const TF_MCP_NAME = "toothfairyai" +const TF_MCP_DEFAULT_REGION = "au" +const TF_MCP_URLS: Record = { + dev: "https://mcp.toothfairylab.link/sse", + au: "https://mcp.toothfairyai.com/sse", +} +const TF_MCP_AVAILABLE_REGIONS = ["au", "dev"] + +function Status(props: { enabled: boolean; loading: boolean }) { + const { theme } = useTheme() + if (props.loading) { + return ⋯ Loading + } + if (props.enabled) { + return ✓ Connected + } + return ○ Disconnected +} + +export function DialogTfMcp() { + const sync = useSync() + const sdk = useSDK() + const toast = useToast() + const [, setRef] = createSignal>() + const [loading, setLoading] = createSignal(false) + const [refreshKey, setRefreshKey] = createSignal(0) + const [credentials, { refetch: refetchCredentials }] = createResource(refreshKey, async () => { + try { + const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json") + const data = (await Bun.file(credPath).json()) as { + api_key?: string + workspace_id?: string + region?: string + } + return data + } catch { + return {} + } + }) + + const tfMcpStatus = createMemo(() => { + const mcpData = sync.data.mcp + const status = mcpData[TF_MCP_NAME] + return status + }) + + const isEnabled = createMemo(() => { + const status = tfMcpStatus() + return status?.status === "connected" + }) + + const options = createMemo[]>(() => { + const creds = credentials() + const hasApiKey = !!creds?.api_key + const enabled = isEnabled() + const loadingMcp = loading() + + return [ + { + value: "toggle", + title: hasApiKey ? (enabled ? "Disconnect" : "Connect") : "Setup Required", + description: hasApiKey ? "Toggle ToothFairyAI MCP connection" : "Add API key via 'opencode auth toothfairyai'", + footer: , + category: "ToothFairyAI MCP", + }, + { + value: "region", + title: `Region: ${creds?.region || TF_MCP_DEFAULT_REGION}`, + description: "Change region (au, eu, us, dev) - MCP only available in au/dev", + footer: Press Space to cycle, + category: "ToothFairyAI MCP", + }, + ] + }) + + const keybinds = createMemo(() => [ + { + keybind: Keybind.parse("space")[0], + title: "toggle/cycle", + onTrigger: async (option: DialogSelectOption) => { + if (loading()) return + const creds = credentials() + + if (option.value === "toggle") { + if (!creds?.api_key) { + toast.show({ + variant: "warning", + message: "Please setup API key first: opencode auth toothfairyai", + duration: 5000, + }) + return + } + + const region = creds?.region || TF_MCP_DEFAULT_REGION + if (!TF_MCP_AVAILABLE_REGIONS.includes(region)) { + toast.show({ + variant: "warning", + message: `MCP not available in ${region}. Only au and dev regions support MCP.`, + duration: 5000, + }) + return + } + + setLoading(true) + try { + if (isEnabled()) { + await sdk.client.mcp.disconnect({ name: TF_MCP_NAME }) + toast.show({ + variant: "success", + message: "ToothFairyAI MCP disconnected", + duration: 3000, + }) + } else { + const url = TF_MCP_URLS[region] || TF_MCP_URLS[TF_MCP_DEFAULT_REGION] + + await sdk.client.mcp.add({ + name: TF_MCP_NAME, + config: { + type: "remote", + url, + headers: { + "x-api-key": creds.api_key, + }, + }, + }) + + toast.show({ + variant: "success", + message: "ToothFairyAI MCP connected", + duration: 3000, + }) + } + const status = await sdk.client.mcp.status() + if (status.data) { + sync.set("mcp", reconcile(status.data)) + } + } catch (error) { + toast.show({ + variant: "error", + message: `Failed to ${isEnabled() ? "disconnect" : "connect"}: ${error}`, + duration: 5000, + }) + } finally { + setLoading(false) + } + } else if (option.value === "region") { + const regions = ["au", "eu", "us", "dev"] as const + const currentRegion = creds?.region || TF_MCP_DEFAULT_REGION + const currentIndex = regions.indexOf(currentRegion as any) + const nextIndex = (currentIndex + 1) % regions.length + const nextRegion = regions[nextIndex] + + const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json") + const existingCreds = creds || {} + await Filesystem.writeJson(credPath, { + ...existingCreds, + region: nextRegion, + }) + + setRefreshKey((k) => k + 1) + + if (!TF_MCP_AVAILABLE_REGIONS.includes(nextRegion)) { + toast.show({ + variant: "warning", + message: `MCP not available in ${nextRegion}. Only au and dev regions support MCP.`, + duration: 5000, + }) + } else { + toast.show({ + variant: "info", + message: `Region changed to ${nextRegion}. Reconnect MCP to apply.`, + duration: 3000, + }) + } + } + }, + }, + ]) + + return ( + { + // Don't close on select + }} + /> + ) +} diff --git a/packages/tfcode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/tfcode/src/cli/cmd/tui/component/prompt/index.tsx index c13b43651..cae64d12f 100644 --- a/packages/tfcode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/tfcode/src/cli/cmd/tui/component/prompt/index.tsx @@ -35,6 +35,7 @@ import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" import { useTextareaKeybindings } from "../textarea-keybindings" import { DialogSkill } from "../dialog-skill" +import { DialogTfMcp } from "../dialog-tf-mcp" export type PromptProps = { sessionID?: string @@ -353,6 +354,17 @@ export function Prompt(props: PromptProps) { )) }, }, + { + title: "TF MCP", + value: "prompt.tf_mcp", + category: "Prompt", + slash: { + name: "tf_mcp", + }, + onSelect: () => { + dialog.replace(() => ) + }, + }, ] }) diff --git a/packages/tfcode/src/provider/models.ts b/packages/tfcode/src/provider/models.ts index c09c5c9a3..03f14f701 100644 --- a/packages/tfcode/src/provider/models.ts +++ b/packages/tfcode/src/provider/models.ts @@ -170,6 +170,9 @@ export namespace ModelsDev { // Only include serverless models if (model.deploymentType && model.deploymentType !== "serverless") continue + // Only include reasoner models for toothfairyai provider + if (!model.reasoner) continue + // Use the full key as the model ID (API expects the exact key) const modelId = key @@ -221,27 +224,13 @@ export namespace ModelsDev { npm: "@toothfairyai/sdk", api: "https://ais.toothfairyai.com", models: { - sorcerer: { - id: "sorcerer", - name: "TF Sorcerer", - family: "groq", - release_date: "2025-01-01", - attachment: true, - reasoning: false, - temperature: true, - tool_call: true, - options: {}, - cost: { input: 0.62, output: 1.75 }, - limit: { context: 128000, output: 16000 }, - modalities: { input: ["text", "image"], output: ["text"] }, - }, - mystica: { - id: "mystica", - name: "TF Mystica", + "mystica-15": { + id: "mystica-15", + name: "TF Mystica 15", family: "fireworks", release_date: "2025-01-01", attachment: true, - reasoning: false, + reasoning: true, temperature: true, tool_call: true, options: {}, diff --git a/packages/tfcode/src/provider/provider.ts b/packages/tfcode/src/provider/provider.ts index 33a63d0fb..08efafa30 100644 --- a/packages/tfcode/src/provider/provider.ts +++ b/packages/tfcode/src/provider/provider.ts @@ -1491,7 +1491,7 @@ export namespace Provider { return undefined } - const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] + const priority = ["mystica-15", "sorcerer-15"] export function sort(models: T[]) { return sortBy( models,