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 { 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 />)
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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: {},
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user