From 28b4f87980f26958b4ceb2fc80512ba20d09304f Mon Sep 17 00:00:00 2001 From: Ismail Pelaseyed Date: Thu, 19 Mar 2026 11:16:32 +0100 Subject: [PATCH] feat: add sub-agent task delegation and UI (#159) --- src/agent/agent.ts | 203 ++++++++++++++++++++++++++++++++++++++++++--- src/grok/tools.ts | 100 ++++++++++++++-------- src/tools/bash.ts | 16 +++- src/types/index.ts | 20 +++++ src/ui/app.tsx | 114 ++++++++++++++++++++----- src/ui/theme.ts | 1 + 6 files changed, 388 insertions(+), 66 deletions(-) diff --git a/src/agent/agent.ts b/src/agent/agent.ts index 07ba6b1..675378b 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -3,7 +3,15 @@ import { createProvider, generateTitle as genTitle, type XaiProvider } from "../ import { createTools } from "../grok/tools.js"; import { BashTool } from "../tools/bash.js"; import { loadCustomInstructions } from "../utils/instructions.js"; -import type { AgentMode, Plan, StreamChunk, ToolCall, ToolResult } from "../types/index.js"; +import type { + AgentMode, + Plan, + StreamChunk, + SubagentStatus, + TaskRequest, + ToolCall, + ToolResult, +} from "../types/index.js"; const MAX_TOOL_ROUNDS = 400; @@ -26,16 +34,31 @@ TOOLS: - write_file: Create new files or overwrite existing ones with full content. - edit_file: Replace a unique string in a file with new content. The old_string must be unique — include enough context lines. - bash: Execute shell commands — searching (grep/rg/find), git, builds, tests, package managers, etc. +- task: Delegate a focused task to a sub-agent. Use general for multi-step execution and explore for fast read-only research. - search_web: Search the web for current information, documentation, APIs, tutorials, etc. - search_x: Search X/Twitter for real-time posts, discussions, opinions, and trends. WORKFLOW: 1. Understand the request -2. Use read_file and bash to explore the codebase -3. Use edit_file for targeted changes, write_file for new files or full rewrites -4. Verify changes by reading modified files -5. Run tests or builds with bash to confirm correctness -6. Use search_web or search_x when you need up-to-date information +2. Decide whether a sub-agent should handle the first investigation pass +3. Use read_file and bash to explore the codebase directly when the task is small or tightly scoped +4. Use edit_file for targeted changes, write_file for new files or full rewrites +5. Verify changes by reading modified files +6. Run tests or builds with bash to confirm correctness +7. Use search_web or search_x when you need up-to-date information + +DEFAULT DELEGATION POLICY: +- Prefer the task tool by default for code review, code quality analysis, architecture research, root-cause investigation, bug triage, or any request that likely needs reading multiple files before acting. +- Use the explore sub-agent for read-only investigation, reviews, research, and "how does this work?" tasks. +- Use the general sub-agent for delegated work that may need editing files, running commands, or producing a concrete implementation. +- Do not wait for the user to explicitly ask for a sub-agent when delegation would clearly help. +- Skip delegation only when the task is trivial, single-file, or you already have the exact answer. + +EXAMPLES: +- "review this change" -> delegate to explore first +- "research how auth works" -> delegate to explore first +- "investigate why this test fails" -> delegate to explore first, then continue with findings +- "refactor this module" -> delegate a focused part to general when helpful IMPORTANT: - Prefer edit_file for surgical changes to existing files — it shows a clean diff. @@ -93,6 +116,40 @@ function buildSystemPrompt(cwd: string, mode: AgentMode, planContext?: string | Current working directory: ${cwd}`; } +function buildSubagentPrompt(request: TaskRequest, cwd: string): string { + const mode: AgentMode = request.agent === "explore" ? "ask" : "agent"; + const role = + request.agent === "explore" + ? "You are the Explore sub-agent. You are read-only and focus on fast codebase research." + : "You are the General sub-agent. You can investigate, edit files, and run commands to complete delegated work."; + + const rules = + request.agent === "explore" + ? [ + "Do not create, modify, or delete files.", + "Prefer `read_file` and search commands over broad shell exploration.", + "Return concise findings for the parent agent.", + ] + : [ + "Work only on the delegated task below.", + "Use tools directly instead of narrating your intent.", + "Return a concise summary for the parent agent with key outcomes and any open risks.", + ]; + + return [ + role, + "", + "You are helping a parent agent. Do not address the end user directly.", + "Focus tightly on the delegated scope and summarize what matters back to the parent agent.", + "", + ...rules, + "", + `Delegated task: ${request.description}`, + "", + buildSystemPrompt(cwd, mode), + ].join("\n"); +} + export class Agent { private provider: XaiProvider; private bash: BashTool; @@ -103,6 +160,7 @@ export class Agent { private modelId: string; private maxTokens: number; private planContext: string | null = null; + private subagentStatusListeners = new Set<(status: SubagentStatus | null) => void>(); constructor(apiKey: string, baseURL?: string, model?: string, maxToolRounds?: number) { this.provider = createProvider(apiKey, baseURL); @@ -152,8 +210,96 @@ export class Agent { this.messages = []; } + onSubagentStatus(listener: (status: SubagentStatus | null) => void): () => void { + this.subagentStatusListeners.add(listener); + return () => { + this.subagentStatusListeners.delete(listener); + }; + } + + private emitSubagentStatus(status: SubagentStatus | null): void { + for (const listener of this.subagentStatusListeners) { + listener(status); + } + } + + private async runTask(request: TaskRequest): Promise { + const childMode: AgentMode = request.agent === "explore" ? "ask" : "agent"; + const childBash = new BashTool(this.bash.getCwd()); + const childTools = createTools(childBash, this.provider, childMode); + const initialDetail = request.agent === "explore" ? "Scanning the codebase" : "Planning delegated work"; + let assistantText = ""; + let lastActivity = initialDetail; + + this.emitSubagentStatus({ + agent: request.agent, + description: request.description, + detail: initialDetail, + }); + + try { + const result = streamText({ + model: this.provider(request.agent === "explore" ? "grok-4-1-fast" : this.modelId), + system: buildSubagentPrompt(request, childBash.getCwd()), + messages: [{ role: "user", content: request.prompt }], + tools: childTools, + stopWhen: stepCountIs(Math.min(this.maxToolRounds, request.agent === "explore" ? 60 : 120)), + maxRetries: 0, + abortSignal: this.abortController?.signal, + temperature: request.agent === "explore" ? 0.2 : 0.5, + maxOutputTokens: Math.min(this.maxTokens, 8_192), + }); + + for await (const part of result.fullStream) { + if (part.type === "text-delta") { + assistantText += part.text; + continue; + } + + if (part.type === "tool-call") { + lastActivity = formatSubagentActivity(part.toolName, part.input); + this.emitSubagentStatus({ + agent: request.agent, + description: request.description, + detail: lastActivity, + }); + } + } + + await result.response; + + const output = assistantText.trim() || `Task completed. Last action: ${lastActivity}`; + return { + success: true, + output, + task: { + agent: request.agent, + description: request.description, + summary: firstLine(output), + activity: lastActivity, + }, + }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + const output = `Task failed: ${msg}`; + return { + success: false, + output, + task: { + agent: request.agent, + description: request.description, + summary: output, + activity: lastActivity, + }, + }; + } finally { + this.emitSubagentStatus(null); + } + } + async *processMessage(userMessage: string): AsyncGenerator { this.abortController = new AbortController(); + this.emitSubagentStatus(null); this.messages.push({ role: "user", content: userMessage }); @@ -161,7 +307,9 @@ export class Agent { let streamOk = false; try { - const tools = createTools(this.bash, this.provider, this.mode); + const tools = createTools(this.bash, this.provider, this.mode, { + runTask: (request) => this.runTask(request), + }); const system = buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext); this.planContext = null; @@ -246,27 +394,62 @@ export class Agent { } } -function toToolCall(part: { toolCallId: string; toolName: string; args?: unknown }): ToolCall { +function toToolCall(part: { toolCallId: string; toolName: string; args?: unknown; input?: unknown }): ToolCall { return { id: part.toolCallId, type: "function", function: { name: part.toolName, - arguments: JSON.stringify(part.args ?? {}), + arguments: JSON.stringify(part.input ?? part.args ?? {}), }, }; } function toToolResult(output: unknown): ToolResult { if (output && typeof output === "object" && "success" in output) { - const r = output as { success: boolean; output?: string; diff?: ToolResult["diff"]; plan?: Plan }; + const r = output as { + success: boolean; + output?: string; + diff?: ToolResult["diff"]; + plan?: Plan; + task?: ToolResult["task"]; + }; return { success: r.success, output: r.output, error: r.success ? undefined : r.output, diff: r.diff, plan: r.plan, + task: r.task, }; } return { success: true, output: String(output) }; } + +function formatSubagentActivity(toolName: string, args?: unknown): string { + const parsed = parseToolArgs(args); + if (toolName === "read_file") return `Read ${parsed.path || "file"}`; + if (toolName === "write_file") return `Write ${parsed.path || "file"}`; + if (toolName === "edit_file") return `Edit ${parsed.path || "file"}`; + if (toolName === "search_web") return `Web search "${truncate(parsed.query || "", 50)}"`; + if (toolName === "search_x") return `X search "${truncate(parsed.query || "", 50)}"`; + if (toolName === "bash") return truncate(parsed.command || "Run command", 70); + return truncate(`${toolName}`, 70); +} + +function parseToolArgs(args: unknown): Record { + if (!args || typeof args !== "object") return {}; + const result: Record = {}; + for (const [key, value] of Object.entries(args)) { + result[key] = typeof value === "string" ? value : JSON.stringify(value); + } + return result; +} + +function firstLine(text: string): string { + return text.trim().split("\n").find(Boolean)?.trim() || "Task completed."; +} + +function truncate(text: string, max: number): string { + return text.length <= max ? text : `${text.slice(0, max - 1)}…`; +} diff --git a/src/grok/tools.ts b/src/grok/tools.ts index 9faa194..d4daaca 100644 --- a/src/grok/tools.ts +++ b/src/grok/tools.ts @@ -3,11 +3,20 @@ import { z } from "zod"; import type { BashTool } from "../tools/bash.js"; import { readFile, writeFile, editFile } from "../tools/file.js"; import type { XaiProvider } from "./client.js"; -import type { AgentMode } from "../types/index.js"; +import type { AgentMode, TaskRequest, ToolResult } from "../types/index.js"; const SEARCH_MODEL = "grok-3-mini-fast"; -export function createTools(bash: BashTool, provider: XaiProvider, mode: AgentMode = "agent") { +interface CreateToolsOptions { + runTask?: (request: TaskRequest) => Promise; +} + +export function createTools( + bash: BashTool, + provider: XaiProvider, + mode: AgentMode = "agent", + options: CreateToolsOptions = {}, +) { const cwd = () => bash.getCwd(); const base = { @@ -53,31 +62,6 @@ export function createTools(bash: BashTool, provider: XaiProvider, mode: AgentMo }, }), - write_file: tool({ - description: - "Create or overwrite a file with the given content. Use for creating new files or completely rewriting existing ones. Returns a diff of the changes.", - inputSchema: z.object({ - path: z.string().describe("File path (relative to cwd or absolute)"), - content: z.string().describe("The full file content to write"), - }), - execute: async ({ path, content }) => { - return writeFile(path, content, cwd()); - }, - }), - - edit_file: tool({ - description: - "Edit a file by replacing a unique string with new content. The old_string must appear exactly once in the file. Include enough surrounding context lines in old_string to make it unique. Returns a diff of the changes.", - inputSchema: z.object({ - path: z.string().describe("File path (relative to cwd or absolute)"), - old_string: z.string().describe("The exact text to find (must be unique in the file)"), - new_string: z.string().describe("The replacement text"), - }), - execute: async ({ path, old_string, new_string }) => { - return editFile(path, old_string, new_string, cwd()); - }, - }), - search_web: tool({ description: "Search the web for current information, documentation, APIs, tutorials, news, or any real-time data. Returns summarized results with sources.", @@ -140,11 +124,60 @@ export function createTools(bash: BashTool, provider: XaiProvider, mode: AgentMo }; - if (mode !== "plan") return base; + const tools: Record = { ...base }; - return { - ...base, - generate_plan: tool({ + if (mode === "agent") { + tools.write_file = tool({ + description: + "Create or overwrite a file with the given content. Use for creating new files or completely rewriting existing ones. Returns a diff of the changes.", + inputSchema: z.object({ + path: z.string().describe("File path (relative to cwd or absolute)"), + content: z.string().describe("The full file content to write"), + }), + execute: async ({ path, content }) => { + return writeFile(path, content, cwd()); + }, + }); + + tools.edit_file = tool({ + description: + "Edit a file by replacing a unique string with new content. The old_string must appear exactly once in the file. Include enough surrounding context lines in old_string to make it unique. Returns a diff of the changes.", + inputSchema: z.object({ + path: z.string().describe("File path (relative to cwd or absolute)"), + old_string: z.string().describe("The exact text to find (must be unique in the file)"), + new_string: z.string().describe("The replacement text"), + }), + execute: async ({ path, old_string, new_string }) => { + return editFile(path, old_string, new_string, cwd()); + }, + }); + + if (options.runTask) { + tools.task = tool({ + description: + "Delegate a focused task to a sub-agent. Prefer this proactively for review, research, investigation, and code quality work instead of waiting for the user to request a sub-agent. Use `general` for multi-step execution and `explore` for fast read-only investigation. Provide a short description plus a detailed prompt for the child agent.", + inputSchema: z.object({ + agent: z + .enum(["general", "explore"]) + .default("general") + .describe("Which sub-agent to use"), + description: z + .string() + .describe("A short label for the delegated task, such as 'Deep code quality analysis'"), + prompt: z + .string() + .describe("Detailed instructions for the sub-agent to complete"), + }), + execute: async ({ agent, description, prompt }) => { + return options.runTask!({ agent, description, prompt }); + }, + }); + } + } + + if (mode !== "plan") return tools; + + tools.generate_plan = tool({ description: "Generate an interactive implementation plan with steps and optional questions for the user. The plan is displayed in a structured UI where the user can review steps and answer questions. Always use this tool when creating plans.", inputSchema: z.object({ @@ -197,6 +230,7 @@ export function createTools(bash: BashTool, provider: XaiProvider, mode: AgentMo plan: { title, summary, steps, questions }, }; }, - }), - }; + }); + + return tools; } diff --git a/src/tools/bash.ts b/src/tools/bash.ts index 3c035ca..5331cb7 100644 --- a/src/tools/bash.ts +++ b/src/tools/bash.ts @@ -1,19 +1,29 @@ import { exec } from "child_process"; +import { stat } from "fs/promises"; +import path from "path"; import { promisify } from "util"; import type { ToolResult } from "../types/index.js"; const execAsync = promisify(exec); export class BashTool { - private cwd: string = process.cwd(); + private cwd: string; + + constructor(initialCwd = process.cwd()) { + this.cwd = initialCwd; + } async execute(command: string, timeout = 30_000): Promise { try { if (command.startsWith("cd ")) { const dir = command.substring(3).trim().replace(/^["']|["']$/g, ""); try { - process.chdir(dir); - this.cwd = process.cwd(); + const nextCwd = path.resolve(this.cwd, dir); + const info = await stat(nextCwd); + if (!info.isDirectory()) { + return { success: false, error: `Cannot change directory: ${nextCwd} is not a directory` }; + } + this.cwd = nextCwd; return { success: true, output: `Changed directory to: ${this.cwd}` }; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); diff --git a/src/types/index.ts b/src/types/index.ts index a5d7bfa..2309f39 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,12 +27,32 @@ export interface Plan { questions?: PlanQuestion[]; } +export interface TaskRequest { + agent: "general" | "explore"; + description: string; + prompt: string; +} + +export interface TaskRun { + agent: "general" | "explore"; + description: string; + summary: string; + activity?: string; +} + +export interface SubagentStatus { + agent: "general" | "explore"; + description: string; + detail: string; +} + export interface ToolResult { success: boolean; output?: string; error?: string; diff?: FileDiff; plan?: Plan; + task?: TaskRun; } export interface ToolCall { diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 34d3d04..1b7acd2 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -4,7 +4,16 @@ import { useKeyboard, useTerminalDimensions } from "@opentui/react"; import type { ScrollBoxRenderable, TextareaRenderable, KeyBinding } from "@opentui/core"; import { decodePasteBytes, PasteEvent } from "@opentui/core"; import type { Agent } from "../agent/agent.js"; -import type { ChatEntry, ToolCall, AgentMode, ModelInfo, FileDiff, Plan, PlanQuestion } from "../types/index.js"; +import type { + ChatEntry, + ToolCall, + AgentMode, + ModelInfo, + FileDiff, + Plan, + PlanQuestion, + SubagentStatus, +} from "../types/index.js"; import { MODES } from "../types/index.js"; import { getModelInfo, MODELS } from "../grok/models.js"; import { saveProjectSettings } from "../utils/settings.js"; @@ -13,6 +22,7 @@ import { Markdown } from "./markdown.js"; import { PlanView, PlanQuestionsPanel, formatPlanAnswers, initialPlanQuestionsState, type PlanAnswers, type PlanQuestionsState } from "./plan.js"; const STAR_PALETTE = ["#777777", "#666666", "#4a4a4a", "#333333", "#222222"]; +const LOADING_SPINNER_FRAMES = ["⬒", "⬔", "⬓", "⬕"]; type Star = { col: number; ch: string }; type Row = { stars: Star[]; grok?: number }; @@ -126,6 +136,7 @@ export function App({ agent, initialMessage, onExit }: AppProps) { const [slashSearchQuery, setSlashSearchQuery] = useState(""); const [pasteBlocks, setPasteBlocks] = useState<{ id: number; content: string; lines: number; isImage?: boolean }[]>([]); const [activePlan, setActivePlan] = useState(null); + const [activeSubagent, setActiveSubagent] = useState(null); const [pqs, setPqs] = useState(initialPlanQuestionsState()); const imageCounterRef = useRef(0); const pasteCounterRef = useRef(0); @@ -176,7 +187,7 @@ export function App({ agent, initialMessage, onExit }: AppProps) { const processMessage = useCallback(async (text: string) => { if (!text.trim() || isProcessingRef.current) return; isProcessingRef.current = true; - setIsProcessing(true); setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); contentAccRef.current = ""; + setIsProcessing(true); setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); setActiveSubagent(null); contentAccRef.current = ""; startTimeRef.current = Date.now(); if (!sessionTitle) agent.generateTitle(text.trim()).then(setSessionTitle).catch(() => {}); setMessages((prev) => [...prev, { type: "user", content: text.trim(), timestamp: new Date(), modeColor: modeInfo.color }]); @@ -229,12 +240,13 @@ export function App({ agent, initialMessage, onExit }: AppProps) { setMessages((p) => [...p, { type: "assistant", content: finalContent, timestamp: new Date(), modeColor: modeInfo.color }]); } - contentAccRef.current = ""; setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); + contentAccRef.current = ""; setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); setActiveSubagent(null); isProcessingRef.current = false; setIsProcessing(false); setTimeout(scrollToBottom, 50); }, [agent, scrollToBottom, modeInfo, model]); useEffect(() => { if (initialMessage && !processedInitial.current) { processedInitial.current = true; processMessage(initialMessage); } }, [initialMessage, processMessage]); + useEffect(() => agent.onSubagentStatus(setActiveSubagent), [agent]); const handleCommand = useCallback((cmd: string): boolean => { const c = cmd.trim().toLowerCase(); @@ -545,8 +557,11 @@ export function App({ agent, initialMessage, onExit }: AppProps) { ))} {/* Active tool calls — pending inline */} {activeToolCalls.map((tc, i) => ( - {toolLabel(tc)} + tc.function.name === "task" + ? + : {toolLabel(tc)} ))} + {activeSubagent && } {/* Streaming assistant content */} {streamContent && ( @@ -717,6 +732,10 @@ function MessageView({ entry, index, t, modeColor }: { entry: ChatEntry; index: return ; } + if (name === "task" && entry.toolResult?.task) { + return ; + } + if (name === "write_file" || name === "edit_file") { const filePath = diff?.filePath || tryParseArg(entry.toolCall, "path") || args; const label = name === "write_file" ? `Write ${filePath}` : `Edit ${filePath}`; @@ -859,25 +878,13 @@ function DiffView({ t, diff }: { t: Theme; diff: FileDiff }) { } function ShimmerText({ t, text }: { t: Theme; text: string }) { - const [pos, setPos] = useState(0); - useEffect(() => { - const id = setInterval(() => setPos(p => (p + 1) % (text.length + 4)), 60); - return () => clearInterval(id); - }, [text]); - - const dim = "#333333"; - const mid = "#555555"; - const bright = "#888888"; - return ( - {"◇ "} - {text.split("").map((ch, i) => { - const dist = Math.abs(i - pos); - const fg = dist === 0 ? bright : dist === 1 ? mid : dim; - return {ch}; - })} + + + + {" "}{text} ); @@ -893,6 +900,65 @@ function InlineTool({ t, pending, children }: { t: Theme; pending: boolean; chil ); } +function SubagentTaskLine({ t, label, pending }: { t: Theme; label: string; pending: boolean }) { + const displayLabel = compactTaskLabel(label); + + return ( + + + {pending ? ( + + + + ) : null} + {pending ? " " : ""} + + {`Sub-agent: ${displayLabel}`} + + + + ); +} + +function LoadingSpinner() { + const [frame, setFrame] = useState(0); + + useEffect(() => { + const id = setInterval(() => setFrame((n) => (n + 1) % LOADING_SPINNER_FRAMES.length), 120); + return () => clearInterval(id); + }, []); + + return <>{LOADING_SPINNER_FRAMES[frame]}; +} + +function SubagentActivity({ t, status }: { t: Theme; status: SubagentStatus }) { + return ( + + + {"→ "}{truncateLine(status.detail, 100)} + + + ); +} + +function TaskResultView({ t, entry }: { t: Theme; entry: ChatEntry }) { + const task = entry.toolResult?.task; + if (!task) return null; + + return ( + + + + + {task.agent} + {": "} + {truncateLine(task.summary, 90)} + + + + ); +} + /* ── Slash Menu ──────────────────────────────────────────────── */ function SlashMenuModal({ t, selectedIndex, width, height, searchQuery, filteredItems }: { @@ -1000,6 +1066,7 @@ function toolArgs(tc?: ToolCall): string { const a = JSON.parse(tc.function.arguments); if (tc.function.name === "bash") return a.command || ""; if (tc.function.name === "read_file" || tc.function.name === "write_file" || tc.function.name === "edit_file") return a.path || ""; + if (tc.function.name === "task") return a.description || ""; return a.query || ""; } catch { return ""; } } @@ -1015,6 +1082,7 @@ function toolLabel(tc: ToolCall): string { if (tc.function.name === "edit_file") return `Edit ${trunc(args, 60)}`; if (tc.function.name === "search_web") return `Web Search "${trunc(args, 60)}"`; if (tc.function.name === "search_x") return `X Search "${trunc(args, 60)}"`; + if (tc.function.name === "task") return `Task ${trunc(args, 60)}`; if (tc.function.name === "generate_plan") return "Generating plan..."; return trunc(`${tc.function.name} ${args}`, 80); } @@ -1023,7 +1091,13 @@ function sanitizeContent(raw: string): string { s = s.replace(/\{"success"\s*:\s*(true|false)\s*,\s*"output"\s*:\s*"[\s\S]*$/m, ""); return s.trim(); } +function compactTaskLabel(label: string): string { + const words = label.trim().split(/\s+/).filter(Boolean); + if (words.length <= 3) return label.trim() || "Working"; + return `${words.slice(0, 3).join(" ")}...`; +} function trunc(s: string, n: number): string { return s.length <= n ? s : s.slice(0, n) + "…"; } +function truncateLine(s: string, n: number): string { return trunc(s.replace(/\s+/g, " ").trim(), n); } function formatDuration(ms: number): string { if (ms < 1000) return ""; const s = Math.round(ms / 1000); diff --git a/src/ui/theme.ts b/src/ui/theme.ts index d897834..71745c2 100644 --- a/src/ui/theme.ts +++ b/src/ui/theme.ts @@ -9,6 +9,7 @@ export const dark = { textDim: "#444444", primary: "#ffffff", accent: "#5c9cf5", + subagentAccent: "#66d9c2", selected: "#ffffff", selectedBg: "#2a2a2a", diffAdded: "#1e3a1e",