Skip to content

Commit 1300d56

Browse files
fazxesclaude
andcommitted
fix(agent): resolve permission schema and UI flash issues
Agent Bridge: - Make sessionId optional in PermissionResponseSchema (fixes permission_response error) - Add build:dev script for compiling sidecar binary - Document agent-bridge rebuild workflow in CLAUDE.md Chat UI: - Add .no-transitions class to prevent tool widget flash during conversation switch - Apply no-transitions during isTransitioning state - Start BashToolWidget collapsed to avoid async Shiki flash - Simplify send/stop button to single component with state-based styling Sidebar: - Skip creating new conversation if current one is empty (Untitled) - Skip loading conversation if already viewing it 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e6ffc30 commit 1300d56

8 files changed

Lines changed: 76 additions & 43 deletions

File tree

‎CLAUDE.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,30 @@ This command:
161161
| CSS/Tailwind | Instant HMR via Vite |
162162
| Rust (`src-tauri/`) | Auto-rebuilds, restarts app |
163163
| Rust crates (`crates/`) | Auto-rebuilds, restarts app |
164+
| agent-bridge (`agent-bridge/`) | **Manual rebuild required** |
165+
166+
### Agent Bridge Sidecar (IMPORTANT)
167+
168+
The agent-bridge is a **compiled Bun binary** that Tauri spawns as a sidecar process. Unlike other code, **changes to agent-bridge require manual rebuilding**:
169+
170+
```bash
171+
cd agent-bridge
172+
npm run build:dev # Compiles to target/debug/agent-bridge
173+
```
174+
175+
Then restart the Tauri app (`Cmd+C` → `pnpm tauri dev`).
176+
177+
**Why manual rebuild?**
178+
179+
- Tauri watches Rust code, not the agent-bridge TypeScript
180+
- The sidecar is a standalone binary (58MB) with embedded Bun runtime
181+
- Located at `target/debug/agent-bridge` in dev mode
182+
183+
**Two build outputs:**
184+
| Script | Output | Purpose |
185+
|--------|--------|---------|
186+
| `npm run build` | `dist/index.js` | JS bundle (requires Bun to run) |
187+
| `npm run build:dev` | `target/debug/agent-bridge` | Standalone binary for Tauri |
164188

165189
### Production Build
166190

‎agent-bridge/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"build": "bun build src/index.ts --outdir dist --target bun --sourcemap=external",
99
"build:bundle": "bun build src/index.ts --outdir dist --target bun --minify",
10+
"build:dev": "bun build --compile --target=bun-darwin-arm64 src/index.ts --outfile ../target/debug/agent-bridge",
1011
"dev": "bun --watch src/index.ts",
1112
"typecheck": "tsc --noEmit",
1213
"start": "bun src/index.ts"

‎agent-bridge/src/schemas.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export type SlashCommandDefinition = z.infer<typeof SlashCommandDefinitionSchema
125125

126126
export const PermissionResponseSchema = z
127127
.object({
128-
sessionId: z.string(),
128+
sessionId: z.string().optional(), // Optional - permission lookup uses requestId only
129129
requestId: z.string(),
130130
decision: DecisionSchema,
131131
always: z.boolean(),

‎apps/agent/src/components/chat/chat-messages.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export const ChatMessages: FC<ChatMessagesProps> = ({
149149
return (
150150
<div
151151
ref={containerRef}
152-
className="flex-1 overflow-y-auto overflow-x-hidden p-4"
152+
className={`flex-1 overflow-y-auto overflow-x-hidden p-4${isTransitioning ? ' no-transitions' : ''}`}
153153
style={{
154154
scrollbarGutter: 'stable both-edges',
155155
visibility: isTransitioning ? 'hidden' : 'visible',

‎apps/agent/src/components/chat/input/chat-input.tsx‎

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -760,47 +760,38 @@ export const ChatInput: FC<ChatInputProps> = ({
760760
</ContextContentBody>
761761
</ContextContent>
762762
</Context>
763-
{/* Send/Stop Button */}
764-
{isAgentRunning && isInputEmpty ? (
765-
<Tooltip>
766-
<TooltipTrigger asChild>
767-
<button
768-
onClick={onStop}
769-
className={cn(
770-
'h-7 w-7 flex items-center justify-center rounded-full',
771-
'bg-destructive text-destructive-foreground',
772-
'transition-all duration-150',
773-
'hover:bg-destructive/90 hover:scale-105',
774-
'active:scale-95',
775-
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50'
776-
)}
777-
>
778-
<Square className="h-3 w-3 fill-current" />
779-
</button>
780-
</TooltipTrigger>
781-
<TooltipContent>Stop (Esc)</TooltipContent>
782-
</Tooltip>
783-
) : (
784-
<Tooltip>
785-
<TooltipTrigger asChild>
786-
<button
787-
onClick={handleSend}
788-
disabled={isInputEmpty}
789-
className={cn(
790-
'h-7 w-7 flex items-center justify-center rounded-full',
791-
'transition-all duration-150',
792-
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50',
793-
isInputEmpty
763+
{/* Send/Stop Button - Single button with state-based styling */}
764+
<Tooltip>
765+
<TooltipTrigger asChild>
766+
<button
767+
onClick={isAgentRunning && isInputEmpty ? onStop : handleSend}
768+
disabled={!isAgentRunning && isInputEmpty}
769+
className={cn(
770+
'h-7 w-7 flex items-center justify-center rounded-full',
771+
'transition-all duration-200 ease-out',
772+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50',
773+
isAgentRunning && isInputEmpty
774+
? 'bg-red-400/80 text-white dark:bg-red-400/70 hover:bg-red-400/90 dark:hover:bg-red-400/80'
775+
: isInputEmpty
794776
? 'bg-muted/50 text-muted-foreground/50 cursor-not-allowed'
795777
: 'bg-primary text-primary-foreground hover:bg-primary/90 hover:scale-105 hover:shadow-[0_0_16px_-2px_var(--primary)] active:scale-95'
796-
)}
797-
>
778+
)}
779+
>
780+
{isAgentRunning && isInputEmpty ? (
781+
<Square className="h-2.5 w-2.5 fill-current" strokeWidth={0} />
782+
) : (
798783
<ArrowUp className="h-4 w-4" />
799-
</button>
800-
</TooltipTrigger>
801-
<TooltipContent>{isAgentRunning ? 'Queue message' : 'Send message'}</TooltipContent>
802-
</Tooltip>
803-
)}
784+
)}
785+
</button>
786+
</TooltipTrigger>
787+
<TooltipContent>
788+
{isAgentRunning && isInputEmpty
789+
? 'Stop (Esc)'
790+
: isAgentRunning
791+
? 'Queue message'
792+
: 'Send message'}
793+
</TooltipContent>
794+
</Tooltip>
804795
</div>
805796
</div>
806797
</div>

‎apps/agent/src/components/chat/tools/bash-tool-widget.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const BashToolWidget: FC<BashToolWidgetProps> = ({
1919
output,
2020
isRunning = false,
2121
}) => {
22-
const [isExpanded, setIsExpanded] = useState(true);
22+
// Start collapsed to prevent layout flash from async Shiki highlighting
23+
const [isExpanded, setIsExpanded] = useState(false);
2324
const [highlightedCommand, setHighlightedCommand] = useState<string>('');
2425

2526
// Syntax highlight the command

‎apps/agent/src/components/layout/primary-sidebar.tsx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,24 @@ export const PrimarySidebar: FC<PrimarySidebarProps> = ({ width }) => {
9797
const [activeTab, setActiveTab] = useState<SidebarTab>('conversations');
9898

9999
const handleStartConversation = useCallback((): void => {
100+
// Skip if current conversation is empty (title still "Untitled" means no message sent)
101+
const activeConv = conversations.find((c) => c.sessionId === activeConversationId);
102+
if (activeConv?.title === 'Untitled') {
103+
return;
104+
}
100105
postMessage({
101106
type: 'conversation:create',
102107
uuid: crypto.randomUUID(),
103108
title: 'Untitled',
104109
});
105-
}, [postMessage]);
110+
}, [conversations, activeConversationId, postMessage]);
106111

107112
const handleLoadConversation = useCallback(
108113
(sessionId: string): void => {
114+
// Skip if already viewing this conversation
115+
if (sessionId === activeConversationId) {
116+
return;
117+
}
109118
// Set loading and transitioning states SYNCHRONOUSLY before any async work
110119
// This ensures opacity-0 is applied before new content renders
111120
setLoadingConversation(true);
@@ -117,7 +126,7 @@ export const PrimarySidebar: FC<PrimarySidebarProps> = ({ width }) => {
117126
session_id: sessionId,
118127
});
119128
},
120-
[setLoadingConversation, setConversationTransitioning, postMessage]
129+
[activeConversationId, setLoadingConversation, setConversationTransitioning, postMessage]
121130
);
122131

123132
const handleOpenQuickSearch = useCallback((): void => {

‎apps/agent/src/globals.css‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,7 @@ html.dark [data-streamdown="code-block-container"] {
664664
opacity: 0.5;
665665
}
666666
}
667+
667668
}
668669

669670
/* Scrollbar styling for WKWebView */
@@ -735,6 +736,12 @@ html.dark ::-webkit-scrollbar-thumb:hover {
735736
contain: layout style paint;
736737
}
737738

739+
/* Disable all transitions during conversation switching to prevent layout flash */
740+
.no-transitions,
741+
.no-transitions * {
742+
transition: none !important;
743+
}
744+
738745
/* For file tree items */
739746
.file-tree-item {
740747
content-visibility: auto;

0 commit comments

Comments
 (0)