feat: ClawTap v0.2.0
Interactive Prompts: - Unified InteractivePrompt type across all 3 adapters (Claude/Codex/Gemini) - InteractivePromptOverlay component with options, text input, countdown - Gemini + Codex pane monitors detect tool confirmation, ask user, plan approval - respondInteractivePrompt routing: permission → respondPermission, options → _selectOption - Claude AskUserQuestion nested questions[0] structure parsing Cross-AI Review: - Client-generated reviewId, removed pendingReview state - FloatingReviewPanel uses CSS display:none instead of unmount (keeps hooks alive) - Child review sessions default to YOLO/bypass permission mode - Send back to parent, send to existing/new review, tab switching, end review - Collapsed review cards with read-only panel for ended reviews - Full reconnect support: active + ended reviews restore correctly AskUserQuestion Tool Card UI: - Dedicated renderer replaces raw JSON display - Options shown with selected (green) / unselected (gray) indicators - Free text answers shown in quoted format with green border - Collapsed summary: question → answer - Shared parseAskQuestionInput utility (client + server) - Historical tool results attached via _result on tool_use blocks Adapter Fixes: - Session→adapter mapping persisted in SQLite (survives server restart) - SESSION_CREATED deferred for pendingRekey adapters (Codex/Gemini) - session-rekeyed handler sends complete SESSION_CREATED with adapter + cwd - Gemini: auto-accept folder trust, privacy notice, IDE nudge, YOLO * prompt - Claude: auto-accept bypass permissions confirmation (v2.1.85+) - Port fallback (EADDRINUSE → try +1), statusLine shell script wrapper Other: - Desktop Enter sends / Shift+Enter newline; Mobile Enter newline - Strip CLAWTAP_REF marker from session list - Active sessions tab shows adapter badge - Rename CLAUDE_UI_PASSWORD → CLAWTAP_PASSWORD Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useChat } from '../hooks/useChat';
|
||||
import { ChatBody } from './ChatBody';
|
||||
import { InteractivePromptOverlay } from './InteractivePromptOverlay';
|
||||
import { getBrand } from '@/lib/adapter-brands';
|
||||
import { extractTextFromBlocks } from '@/lib/content-utils';
|
||||
import { api } from '@/lib/api';
|
||||
@@ -12,15 +13,16 @@ export interface ReviewEntry {
|
||||
childSessionId: string;
|
||||
childAdapter: string;
|
||||
reviewTitle?: string;
|
||||
prompt?: string;
|
||||
permissionMode?: string;
|
||||
}
|
||||
|
||||
interface ReviewPanelProps {
|
||||
reviews: ReviewEntry[];
|
||||
onEnd: (reviewId: string) => void;
|
||||
onMinimize: () => void;
|
||||
initialPrompt?: string;
|
||||
cwd?: string;
|
||||
onSessionCreated?: (childSessionId: string) => void;
|
||||
onSessionCreated?: (reviewId: string, childSessionId: string) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
@@ -30,23 +32,24 @@ export interface ReviewPanelHandle {
|
||||
|
||||
// ===== ReviewTab (one per review, keeps useChat hook alive) =====
|
||||
|
||||
const ReviewTab = React.memo(function ReviewTab({ review, cwd, initialPrompt, onSessionCreated, isActive, readOnly, sendRef }: {
|
||||
const ReviewTab = React.memo(function ReviewTab({ review, cwd, onSessionCreated, isActive, readOnly, sendRef }: {
|
||||
review: ReviewEntry;
|
||||
cwd?: string;
|
||||
initialPrompt?: string;
|
||||
onSessionCreated?: (sid: string) => void;
|
||||
onSessionCreated?: (reviewId: string, sid: string) => void;
|
||||
isActive: boolean;
|
||||
readOnly?: boolean;
|
||||
sendRef?: React.MutableRefObject<Map<string, (text: string) => void>>;
|
||||
}) {
|
||||
const {
|
||||
messages, streaming, liveStatus, toolStatuses,
|
||||
messages, streaming, pendingResponse, liveStatus, toolStatuses,
|
||||
sendMessage, abort, sessionId: chatSessionId,
|
||||
interactivePrompt, respondPrompt,
|
||||
} = useChat(
|
||||
review.childSessionId || undefined,
|
||||
cwd,
|
||||
review.childAdapter,
|
||||
initialPrompt,
|
||||
review.prompt,
|
||||
review.permissionMode,
|
||||
);
|
||||
|
||||
// Notify parent when child session is created
|
||||
@@ -54,9 +57,9 @@ const ReviewTab = React.memo(function ReviewTab({ review, cwd, initialPrompt, on
|
||||
useEffect(() => {
|
||||
if (chatSessionId && !review.childSessionId && onSessionCreated && !sessionCreatedRef.current) {
|
||||
sessionCreatedRef.current = true;
|
||||
onSessionCreated(chatSessionId);
|
||||
onSessionCreated(review.reviewId, chatSessionId);
|
||||
}
|
||||
}, [chatSessionId, review.childSessionId, onSessionCreated]);
|
||||
}, [chatSessionId, review.childSessionId, onSessionCreated, review.reviewId]);
|
||||
|
||||
// Register sendMessage in parent's ref map for sendToReview
|
||||
useEffect(() => {
|
||||
@@ -89,6 +92,7 @@ const ReviewTab = React.memo(function ReviewTab({ review, cwd, initialPrompt, on
|
||||
<ChatBody
|
||||
messages={messages}
|
||||
streaming={streaming}
|
||||
pendingResponse={pendingResponse}
|
||||
liveStatus={liveStatus}
|
||||
toolStatuses={toolStatuses || new Map()}
|
||||
onSend={sendMessage}
|
||||
@@ -100,6 +104,12 @@ const ReviewTab = React.memo(function ReviewTab({ review, cwd, initialPrompt, on
|
||||
inputPlaceholder={`Reply to ${brand.displayName} review...`}
|
||||
className="flex-1"
|
||||
/>
|
||||
{interactivePrompt && (
|
||||
<InteractivePromptOverlay
|
||||
prompt={interactivePrompt}
|
||||
onRespond={respondPrompt}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -107,7 +117,7 @@ const ReviewTab = React.memo(function ReviewTab({ review, cwd, initialPrompt, on
|
||||
// ===== Main Panel =====
|
||||
|
||||
export const FloatingReviewPanel = forwardRef<ReviewPanelHandle, ReviewPanelProps>(
|
||||
function FloatingReviewPanel({ reviews, onEnd, onMinimize, initialPrompt, cwd, onSessionCreated, readOnly }, ref) {
|
||||
function FloatingReviewPanel({ reviews, onEnd, onMinimize, cwd, onSessionCreated, readOnly }, ref) {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(Math.max(0, reviews.length - 1));
|
||||
|
||||
// Keep activeTabIndex in bounds
|
||||
@@ -162,7 +172,7 @@ export const FloatingReviewPanel = forwardRef<ReviewPanelHandle, ReviewPanelProp
|
||||
const tabActive = i === activeTabIndex;
|
||||
return (
|
||||
<div
|
||||
key={r.reviewId || `tab-${i}`}
|
||||
key={r.reviewId}
|
||||
className="flex items-center gap-0.5 text-xs whitespace-nowrap"
|
||||
style={{
|
||||
color: tabActive ? b.color : '#71717a',
|
||||
@@ -229,11 +239,10 @@ export const FloatingReviewPanel = forwardRef<ReviewPanelHandle, ReviewPanelProp
|
||||
{/* Tabs — ALL rendered to keep hooks alive, only active one visible via CSS */}
|
||||
{reviews.map((r, i) => (
|
||||
<ReviewTab
|
||||
key={r.reviewId || `pending-${i}`}
|
||||
key={r.reviewId}
|
||||
review={r}
|
||||
cwd={cwd}
|
||||
initialPrompt={i === reviews.length - 1 ? initialPrompt : undefined}
|
||||
onSessionCreated={i === reviews.length - 1 ? onSessionCreated : undefined}
|
||||
onSessionCreated={!r.childSessionId ? onSessionCreated : undefined}
|
||||
isActive={i === activeTabIndex}
|
||||
readOnly={readOnly}
|
||||
sendRef={sendRefs}
|
||||
|
||||
Reference in New Issue
Block a user