42861ea7fa
Multi-adapter mobile UI for AI coding assistants. Supports Claude Code, Codex CLI, and Gemini CLI through one interface. Features: - Real-time bidirectional sync via tmux + WebSocket - Cross-AI review (send one AI's output to another for review) - Multi-review tabs with minimize/expand - Push notifications (PWA) with smart session-aware filtering - Three-channel event system (hooks, file watcher, pane monitor) - Voice input, image paste, draft persistence - Terminal-native design (JetBrains Mono, dark theme, pixel art claw) - CLI with --adapter flag on every command - Zero-overhead fire-and-forget hooks
423 lines
14 KiB
Markdown
423 lines
14 KiB
Markdown
# Review Panel UX Fixes Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Fix Cross-AI Review UX issues: marker leaks, panel minimize/expand, send-back button, icon polish, read-only history, adapter icons.
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-03-25-review-panel-ux-fixes-design.md`
|
|
|
|
---
|
|
|
|
### Task 1: Fix marker bugs (Session List + trailing `\\n`)
|
|
|
|
**Files:**
|
|
- `server/adapters/codex/codex-tmux-adapter.ts`
|
|
- `src/lib/content-utils.ts`
|
|
|
|
- [ ] **Step 1: Fix `stripMarker` regex to handle literal `\\n`**
|
|
|
|
In `src/lib/content-utils.ts` line 5, change:
|
|
```typescript
|
|
const CODETAP_REF_REGEX = /^\[CODETAP_REF:[^\]]+\]\n?/;
|
|
```
|
|
To:
|
|
```typescript
|
|
const CODETAP_REF_REGEX = /^\[CODETAP_REF:[^\]]+\](?:\\n|\n)?/;
|
|
```
|
|
|
|
This matches both real newline (`\n`) and literal two-char `\\n` (which Codex sendMessage produces).
|
|
|
|
- [ ] **Step 2: Strip marker from `firstPrompt` in Codex adapter**
|
|
|
|
In `server/adapters/codex/codex-tmux-adapter.ts` around line 445, after extracting text:
|
|
```typescript
|
|
if (text) session.firstPrompt = text.substring(0, 200);
|
|
```
|
|
|
|
Change to:
|
|
```typescript
|
|
if (text) {
|
|
// Strip CODETAP_REF marker if present
|
|
const stripped = text.replace(/^\[CODETAP_REF:[^\]]+\](?:\\n|\n)?/, '');
|
|
session.firstPrompt = stripped.substring(0, 200);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Verify + commit**
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
git add src/lib/content-utils.ts server/adapters/codex/codex-tmux-adapter.ts
|
|
git commit -m "fix: strip CODETAP_REF marker from session list + handle literal \\n"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Fix send-back button + icon polish + copy feedback
|
|
|
|
**Files:**
|
|
- `src/components/ChatBody.tsx`
|
|
- `src/components/MessageBubble.tsx`
|
|
|
|
- [ ] **Step 1: Fix `showActions` to include `onSendBack` (ChatBody.tsx line 199)**
|
|
|
|
**ROOT CAUSE:** `showActions` requires `sendTargets` but FloatingReviewPanel only passes `onSendBack`.
|
|
|
|
Change line 199 from:
|
|
```typescript
|
|
showActions={msg.role === 'assistant' && !streaming && !!sendTargets && sendTargets.length > 0}
|
|
```
|
|
To:
|
|
```typescript
|
|
showActions={msg.role === 'assistant' && !streaming && (!!onSendBack || (!!sendTargets && sendTargets.length > 0))}
|
|
```
|
|
|
|
- [ ] **Step 2: Remove border from icon buttons (MessageBubble.tsx lines 186-210)**
|
|
|
|
Change copy button className (line 188) from:
|
|
```
|
|
"flex items-center justify-center w-7 h-7 text-text-dim border border-border rounded-md hover:bg-white/5 transition-colors"
|
|
```
|
|
To:
|
|
```
|
|
"flex items-center justify-center w-6 h-6 text-text-dim/40 hover:text-text-dim hover:bg-white/5 rounded transition-colors"
|
|
```
|
|
|
|
Change send-back button className (line 196) from:
|
|
```
|
|
"flex items-center justify-center w-7 h-7 text-green-400 border border-green-400/30 rounded-md hover:bg-green-400/10 transition-colors"
|
|
```
|
|
To:
|
|
```
|
|
"flex items-center justify-center w-6 h-6 text-green-400/40 hover:text-green-400 hover:bg-green-400/10 rounded transition-colors"
|
|
```
|
|
|
|
Apply similar change to the SendDropdown button if it has border.
|
|
|
|
- [ ] **Step 3: Reduce icon size and stroke width (MessageBubble.tsx lines 34-59)**
|
|
|
|
In all three icon components (CopyIcon, SendIcon, SendBackIcon), change:
|
|
```
|
|
width="14" height="14" ... strokeWidth="2"
|
|
```
|
|
To:
|
|
```
|
|
width="12" height="12" ... strokeWidth="1.5"
|
|
```
|
|
|
|
- [ ] **Step 4: Add copy feedback — checkmark confirmation**
|
|
|
|
Add `useState` import. Add state inside `MessageBubble`:
|
|
```typescript
|
|
const [copied, setCopied] = useState(false);
|
|
```
|
|
|
|
Change the copy button onClick (line 187):
|
|
```typescript
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(extractTextFromBlocks(content));
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
}}
|
|
```
|
|
|
|
Change the copy button icon rendering:
|
|
```tsx
|
|
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
```
|
|
|
|
Add CheckIcon component:
|
|
```typescript
|
|
function CheckIcon() {
|
|
return (
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
);
|
|
}
|
|
```
|
|
|
|
When `copied` is true, change button color to green briefly:
|
|
```typescript
|
|
className={`flex items-center justify-center w-6 h-6 rounded transition-colors ${
|
|
copied ? 'text-green-400' : 'text-text-dim/40 hover:text-text-dim hover:bg-white/5'
|
|
}`}
|
|
```
|
|
|
|
- [ ] **Step 5: Verify + commit**
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
git add src/components/ChatBody.tsx src/components/MessageBubble.tsx
|
|
git commit -m "fix: send-back button visible, icon polish (no border, smaller), copy checkmark feedback"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Panel minimize — thin bar above input
|
|
|
|
**Files:**
|
|
- `src/components/FloatingReviewPanel.tsx`
|
|
- `src/components/ChatBody.tsx` (placeholder prop)
|
|
|
|
- [ ] **Step 1: Minimized bar rendered by ChatView (NOT FloatingReviewPanel)**
|
|
|
|
The minimized bar must sit between the message scroll area and the input — in the normal document flow. FloatingReviewPanel can't do this because it renders as an overlay. Solution: ChatView renders the bar directly. FloatingReviewPanel returns `null` when `panelState === 'minimized'`.
|
|
|
|
In FloatingReviewPanel, change the minimized block (lines 57-67) to:
|
|
```tsx
|
|
if (panelState === 'minimized') return null;
|
|
```
|
|
|
|
In ChatView, add a `ReviewMinimizedBar` inline component (or extract to a small file). Render it between ChatBody and the footer, using `renderAboveInput` slot on ChatBody:
|
|
|
|
```tsx
|
|
// In ChatView's renderAboveInput callback:
|
|
renderAboveInput={() => (
|
|
<>
|
|
{activeReview && reviewPanelState === 'minimized' && (
|
|
<div
|
|
className="flex items-center justify-between px-3 py-1.5"
|
|
style={{ background: `${getBrand(activeReview.childAdapter).color}08`, borderTop: `1px solid ${getBrand(activeReview.childAdapter).color}25` }}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-1.5 h-1.5 rounded-full animate-pulse" style={{ background: getBrand(activeReview.childAdapter).color }} />
|
|
<span className="text-[10px] font-semibold" style={{ color: getBrand(activeReview.childAdapter).color }}>
|
|
{getBrand(activeReview.childAdapter).displayName}
|
|
</span>
|
|
<span className="text-[10px] text-text-dim/50">{activeReview.reviewTitle || 'review'} · active</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button onClick={() => setReviewPanelState('expanded')} className="text-[10px] transition-colors" style={{ color: `${getBrand(activeReview.childAdapter).color}80` }}>
|
|
▲ Expand
|
|
</button>
|
|
<button onClick={handleEndReview} className="text-[10px] text-red-400/80 hover:text-red-400 transition-colors">
|
|
End
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<StatusBar ... />
|
|
</>
|
|
)}
|
|
```
|
|
|
|
Note: no message count shown — parent doesn't have access to child message count. Show "active" instead.
|
|
|
|
- [ ] **Step 2: Add ▼ Minimize button to expanded panel header (lines 98-113)**
|
|
|
|
In the expanded panel header, add a minimize button next to End:
|
|
|
|
```tsx
|
|
<button
|
|
onClick={() => onPanelStateChange('minimized')}
|
|
className="text-xs text-text-dim/50 hover:text-text-dim px-2 py-1 rounded hover:bg-white/5 transition-colors"
|
|
>▼</button>
|
|
```
|
|
|
|
- [ ] **Step 3: Update child input placeholder**
|
|
|
|
In FloatingReviewPanel, pass a custom placeholder to ChatBody. Add `inputPlaceholder` prop to ChatBody:
|
|
|
|
```typescript
|
|
// ChatBody props
|
|
inputPlaceholder?: string;
|
|
```
|
|
|
|
In ChatBody, pass to ShimmerInput:
|
|
```tsx
|
|
<ShimmerInput placeholder={inputPlaceholder || "Send a message..."} ... />
|
|
```
|
|
|
|
FloatingReviewPanel passes:
|
|
```tsx
|
|
inputPlaceholder={`Reply to ${brand.displayName} review...`}
|
|
```
|
|
|
|
- [ ] **Step 4: Verify + commit**
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
git add src/components/FloatingReviewPanel.tsx src/components/ChatView.tsx src/components/ChatBody.tsx
|
|
git commit -m "feat: review panel minimizes to thin bar above input, custom placeholder"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: CollapsedReviewCard onClick + read-only panel
|
|
|
|
**Files:**
|
|
- `src/components/CollapsedReviewCard.tsx`
|
|
- `src/components/ChatView.tsx`
|
|
- `src/components/FloatingReviewPanel.tsx`
|
|
|
|
- [ ] **Step 1: Pass `childSessionId` to CollapsedReviewCard**
|
|
|
|
In ChatView `renderReviewMarkers` (line 268), the review object has `child_cli_session_id`. Pass it:
|
|
|
|
```tsx
|
|
<CollapsedReviewCard
|
|
adapter={review.child_adapter}
|
|
title={review.review_title}
|
|
messageCount={review.message_count || 0}
|
|
summary="Tap to view review conversation"
|
|
onClick={() => handleOpenReadOnlyReview(review)}
|
|
/>
|
|
```
|
|
|
|
Add handler in ChatView:
|
|
```typescript
|
|
const handleOpenReadOnlyReview = useCallback((review: any) => {
|
|
setActiveReview({
|
|
reviewId: review.id,
|
|
childSessionId: review.child_cli_session_id,
|
|
childCliSessionId: review.child_cli_session_id,
|
|
childAdapter: review.child_adapter,
|
|
anchorMessageId: review.anchor_message_id,
|
|
reviewTitle: review.review_title,
|
|
});
|
|
setReviewPanelState('expanded');
|
|
setReadOnlyReview(true); // NEW state
|
|
}, []);
|
|
```
|
|
|
|
Add state:
|
|
```typescript
|
|
const [readOnlyReview, setReadOnlyReview] = useState(false);
|
|
```
|
|
|
|
- [ ] **Step 2: Add `readOnly` prop to FloatingReviewPanel**
|
|
|
|
```typescript
|
|
interface FloatingReviewPanelProps {
|
|
// ... existing props
|
|
readOnly?: boolean;
|
|
}
|
|
```
|
|
|
|
When `readOnly`:
|
|
- Header: gray instead of green, "ended" label, ✕ Close instead of End
|
|
- No ShimmerInput — show "Review ended — read only" text
|
|
- No send-back action
|
|
|
|
Pass to ChatBody:
|
|
```tsx
|
|
<ChatBody
|
|
...
|
|
disabled={readOnly}
|
|
onSendBack={readOnly ? undefined : handleSendBack}
|
|
/>
|
|
```
|
|
|
|
If readOnly, don't render ShimmerInput in ChatBody. Add a `hideInput` prop to ChatBody:
|
|
```typescript
|
|
hideInput?: boolean;
|
|
```
|
|
|
|
- [ ] **Step 3: Update onEnd for read-only panel**
|
|
|
|
FloatingReviewPanel uses the existing `onEnd` callback. The `readOnly` prop controls what the button says:
|
|
```tsx
|
|
<button onClick={onEnd}>
|
|
{readOnly ? '✕' : 'End'}
|
|
</button>
|
|
```
|
|
|
|
In ChatView's `onEnd` handler, check `readOnlyReview`:
|
|
```tsx
|
|
onEnd={async () => {
|
|
if (!readOnlyReview && activeReview.reviewId) {
|
|
try { await api.endReview(activeReview.reviewId); } catch {}
|
|
}
|
|
setActiveReview(null);
|
|
setReviewPanelState('hidden');
|
|
setReviewInitialPrompt(null);
|
|
setReviewCwd(null);
|
|
setReadOnlyReview(false);
|
|
}}
|
|
```
|
|
|
|
Also reset `readOnlyReview` in `handleReviewSelect` (when opening a new active review):
|
|
```typescript
|
|
setReadOnlyReview(false);
|
|
```
|
|
|
|
- [ ] **Step 4: Verify + commit**
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
git add src/components/CollapsedReviewCard.tsx src/components/ChatView.tsx src/components/FloatingReviewPanel.tsx src/components/ChatBody.tsx
|
|
git commit -m "feat: collapsed review card opens read-only panel with child session history"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Adapter icons from thesvg.org
|
|
|
|
**Files:**
|
|
- `src/components/AdapterIcon.tsx`
|
|
|
|
- [ ] **Step 1: Fetch SVGs from thesvg.org**
|
|
|
|
Visit https://www.thesvg.org/ and search for:
|
|
- "Anthropic" or "Claude" → get the official Anthropic logo SVG
|
|
- "OpenAI" → get the official OpenAI logo SVG
|
|
|
|
- [ ] **Step 2: Update ClaudeIcon and CodexIcon**
|
|
|
|
Replace the SVG paths in `AdapterIcon.tsx` (lines 10-37) with the official ones from thesvg.org. Keep:
|
|
- `fill="currentColor"` for color control
|
|
- `viewBox` matching the original SVG
|
|
- `width={size} height={size}` props
|
|
|
|
- [ ] **Step 3: Verify + commit**
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
git add src/components/AdapterIcon.tsx
|
|
git commit -m "feat: use official adapter icons from thesvg.org"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: E2E Verification
|
|
|
|
- [ ] **Step 1:** Start server, create Codex session → verify no marker in session list
|
|
- [ ] **Step 2:** Open Codex session → verify no `\\n` at start of first message
|
|
- [ ] **Step 3:** Create Claude session → send message → Click send icon → Direct send → verify panel opens with Codex response
|
|
- [ ] **Step 4:** Verify send-back ↩ icon appears on child responses
|
|
- [ ] **Step 5:** Verify copy icon → click → ✓ checkmark appears → reverts after 2s
|
|
- [ ] **Step 6:** Verify icon buttons have no border, smaller size
|
|
- [ ] **Step 7:** Click ▼ minimize → verify thin bar appears above input → parent input usable
|
|
- [ ] **Step 8:** Click ▲ Expand → panel opens again
|
|
- [ ] **Step 9:** Click End → verify panel closes → review markers appear in history
|
|
- [ ] **Step 10:** Click collapsed review card → verify read-only panel opens (no input, gray header)
|
|
- [ ] **Step 11:** Close read-only panel → verify return to normal chat
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
### showActions bug fix
|
|
Before: `showActions = assistant && !streaming && !!sendTargets && sendTargets.length > 0`
|
|
After: `showActions = assistant && !streaming && (!!onSendBack || (!!sendTargets && sendTargets.length > 0))`
|
|
FloatingReviewPanel passes `onSendBack` but not `sendTargets` → now shows action buttons ✅
|
|
|
|
### Minimized bar placement
|
|
Renders as normal flow element (not absolute) between ChatBody and input. Parent chat is fully scrollable and input is fully usable. ✅
|
|
|
|
### Read-only panel
|
|
Uses same FloatingReviewPanel with `readOnly` flag. RECONNECT to child session for history. No input, no send-back. ✅
|
|
|
|
### Files changed
|
|
| File | Changes |
|
|
|------|---------|
|
|
| `src/lib/content-utils.ts` | Fix stripMarker regex |
|
|
| `server/adapters/codex/codex-tmux-adapter.ts` | Strip marker from firstPrompt |
|
|
| `src/components/ChatBody.tsx` | Fix showActions, add inputPlaceholder/hideInput |
|
|
| `src/components/MessageBubble.tsx` | Icon polish, copy feedback, no border |
|
|
| `src/components/FloatingReviewPanel.tsx` | Thin bar minimize, readOnly, custom placeholder |
|
|
| `src/components/ChatView.tsx` | Minimized bar, read-only review handler |
|
|
| `src/components/CollapsedReviewCard.tsx` | Pass onClick with review data |
|
|
| `src/components/AdapterIcon.tsx` | Official SVGs from thesvg.org |
|