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:
kuannnn
2026-03-27 14:46:00 +08:00
parent 16f75379af
commit 0fcf66fc22
50 changed files with 2179 additions and 400 deletions
+42 -11
View File
@@ -12,7 +12,7 @@ import {
authMiddleware,
} from './auth.js';
import './adapters/init.js';
import { initAll, listAvailable, get as getAdapter, getAll as getAllAdapters, cleanupAll, DEFAULT_ADAPTER } from './adapters/registry.js';
import { initAll, installAllHooks, listAvailable, get as getAdapter, getAll as getAllAdapters, cleanupAll, DEFAULT_ADAPTER } from './adapters/registry.js';
import { initPush, getVapidPublicKey, saveSubscription, removeSubscription, getPendingSessions } from './push.js';
import {
setupSessionManager,
@@ -25,7 +25,7 @@ import {
import { WebSocketTransport } from './transport/websocket-transport.js';
import { loadConfig } from './config.js';
import type { AppConfig } from './config.js';
import { initDB, closeDB, sessionReviews, savedInstructions } from './db.js';
import { initDB, closeDB, sessionReviews, sessionAdapters, savedInstructions } from './db.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -99,6 +99,10 @@ async function start(): Promise<void> {
)
);
const allSessions = results.flat();
// Persist session→adapter mapping so server knows which adapter owns each session
for (const s of allSessions) {
if (s.sessionId && s.adapter) sessionAdapters.set(s.sessionId, s.adapter);
}
allSessions.sort((a, b) => {
const aTime = typeof a.lastModified === 'number' ? a.lastModified : new Date(a.lastModified || 0).getTime();
const bTime = typeof b.lastModified === 'number' ? b.lastModified : new Date(b.lastModified || 0).getTime();
@@ -251,9 +255,9 @@ async function start(): Promise<void> {
// Register a review after the child session is already created via QUERY
app.post('/api/reviews/register', authMiddleware, async (req: Request, res: Response) => {
try {
const { parentCliSessionId, childSessionId, targetAdapter, anchorMessageId, prompt, title } = req.body;
if (!parentCliSessionId || !childSessionId) {
return res.status(400).json({ error: 'parentCliSessionId and childSessionId required' });
const { reviewId, parentCliSessionId, childSessionId, targetAdapter, anchorMessageId, prompt, title } = req.body;
if (!reviewId || !parentCliSessionId || !childSessionId) {
return res.status(400).json({ error: 'reviewId, parentCliSessionId and childSessionId required' });
}
// Find which adapter owns the parent session
@@ -262,7 +266,6 @@ async function start(): Promise<void> {
if (a.getSession(parentCliSessionId)) { parentAdapterName = name; break; }
}
const reviewId = randomUUID();
sessionReviews.create(reviewId, parentCliSessionId, childSessionId, targetAdapter, parentAdapterName, anchorMessageId, prompt, title);
// Ensure adapter mapping exists for the child session
@@ -436,21 +439,49 @@ async function start(): Promise<void> {
}
});
// Initialize all adapters (registers hook routes, configures CLI hooks)
// Register adapter routes (before listen — routes don't depend on port)
initAll(app);
setupSessionManager();
// --- Initialize and Listen ---
// --- Find available port and Listen ---
await initAuth(config);
initPush(config);
writeFileSync(config.paths.pid, String(process.pid));
const protocol = config.https ? 'https' : 'http';
server.listen(config.port, '0.0.0.0', () => {
console.log(`ClawTap running on ${protocol}://0.0.0.0:${config.port}${config.https ? ' (HTTPS)' : ''}`);
const actualPort = await new Promise<number>((resolve, reject) => {
const maxRetries = 10;
let attempt = 0;
function tryListen(port: number) {
const onError = (err: NodeJS.ErrnoException) => {
if (err.code === 'EADDRINUSE' && attempt < maxRetries) {
attempt++;
const nextPort = port + 1;
console.log(`Port ${port} in use, trying ${nextPort}...`);
server.close(() => tryListen(nextPort));
} else {
reject(err);
}
};
server.once('error', onError);
server.listen(port, '0.0.0.0', () => {
server.removeListener('error', onError);
resolve(port);
});
}
tryListen(config.port);
});
// Update config with actual port (may differ if fallback occurred)
config.port = actualPort;
// Install hooks AFTER port is confirmed (hooks embed the port in CLI configs)
installAllHooks(actualPort);
writeFileSync(config.paths.pid, String(process.pid));
console.log(`ClawTap running on ${protocol}://0.0.0.0:${actualPort}${config.https ? ' (HTTPS)' : ''}`);
// --- Graceful Shutdown ---
async function shutdown(signal: string): Promise<void> {