feat: better management of prompts

This commit is contained in:
Gab 2026-03-26 13:42:50 +11:00
parent d5fa434c43
commit c095414857
4 changed files with 224 additions and 19 deletions

View File

@ -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<string, string> = {
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 <span style={{ fg: theme.textMuted }}> Loading</span>
}
if (props.enabled) {
return <span style={{ fg: theme.success, attributes: TextAttributes.BOLD }}> Connected</span>
}
return <span style={{ fg: theme.textMuted }}> Disconnected</span>
}
export function DialogTfMcp() {
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
const [, setRef] = createSignal<DialogSelectRef<unknown>>()
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<DialogSelectOption<string>[]>(() => {
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: <Status enabled={enabled} loading={loadingMcp} />,
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: <span style={{ fg: "gray" }}>Press Space to cycle</span>,
category: "ToothFairyAI MCP",
},
]
})
const keybinds = createMemo(() => [
{
keybind: Keybind.parse("space")[0],
title: "toggle/cycle",
onTrigger: async (option: DialogSelectOption<string>) => {
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 (
<DialogSelect
ref={setRef}
title="ToothFairyAI MCP"
options={options()}
keybind={keybinds()}
onSelect={() => {
// Don't close on select
}}
/>
)
}

View File

@ -35,6 +35,7 @@ import { useToast } from "../../ui/toast"
import { useKV } from "../../context/kv" import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings" import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill" import { DialogSkill } from "../dialog-skill"
import { DialogTfMcp } from "../dialog-tf-mcp"
export type PromptProps = { export type PromptProps = {
sessionID?: string 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(() => <DialogTfMcp />)
},
},
] ]
}) })

View File

@ -170,6 +170,9 @@ export namespace ModelsDev {
// Only include serverless models // Only include serverless models
if (model.deploymentType && model.deploymentType !== "serverless") continue 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) // Use the full key as the model ID (API expects the exact key)
const modelId = key const modelId = key
@ -221,27 +224,13 @@ export namespace ModelsDev {
npm: "@toothfairyai/sdk", npm: "@toothfairyai/sdk",
api: "https://ais.toothfairyai.com", api: "https://ais.toothfairyai.com",
models: { models: {
sorcerer: { "mystica-15": {
id: "sorcerer", id: "mystica-15",
name: "TF Sorcerer", name: "TF Mystica 15",
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",
family: "fireworks", family: "fireworks",
release_date: "2025-01-01", release_date: "2025-01-01",
attachment: true, attachment: true,
reasoning: false, reasoning: true,
temperature: true, temperature: true,
tool_call: true, tool_call: true,
options: {}, options: {},

View File

@ -1491,7 +1491,7 @@ export namespace Provider {
return undefined return undefined
} }
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] const priority = ["mystica-15", "sorcerer-15"]
export function sort<T extends { id: string }>(models: T[]) { export function sort<T extends { id: string }>(models: T[]) {
return sortBy( return sortBy(
models, models,