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
23 changes: 23 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# AGENT INSTRUCTIONS

**Edits to this file must be minimal and token-efficient.** Think carefully about how to represent information concisely. Avoid redundant examples or verbose explanations when the knowledge can be conveyed in a sentence or two.

## Project Context

- Project is named `mux`
Expand Down Expand Up @@ -365,6 +367,27 @@ If IPC is hard to test, fix the test infrastructure or IPC layer, don't work aro

**For per-operation state tied to async workflows, parent components should own all localStorage operations.** Child components should notify parents of user intent without manipulating storage directly, preventing bugs from stale or orphaned state across component lifecycles.

**Always use persistedState helpers (`usePersistedState`, `readPersistedState`, `updatePersistedState`) instead of direct `localStorage` calls** - provides cross-component sync and consistent error handling.

**Avoid destructuring props in function signatures** - Use `props.fieldName` instead of destructuring in the parameter list. Destructuring duplicates field names and makes refactoring more cumbersome.

```typescript
// ❌ BAD - Duplicates field names, harder to refactor
export function MyComponent({
field1,
field2,
field3,
onAction,
}: MyComponentProps) {
return <div onClick={onAction}>{field1}</div>;
}

// ✅ GOOD - Single source of truth, easier to refactor
export function MyComponent(props: MyComponentProps) {
return <div onClick={props.onAction}>{props.field1}</div>;
}
```

## Module Imports

- **NEVER use dynamic imports** - Always use static `import` statements at the top of files. Dynamic imports (`await import()`) are a code smell that indicates improper module structure.
Expand Down
10 changes: 5 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useResumeManager } from "./hooks/useResumeManager";
import { useUnreadTracking } from "./hooks/useUnreadTracking";
import { useAutoCompactContinue } from "./hooks/useAutoCompactContinue";
import { useWorkspaceStoreRaw, useWorkspaceRecency } from "./stores/WorkspaceStore";
import { FirstMessageInput } from "./components/FirstMessageInput";
import { ChatInput } from "./components/ChatInput/index";

import { useStableReference, compareMaps } from "./hooks/useStableReference";
import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandRegistryContext";
Expand Down Expand Up @@ -120,10 +120,9 @@ function AppInner() {
window.history.replaceState(null, "", newHash);
}

// Update window title with workspace name (prefer displayName if available)
// Update window title with workspace name
const metadata = workspaceMetadata.get(selectedWorkspace.workspaceId);
const workspaceName =
metadata?.displayName ?? metadata?.name ?? selectedWorkspace.workspaceId;
const workspaceName = metadata?.name ?? selectedWorkspace.workspaceId;
const title = `${workspaceName} - ${selectedWorkspace.projectName} - cmux`;
void window.api.window.setTitle(title);
} else {
Expand Down Expand Up @@ -630,7 +629,8 @@ function AppInner() {
return (
<ModeProvider projectPath={projectPath}>
<ThinkingProvider projectPath={projectPath}>
<FirstMessageInput
<ChatInput
variant="creation"
projectPath={projectPath}
projectName={projectName}
onWorkspaceCreated={(metadata) => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { StreamingBarrier } from "./Messages/ChatBarrier/StreamingBarrier";
import { RetryBarrier } from "./Messages/ChatBarrier/RetryBarrier";
import { PinnedTodoList } from "./PinnedTodoList";
import { getAutoRetryKey, VIM_ENABLED_KEY } from "@/constants/storage";
import { ChatInput, type ChatInputAPI } from "./ChatInput";
import { ChatInput, type ChatInputAPI } from "./ChatInput/index";
import { RightSidebar, type TabType } from "./RightSidebar";
import { useResizableSidebar } from "@/hooks/useResizableSidebar";
import {
Expand Down Expand Up @@ -462,6 +462,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
</div>

<ChatInput
variant="workspace"
workspaceId={workspaceId}
onMessageSent={handleMessageSent}
onTruncateHistory={handleClearHistory}
Expand Down
31 changes: 31 additions & 0 deletions src/components/ChatInput/CreationCenterContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";

interface CreationCenterContentProps {
projectName: string;
isSending: boolean;
}

/**
* Center content displayed during workspace creation
* Shows either a loading spinner or welcome message
*/
export function CreationCenterContent({ projectName, isSending }: CreationCenterContentProps) {
return (
<div className="flex flex-1 items-center justify-center">
{isSending ? (
<div className="text-center">
<div className="bg-accent mb-3 inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent"></div>
<p className="text-muted text-sm">Creating workspace...</p>
</div>
) : (
<div className="max-w-2xl px-8 text-center">
<h1 className="text-foreground mb-4 text-2xl font-semibold">{projectName}</h1>
<p className="text-muted text-sm leading-relaxed">
Describe what you want to build. A new workspace will be created with an automatically
generated branch name. Configure runtime and model options below.
</p>
</div>
)}
</div>
);
}
79 changes: 79 additions & 0 deletions src/components/ChatInput/CreationControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import { RUNTIME_MODE, type RuntimeMode } from "@/types/runtime";
import { TooltipWrapper, Tooltip } from "../Tooltip";
import { Select } from "../Select";

interface CreationControlsProps {
branches: string[];
trunkBranch: string;
onTrunkBranchChange: (branch: string) => void;
runtimeMode: RuntimeMode;
sshHost: string;
onRuntimeChange: (mode: RuntimeMode, host: string) => void;
disabled: boolean;
}

/**
* Additional controls shown only during workspace creation
* - Trunk branch selector (which branch to fork from)
* - Runtime mode (local vs SSH)
*/
export function CreationControls(props: CreationControlsProps) {
return (
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
{/* Trunk Branch Selector */}
{props.branches.length > 0 && (
<div className="flex items-center gap-1" data-component="TrunkBranchGroup">
<label htmlFor="trunk-branch" className="text-muted text-xs">
From:
</label>
<Select
id="trunk-branch"
value={props.trunkBranch}
options={props.branches}
onChange={props.onTrunkBranchChange}
disabled={props.disabled}
className="max-w-[120px]"
/>
</div>
)}

{/* Runtime Selector */}
<div className="flex items-center gap-1" data-component="RuntimeSelectorGroup">
<label className="text-muted text-xs">Runtime:</label>
<Select
value={props.runtimeMode}
options={[
{ value: RUNTIME_MODE.LOCAL, label: "Local" },
{ value: RUNTIME_MODE.SSH, label: "SSH" },
]}
onChange={(newMode) => {
const mode = newMode as RuntimeMode;
props.onRuntimeChange(mode, mode === RUNTIME_MODE.LOCAL ? "" : props.sshHost);
}}
disabled={props.disabled}
aria-label="Runtime mode"
/>
{props.runtimeMode === RUNTIME_MODE.SSH && (
<input
type="text"
value={props.sshHost}
onChange={(e) => props.onRuntimeChange(RUNTIME_MODE.SSH, e.target.value)}
placeholder="user@host"
disabled={props.disabled}
className="bg-separator text-foreground border-border-medium focus:border-accent w-32 rounded border px-1 py-0.5 text-xs focus:outline-none disabled:opacity-50"
/>
)}
<TooltipWrapper inline>
<span className="text-muted cursor-help text-xs">?</span>
<Tooltip className="tooltip" align="center" width="wide">
<strong>Runtime:</strong>
<br />
• Local: git worktree in ~/.cmux/src
<br />• SSH: remote clone in ~/cmux on SSH host
</Tooltip>
</TooltipWrapper>
</div>
</div>
);
}
Loading