Skip to content
This repository was archived by the owner on Mar 10, 2026. It is now read-only.
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
95 changes: 50 additions & 45 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,52 +100,57 @@ export default function BranchToolbar({
if (!activeThreadId || !activeProject) return null;

return (
<div className="mx-auto flex w-full max-w-3xl items-center justify-between pb-3 pt-1">
<div className="flex items-center gap-2">
{envLocked || activeWorktreePath ? (
<span className="border border-transparent px-[calc(--spacing(2)-1px)] text-sm font-medium text-muted-foreground/70 sm:text-xs">
{activeWorktreePath ? "Worktree" : "Local"}
</span>
) : (
<div className="inline-flex items-center gap-1 rounded border border-foreground/10 bg-background/60 p-1 shadow-[0_8px_24px_-20px_rgba(0,0,0,0.65)]">
<button
type="button"
className={cn(
"inline-flex h-9 items-center justify-center rounded px-4 text-sm font-semibold transition-colors",
effectiveEnvMode === "local"
? "bg-foreground/10 text-foreground"
: "text-muted-foreground hover:bg-foreground/5 hover:text-foreground/85",
)}
onClick={() => onEnvModeChange("local")}
>
Local
</button>
<button
type="button"
className={cn(
"inline-flex h-9 items-center justify-center rounded px-4 text-sm font-semibold transition-colors",
effectiveEnvMode === "worktree"
? "bg-foreground/10 text-foreground"
: "text-muted-foreground hover:bg-foreground/5 hover:text-foreground/85",
)}
onClick={() => onEnvModeChange("worktree")}
>
New Worktree
</button>
</div>
)}
</div>
<div className="px-3 pb-3 pt-1 sm:px-5">
<div className="@container/branch-toolbar mx-auto flex w-full max-w-3xl min-w-0 items-center justify-between gap-2 overflow-hidden">
<div className="flex min-w-0 shrink items-center gap-2">
{envLocked || activeWorktreePath ? (
<span className="border border-transparent px-[calc(--spacing(2)-1px)] text-sm font-medium text-muted-foreground/70 sm:text-xs">
{activeWorktreePath ? "Worktree" : "Local"}
</span>
) : (
<div className="inline-flex min-w-0 items-center gap-1 rounded border border-foreground/10 bg-background/60 p-0.5 shadow-[0_8px_24px_-20px_rgba(0,0,0,0.65)]">
<button
type="button"
className={cn(
"inline-flex h-7 items-center justify-center rounded px-2.5 text-xs font-semibold whitespace-nowrap transition-colors @lg/branch-toolbar:h-8 @lg/branch-toolbar:px-4 @lg/branch-toolbar:text-sm",
effectiveEnvMode === "local"
? "bg-foreground/10 text-foreground"
: "text-muted-foreground hover:bg-foreground/5 hover:text-foreground/85",
)}
onClick={() => onEnvModeChange("local")}
>
Local
</button>
<button
type="button"
className={cn(
"inline-flex h-7 items-center justify-center rounded px-2.5 text-xs font-semibold whitespace-nowrap transition-colors @lg/branch-toolbar:h-8 @lg/branch-toolbar:px-4 @lg/branch-toolbar:text-sm",
effectiveEnvMode === "worktree"
? "bg-foreground/10 text-foreground"
: "text-muted-foreground hover:bg-foreground/5 hover:text-foreground/85",
)}
onClick={() => onEnvModeChange("worktree")}
>
<span className="@lg/branch-toolbar:hidden">Worktree</span>
<span className="hidden @lg/branch-toolbar:inline">New Worktree</span>
</button>
</div>
)}
</div>

<BranchToolbarBranchSelector
activeProjectCwd={activeProject.cwd}
activeThreadBranch={activeThreadBranch}
activeWorktreePath={activeWorktreePath}
branchCwd={branchCwd}
effectiveEnvMode={effectiveEnvMode}
envLocked={envLocked}
onSetThreadBranch={setThreadBranch}
{...(onComposerFocusRequest ? { onComposerFocusRequest } : {})}
/>
<div className="min-w-0 shrink-0">
<BranchToolbarBranchSelector
activeProjectCwd={activeProject.cwd}
activeThreadBranch={activeThreadBranch}
activeWorktreePath={activeWorktreePath}
branchCwd={branchCwd}
effectiveEnvMode={effectiveEnvMode}
envLocked={envLocked}
onSetThreadBranch={setThreadBranch}
{...(onComposerFocusRequest ? { onComposerFocusRequest } : {})}
/>
</div>
</div>
</div>
);
}
3 changes: 2 additions & 1 deletion apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "react";

import { gitBranchesQueryOptions, gitQueryKeys, invalidateGitQueries } from "../lib/gitReactQuery";
import { cn } from "../lib/utils";
import { readNativeApi } from "../nativeApi";
import {
dedupeRemoteBranchesWithLocalMatches,
Expand Down Expand Up @@ -311,7 +312,7 @@ export function BranchToolbarBranchSelector({
<Button
variant="toolbar"
size="toolbar"
className="min-w-[10.5rem] justify-between px-3 text-xs font-medium"
className="w-[8.5rem] min-w-0 justify-between px-2.5 text-xs font-medium @lg/branch-toolbar:w-[10rem] @lg/branch-toolbar:px-3 @xl/branch-toolbar:min-w-[10.5rem] @xl/branch-toolbar:w-auto"
/>
}
disabled={branchesQuery.isLoading || isBranchActionPending}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ChatSplitView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export default function ChatSplitView({
}}
/>
<div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
<ChatView key={id} threadId={id} />
<ChatView key={id} threadId={id} splitPaneCount={paneIds.length} />
</div>
</div>
))}
Expand Down
117 changes: 81 additions & 36 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,36 @@ function formatWorkingTimer(startIso: string, endIso: string): string | null {
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}

function WorkingIndicatorRow(props: { createdAt: string | null; nowIso: string }) {
const elapsedLabel = props.createdAt
? `Working for ${formatWorkingTimer(props.createdAt, props.nowIso) ?? "0s"}`
: "Working...";

return (
<div aria-live="polite" className="flex items-start gap-4 px-1 py-0.5" role="status">
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded bg-primary text-primary-foreground">
<BotIcon className="size-4" />
</div>
<div className="min-w-0 flex-1 pt-0.5">
<div className="inline-flex max-w-full items-center gap-3 rounded-2xl border border-border/80 bg-card/75 px-3 py-2 shadow-sm shadow-black/5 backdrop-blur-xs">
<div aria-hidden="true" className="flex shrink-0 items-center gap-2">
<span className="size-2 rounded-full bg-primary/80 animate-pulse" />
<span className="relative block h-1.5 w-14 overflow-hidden rounded-full bg-primary/10">
<span className="absolute inset-0 rounded-full bg-linear-to-r from-primary/0 via-primary/75 to-primary/0 bg-[length:200%_100%] animate-skeleton" />
</span>
</div>
<div className="min-w-0">
<p className="font-mono text-[10px] uppercase tracking-[0.24em] text-primary/75">
Agent active
</p>
<p className="truncate text-[12px] text-foreground/80">{elapsedLabel}</p>
</div>
</div>
</div>
</div>
);
}

const LAST_EDITOR_KEY = "t3code:last-editor";
const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";
const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;
Expand Down Expand Up @@ -641,9 +671,10 @@ const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {

interface ChatViewProps {
threadId: ThreadId;
splitPaneCount?: number;
}

export default function ChatView({ threadId }: ChatViewProps) {
export default function ChatView({ threadId, splitPaneCount = 1 }: ChatViewProps) {
const threads = useStore((store) => store.threads);
const projects = useStore((store) => store.projects);
const markThreadVisited = useStore((store) => store.markThreadVisited);
Expand Down Expand Up @@ -3502,6 +3533,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
diffToggleShortcutLabel={diffPanelShortcutLabel}
gitCwd={gitCwd}
diffOpen={diffOpen}
iconOnlyActions={splitPaneCount >= 3}
onRunProjectScript={(script) => {
void runProjectScript(script);
}}
Expand Down Expand Up @@ -4095,6 +4127,7 @@ interface ChatHeaderProps {
diffToggleShortcutLabel: string | null;
gitCwd: string | null;
diffOpen: boolean;
iconOnlyActions?: boolean;
onRunProjectScript: (script: ProjectScript) => void;
onAddProjectScript: (input: NewProjectScriptInput) => Promise<void>;
onUpdateProjectScript: (scriptId: string, input: NewProjectScriptInput) => Promise<void>;
Expand All @@ -4114,19 +4147,21 @@ const ChatHeaderActions = memo(function ChatHeaderActions({
diffToggleShortcutLabel,
gitCwd,
diffOpen,
iconOnlyActions = false,
onRunProjectScript,
onAddProjectScript,
onUpdateProjectScript,
onToggleDiff,
isGitRepo,
}: ChatHeaderActionsProps) {
return (
<div className="@container/header-actions flex min-w-0 items-center justify-end gap-2 @sm/header-actions:gap-3">
<div className="flex min-w-0 items-center justify-end gap-2">
{activeProjectScripts && (
<ProjectScriptsControl
scripts={activeProjectScripts}
keybindings={keybindings}
preferredScriptId={preferredScriptId}
iconOnly={iconOnlyActions}
onRunScript={onRunProjectScript}
onAddScript={onAddProjectScript}
onUpdateScript={onUpdateProjectScript}
Expand All @@ -4136,10 +4171,17 @@ const ChatHeaderActions = memo(function ChatHeaderActions({
<OpenInPicker
keybindings={keybindings}
availableEditors={availableEditors}
iconOnly={iconOnlyActions}
openInCwd={openInCwd}
/>
)}
{activeProjectName && <GitActionsControl gitCwd={gitCwd} activeThreadId={activeThreadId} />}
{activeProjectName && (
<GitActionsControl
gitCwd={gitCwd}
activeThreadId={activeThreadId}
iconOnly={iconOnlyActions}
/>
)}
<Tooltip>
<TooltipTrigger
render={
Expand Down Expand Up @@ -4181,13 +4223,14 @@ const ChatHeader = memo(function ChatHeader({
diffToggleShortcutLabel,
gitCwd,
diffOpen,
iconOnlyActions,
onRunProjectScript,
onAddProjectScript,
onUpdateProjectScript,
onToggleDiff,
}: ChatHeaderProps) {
return (
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="@container/chat-header flex min-w-0 flex-1 items-center gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3">
<SidebarTrigger className="size-7 shrink-0 rounded md:hidden" />
<div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden text-sm">
Expand All @@ -4212,21 +4255,22 @@ const ChatHeader = memo(function ChatHeader({
</Badge>
)}
</div>
<ChatHeaderActions
activeThreadId={activeThreadId}
activeProjectName={activeProjectName}
isGitRepo={isGitRepo}
openInCwd={openInCwd}
activeProjectScripts={activeProjectScripts}
preferredScriptId={preferredScriptId}
keybindings={keybindings}
availableEditors={availableEditors}
diffToggleShortcutLabel={diffToggleShortcutLabel}
gitCwd={gitCwd}
diffOpen={diffOpen}
onRunProjectScript={onRunProjectScript}
onAddProjectScript={onAddProjectScript}
onUpdateProjectScript={onUpdateProjectScript}
<ChatHeaderActions
activeThreadId={activeThreadId}
activeProjectName={activeProjectName}
isGitRepo={isGitRepo}
openInCwd={openInCwd}
activeProjectScripts={activeProjectScripts}
preferredScriptId={preferredScriptId}
keybindings={keybindings}
availableEditors={availableEditors}
diffToggleShortcutLabel={diffToggleShortcutLabel}
gitCwd={gitCwd}
diffOpen={diffOpen}
iconOnlyActions={iconOnlyActions}
onRunProjectScript={onRunProjectScript}
onAddProjectScript={onAddProjectScript}
onUpdateProjectScript={onUpdateProjectScript}
onToggleDiff={onToggleDiff}
/>
</div>
Expand Down Expand Up @@ -5571,21 +5615,7 @@ const MessagesTimeline = memo(function MessagesTimeline({
)}

{row.kind === "working" && (
<div className="flex items-center gap-2 py-0.5 pl-1.5">
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/30" />
<div className="flex items-center gap-2 pt-1 text-[11px] text-muted-foreground/70">
<span className="inline-flex items-center gap-[3px]">
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse" />
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:200ms]" />
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:400ms]" />
</span>
<span>
{row.createdAt
? `Working for ${formatWorkingTimer(row.createdAt, nowIso) ?? "0s"}`
: "Working..."}
</span>
</div>
</div>
<WorkingIndicatorRow createdAt={row.createdAt} nowIso={nowIso} />
)}
</div>
);
Expand Down Expand Up @@ -6101,10 +6131,12 @@ const OpenInPicker = memo(function OpenInPicker({
keybindings,
availableEditors,
openInCwd,
iconOnly = false,
}: {
keybindings: ResolvedKeybindingsConfig;
availableEditors: ReadonlyArray<EditorId>;
openInCwd: string | null;
iconOnly?: boolean;
}) {
const [lastEditor, setLastEditor] = useState<EditorId>(() => {
const stored = localStorage.getItem(LAST_EDITOR_KEY);
Expand Down Expand Up @@ -6187,13 +6219,26 @@ const OpenInPicker = memo(function OpenInPicker({
<Button
size="toolbar"
variant="toolbar"
className={cn(
"gap-0 px-2.5",
!iconOnly && "@2xl/chat-header:gap-2 @2xl/chat-header:px-3",
)}
disabled={!effectiveEditor || !openInCwd}
onClick={() => openInEditor(effectiveEditor)}
>
{primaryOption?.Icon && <primaryOption.Icon aria-hidden="true" className="size-3.5" />}
<span>Open</span>
<span
className={cn(
"sr-only",
!iconOnly && "@2xl/chat-header:not-sr-only @2xl/chat-header:ml-0.5",
)}
>
Open
</span>
</Button>
<GroupSeparator className="hidden bg-foreground/10 @sm/header-actions:block" />
<GroupSeparator
className={cn("hidden bg-foreground/10", !iconOnly && "@2xl/chat-header:block")}
/>
<Menu>
<MenuTrigger render={<Button aria-label="Open options" size="toolbar-icon" variant="toolbar" />}>
<ChevronDownIcon aria-hidden="true" className="size-4" />
Expand Down
Loading
Loading