From a29417c24952444f92d8038de04e07b31c8dae67 Mon Sep 17 00:00:00 2001 From: Om-Beast Date: Sat, 18 Jul 2026 22:30:14 +0530 Subject: [PATCH] feat: add /tools REPL command --- internal/agent/loop.go | 5 +++++ internal/tools/tools.go | 24 ++++++++++++++++++++++++ internal/tui/model.go | 31 +++++++++++++++++++++++++++++++ internal/tui/repl_test.go | 5 ++++- 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 2fe325f..c7a6777 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -698,6 +698,11 @@ func (a *Agent) SetModel(model string) { // Model returns the current model codename. func (a *Agent) Model() string { return a.cfg.Model } +// ToolList returns the registered tools in registration order. +func (a *Agent) ToolList() []tools.ToolInfo { + return a.tools.ToolList() +} + // Provider returns the active backend kind (e.g. "xshellz", "ollama"). func (a *Agent) Provider() string { if a.cfg.Provider == "" { diff --git a/internal/tools/tools.go b/internal/tools/tools.go index 9fb1cc0..e430f5d 100644 --- a/internal/tools/tools.go +++ b/internal/tools/tools.go @@ -30,6 +30,13 @@ type FunctionDef struct { Parameters json.RawMessage `json:"parameters"` } +// ToolInfo contains human-readable information about a registered tool. +type ToolInfo struct { + Name string + Description string + Mutating bool +} + // Registry holds the tools available to the agent, preserving order. type Registry struct { tools map[string]Tool @@ -73,6 +80,23 @@ func (r *Registry) Definitions() []Definition { return defs } +// ToolList returns the registered tools in registration order. +func (r *Registry) ToolList() []ToolInfo { + out := make([]ToolInfo, 0, len(r.order)) + + for _, name := range r.order { + t := r.tools[name] + + out = append(out, ToolInfo{ + Name: t.Name(), + Description: t.Description(), + Mutating: t.Mutating(), + }) + } + + return out +} + // DefaultRegistry returns the standard coding-agent toolset. func DefaultRegistry() *Registry { bg := newBGManager() // shared by bash / bash_output / kill_shell diff --git a/internal/tui/model.go b/internal/tui/model.go index beec13c..7b049e7 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -1303,6 +1303,7 @@ var slashCmds = []slashCmd{ {"/clear", "reset the conversation"}, {"/purge", "delete ALL saved sessions"}, {"/update", "update to the latest release"}, + {"/tools", "list all available tools"}, {"/help", "show detailed help"}, {"/exit", "quit (the session is saved)"}, } @@ -2044,6 +2045,25 @@ func (m model) command(input string) (tea.Model, tea.Cmd) { return m.attachSession(fields) case "/help": return m, tea.Printf("%s", renderHelp(m.md, m.width)) + case "/tools": + var b strings.Builder + + b.WriteString("### Available Tools\n\n") + + for _, t := range m.agent.ToolList() { + mode := "Read-only" + if t.Mutating { + mode = "Permission-gated" + } + + fmt.Fprintf(&b, "- **%s** (%s) — %s\n", + t.Name, + mode, + t.Description, + ) + } + + return m, tea.Printf("%s", renderTools(m.md, m.width, b.String())) case "/learn": // Run the project-learn task as a turn (writing BORG.md prompts for // permission like any edit). Same harness config as `borg learn`. @@ -2604,6 +2624,17 @@ func renderHelp(md *mdRenderer, width int) string { return out } +func renderTools(md *mdRenderer, width int, text string) string { + if md != nil { + return strings.TrimRight(md.render(text, width), "\n") + } + out, err := glamour.Render(text, "dark") + if err != nil { + return text + } + return out +} + // privacyURL is borg's canonical privacy policy. const privacyURL = "https://www.xshellz.com/privacy-policy" diff --git a/internal/tui/repl_test.go b/internal/tui/repl_test.go index b629c86..12fd0fe 100644 --- a/internal/tui/repl_test.go +++ b/internal/tui/repl_test.go @@ -163,11 +163,14 @@ func TestCommandHelpExitUnknownPurge(t *testing.T) { _, cmd := slash(m, "/help") require.NotNil(t, cmd) + _, cmd = slash(m, "/tools") + require.NotNil(t, cmd) + mq, _ := slash(m, "/exit") require.True(t, mq.quitting) _, cmd = slash(m, "/bogus") - require.NotNil(t, cmd) // prints an "unknown command" line + require.NotNil(t, cmd) mp, _ := slash(m, "/purge") require.Equal(t, modeConfirmPurge, mp.mode)