Skip to content

Commit ca2367a

Browse files
authored
🤖 feat: sub-workspaces as subagents (#1219)
Implements “sub-workspaces as subagents” by introducing agent Tasks backed by child workspaces spawned via the new `task` tool. - Built-in presets: `research`, `explore` - Config + UI for max parallel tasks / nesting depth - Restart-safe orchestration (queueing, report delivery to parent, auto-resume) - Explicit reporting via `agent_report` + leaf auto-cleanup - Sidebar nesting for child workspaces Validation: - `make static-check` - `bun test src/node/services/tools/task.test.ts src/node/services/taskService.test.ts` --- <details> <summary>📋 Implementation Plan</summary> # 🤖 Sub-workspaces as subagents (Mux) ## Decisions (confirmed) - **Lifecycle:** auto-delete the subagent workspace after it completes (and after its child tasks complete). - **Isolation (runtime-aware):** create subagent workspaces using the **parent workspace’s runtime** (`runtimeConfig`); prefer `runtime.forkWorkspace(...)` (when implemented) so the child starts from the parent’s branch. - **Results:** when the subagent finishes, it calls `agent_report` and we post the report back into the parent workspace. - **Limits (configurable):** max parallel subagents + max nesting depth (defaults: 3 parallel, depth 3). - **Durability:** if Mux restarts while tasks are running, tasks resume and the parent awaits existing tasks (no duplicate spawns). - **Delegation:** expose a `task` tool so any agent workspace can spawn sub-agent tasks (depth-limited). - **Built-in presets (v1):** **Research** + **Explore**. ## Recommended approach: Workspace Tasks *(net +~1700 LoC product code)* Represent each subagent as a **Task** (as described in `subagents.md`), implemented as a **child workspace** plus orchestration. This keeps the v1 scope small while keeping the API surface *task-shaped* so we can later reuse it for non-agent tasks (e.g., background bashes). ### High-level architecture ```mermaid flowchart TD Parent["Parent workspace"] TaskTool["tool: task"] Spawn["Task.create(parentId, kind=agent, agentType, prompt)"] Child["Child workspace (agent)"] ReportTool["tool-call-end: agent_report"] Report["Append report message to parent history + emit chat event"] Cleanup["Remove child workspace + delete runtime resources"] StreamEndNoReport["stream-end (no report)"] Reminder["Send reminder: toolPolicy requires agent_report"] Parent --> TaskTool --> Spawn --> Child Child --> ReportTool --> Report --> Cleanup Child --> StreamEndNoReport --> Reminder --> Child ``` ### Data model <details> <summary>Alignment with <code>subagents.md</code> (what we’re matching)</summary> - **Agent identity**: Claude’s `agentId` maps cleanly to Mux’s `workspaceId` for the child workspace. - **Spawning**: Claude’s `Task(subagent_type=…, prompt=…)` becomes Mux tool `task`, backed by `Task.create({ parentWorkspaceId, kind: "agent", agentType, prompt })`. - **Tool filtering**: Claude’s `tools`/`disallowedTools` maps to Mux’s existing `toolPolicy` (applied in order). - **Result propagation**: Agent tasks use an explicit `agent_report` tool call (child → parent) plus a backend retry if the tool wasn’t called. (Future: bash tasks can map to existing background bash output, or be unified behind a `Task.output` API.) - **Background vs foreground**: `task({ run_in_background: true, ... })` returns immediately; otherwise the tool blocks until the child calls `agent_report` (with a timeout). </details> Extend workspace metadata with optional fields: - `parentWorkspaceId?: string` — enables nesting in the UI - `agentType?: "research" | "explore" | string` — selects an agent preset (These are optional so existing configs stay valid.) ### Agent presets (built-in) Create a small registry of agent presets that define: - `toolPolicy` (enforced) - `systemPrompt` (preset-defined; can **replace** or append; v1 uses **replace** so each subagent can fully override the parent’s user instructions) Implementation detail: for agent task workspaces, treat the preset’s `systemPrompt` as the **effective** prompt (internal mode), instead of always appending to the parent workspace’s system message. - A required reporting mechanism: the agent must call `agent_report` exactly once when it has a final answer Initial presets: - **Research**: allow `web_search` + `web_fetch` (and optionally `file_read`), disallow edits. - **Explore**: allow *read-only* repo exploration (likely `file_read` + `bash` for `rg`/`git`), disallow file edits. Both presets should **enable**: - `task` (so agents can spawn subagents when useful) - `agent_report` (so leaf tasks have a single, explicit channel for reporting back) Enforce max nesting depth from settings (default 3) in the backend to prevent runaway recursion. > Note: Mux doesn’t currently have a “grep/glob” tool; Explore will either need `bash` or we add a future safe-search tool. --- ## Implementation steps ### 1) Schemas + types (IPC boundary) **Net +~50 LoC** - Extend: - `WorkspaceMetadataSchema` / `FrontendWorkspaceMetadataSchema` (`src/common/orpc/schemas/workspace.ts`) - `WorkspaceConfigSchema` (`src/common/orpc/schemas/project.ts`) - Thread the new fields through `WorkspaceMetadata` / `FrontendWorkspaceMetadata` types. ### 2) Persist config (workspace tree + task settings) **Net +~320 LoC** - Workspace tree fields - Ensure config write paths preserve `parentWorkspaceId` and `agentType`. - Update `Config.getAllWorkspaceMetadata()` to include the new fields when constructing metadata. - Task settings (global; shown in Settings UI) - Persist `taskSettings` in `~/.mux/config.json`, e.g.: - `maxParallelAgentTasks` (default 3) - `maxTaskNestingDepth` (default 3) - Settings UI - Add a small Settings section (e.g. “Tasks”) with two number inputs. - Read via `api.config.getConfig()`; persist via `api.config.saveConfig()`. - Clamp to safe ranges (e.g., parallel 1–10, depth 1–5) and show the defaults. - Task durability fields (per agent task workspace) - Persist a minimal task state for child workspaces (e.g., `taskStatus: queued|running|awaiting_report`) so we can rehydrate and resume after restart. ### 3) Backend Task API: Task.create **Net +~450 LoC** Add a new **task** operation (ORPC + service) that is intentionally generic: - `Task.create({ parentWorkspaceId, kind, ... })` - Return a task-shaped result: `{ taskId, kind, status }`. V1: implement `kind: "agent"` (sub-workspace agent task): 1. Validate parent workspace exists. 2. Enforce limits from `taskSettings` (configurable): - Max nesting depth (`maxTaskNestingDepth`, default 3) by walking the `parentWorkspaceId` chain. - Max parallel agent tasks (`maxParallelAgentTasks`, default 3) by counting running agent tasks globally (across the app). - If parallel limit is reached: persist as `status: "queued"` and start later (FIFO). 3. Create a new child workspace ID + generated name (e.g., `agent_research_<id>`; must match `[a-z0-9_-]{1,64}`). 4. **Runtime-aware:** create the child workspace using the parent workspace’s `runtimeConfig` (Local/Worktree/SSH). - Prefer `runtime.forkWorkspace(...)` (when implemented) so the child starts from the parent’s branch. - Otherwise fall back to `runtime.createWorkspace(...)` with the same runtime config (no branch isolation). 5. Write workspace config entry including `{ parentWorkspaceId, agentType, taskStatus }`. 6. When the task is started, send the initial prompt message into the child workspace. Durability / restart: - On app startup, rehydrate queued/running tasks from config and resume them: - queued tasks are scheduled respecting `maxParallelAgentTasks` - running tasks get a synthetic “Mux restarted; continue + call agent_report” message. - Parent await semantics (restart-safe): - While a parent workspace has any descendant agent tasks in `queued|running|awaiting_report`, treat it as “awaiting” and avoid starting new subagent tasks from it. - When the final descendant task reports, automatically resume any parent partial stream that was waiting on the `task` tool call. Design note: keep the return type “task-shaped” (e.g., `{ taskId, kind, status }`) so we can later add `kind: "bash"` tasks that wrap existing background bashes. ### 4) Tool: `task` (agents can spawn sub-agents) **Net +~250 LoC** Expose a Claude-like `Task` tool to the LLM (but backed by Mux workspaces): - Tool: `task` - Input (v1): `{ subagent_type: string, prompt: string, description?: string, run_in_background?: boolean }` - Behavior: - Spawn (or enqueue) a child agent task via `Task.create({ parentWorkspaceId: <current workspaceId>, kind: "agent", agentType: subagent_type, prompt, ... })`. - If `run_in_background` is true: return immediately `{ status: "queued" | "running", taskId }`. - Otherwise: block (potentially across queue + execution) until the child calls `agent_report` (or timeout) and return `{ status: "completed", reportMarkdown }`. - Durability: if this foreground wait is interrupted (app restart), the child task continues; when it reports, we persist the tool output into the parent message and auto-resume the parent stream. - Wire-up: add to `TOOL_DEFINITIONS` + register in `getToolsForModel()`; inject `taskService` into ToolConfiguration so the tool can call `Task.create`. - Guardrails - Enforce `maxTaskNestingDepth` and `maxParallelAgentTasks` from settings (defaults: depth 3, parallel 3). - If parallel limit is reached, new tasks are queued and the parent blocks/awaits until a slot is available. - Disallow spawning new tasks after the workspace has called `agent_report`. ### 5) Enforce preset tool policy + system prompt **Net +~130 LoC** In the backend send/stream path: - Compute an effective tool policy: - `effectivePolicy = [...(options.toolPolicy ?? []), ...presetPolicy]` - Apply *presetPolicy last* so callers cannot re-enable restricted tools. - System prompt strategy for agent task workspaces (per preset): - **Replace (default):** ignore the parent workspace’s user instructions and use the preset’s `systemPrompt` as the effective instructions (internal-only agent mode). - Implementation: add an internal system-message variant (e.g., `"agent"`) that starts from an empty base prompt (no user custom instructions), then apply `preset.systemPrompt`. - **Append (optional):** keep the normal workspace system message and append preset instructions. - Ensure the preset prompt covers: - When/how to delegate via the `task` tool (available `subagent_type`s). - When/how to call `agent_report` (final answer only; after any spawned tasks complete). ### 6) Auto-report back + auto-delete (orchestrator) **Net +~450 LoC** Add a small reporting tool + orchestrator that ensures the child reports back explicitly, and make it durable across restarts. - Tool: `agent_report` - Input: `{ reportMarkdown: string, title?: string }` (or similar) - Execution: no side effects; return `{ success: true }` (the backend uses the tool-call args as the report payload) - Wire-up: add to `TOOL_DEFINITIONS` + register in `getToolsForModel()` as a non-runtime tool - Orchestrator behavior - Primary path: handle `tool-call-end` for `agent_report` 1. Validate `workspaceId` is an agent task workspace and has `parentWorkspaceId`. 2. Persist completion (durable): - Update child workspace config: `taskStatus: "reported"` (+ `reportedAt`). 3. Deliver report to the parent (durable): - Append an assistant message to the parent workspace history (so the user can read the report). - If the parent has a partial assistant message containing a pending `task` tool call, update that tool part from `input-available` → `output-available` with `{ reportMarkdown, title }` (like the `ask_user_question` restart-safe fallback). - Emit `tool-call-end` + `workspace.onChat` events so the UI updates immediately. 4. Auto-resume the parent (durable tool call semantics): - If the parent has a partial message and **no active stream**, call `workspace.resumeStream(parent)` after writing the tool output. - Only auto-resume once the parent has **no remaining running descendant tasks** (so it doesn’t spawn duplicates). 5. Cleanup: - If the task has no remaining child tasks, delete the workspace + runtime resources (branch/worktree if applicable). - Otherwise, mark it pending cleanup and delete it once its subtree is gone. - Enforcement path: if a stream ends without an `agent_report` call 1. Send a synthetic "please report now" message into the child workspace with a toolPolicy that **requires only** `agent_report`. 2. If still missing after one retry, fall back to posting the child's final text parts (last resort) and clean up to avoid hanging sub-workspaces. ### 7) UI: nested sidebar rows **Net +~100 LoC** - Update sorting/rendering so child workspaces appear directly below the parent with indentation. - Add a small `depth` prop to `WorkspaceListItem` and adjust left padding. ### 8) No user-facing launcher (agent-orchestrated only) **Net +~0 LoC** - Do **not** add slash commands / command palette actions for spawning tasks. - Tasks are launched exclusively via the model calling the `task` tool from the parent workspace. ### 9) Tests **~200 LoC tests (not counted in product LoC estimate)** - Unit test: workspace tree flattening preserves parent→child adjacency. - Unit/integration test: `task` tool spawns/enqueues a child agent task and enforces `maxTaskNestingDepth`. - Unit/integration test: queueing respects `maxParallelAgentTasks` (extra tasks stay queued until a slot frees). - Unit/integration test: `agent_report` posts report to parent, updates waiting `task` tool output (restart-safe), and triggers cleanup (and reminder path when missing). - Unit test: toolPolicy merge guarantees presets can’t be overridden. <details> <summary>Follow-ups (explicitly out of scope for v1)</summary> - More presets (Review, Writer). “Writer” likely needs **non-auto-delete** so the branch/diff persists. - `Task.create(kind: "bash")` tasks that wrap existing background bashes (and optionally render under the parent like agent tasks). - Safe “code search” tools (Glob/Grep) to avoid granting `bash` to Explore. - Deeper nesting UX (collapse/expand, depth cap visuals). </details> </details> --- _Generated with `codex cli` • Model: `gpt-5.2` • Thinking: `xhigh`_ <!-- mux-attribution: model=gpt-5.2 thinking=xhigh --> --------- Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 32a9d27 commit ca2367a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+7103
-85
lines changed

.storybook/mocks/orpc.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ import type {
1313
} from "@/common/orpc/types";
1414
import type { ChatStats } from "@/common/types/chatStats";
1515
import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace";
16+
import {
17+
DEFAULT_TASK_SETTINGS,
18+
normalizeSubagentAiDefaults,
19+
normalizeTaskSettings,
20+
type SubagentAiDefaults,
21+
type TaskSettings,
22+
} from "@/common/types/tasks";
1623
import { createAsyncMessageQueue } from "@/common/utils/asyncMessageQueue";
1724

1825
/** Session usage data structure matching SessionUsageFileSchema */
@@ -46,6 +53,10 @@ export interface MockSessionUsage {
4653
export interface MockORPCClientOptions {
4754
projects?: Map<string, ProjectConfig>;
4855
workspaces?: FrontendWorkspaceMetadata[];
56+
/** Initial task settings for config.getConfig (e.g., Settings → Tasks section) */
57+
taskSettings?: Partial<TaskSettings>;
58+
/** Initial per-subagent AI defaults for config.getConfig (e.g., Settings → Tasks section) */
59+
subagentAiDefaults?: SubagentAiDefaults;
4960
/** Per-workspace chat callback. Return messages to emit, or use the callback for streaming. */
5061
onChat?: (workspaceId: string, emit: (msg: WorkspaceChatMessage) => void) => (() => void) | void;
5162
/** Mock for executeBash per workspace */
@@ -123,6 +134,8 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
123134
mcpServers = new Map(),
124135
mcpOverrides = new Map(),
125136
mcpTestResults = new Map(),
137+
taskSettings: initialTaskSettings,
138+
subagentAiDefaults: initialSubagentAiDefaults,
126139
} = options;
127140

128141
// Feature flags
@@ -140,6 +153,8 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
140153
};
141154

142155
const workspaceMap = new Map(workspaces.map((w) => [w.id, w]));
156+
let taskSettings = normalizeTaskSettings(initialTaskSettings ?? DEFAULT_TASK_SETTINGS);
157+
let subagentAiDefaults = normalizeSubagentAiDefaults(initialSubagentAiDefaults ?? {});
143158

144159
const mockStats: ChatStats = {
145160
consumers: [],
@@ -172,6 +187,16 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
172187
getSshHost: async () => null,
173188
setSshHost: async () => undefined,
174189
},
190+
config: {
191+
getConfig: async () => ({ taskSettings, subagentAiDefaults }),
192+
saveConfig: async (input: { taskSettings: unknown; subagentAiDefaults?: unknown }) => {
193+
taskSettings = normalizeTaskSettings(input.taskSettings);
194+
if (input.subagentAiDefaults !== undefined) {
195+
subagentAiDefaults = normalizeSubagentAiDefaults(input.subagentAiDefaults);
196+
}
197+
return undefined;
198+
},
199+
},
175200
providers: {
176201
list: async () => providersList,
177202
getConfig: async () => providersConfig,

src/browser/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
getModelKey,
4141
} from "@/common/constants/storage";
4242
import { migrateGatewayModel } from "@/browser/hooks/useGatewayModels";
43-
import { enforceThinkingPolicy } from "@/browser/utils/thinking/policy";
43+
import { enforceThinkingPolicy } from "@/common/utils/thinking/policy";
4444
import { getDefaultModel } from "@/browser/hooks/useModelsFromSettings";
4545
import type { BranchListResult } from "@/common/orpc/types";
4646
import { useTelemetry } from "./hooks/useTelemetry";

src/browser/components/AIView.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import { ReviewsBanner } from "./ReviewsBanner";
7373
import type { ReviewNoteData } from "@/common/types/review";
7474
import { PopoverError } from "./PopoverError";
7575
import { ConnectionStatusIndicator } from "./ConnectionStatusIndicator";
76+
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
7677

7778
interface AIViewProps {
7879
workspaceId: string;
@@ -99,6 +100,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
99100
status,
100101
}) => {
101102
const { api } = useAPI();
103+
const { workspaceMetadata } = useWorkspaceContext();
102104
const chatAreaRef = useRef<HTMLDivElement>(null);
103105

104106
// Track which right sidebar tab is selected (listener: true to sync with RightSidebar changes)
@@ -134,6 +136,14 @@ const AIViewInner: React.FC<AIViewProps> = ({
134136
const { statsTabState } = useFeatureFlags();
135137
const statsEnabled = Boolean(statsTabState?.enabled);
136138
const workspaceState = useWorkspaceState(workspaceId);
139+
const meta = workspaceMetadata.get(workspaceId);
140+
const isQueuedAgentTask = Boolean(meta?.parentWorkspaceId) && meta?.taskStatus === "queued";
141+
const queuedAgentTaskPrompt =
142+
isQueuedAgentTask && typeof meta?.taskPrompt === "string" && meta.taskPrompt.trim().length > 0
143+
? meta.taskPrompt
144+
: null;
145+
const shouldShowQueuedAgentTaskPrompt =
146+
Boolean(queuedAgentTaskPrompt) && (workspaceState?.messages.length ?? 0) === 0;
137147
const aggregator = useWorkspaceAggregator(workspaceId);
138148
const workspaceUsage = useWorkspaceUsage(workspaceId);
139149

@@ -727,6 +737,14 @@ const AIViewInner: React.FC<AIViewProps> = ({
727737
}
728738
/>
729739
)}
740+
{shouldShowQueuedAgentTaskPrompt && (
741+
<QueuedMessage
742+
message={{
743+
id: `queued-agent-task-${workspaceId}`,
744+
content: queuedAgentTaskPrompt ?? "",
745+
}}
746+
/>
747+
)}
730748
{workspaceState?.queuedMessage && (
731749
<QueuedMessage
732750
message={workspaceState.queuedMessage}
@@ -768,14 +786,25 @@ const AIViewInner: React.FC<AIViewProps> = ({
768786
/>
769787
<ReviewsBanner workspaceId={workspaceId} />
770788
<ConnectionStatusIndicator />
789+
{isQueuedAgentTask && (
790+
<div className="border-border-medium bg-background-secondary text-muted mb-2 rounded-md border px-3 py-2 text-xs">
791+
This agent task is queued and will start automatically when a parallel slot is
792+
available.
793+
</div>
794+
)}
771795
<ChatInput
772796
variant="workspace"
773797
workspaceId={workspaceId}
774798
runtimeType={getRuntimeTypeForTelemetry(runtimeConfig)}
775799
onMessageSent={handleMessageSent}
776800
onTruncateHistory={handleClearHistory}
777801
onProviderConfig={handleProviderConfig}
778-
disabled={!projectName || !workspaceName}
802+
disabled={!projectName || !workspaceName || isQueuedAgentTask}
803+
disabledReason={
804+
isQueuedAgentTask
805+
? "Queued — waiting for an available parallel task slot. This will start automatically."
806+
: undefined
807+
}
779808
isCompacting={isCompacting}
780809
editingMessage={editingMessage}
781810
onCancelEdit={handleCancelEdit}

src/browser/components/ChatInput/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ModelSettings } from "../ModelSettings";
2222
import { useAPI } from "@/browser/contexts/API";
2323
import { useThinkingLevel } from "@/browser/hooks/useThinkingLevel";
2424
import { migrateGatewayModel } from "@/browser/hooks/useGatewayModels";
25-
import { enforceThinkingPolicy } from "@/browser/utils/thinking/policy";
25+
import { enforceThinkingPolicy } from "@/common/utils/thinking/policy";
2626
import { useSendMessageOptions } from "@/browser/hooks/useSendMessageOptions";
2727
import {
2828
getModelKey,
@@ -1484,6 +1484,12 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
14841484
: `${formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel`;
14851485
return `Edit your message... (${cancelHint}, ${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send)`;
14861486
}
1487+
if (disabled) {
1488+
const disabledReason = props.disabledReason;
1489+
if (typeof disabledReason === "string" && disabledReason.trim().length > 0) {
1490+
return disabledReason;
1491+
}
1492+
}
14871493
if (isCompacting) {
14881494
const interruptKeybind = vimEnabled
14891495
? KEYBINDS.INTERRUPT_STREAM_VIM

src/browser/components/ChatInput/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface ChatInputWorkspaceVariant {
2828
onEditLastUserMessage?: () => void;
2929
canInterrupt?: boolean;
3030
disabled?: boolean;
31+
/** Optional explanation displayed when input is disabled */
32+
disabledReason?: string;
3133
onReady?: (api: ChatInputAPI) => void;
3234
autoCompactionCheck?: AutoCompactionCheckResult; // Computed in parent (AIView) to avoid duplicate calculation
3335
/** Reviews currently attached to chat (from useReviews hook) */

src/browser/components/ModelSelector.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface ModelSelectorProps {
2020
onChange: (value: string) => void;
2121
models: string[];
2222
hiddenModels?: string[];
23+
emptyLabel?: string;
24+
inputPlaceholder?: string;
2325
onComplete?: () => void;
2426
defaultModel?: string | null;
2527
onSetDefaultModel?: (model: string) => void;
@@ -39,6 +41,8 @@ export const ModelSelector = forwardRef<ModelSelectorRef, ModelSelectorProps>(
3941
onChange,
4042
models,
4143
hiddenModels = [],
44+
emptyLabel,
45+
inputPlaceholder,
4246
onComplete,
4347
defaultModel,
4448
onSetDefaultModel,
@@ -229,6 +233,19 @@ export const ModelSelector = forwardRef<ModelSelectorRef, ModelSelectorProps>(
229233
}, [highlightedIndex]);
230234

231235
if (!isEditing) {
236+
if (value.trim().length === 0) {
237+
return (
238+
<div ref={containerRef} className="relative flex items-center gap-1">
239+
<div
240+
className="text-muted-light hover:bg-hover flex cursor-pointer items-center gap-1 rounded-sm px-1 py-0.5 text-[11px] transition-colors duration-200"
241+
onClick={handleClick}
242+
>
243+
<span>{emptyLabel ?? ""}</span>
244+
</div>
245+
</div>
246+
);
247+
}
248+
232249
const gatewayActive = gateway.isModelRoutingThroughGateway(value);
233250

234251
// Parse provider and model name from value (format: "provider:model-name")
@@ -276,7 +293,7 @@ export const ModelSelector = forwardRef<ModelSelectorRef, ModelSelectorProps>(
276293
value={inputValue}
277294
onChange={handleInputChange}
278295
onKeyDown={handleKeyDown}
279-
placeholder="provider:model-name"
296+
placeholder={inputPlaceholder ?? "provider:model-name"}
280297
className="text-light bg-dark border-border-light font-monospace focus:border-exec-mode w-48 rounded-sm border px-1 py-0.5 text-[10px] leading-[11px] outline-none"
281298
/>
282299
{error && (

src/browser/components/ProjectSidebar.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
partitionWorkspacesByAge,
1818
formatDaysThreshold,
1919
AGE_THRESHOLDS_DAYS,
20+
computeWorkspaceDepthMap,
2021
} from "@/browser/utils/ui/workspaceFiltering";
2122
import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip";
2223
import { SidebarCollapseButton } from "./ui/SidebarCollapseButton";
@@ -609,6 +610,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
609610
{(() => {
610611
const allWorkspaces =
611612
sortedWorkspacesByProject.get(projectPath) ?? [];
613+
const depthByWorkspaceId = computeWorkspaceDepthMap(allWorkspaces);
612614
const { recent, buckets } = partitionWorkspacesByAge(
613615
allWorkspaces,
614616
workspaceRecency
@@ -626,6 +628,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
626628
onSelectWorkspace={handleSelectWorkspace}
627629
onRemoveWorkspace={handleRemoveWorkspace}
628630
onToggleUnread={_onToggleUnread}
631+
depth={depthByWorkspaceId[metadata.id] ?? 0}
629632
/>
630633
);
631634

src/browser/components/Settings/SettingsModal.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from "react";
2-
import { Settings, Key, Cpu, X, Briefcase, FlaskConical } from "lucide-react";
2+
import { Settings, Key, Cpu, X, Briefcase, FlaskConical, Bot } from "lucide-react";
33
import { useSettings } from "@/browser/contexts/SettingsContext";
44
import { Dialog, DialogContent, DialogTitle, VisuallyHidden } from "@/browser/components/ui/dialog";
55
import { GeneralSection } from "./sections/GeneralSection";
6+
import { TasksSection } from "./sections/TasksSection";
67
import { ProvidersSection } from "./sections/ProvidersSection";
78
import { ModelsSection } from "./sections/ModelsSection";
89
import { Button } from "@/browser/components/ui/button";
@@ -17,6 +18,12 @@ const SECTIONS: SettingsSection[] = [
1718
icon: <Settings className="h-4 w-4" />,
1819
component: GeneralSection,
1920
},
21+
{
22+
id: "tasks",
23+
label: "Agents",
24+
icon: <Bot className="h-4 w-4" />,
25+
component: TasksSection,
26+
},
2027
{
2128
id: "providers",
2229
label: "Providers",

0 commit comments

Comments
 (0)