mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-05 00:23:10 +00:00
feat: better preidctiosn
This commit is contained in:
179
packages/tfcode/src/cli/cmd/tui/component/dialog-tf-hooks.tsx
Normal file
179
packages/tfcode/src/cli/cmd/tui/component/dialog-tf-hooks.tsx
Normal 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
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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 />)
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user