From 5b9cc6c0de01e277de1c6d6f55052bf492344100 Mon Sep 17 00:00:00 2001 From: Gab Date: Sat, 28 Mar 2026 16:48:01 +1100 Subject: [PATCH] feat: prompts --- .../src/components/dialog-select-prompt.tsx | 12 +++- packages/app/src/components/prompt-input.tsx | 11 ++++ .../components/prompt-input/slash-popover.tsx | 6 +- .../app/src/context/global-sync/bootstrap.ts | 7 ++- packages/app/src/i18n/en.ts | 1 + .../__pycache__/config.cpython-313.pyc | Bin 8430 -> 8455 bytes .../tf_sync/__pycache__/tools.cpython-313.pyc | Bin 7887 -> 9567 bytes packages/tf-sync/src/tf_sync/tools.py | 10 ++-- packages/tfcode/src/agent/agent.ts | 16 +++-- packages/tfcode/src/cli/cmd/tui/app.tsx | 32 ++++++---- .../cli/cmd/tui/component/dialog-prompt.tsx | 56 ++++++++++++++++++ .../cli/cmd/tui/component/dialog-prompts.tsx | 51 ++++++++++++++++ .../cmd/tui/component/prompt/autocomplete.tsx | 41 +------------ .../cli/cmd/tui/component/prompt/index.tsx | 23 +++++++ .../tfcode/src/cli/cmd/tui/context/sync.tsx | 4 +- packages/tfcode/src/command/index.ts | 16 ++++- packages/tfcode/src/skill/index.ts | 20 +++++-- 17 files changed, 237 insertions(+), 69 deletions(-) create mode 100644 packages/tfcode/src/cli/cmd/tui/component/dialog-prompt.tsx create mode 100644 packages/tfcode/src/cli/cmd/tui/component/dialog-prompts.tsx 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 a02a85aee53fa3b7d9b4b4dbd66ccbf644dbe00f..b2ca75f1c5773a2e18741ef583604f3d053eeece 100644 GIT binary patch delta 913 zcmZva-%C?*6u|d&_ulE|=DaqiZF37>x|C@zQ4hJCn~6>vI{E=E$?Hu<+wM5KNsiDc zVi6(HA&?My=^-C_h}%oP)PvDmFA>^>pnsr3@-^stzCuDY*yo<}JwLzae18lzQvP3l zzt_co>dW`VhI3E`HMZg!O1)VYaV z9wTxuOJGutdTwX8{h_-fSdgnMjvwVsZyyf=LLAqWQwmA-9(?QRw0D$SZndAxK0=s7 zW01yWZ$cjB%DDa9JHS{DKl@TFja{t;ma!LF@3D(X@;^fu;?QK6AUR9;$K-)Lol-hT z6PgL+?gZxzYo=!Cz&{IDOE(P2;&fn;74UK3=YbS=S#R18TxW0rpR^rkCVpsJiwskm z%Y*_)jd3I(O8PJ^1tZMDr@_Mt)z-SjyuJpHEsF*w`I#flnq~D zB1v?5iLA|h{#664MYuu+QTrfCv=E38MCmb-2s!?*J>zJ&zDWJw)X*m>wC%?S?W=4I zd(|~o$9L)g%i@lDVq})048j~iOi?tTlUgE_3ID5DBm<$2<&Kn3s3O6Hz0vX7J-#QC z6Wv|&I8A2F@fekbBA6BUyL-obo)(Y+FhN`ZJi>ji8L`|nz6mGgMSgt8{vFP`eb*_g z$X~*IWKa=$*a z^B49&Y=tReG=Q5uv!h}d;+^(Ta37(QK+hJs2;GFU9F8IJ85OU_xiEYwMETar{YmGDK&UiM>-5D|M zA}n~Ix)7p^2#EqYcG;bmK~Oh#C@8wT z)kF=gJ*&w*|MC0dKye6RSn9wh@&pUvFL_sqkdqL?clLf3F%RrdZNAgwbrae+R0bM8 zQ@Z3)F5>2UrI)cJ?z=UnVPC^7Hf%m>sIuz=lpiGwQjrV^vg3sRLJpWDFSP@W;3QDG z<;`Zwv!zTL_-}bWzm$PE<{P7I246QG9J|O4h>}At~mU$P%MZQ`E^j7Bf(;zzhZGNWPaAWsDRdi5tyt*ce{$ZL$h}@%6Ge zD*kglbCi%KqzK~0MDKZ$Il=;mXqF{KsNkAka|`=1BXbj9WEB9;itAi`7U?gGh5roZEl^qivBrV3`FfhFPPhb zAY;?`v9qQ7Xc`yg()gAuwvx^i%eqleC0IdwFe+EM|CM%(@?x9*-Q>apTZy{tmaXzgB0* A@Bjb+ 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 3894eee908bd7bf88bd0e7a1cd783852f179f4f4..44b00d7f4f150aaf70326b2be645bd2bd2de52ee 100644 GIT binary patch delta 3794 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 delta 2317 zcmZ8iU2GIp6u$Gjv$H?lZFk#lf41Aw3WIHFq$LyqEp5?m>lQmLf~+jtcD5{Tr}fS( zQeKKa8J{o{BjLf|8z#h;ebdAQDGxpwVj|6`@yQT;FpF%0N+OYSO0|7r$i@|=$y`vb4i}^Nr4IpTnpo3Qlb)* z#c?_5p&lkn<4V#?y-Ag-NgwqkNF(VWy4*x`Pg)uf@B~7rpJ^4)dbeope z>i(uxazUiQ4V52MfhN?{zy^OmuLs#Gq}Qaqpb5bH5YvR?M2|9UZCXgzrd0_J#E0#Y z^*Y>jfunWXzKpOhBMV`z{_-<*dTFVUuvVN&?n(VabXVhDVaQx@mC0p0zhq_$r7!qz zIj*jBPAqb(HL!z<2ns?Nz!hh~2Cc<)9YO@bix5Sq2XMtg-m+-}u44#I2+asB0IrlP zEM;xlin5S5Z5qc{ZMckPGM1gS^K+T3ZTo0`wrCqxCPNcWMoz0f+=o!-+?5|BdN!AH zAUq!%i6D>XozJ}k$NLCX(-2pPBag94%eu51W?iPibWPg7q4O}ELPHC(=5>P@D1F78 zn=-6o!G751^r$`U>~5afm5jZzY@}Stpmd4S1Q^r8v>hgARgIq60|G^BEEZHli8;O> zBv+iz&)M_^RKAGNkHA9QjZy-E-K!U+zD+>E8g%|sTb;eWgv@sIl}`9R#rXn;bxb;$d!Ju8_5? zd=Bo1DLxFkL1!e8;(DBCkyzG?d`$!EoZ4^WSE|g!>-HD*~kqD?bG^pewi=D8{XzZ zeZ*8|#5(diq}%k)c%3VaQRjBBZo*V&lqqy-$Cy!dae%KXbqTBD)W=wQZ$O}YBtgy~ z>DbWa8F5C@J%fUCE7sfq{m}HZ5uG^HsQKuGbF-yol`%X}HI|`_`!tCMa0ac?7pdWT zu>oYv?4n^|nPQ=Bj4G2<3ogJ|WYH{axCW2a3)j6u%JDWyT@N?h4|m)Pcl;Rc{75d# zq@neGefPb3=WOdm&hOO54-8Zk5{j=!8rN$Z)+5c2JYq;K6Va>w^H)0wzw&n_L_)F0 zg!6`f@rRz41rVNATAZ8l-t=iW?8Ad2KJs;Rc%OK?h6nL>?ZM%G@?9{F*gb@*JNqQW zesx3d1hw7LAVd32S>fl>4NSfztW5ef+@_aKbcw6A+kz_uH01 z#-UTFLqAxEHHA)~JnQ`29&5!g(hR~m01GL!6+9>0(I_o|>ibe}M>p4sV)ftiGDy&~ z(9kBWocDLN%6U-yL7eNm602DL8kf?dvI}N{wwKzK#tub$0~9YxBF_W zLSU*KlDWjy(-i_!xwf8bzZNJHm~QT=5a*}Pu1GJZUcFQ%Fx^O1a4G|-o4Z~WF*yGN DxQFYT 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 })