feat: better preidctiosn

This commit is contained in:
Gab
2026-03-26 15:07:30 +11:00
parent a42f8fa99f
commit 12ae1cb9b5
6 changed files with 647 additions and 22 deletions

View File

@@ -0,0 +1,179 @@
import { createMemo, createSignal, createResource } from "solid-js"
import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
import { useTheme } from "../context/theme"
import { TextAttributes } from "@opentui/core"
import { Global } from "@/global"
import path from "path"
import { useToast } from "../ui/toast"
import { Keybind } from "@/util/keybind"
interface Hook {
id: string
name: string
description?: string
remote_environment_name?: string
code_execution_instructions?: string
predefined_code_snippet?: string
execute_as_static_script?: boolean
execute_as_python_tool?: boolean
secrets_injected?: boolean
created_at?: string
updated_at?: string
}
const TF_DEFAULT_REGION = "au"
const REGION_API_URLS: Record<string, string> = {
dev: "https://api.toothfairylab.link",
au: "https://api.toothfairyai.com",
eu: "https://api.eu.toothfairyai.com",
us: "https://api.us.toothfairyai.com",
}
function HookStatus(props: { hook: Hook }) {
const { theme } = useTheme()
const features = []
if (props.hook.execute_as_static_script) features.push("Static")
if (props.hook.execute_as_python_tool) features.push("Python Tool")
if (props.hook.secrets_injected) features.push("Secrets")
if (props.hook.remote_environment_name) features.push("Remote")
if (features.length === 0) {
return <span style={{ fg: theme.textMuted }}>Default</span>
}
return (
<span style={{ fg: theme.success }}>
{features.map((f, i) => (
<>
{i > 0 && <span style={{ fg: theme.textMuted }}>·</span>}
{f}
</>
))}
</span>
)
}
export function DialogTfHooks() {
const toast = useToast()
const { theme } = useTheme()
const [, setRef] = createSignal<DialogSelectRef<unknown>>()
const [loading, setLoading] = createSignal(false)
const [refreshKey, setRefreshKey] = createSignal(0)
const [credentials] = 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 [hooks, { refetch: refetchHooks }] = createResource(refreshKey, async () => {
const creds = credentials()
if (!creds?.api_key || !creds?.workspace_id) {
return []
}
setLoading(true)
try {
const region = creds.region || TF_DEFAULT_REGION
const baseUrl = REGION_API_URLS[region] || REGION_API_URLS[TF_DEFAULT_REGION]
const response = await fetch(`${baseUrl}/hook/list`, {
method: "GET",
headers: {
"x-api-key": creds.api_key,
},
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`HTTP ${response.status}: ${errorText}`)
}
const data = (await response.json()) as { success?: boolean; hooks?: Hook[] }
return data.hooks || []
} catch (error) {
toast.show({
variant: "error",
message: `Failed to fetch hooks: ${error}`,
duration: 5000,
})
return []
} finally {
setLoading(false)
}
})
const options = createMemo<DialogSelectOption<Hook>[]>(() => {
const hooksList = hooks() || []
const isLoading = loading()
if (isLoading) {
return [
{
value: { id: "loading", name: "Loading..." } as Hook,
title: "Loading hooks...",
description: "Fetching CodeExecutionEnvironments from ToothFairyAI",
footer: <span style={{ fg: theme.textMuted }}></span>,
category: "Code Execution Environments",
},
]
}
if (hooksList.length === 0) {
return [
{
value: { id: "empty", name: "No hooks found" } as Hook,
title: "No CodeExecutionEnvironments found",
description: "Create hooks in ToothFairyAI Settings > Hooks",
footer: <span style={{ fg: theme.textMuted }}></span>,
category: "Code Execution Environments",
},
]
}
return hooksList.map((hook) => ({
value: hook,
title: hook.name,
description: hook.description || "No description",
footer: <HookStatus hook={hook} />,
category: "Code Execution Environments",
}))
})
const keybinds = createMemo(() => [
{
keybind: Keybind.parse("space")[0],
title: "refresh",
onTrigger: async () => {
setRefreshKey((k) => k + 1)
toast.show({
variant: "info",
message: "Refreshing hooks...",
duration: 2000,
})
},
},
])
return (
<DialogSelect
ref={setRef}
title="Code Execution Environments"
options={options()}
keybind={keybinds()}
onSelect={(option) => {
if (option.value.id === "loading" || option.value.id === "empty") return
// Don't close on select - just show the hook details
}}
/>
)
}

View File

@@ -36,6 +36,7 @@ import { useKV } from "../../context/kv"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
import { DialogTfMcp } from "../dialog-tf-mcp"
import { DialogTfHooks } from "../dialog-tf-hooks"
export type PromptProps = {
sessionID?: string
@@ -365,6 +366,17 @@ export function Prompt(props: PromptProps) {
dialog.replace(() => <DialogTfMcp />)
},
},
{
title: "Hooks",
value: "prompt.hooks",
category: "Prompt",
slash: {
name: "hooks",
},
onSelect: () => {
dialog.replace(() => <DialogTfHooks />)
},
},
]
})

View File

@@ -136,30 +136,41 @@ export function createToothFairyAI(options: ToothFairyAIProviderSettings = {}):
for (const line of lines) {
if (line.startsWith("data: ")) {
const json = line.slice(6).trim()
if (json && !json.startsWith('{"status":')) {
filtered.push(line)
// Log tool calls and finish_reason
// Filter out connection status messages like {"status":"initialising"}, {"status":"connected"}
// These are internal progress indicators, not OpenAI-format chunks
if (json) {
try {
const parsed = JSON.parse(json)
if (parsed.choices?.[0]?.delta?.tool_calls) {
log.debug("stream tool_calls", {
tool_calls: parsed.choices[0].delta.tool_calls,
})
if (parsed.status === "initialising" || parsed.status === "connected") {
log.debug("filtered connection status", { status: parsed.status })
continue
}
if (parsed.choices?.[0]?.finish_reason) {
log.info("stream finish_reason", {
finish_reason: parsed.choices[0].finish_reason,
})
}
if (parsed.usage) {
log.info("stream usage", {
prompt_tokens: parsed.usage.prompt_tokens,
completion_tokens: parsed.usage.completion_tokens,
total_tokens: parsed.usage.total_tokens,
})
}
} catch {}
} catch {
// Not valid JSON, keep the line
}
}
filtered.push(line)
// Log tool calls and finish_reason
try {
const parsed = JSON.parse(json)
if (parsed.choices?.[0]?.delta?.tool_calls) {
log.debug("stream tool_calls", {
tool_calls: parsed.choices[0].delta.tool_calls,
})
}
if (parsed.choices?.[0]?.finish_reason) {
log.info("stream finish_reason", {
finish_reason: parsed.choices[0].finish_reason,
})
}
if (parsed.usage) {
log.info("stream usage", {
prompt_tokens: parsed.usage.prompt_tokens,
completion_tokens: parsed.usage.completion_tokens,
total_tokens: parsed.usage.total_tokens,
})
}
} catch {}
} else {
filtered.push(line)
}