mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-29 21:33:54 +00:00
feat: better management of prompts
This commit is contained in:
parent
d5fa434c43
commit
c095414857
204
packages/tfcode/src/cli/cmd/tui/component/dialog-tf-mcp.tsx
Normal file
204
packages/tfcode/src/cli/cmd/tui/component/dialog-tf-mcp.tsx
Normal 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
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -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(() => <DialogTfMcp />)
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@ -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: {},
|
||||
|
||||
@ -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<T extends { id: string }>(models: T[]) {
|
||||
return sortBy(
|
||||
models,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user