Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions frontend/src/components/AgentMonitor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@
let knownProjects = $state([]);
let modelsByProvider = $state({});
let allModels = $state([]);
let runnableTools = $state([]);
let pollTimer;
let logTimer;

async function loadTools() {
try {
const r = await fetch("/api/tools");
if (r.ok) runnableTools = await r.json();
} catch { /* ignored */ }
}

async function loadSessionData() {
try {
const sessions = await fetchSessions();
Expand Down Expand Up @@ -221,6 +229,7 @@
onMount(() => {
loadAgents();
loadSessionData();
loadTools();
pollTimer = setInterval(() => {
loadAgents();
if (selectedAgent?.status === "running") {
Expand Down Expand Up @@ -535,10 +544,12 @@
<div class="qr-field">
<label for="qr-tool">Tool</label>
<select id="qr-tool" bind:value={quickForm.tool} onchange={() => { quickForm.model = ''; }}>
<option value="claude">Claude</option>
<option value="codex">Codex</option>
<option value="gemini">Gemini</option>
<option value="opencode">OpenCode</option>
{#each runnableTools.filter(t => t.installed) as t (t.id)}
<option value={t.id}>{t.name}</option>
{/each}
{#if !runnableTools.length}
<option value="claude">Claude</option>
{/if}
</select>
</div>
<div class="qr-field">
Expand Down
26 changes: 18 additions & 8 deletions frontend/src/components/ChatView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@
let knownProjects = $state([]);
let modelsByProvider = $state({});
let allModels = $state([]);
let runnableTools = $state([]);
let messagesEnd = $state();

async function loadTools() {
try {
const r = await fetch("/api/tools");
if (r.ok) runnableTools = await r.json();
} catch { /* ignored */ }
}

async function loadSessionData() {
try {
const r = await fetch("/api/sessions");
Expand Down Expand Up @@ -180,6 +188,7 @@
onMount(() => {
loadChats();
loadSessionData();
loadTools();
});
</script>

Expand Down Expand Up @@ -215,10 +224,12 @@
<div class="chat-form-field">
<label for="chat-tool">Tool</label>
<select id="chat-tool" bind:value={newChatForm.tool} onchange={() => { newChatForm.model = ''; }}>
<option value="claude">Claude</option>
<option value="codex">Codex</option>
<option value="gemini">Gemini</option>
<option value="opencode">OpenCode</option>
{#each runnableTools.filter(t => t.installed) as t (t.id)}
<option value={t.id}>{t.name}</option>
{/each}
{#if !runnableTools.length}
<option value="claude">Claude</option>
{/if}
</select>
</div>
<div class="chat-form-field">
Expand Down Expand Up @@ -335,10 +346,9 @@
<div class="chat-input-opts">
<select class="chat-opt-select" bind:value={msgTool} title="Tool for this message">
<option value="">{activeChat.tool}</option>
<option value="claude">Claude</option>
<option value="codex">Codex</option>
<option value="gemini">Gemini</option>
<option value="opencode">OpenCode</option>
{#each runnableTools.filter(t => t.installed && t.id !== activeChat.tool) as t (t.id)}
<option value={t.id}>{t.name}</option>
{/each}
</select>
<select class="chat-opt-select" bind:value={msgModel} title="Model for this message">
<option value="">{activeChat.model || 'default'}</option>
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/components/Scheduler.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
let allModels = $state([]);
let knownProjects = $state([]);
let customProject = $state(false);
let runnableTools = $state([]);

async function loadTools() {
try {
const r = await fetch("/api/tools");
if (r.ok) runnableTools = await r.json();
} catch { /* ignored */ }
}

async function loadSessionData() {
try {
Expand Down Expand Up @@ -94,6 +102,7 @@

load();
loadSessionData();
loadTools();
loadBoards();
let pollInterval = setInterval(load, 10000);

Expand Down Expand Up @@ -342,10 +351,12 @@
<div class="form-group">
<label for="job-tool">Tool</label>
<select id="job-tool" bind:value={form.tool} onchange={() => { form.model = ''; }}>
<option value="claude">Claude</option>
<option value="codex">Codex</option>
<option value="gemini">Gemini</option>
<option value="opencode">OpenCode</option>
{#each runnableTools.filter(t => t.installed) as t (t.id)}
<option value={t.id}>{t.name}</option>
{/each}
{#if !runnableTools.length}
<option value="claude">Claude</option>
{/if}
</select>
</div>
<div class="form-group">
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/ToolInventory.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@
<span class="tool-name">{tool.name}</span>
<span class="tool-absent">Not found</span>
</div>
{#if tool.installHint}
<div class="tool-detail" style="padding:.3rem .6rem .4rem">
<div class="tool-row"><span class="row-label">Install</span><code class="install-hint">{tool.installHint}</code></div>
</div>
{/if}
</div>
{/each}
</div>
Expand Down
15 changes: 8 additions & 7 deletions internal/inventory/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package inventory
import "time"

type ToolInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Installed bool `json:"installed"`
Version string `json:"version,omitempty"`
BinaryPath string `json:"binaryPath,omitempty"`
ConfigDir string `json:"configDir,omitempty"`
DataDir string `json:"dataDir,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Installed bool `json:"installed"`
Version string `json:"version,omitempty"`
BinaryPath string `json:"binaryPath,omitempty"`
ConfigDir string `json:"configDir,omitempty"`
DataDir string `json:"dataDir,omitempty"`
InstallHint string `json:"installHint,omitempty"`
}

type ModelUsage struct {
Expand Down
45 changes: 38 additions & 7 deletions internal/inventory/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,45 @@ import (
)

type toolDef struct {
ID string
Name string
Binary string
ConfigDir string // relative to home
DataDir string // relative to home
ID string
Name string
Binary string
ConfigDir string // relative to home
DataDir string // relative to home
VersionFlag string
InstallHint string
}

var toolDefs = []toolDef{
{
ID: "claude", Name: "Claude Code", Binary: "claude",
ConfigDir: ".claude", DataDir: ".claude/projects",
VersionFlag: "--version",
InstallHint: "npm install -g @anthropic-ai/claude-code",
},
{
ID: "codex", Name: "Codex CLI", Binary: "codex",
ConfigDir: ".codex", DataDir: ".codex",
VersionFlag: "--version",
InstallHint: "npm install -g @openai/codex",
},
{
ID: "copilot", Name: "Copilot CLI", Binary: "github-copilot",
ConfigDir: ".copilot", DataDir: ".copilot/session-state",
VersionFlag: "--version",
InstallHint: "npm install -g @github/copilot",
},
{
ID: "gemini", Name: "Gemini CLI", Binary: "gemini",
ConfigDir: ".gemini", DataDir: ".gemini/tmp",
VersionFlag: "--version",
InstallHint: "npm install -g @google/gemini-cli",
},
{
ID: "opencode", Name: "OpenCode", Binary: "opencode",
ConfigDir: ".config/opencode", DataDir: ".local/share/opencode",
VersionFlag: "--version",
InstallHint: "go install github.com/opencode-ai/opencode@latest",
},
{
ID: "cursor-cli", Name: "Cursor Agent (CLI)", Binary: "agent",
Expand All @@ -54,35 +60,48 @@ var toolDefs = []toolDef{
ID: "cursor-ide", Name: "Cursor (IDE)", Binary: "cursor",
ConfigDir: ".config/Cursor", DataDir: ".config/Cursor/User/globalStorage",
VersionFlag: "--version",
InstallHint: "Download from cursor.com",
},
{
ID: "antigravity", Name: "Antigravity", Binary: "antigravity",
ConfigDir: ".gemini/antigravity", DataDir: ".gemini/antigravity/conversations",
VersionFlag: "--version",
InstallHint: "Download from developers.google.com/gemini",
},
{
ID: "agy", Name: "Agy CLI", Binary: "agy",
ConfigDir: ".gemini/antigravity-cli", DataDir: ".gemini/antigravity-cli/conversations",
VersionFlag: "--version",
InstallHint: "npm install -g agy",
},
{
ID: "aider", Name: "Aider", Binary: "aider",
VersionFlag: "--version",
InstallHint: "pip install aider-chat",
},
{
ID: "goose", Name: "Goose", Binary: "goose",
ConfigDir: ".config/goose",
VersionFlag: "--version",
InstallHint: "Download from github.com/block/goose/releases",
},
{
ID: "amp", Name: "Amp", Binary: "amp",
ConfigDir: ".config/amp",
VersionFlag: "--version",
InstallHint: "npm install -g @sourcegraph/amp",
},
{
ID: "devin", Name: "Devin", Binary: "devin",
ConfigDir: ".config/devin",
VersionFlag: "--version",
InstallHint: "Download from devin.ai",
},
{
ID: "amazonq", Name: "Amazon Q Developer", Binary: "q",
ConfigDir: ".aws/amazonq",
VersionFlag: "--version",
InstallHint: "Download from aws.amazon.com/q/developer",
},
{
ID: "continue", Name: "Continue.dev", Binary: "continue",
Expand All @@ -91,9 +110,11 @@ var toolDefs = []toolDef{
{
ID: "windsurf", Name: "Windsurf", Binary: "windsurf",
ConfigDir: ".codeium/windsurf",
InstallHint: "Download from windsurf.com",
},
{
ID: "trae", Name: "Trae", Binary: "trae",
InstallHint: "Download from trae.ai",
},
{
ID: "kilo", Name: "Kilo Code", Binary: "kilo",
Expand All @@ -105,6 +126,15 @@ var toolDefs = []toolDef{
},
}

func InstallHint(binary string) string {
for _, def := range toolDefs {
if def.Binary == binary {
return def.InstallHint
}
}
return ""
}

func init() {
if runtime.GOOS == "darwin" {
for i := range toolDefs {
Expand All @@ -122,8 +152,9 @@ func scanTools() []ToolInfo {

for _, def := range toolDefs {
t := ToolInfo{
ID: def.ID,
Name: def.Name,
ID: def.ID,
Name: def.Name,
InstallHint: def.InstallHint,
}

if bin, err := exec.LookPath(def.Binary); err == nil {
Expand Down
Loading
Loading