diff --git a/packages/app/src/components/dialog-select-prompt.tsx b/packages/app/src/components/dialog-select-prompt.tsx index 8851905d4..928dd02a4 100644 --- a/packages/app/src/components/dialog-select-prompt.tsx +++ b/packages/app/src/components/dialog-select-prompt.tsx @@ -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 }) => { diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 34f83b13e..cba10bb20 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -617,6 +617,17 @@ export const PromptInput: Component = (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] }) diff --git a/packages/app/src/components/prompt-input/slash-popover.tsx b/packages/app/src/components/prompt-input/slash-popover.tsx index 65eb01c79..aac238117 100644 --- a/packages/app/src/components/prompt-input/slash-popover.tsx +++ b/packages/app/src/components/prompt-input/slash-popover.tsx @@ -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,7 +122,9 @@ export const PromptPopover: Component = (props) => { ? props.t("prompt.slash.badge.skill") : cmd.source === "mcp" ? props.t("prompt.slash.badge.mcp") - : props.t("prompt.slash.badge.custom")} + : cmd.source === "prompt" + ? props.t("prompt.slash.badge.prompt") + : props.t("prompt.slash.badge.custom")} diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 13494b7ad..a1c1f7904 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -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!)), diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 758b99694..8620fc469 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -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", diff --git a/packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc b/packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc index a02a85aee..b2ca75f1c 100644 Binary files a/packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc and b/packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc differ diff --git a/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc b/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc index 3894eee90..44b00d7f4 100644 Binary files a/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc and b/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc differ diff --git a/packages/tf-sync/src/tf_sync/tools.py b/packages/tf-sync/src/tf_sync/tools.py index 7e14c5ecc..db8057c86 100644 --- a/packages/tf-sync/src/tf_sync/tools.py +++ b/packages/tf-sync/src/tf_sync/tools.py @@ -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 = {} diff --git a/packages/tfcode/src/agent/agent.ts b/packages/tfcode/src/agent/agent.ts index e1a9e014a..91282d018 100644 --- a/packages/tfcode/src/agent/agent.ts +++ b/packages/tfcode/src/agent/agent.ts @@ -265,8 +265,7 @@ export namespace Agent { } async function loadTFCoderAgents(): Promise { - // 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 { - 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 [] } } diff --git a/packages/tfcode/src/cli/cmd/tui/app.tsx b/packages/tfcode/src/cli/cmd/tui/app.tsx index d2521dc8f..65aba72e3 100644 --- a/packages/tfcode/src/cli/cmd/tui/app.tsx +++ b/packages/tfcode/src/cli/cmd/tui/app.tsx @@ -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: { - - - - - + + + + + - - - - - + + + + + @@ -491,6 +492,17 @@ function App() { dialog.replace(() => ) }, }, + { + title: "Switch prompts", + value: "prompt.list", + category: "Agent", + slash: { + name: "prompts", + }, + onSelect: () => { + dialog.replace(() => ) + }, + }, { title: "Agent cycle", value: "agent.cycle", diff --git a/packages/tfcode/src/cli/cmd/tui/component/dialog-prompt.tsx b/packages/tfcode/src/cli/cmd/tui/component/dialog-prompt.tsx new file mode 100644 index 000000000..d0103211e --- /dev/null +++ b/packages/tfcode/src/cli/cmd/tui/component/dialog-prompt.tsx @@ -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[]>(() => { + 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 +} diff --git a/packages/tfcode/src/cli/cmd/tui/component/dialog-prompts.tsx b/packages/tfcode/src/cli/cmd/tui/component/dialog-prompts.tsx new file mode 100644 index 000000000..a079278c0 --- /dev/null +++ b/packages/tfcode/src/cli/cmd/tui/component/dialog-prompts.tsx @@ -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 ( + { + 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() + } + }} + /> + ) +} diff --git a/packages/tfcode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/tfcode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index e482ff355..c9fcd93b1 100644 --- a/packages/tfcode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/tfcode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -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() diff --git a/packages/tfcode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/tfcode/src/cli/cmd/tui/component/prompt/index.tsx index 433d661ee..ec7fc21c7 100644 --- a/packages/tfcode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/tfcode/src/cli/cmd/tui/component/prompt/index.tsx @@ -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(() => ( + { + input.setText(prompt) + setStore("prompt", { + input: prompt, + parts: [], + }) + input.gotoBufferEnd() + }} + /> + )) + }, + }, { title: "TF MCP", value: "prompt.tf_mcp", diff --git a/packages/tfcode/src/cli/cmd/tui/context/sync.tsx b/packages/tfcode/src/cli/cmd/tui/context/sync.tsx index 885307b8a..65a0c1c19 100644 --- a/packages/tfcode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/tfcode/src/cli/cmd/tui/context/sync.tsx @@ -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) diff --git a/packages/tfcode/src/command/index.ts b/packages/tfcode/src/command/index.ts index ff9382610..4ebd1e9ca 100644 --- a/packages/tfcode/src/command/index.ts +++ b/packages/tfcode/src/command/index.ts @@ -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 = {} @@ -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 }) diff --git a/packages/tfcode/src/skill/index.ts b/packages/tfcode/src/skill/index.ts index 34b9d555c..7a18f6c83 100644 --- a/packages/tfcode/src/skill/index.ts +++ b/packages/tfcode/src/skill/index.ts @@ -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 })