mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-05 08:33:10 +00:00
feat: prompts
This commit is contained in:
@@ -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>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<App />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</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"
|
||||
@@ -202,16 +207,21 @@ export namespace Skill {
|
||||
const all = Effect.fn("Skill.all")(function* () {
|
||||
const cache = yield* ensure()
|
||||
const skills = Object.values(cache.skills)
|
||||
|
||||
|
||||
// Add TF agent skills from synced tools (only agent_skill type, not coder_agent)
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user