feat: add sub-agent task delegation and UI (#159)

This commit is contained in:
Ismail Pelaseyed authored and GitHub committed 2026-03-19 11:16:32 +01:00
1 parent 8f8641cfcb
commit 28b4f87980
6 files changed
+388 -66

No files matched your search

+193 -10
View File
@@ -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<ToolResult> {
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<StreamChunk, void, unknown> {
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<string, string> {
if (!args || typeof args !== "object") return {};
const result: Record<string, string> = {};
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)}…`;
}
+67 -33
View File
@@ -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<ToolResult>;
}
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<string, any> = { ...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;
}
+13 -3
View File
@@ -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<ToolResult> {
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);
+20
View File
@@ -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 {
+94 -20
View File
@@ -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<Plan | null>(null);
const [activeSubagent, setActiveSubagent] = useState<SubagentStatus | null>(null);
const [pqs, setPqs] = useState<PlanQuestionsState>(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) => (
<InlineTool key={i} t={t} pending>{toolLabel(tc)}</InlineTool>
tc.function.name === "task"
? <SubagentTaskLine key={i} t={t} label={toolArgs(tc) || "Working"} pending />
: <InlineTool key={i} t={t} pending>{toolLabel(tc)}</InlineTool>
))}
{activeSubagent && <SubagentActivity t={t} status={activeSubagent} />}
{/* Streaming assistant content */}
{streamContent && (
<box paddingLeft={3} marginTop={1} flexShrink={0}>
@@ -717,6 +732,10 @@ function MessageView({ entry, index, t, modeColor }: { entry: ChatEntry; index:
return <PlanView plan={plan} t={t} />;
}
if (name === "task" && entry.toolResult?.task) {
return <TaskResultView t={t} entry={entry} />;
}
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 (
<box paddingLeft={3}>
<text>
<span style={{ fg: "#555555" }}>{"◇ "}</span>
{text.split("").map((ch, i) => {
const dist = Math.abs(i - pos);
const fg = dist === 0 ? bright : dist === 1 ? mid : dim;
return <span key={i} style={{ fg }}>{ch}</span>;
})}
<span style={{ fg: t.textMuted }}>
<LoadingSpinner />
</span>
<span style={{ fg: t.textMuted }}>{" "}{text}</span>
</text>
</box>
);
@@ -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 (
<box paddingLeft={3}>
<text>
{pending ? (
<span style={{ fg: t.subagentAccent }}>
<LoadingSpinner />
</span>
) : null}
{pending ? " " : ""}
<span style={{ fg: t.subagentAccent }}>
<b>{`Sub-agent: ${displayLabel}`}</b>
</span>
</text>
</box>
);
}
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 (
<box paddingLeft={5}>
<text fg={t.textMuted}>
{"→ "}{truncateLine(status.detail, 100)}
</text>
</box>
);
}
function TaskResultView({ t, entry }: { t: Theme; entry: ChatEntry }) {
const task = entry.toolResult?.task;
if (!task) return null;
return (
<box gap={0}>
<SubagentTaskLine t={t} label={task.description} pending={false} />
<box paddingLeft={5}>
<text fg={t.text}>
{task.agent}
{": "}
{truncateLine(task.summary, 90)}
</text>
</box>
</box>
);
}
/* ── 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);
+1
View File
@@ -9,6 +9,7 @@ export const dark = {
textDim: "#444444",
primary: "#ffffff",
accent: "#5c9cf5",
subagentAccent: "#66d9c2",
selected: "#ffffff",
selectedBg: "#2a2a2a",
diffAdded: "#1e3a1e",