Files
clawtap/docs/superpowers/plans/2026-03-25-review-panel-ux-fixes.md
kuannnn 42861ea7fa feat: ClawTap v0.1.0 — initial release
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
2026-03-26 10:40:26 +08:00

14 KiB

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:

const CODETAP_REF_REGEX = /^\[CODETAP_REF:[^\]]+\]\n?/;

To:

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:

if (text) session.firstPrompt = text.substring(0, 200);

Change to:

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
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:

showActions={msg.role === 'assistant' && !streaming && !!sendTargets && sendTargets.length > 0}

To:

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:

const [copied, setCopied] = useState(false);

Change the copy button onClick (line 187):

onClick={() => {
  navigator.clipboard.writeText(extractTextFromBlocks(content));
  setCopied(true);
  setTimeout(() => setCopied(false), 2000);
}}

Change the copy button icon rendering:

{copied ? <CheckIcon /> : <CopyIcon />}

Add CheckIcon component:

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:

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
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:

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:

// 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:

<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:

// ChatBody props
inputPlaceholder?: string;

In ChatBody, pass to ShimmerInput:

<ShimmerInput placeholder={inputPlaceholder || "Send a message..."} ... />

FloatingReviewPanel passes:

inputPlaceholder={`Reply to ${brand.displayName} review...`}
  • Step 4: Verify + commit
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:

<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:

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:

const [readOnlyReview, setReadOnlyReview] = useState(false);
  • Step 2: Add readOnly prop to FloatingReviewPanel
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:

<ChatBody
  ...
  disabled={readOnly}
  onSendBack={readOnly ? undefined : handleSendBack}
/>

If readOnly, don't render ShimmerInput in ChatBody. Add a hideInput prop to ChatBody:

hideInput?: boolean;
  • Step 3: Update onEnd for read-only panel

FloatingReviewPanel uses the existing onEnd callback. The readOnly prop controls what the button says:

<button onClick={onEnd}>
  {readOnly ? '✕' : 'End'}
</button>

In ChatView's onEnd handler, check readOnlyReview:

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):

setReadOnlyReview(false);
  • Step 4: Verify + commit
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

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