mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-29 21:33:54 +00:00
feat: prompts
This commit is contained in:
parent
3cc0f7401a
commit
5b9cc6c0de
@ -30,8 +30,18 @@ export const DialogSelectPrompt: Component = () => {
|
||||
const prompts = createMemo(() => {
|
||||
const all = promptsQuery.data ?? []
|
||||
const agentId = tfAgentId()
|
||||
console.log("[DialogSelectPrompt] All prompts:", all.length, "agentId:", agentId, "all:", all)
|
||||
if (!agentId) return []
|
||||
return all.filter((p) => p.available_to_agents?.includes(agentId))
|
||||
const filtered = all.filter((p) => p.available_to_agents?.includes(agentId))
|
||||
console.log(
|
||||
"[DialogSelectPrompt] Filtered prompts:",
|
||||
filtered.length,
|
||||
"for agentId:",
|
||||
agentId,
|
||||
"filtered:",
|
||||
filtered,
|
||||
)
|
||||
return filtered
|
||||
})
|
||||
|
||||
const applyPrompt = (p: { interpolation_string: string }) => {
|
||||
|
||||
@ -617,6 +617,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
source: cmd.source,
|
||||
}))
|
||||
|
||||
console.log("[slashCommands] builtin:", builtin.length, "custom:", custom.length)
|
||||
const promptsBuiltin = builtin.filter((c) => c.trigger === "prompts")
|
||||
const promptsCustom = custom.filter((c) => c.trigger === "prompts")
|
||||
if (promptsBuiltin.length > 0) console.log("[slashCommands] promptsBuiltin:", promptsBuiltin)
|
||||
if (promptsCustom.length > 0) console.log("[slashCommands] promptsCustom:", promptsCustom)
|
||||
if (sync.data.command.length > 0)
|
||||
console.log(
|
||||
"[slashCommands] sync.data.command:",
|
||||
sync.data.command.map((c) => ({ name: c.name, source: c.source })),
|
||||
)
|
||||
|
||||
return [...custom, ...builtin]
|
||||
})
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ export interface SlashCommand {
|
||||
description?: string
|
||||
keybind?: string
|
||||
type: "builtin" | "custom"
|
||||
source?: "command" | "mcp" | "skill"
|
||||
source?: "command" | "mcp" | "skill" | "prompt"
|
||||
}
|
||||
|
||||
type PromptPopoverProps = {
|
||||
@ -122,6 +122,8 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
|
||||
? props.t("prompt.slash.badge.skill")
|
||||
: cmd.source === "mcp"
|
||||
? props.t("prompt.slash.badge.mcp")
|
||||
: cmd.source === "prompt"
|
||||
? props.t("prompt.slash.badge.prompt")
|
||||
: props.t("prompt.slash.badge.custom")}
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
@ -150,7 +150,12 @@ export async function bootstrapDirectory(input: {
|
||||
|
||||
Promise.all([
|
||||
input.sdk.path.get().then((x) => input.setStore("path", x.data!)),
|
||||
input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])),
|
||||
input.sdk.command.list().then((x) => {
|
||||
console.log("[bootstrap] command.list result:", x.data?.length, "commands")
|
||||
const promptsCmd = x.data?.find((c) => c.name === "prompts")
|
||||
if (promptsCmd) console.log("[bootstrap] Found 'prompts' command in server response:", promptsCmd)
|
||||
return input.setStore("command", x.data ?? [])
|
||||
}),
|
||||
input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)),
|
||||
input.loadSessions(input.directory),
|
||||
input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)),
|
||||
|
||||
@ -274,6 +274,7 @@ export const dict = {
|
||||
"prompt.slash.badge.custom": "custom",
|
||||
"prompt.slash.badge.skill": "skill",
|
||||
"prompt.slash.badge.mcp": "mcp",
|
||||
"prompt.slash.badge.prompt": "prompt",
|
||||
"prompt.context.active": "active",
|
||||
"prompt.context.includeActiveFile": "Include active file",
|
||||
"prompt.context.removeActiveFile": "Remove active file from context",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -201,25 +201,25 @@ def sync_tools(config: TFConfig) -> ToolSyncResult:
|
||||
try:
|
||||
client = config.get_client()
|
||||
|
||||
# Sync agent functions (includes API Functions and Agent Skills)
|
||||
# Sync agent functions (API auto-paginates up to 5000)
|
||||
func_result = client.agent_functions.list()
|
||||
tools = [parse_function(f) for f in func_result.items]
|
||||
|
||||
# Sync coder agents
|
||||
# Sync coder agents (API auto-paginates up to 5000)
|
||||
try:
|
||||
agents_result = client.agents.list()
|
||||
for agent in agents_result.items:
|
||||
if getattr(agent, 'mode', None) == 'coder':
|
||||
tools.append(parse_agent(agent))
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Sync prompts
|
||||
# Sync prompts (API auto-paginates up to 5000)
|
||||
prompts = []
|
||||
try:
|
||||
prompts_result = client.prompts.list()
|
||||
prompts = [parse_prompt(p) for p in prompts_result.items]
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
by_type = {}
|
||||
|
||||
@ -265,8 +265,7 @@ export namespace Agent {
|
||||
}
|
||||
|
||||
async function loadTFCoderAgents(): Promise<Info[]> {
|
||||
// tools.json is synced to ~/.tfcode/tools.json by the CLI
|
||||
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
try {
|
||||
const content = await Bun.file(toolsPath).text()
|
||||
const data = JSON.parse(content)
|
||||
@ -309,18 +308,27 @@ export namespace Agent {
|
||||
export interface TFPrompt {
|
||||
id: string
|
||||
label: string
|
||||
description?: string
|
||||
interpolation_string: string
|
||||
available_to_agents?: string[]
|
||||
}
|
||||
|
||||
const debugFile = (msg: string) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
const line = `[${timestamp}] ${msg}\n`
|
||||
Bun.write("/tmp/tfcode-debug.log", line).catch(() => {})
|
||||
}
|
||||
|
||||
export async function loadTFPrompts(): Promise<TFPrompt[]> {
|
||||
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
try {
|
||||
const content = await Bun.file(toolsPath).text()
|
||||
const data = JSON.parse(content)
|
||||
debugFile(`[loadTFPrompts] File loaded, success: ${data.success}, prompts count: ${data.prompts?.length ?? 0}`)
|
||||
if (!data.success || !data.prompts) return []
|
||||
return data.prompts as TFPrompt[]
|
||||
} catch {
|
||||
} catch (e) {
|
||||
debugFile(`[loadTFPrompts] Error loading: ${e}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import { DialogHelp } from "./ui/dialog-help"
|
||||
import { DialogChangelog } from "./ui/dialog-changelog"
|
||||
import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command"
|
||||
import { DialogAgent } from "@tui/component/dialog-agent"
|
||||
import { DialogPrompts } from "@tui/component/dialog-prompts"
|
||||
import { DialogSessionList } from "@tui/component/dialog-session-list"
|
||||
import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list"
|
||||
import { KeybindProvider } from "@tui/context/keybind"
|
||||
@ -156,17 +157,17 @@ export function tui(input: {
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
@ -491,6 +492,17 @@ function App() {
|
||||
dialog.replace(() => <DialogMcp />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Switch prompts",
|
||||
value: "prompt.list",
|
||||
category: "Agent",
|
||||
slash: {
|
||||
name: "prompts",
|
||||
},
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogPrompts />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Agent cycle",
|
||||
value: "agent.cycle",
|
||||
|
||||
56
packages/tfcode/src/cli/cmd/tui/component/dialog-prompt.tsx
Normal file
56
packages/tfcode/src/cli/cmd/tui/component/dialog-prompt.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
|
||||
import { createResource, createMemo } from "solid-js"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
|
||||
export type DialogPromptProps = {
|
||||
onSelect: (prompt: string) => void
|
||||
}
|
||||
|
||||
export function DialogPrompt(props: DialogPromptProps) {
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const local = useLocal()
|
||||
dialog.setSize("large")
|
||||
|
||||
const [prompts] = createResource(async () => {
|
||||
const result = await sdk.client.app.prompts()
|
||||
return result.data ?? []
|
||||
})
|
||||
|
||||
const currentAgent = createMemo(() => local.agent.current())
|
||||
const tfAgentId = createMemo(() => currentAgent()?.options?.tf_agent_id as string | undefined)
|
||||
|
||||
const filteredPrompts = createMemo(() => {
|
||||
const all = prompts() ?? []
|
||||
const agentId = tfAgentId()
|
||||
const debugFile = (msg: string) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
const line = `[${timestamp}] ${msg}\n`
|
||||
Bun.write("/tmp/tfcode-debug.log", line).catch(() => {})
|
||||
}
|
||||
debugFile(`[DialogPrompt] All prompts: ${all.length}, agentId: ${agentId}`)
|
||||
if (!agentId) return []
|
||||
const filtered = all.filter((p) => p.available_to_agents?.includes(agentId))
|
||||
debugFile(`[DialogPrompt] Filtered prompts: ${filtered.length} for agentId: ${agentId}`)
|
||||
return filtered
|
||||
})
|
||||
|
||||
const options = createMemo<DialogSelectOption<string>[]>(() => {
|
||||
const list = filteredPrompts()
|
||||
const maxWidth = Math.max(0, ...list.map((p) => p.label.length))
|
||||
return list.map((prompt) => ({
|
||||
title: prompt.label.padEnd(maxWidth),
|
||||
description: prompt.description?.replace(/\s+/g, " ").trim(),
|
||||
value: prompt.interpolation_string,
|
||||
category: "Prompts",
|
||||
onSelect: () => {
|
||||
props.onSelect(prompt.interpolation_string)
|
||||
dialog.clear()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
return <DialogSelect title="Prompts" placeholder="Search prompts..." options={options()} />
|
||||
}
|
||||
51
packages/tfcode/src/cli/cmd/tui/component/dialog-prompts.tsx
Normal file
51
packages/tfcode/src/cli/cmd/tui/component/dialog-prompts.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { usePromptRef } from "@tui/context/prompt"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
|
||||
export function DialogPrompts() {
|
||||
const local = useLocal()
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const promptRef = usePromptRef()
|
||||
|
||||
const currentAgent = local.agent.current()
|
||||
const agentId = currentAgent.options?.tf_agent_id as string | undefined
|
||||
|
||||
const options = createMemo(() => {
|
||||
const prompts = sync.data.prompts || []
|
||||
return prompts
|
||||
.filter((prompt) => {
|
||||
if (!agentId) return false
|
||||
return (
|
||||
!prompt.available_to_agents ||
|
||||
prompt.available_to_agents.length === 0 ||
|
||||
prompt.available_to_agents.includes(agentId)
|
||||
)
|
||||
})
|
||||
.map((prompt) => ({
|
||||
value: prompt.label,
|
||||
title: prompt.label,
|
||||
description: prompt.description || "Prompt template",
|
||||
}))
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Select prompt"
|
||||
options={options()}
|
||||
onSelect={(option) => {
|
||||
const prompt = sync.data.prompts.find((p) => p.label === option.value)
|
||||
if (prompt && promptRef.current) {
|
||||
promptRef.current.set({
|
||||
input: prompt.interpolation_string,
|
||||
parts: [],
|
||||
})
|
||||
dialog.clear()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -356,42 +356,6 @@ export function Autocomplete(props: {
|
||||
)
|
||||
})
|
||||
|
||||
const tfPrompts = createMemo(() => {
|
||||
if (!store.visible || store.visible === "/") return []
|
||||
|
||||
const currentAgent = local.agent.current()
|
||||
const agentId = currentAgent.options?.tf_agent_id as string | undefined
|
||||
if (!agentId) return []
|
||||
|
||||
const options: AutocompleteOption[] = []
|
||||
const width = props.anchor().width - 4
|
||||
|
||||
const prompts = sync.data.prompts || []
|
||||
|
||||
for (const prompt of prompts) {
|
||||
const isAvailable =
|
||||
!prompt.available_to_agents ||
|
||||
prompt.available_to_agents.length === 0 ||
|
||||
prompt.available_to_agents.includes(agentId)
|
||||
|
||||
if (isAvailable) {
|
||||
options.push({
|
||||
display: Locale.truncateMiddle("@" + prompt.label, width),
|
||||
value: prompt.label,
|
||||
description: "Prompt template",
|
||||
onSelect: () => {
|
||||
const cursor = props.input().logicalCursor
|
||||
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
||||
props.input().insertText(prompt.interpolation_string)
|
||||
props.input().cursorOffset = Bun.stringWidth(prompt.interpolation_string)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const commands = createMemo((): AutocompleteOption[] => {
|
||||
const results: AutocompleteOption[] = [...command.slashes()]
|
||||
|
||||
@ -425,12 +389,9 @@ export function Autocomplete(props: {
|
||||
const filesValue = files()
|
||||
const agentsValue = agents()
|
||||
const commandsValue = commands()
|
||||
const promptsValue = tfPrompts()
|
||||
|
||||
const mixed: AutocompleteOption[] =
|
||||
store.visible === "@"
|
||||
? [...agentsValue, ...promptsValue, ...(filesValue || []), ...mcpResources()]
|
||||
: [...commandsValue]
|
||||
store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue]
|
||||
|
||||
const searchValue = search()
|
||||
|
||||
|
||||
@ -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 { DialogPrompt } from "../dialog-prompt"
|
||||
import { DialogTfMcp } from "../dialog-tf-mcp"
|
||||
import { DialogTfHooks } from "../dialog-tf-hooks"
|
||||
|
||||
@ -355,6 +356,28 @@ export function Prompt(props: PromptProps) {
|
||||
))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Prompts",
|
||||
value: "prompt.prompts",
|
||||
category: "Prompt",
|
||||
slash: {
|
||||
name: "prompts",
|
||||
},
|
||||
onSelect: () => {
|
||||
dialog.replace(() => (
|
||||
<DialogPrompt
|
||||
onSelect={(prompt) => {
|
||||
input.setText(prompt)
|
||||
setStore("prompt", {
|
||||
input: prompt,
|
||||
parts: [],
|
||||
})
|
||||
input.gotoBufferEnd()
|
||||
}}
|
||||
/>
|
||||
))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "TF MCP",
|
||||
value: "prompt.tf_mcp",
|
||||
|
||||
@ -31,6 +31,7 @@ import type { Path } from "@opencode-ai/sdk"
|
||||
import type { Workspace } from "@opencode-ai/sdk/v2"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Global } from "@/global"
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
@ -82,6 +83,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
label: string
|
||||
interpolation_string: string
|
||||
available_to_agents?: string[]
|
||||
description?: string
|
||||
}>
|
||||
}>({
|
||||
provider_next: {
|
||||
@ -123,7 +125,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
async function loadTFPrompts() {
|
||||
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
try {
|
||||
const content = await Bun.file(toolsPath).text()
|
||||
const data = JSON.parse(content)
|
||||
|
||||
@ -4,6 +4,7 @@ import { makeRunPromise } from "@/effect/run-service"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import z from "zod"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Config } from "../config/config"
|
||||
import { MCP } from "../mcp"
|
||||
import { Skill } from "../skill"
|
||||
@ -75,6 +76,12 @@ export namespace Command {
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const log = (msg: string) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
const line = `[${timestamp}] ${msg}\n`
|
||||
Bun.write("/tmp/tfcode-debug.log", line).catch(() => {})
|
||||
}
|
||||
|
||||
const init = Effect.fn("Command.state")(function* (ctx) {
|
||||
const cfg = yield* Effect.promise(() => Config.get())
|
||||
const commands: Record<string, Info> = {}
|
||||
@ -115,6 +122,7 @@ export namespace Command {
|
||||
}
|
||||
|
||||
for (const [name, prompt] of Object.entries(yield* Effect.promise(() => MCP.prompts()))) {
|
||||
log(`[Command.init] MCP prompt: ${name}`)
|
||||
commands[name] = {
|
||||
name,
|
||||
source: "mcp",
|
||||
@ -166,7 +174,13 @@ export namespace Command {
|
||||
|
||||
const list = Effect.fn("Command.list")(function* () {
|
||||
const state = yield* InstanceState.get(cache)
|
||||
return Object.values(state.commands)
|
||||
const commands = Object.values(state.commands)
|
||||
log(`[Command.list] Total commands: ${commands.length}`)
|
||||
const promptsCmd = commands.find((c) => c.name === "prompts")
|
||||
if (promptsCmd) {
|
||||
log(`[Command.list] Found 'prompts' command with source: ${promptsCmd.source}`)
|
||||
}
|
||||
return commands
|
||||
})
|
||||
|
||||
return Service.of({ get, list })
|
||||
|
||||
@ -20,6 +20,11 @@ import { Discovery } from "./discovery"
|
||||
|
||||
export namespace Skill {
|
||||
const log = Log.create({ service: "skill" })
|
||||
const debugFile = (msg: string) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
const line = `[${timestamp}] ${msg}\n`
|
||||
Bun.write("/tmp/tfcode-debug.log", line).catch(() => {})
|
||||
}
|
||||
const EXTERNAL_DIRS = [".claude", ".agents"]
|
||||
const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
|
||||
const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
|
||||
@ -207,11 +212,16 @@ export namespace Skill {
|
||||
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
|
||||
try {
|
||||
const content = yield* Effect.promise(() => Bun.file(toolsPath).text())
|
||||
const data = JSON.parse(content) as { success: boolean; tools: Array<{ tool_type: string; name: string; description?: string; id: string }> }
|
||||
const data = JSON.parse(content) as {
|
||||
success: boolean
|
||||
tools: Array<{ tool_type: string; name: string; description?: string; id: string }>
|
||||
}
|
||||
if (data.success && data.tools) {
|
||||
debugFile(`[Skill.all] tools.json: ${data.tools.length} tools`)
|
||||
for (const tool of data.tools) {
|
||||
// Only include agent_skill (from is_agent_skill=True), not coder_agent
|
||||
if (tool.tool_type === "agent_skill") {
|
||||
debugFile(`[Skill.all] Found agent_skill: ${tool.name}`)
|
||||
skills.push({
|
||||
name: tool.name,
|
||||
description: tool.description || "ToothFairyAI Agent Skill",
|
||||
@ -221,10 +231,12 @@ export namespace Skill {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (e) {
|
||||
debugFile(`[Skill.all] Error loading TF tools: ${e}`)
|
||||
// Ignore errors loading TF tools
|
||||
}
|
||||
|
||||
debugFile(`[Skill.all] Total skills: ${skills.length} names: ${skills.map((s) => s.name).join(", ")}`)
|
||||
return skills
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user