mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-24 09:35:05 +00:00
feat: hooks
This commit is contained in:
2
bun.lock
2
bun.lock
@@ -381,7 +381,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tfcode": {
|
"packages/tfcode": {
|
||||||
"name": "tfcode",
|
"name": "tfcode",
|
||||||
"version": "1.0.11",
|
"version": "1.0.14",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tfcode": "./bin/tfcode",
|
"tfcode": "./bin/tfcode",
|
||||||
},
|
},
|
||||||
|
|||||||
78
packages/app/src/components/dialog-select-prompt.tsx
Normal file
78
packages/app/src/components/dialog-select-prompt.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
|
import { Component, createMemo, Show } from "solid-js"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { List } from "@opencode-ai/ui/list"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useLocal } from "@/context/local"
|
||||||
|
import { usePrompt, type Prompt } from "@/context/prompt"
|
||||||
|
import type { AppPromptsResponse } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
|
export const DialogSelectPrompt: Component = () => {
|
||||||
|
const sdk = useSDK()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const language = useLanguage()
|
||||||
|
const local = useLocal()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const promptsQuery = useQuery(() => ({
|
||||||
|
queryKey: ["prompts"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const result = await sdk.client.app.prompts()
|
||||||
|
return result.data as AppPromptsResponse
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const currentAgent = createMemo(() => local.agent.current())
|
||||||
|
const tfAgentId = createMemo(() => currentAgent()?.options?.tf_agent_id as string | undefined)
|
||||||
|
|
||||||
|
const prompts = createMemo(() => {
|
||||||
|
const all = promptsQuery.data ?? []
|
||||||
|
const agentId = tfAgentId()
|
||||||
|
if (!agentId) return []
|
||||||
|
return all.filter((p) => p.available_to_agents?.includes(agentId))
|
||||||
|
})
|
||||||
|
|
||||||
|
const applyPrompt = (p: { interpolation_string: string }) => {
|
||||||
|
const text = p.interpolation_string
|
||||||
|
const parts: Prompt = [{ type: "text", content: text, start: 0, end: text.length }]
|
||||||
|
prompt.set(parts, text.length)
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
title={language.t("dialog.prompt.title")}
|
||||||
|
description={language.t("dialog.prompt.description", {
|
||||||
|
agent: currentAgent()?.name ?? "",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={!promptsQuery.isLoading}
|
||||||
|
fallback={<div class="p-4 text-center">{language.t("common.loading.ellipsis")}</div>}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
||||||
|
emptyMessage={language.t("dialog.prompt.empty")}
|
||||||
|
key={(x) => x?.id ?? ""}
|
||||||
|
items={prompts}
|
||||||
|
filterKeys={["label", "description"]}
|
||||||
|
sortBy={(a, b) => a.label.localeCompare(b.label)}
|
||||||
|
onSelect={(x) => {
|
||||||
|
if (x) applyPrompt(x)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(i) => (
|
||||||
|
<div class="w-full flex flex-col gap-0.5 min-w-0">
|
||||||
|
<span class="truncate text-13-medium">{i.label}</span>
|
||||||
|
<Show when={i.description}>
|
||||||
|
<span class="text-11-regular text-text-weaker truncate">{i.description}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Show>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.description": "Switch to the next agent",
|
"command.agent.cycle.description": "Switch to the next agent",
|
||||||
"command.agent.cycle.reverse": "Cycle agent backwards",
|
"command.agent.cycle.reverse": "Cycle agent backwards",
|
||||||
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
||||||
|
"command.prompts.select": "Select prompt",
|
||||||
|
"command.prompts.select.description": "Select a ToothFairyAI prompt for the current agent",
|
||||||
"command.model.variant.cycle": "Cycle thinking effort",
|
"command.model.variant.cycle": "Cycle thinking effort",
|
||||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||||
"command.prompt.mode.shell": "Shell",
|
"command.prompt.mode.shell": "Shell",
|
||||||
@@ -296,6 +298,10 @@ export const dict = {
|
|||||||
"dialog.mcp.description": "{{enabled}} of {{total}} enabled",
|
"dialog.mcp.description": "{{enabled}} of {{total}} enabled",
|
||||||
"dialog.mcp.empty": "No MCPs configured",
|
"dialog.mcp.empty": "No MCPs configured",
|
||||||
|
|
||||||
|
"dialog.prompt.title": "Select prompt",
|
||||||
|
"dialog.prompt.description": "Prompts for {{agent}}",
|
||||||
|
"dialog.prompt.empty": "No prompts available for this agent",
|
||||||
|
|
||||||
"dialog.lsp.empty": "LSPs auto-detected from file types",
|
"dialog.lsp.empty": "LSPs auto-detected from file types",
|
||||||
"dialog.plugins.empty": "Plugins configured in opencode.json",
|
"dialog.plugins.empty": "Plugins configured in opencode.json",
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useTerminal } from "@/context/terminal"
|
|||||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||||
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||||
|
import { DialogSelectPrompt } from "@/components/dialog-select-prompt"
|
||||||
import { DialogFork } from "@/components/dialog-fork"
|
import { DialogFork } from "@/components/dialog-fork"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { findLast } from "@opencode-ai/util/array"
|
import { findLast } from "@opencode-ai/util/array"
|
||||||
@@ -376,6 +377,13 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
keybind: "shift+mod+.",
|
keybind: "shift+mod+.",
|
||||||
onSelect: () => local.agent.move(-1),
|
onSelect: () => local.agent.move(-1),
|
||||||
}),
|
}),
|
||||||
|
agentCommand({
|
||||||
|
id: "prompts.select",
|
||||||
|
title: language.t("command.prompts.select"),
|
||||||
|
description: language.t("command.prompts.select.description"),
|
||||||
|
slash: "prompts",
|
||||||
|
onSelect: () => dialog.show(() => <DialogSelectPrompt />),
|
||||||
|
}),
|
||||||
modelCommand({
|
modelCommand({
|
||||||
id: "model.variant.cycle",
|
id: "model.variant.cycle",
|
||||||
title: language.t("command.model.variant.cycle"),
|
title: language.t("command.model.variant.cycle"),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import path from "path"
|
|||||||
|
|
||||||
import { createClient } from "@hey-api/openapi-ts"
|
import { createClient } from "@hey-api/openapi-ts"
|
||||||
|
|
||||||
await $`bun dev generate > ${dir}/openapi.json`.cwd(path.resolve(dir, "../../opencode"))
|
await $`bun dev generate > ${dir}/openapi.json`.cwd(path.resolve(dir, "../../tfcode"))
|
||||||
|
|
||||||
await createClient({
|
await createClient({
|
||||||
input: "./openapi.json",
|
input: "./openapi.json",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
AppAgentsResponses,
|
AppAgentsResponses,
|
||||||
AppLogErrors,
|
AppLogErrors,
|
||||||
AppLogResponses,
|
AppLogResponses,
|
||||||
|
AppPromptsResponses,
|
||||||
AppSkillsResponses,
|
AppSkillsResponses,
|
||||||
Auth as Auth3,
|
Auth as Auth3,
|
||||||
AuthRemoveErrors,
|
AuthRemoveErrors,
|
||||||
@@ -3858,6 +3859,36 @@ export class App extends HeyApiClient {
|
|||||||
...params,
|
...params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List ToothFairyAI prompts
|
||||||
|
*
|
||||||
|
* Get prompts assigned to a specific agent or all prompts.
|
||||||
|
*/
|
||||||
|
public prompts<ThrowOnError extends boolean = false>(
|
||||||
|
parameters?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
},
|
||||||
|
options?: Options<never, ThrowOnError>,
|
||||||
|
) {
|
||||||
|
const params = buildClientParams(
|
||||||
|
[parameters],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{ in: "query", key: "directory" },
|
||||||
|
{ in: "query", key: "workspace" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return (options?.client ?? this.client).get<AppPromptsResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/prompts",
|
||||||
|
...options,
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Lsp extends HeyApiClient {
|
export class Lsp extends HeyApiClient {
|
||||||
|
|||||||
@@ -1919,6 +1919,7 @@ export type Agent = {
|
|||||||
}
|
}
|
||||||
variant?: string
|
variant?: string
|
||||||
prompt?: string
|
prompt?: string
|
||||||
|
goals?: string
|
||||||
options: {
|
options: {
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
@@ -4996,6 +4997,31 @@ export type AppSkillsResponses = {
|
|||||||
|
|
||||||
export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses]
|
export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses]
|
||||||
|
|
||||||
|
export type AppPromptsData = {
|
||||||
|
body?: never
|
||||||
|
path?: never
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
}
|
||||||
|
url: "/prompts"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppPromptsResponses = {
|
||||||
|
/**
|
||||||
|
* List of prompts
|
||||||
|
*/
|
||||||
|
200: Array<{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
interpolation_string: string
|
||||||
|
available_to_agents?: Array<string>
|
||||||
|
description?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppPromptsResponse = AppPromptsResponses[keyof AppPromptsResponses]
|
||||||
|
|
||||||
export type LspStatusData = {
|
export type LspStatusData = {
|
||||||
body?: never
|
body?: never
|
||||||
path?: never
|
path?: never
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.0.11",
|
"version": "1.0.14",
|
||||||
"name": "tfcode",
|
"name": "tfcode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export namespace Agent {
|
|||||||
available_to_agents?: string[]
|
available_to_agents?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTFPrompts(): Promise<TFPrompt[]> {
|
export async function loadTFPrompts(): Promise<TFPrompt[]> {
|
||||||
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
|
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
|
||||||
try {
|
try {
|
||||||
const content = await Bun.file(toolsPath).text()
|
const content = await Bun.file(toolsPath).text()
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function HookStatus(props: { hook: Hook }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogTfHooks() {
|
export function DialogTfHooks(props: { onSelect?: (hook: Hook) => void }) {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [, setRef] = createSignal<DialogSelectRef<unknown>>()
|
const [, setRef] = createSignal<DialogSelectRef<unknown>>()
|
||||||
@@ -86,7 +86,9 @@ export function DialogTfHooks() {
|
|||||||
const region = creds.region || TF_DEFAULT_REGION
|
const region = creds.region || TF_DEFAULT_REGION
|
||||||
const baseUrl = REGION_API_URLS[region] || REGION_API_URLS[TF_DEFAULT_REGION]
|
const baseUrl = REGION_API_URLS[region] || REGION_API_URLS[TF_DEFAULT_REGION]
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/hook/list`, {
|
const url = new URL(`${baseUrl}/hook/list`)
|
||||||
|
url.searchParams.set("workspaceid", creds.workspace_id)
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"x-api-key": creds.api_key,
|
"x-api-key": creds.api_key,
|
||||||
@@ -98,8 +100,8 @@ export function DialogTfHooks() {
|
|||||||
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as { success?: boolean; hooks?: Hook[] }
|
const data = (await response.json()) as Hook[]
|
||||||
return data.hooks || []
|
return Array.isArray(data) ? data : []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.show({
|
toast.show({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -172,7 +174,7 @@ export function DialogTfHooks() {
|
|||||||
keybind={keybinds()}
|
keybind={keybinds()}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
if (option.value.id === "loading" || option.value.id === "empty") return
|
if (option.value.id === "loading" || option.value.id === "empty") return
|
||||||
// Don't close on select - just show the hook details
|
props.onSelect?.(option.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -374,7 +374,22 @@ export function Prompt(props: PromptProps) {
|
|||||||
name: "hooks",
|
name: "hooks",
|
||||||
},
|
},
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
dialog.replace(() => <DialogTfHooks />)
|
dialog.replace(() => (
|
||||||
|
<DialogTfHooks
|
||||||
|
onSelect={(hook) => {
|
||||||
|
const parts = []
|
||||||
|
if (hook.code_execution_instructions) parts.push(hook.code_execution_instructions)
|
||||||
|
if (hook.predefined_code_snippet) parts.push("\n\n```python\n" + hook.predefined_code_snippet + "\n```")
|
||||||
|
const text = parts.join("")
|
||||||
|
if (text) {
|
||||||
|
input.setText(text)
|
||||||
|
setStore("prompt", { input: text, parts: [] })
|
||||||
|
input.gotoBufferEnd()
|
||||||
|
}
|
||||||
|
dialog.clear()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -454,6 +454,38 @@ export namespace Server {
|
|||||||
return c.json(skills)
|
return c.json(skills)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.get(
|
||||||
|
"/prompts",
|
||||||
|
describeRoute({
|
||||||
|
summary: "List ToothFairyAI prompts",
|
||||||
|
description: "Get prompts assigned to a specific agent or all prompts.",
|
||||||
|
operationId: "app.prompts",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "List of prompts",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(
|
||||||
|
z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
interpolation_string: z.string(),
|
||||||
|
available_to_agents: z.array(z.string()).optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const prompts = await Agent.loadTFPrompts()
|
||||||
|
return c.json(prompts)
|
||||||
|
},
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"/lsp",
|
"/lsp",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
|
|||||||
Reference in New Issue
Block a user