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
This commit is contained in:
kuannnn
2026-03-18 10:24:45 +08:00
commit 42861ea7fa
151 changed files with 33897 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
import type { ContentBlock } from '../claude/message-utils.js';
export type { ContentBlock };
/** A tool call embedded in a Gemini session message */
export interface GeminiToolCall {
id: string;
name: string;
args: Record<string, unknown>;
result?: unknown[];
status: 'running' | 'success' | 'error' | 'cancelled';
timestamp?: string;
displayName?: string;
description?: string;
}
/**
* Extract plain text from a user message's content.
* Gemini user content is either an array of { text } objects or a plain string.
*/
export function extractUserText(content: unknown): string {
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.filter((item): item is { text: string } => item != null && typeof item.text === 'string')
.map((item) => item.text)
.join('\n');
}
return '';
}
/** Alias — Gemini assistant content is always a string, but use same extractor for safety */
export const extractGeminiText = extractUserText;
/**
* Convert Gemini's embedded toolCalls array into standard tool_use + tool_result ContentBlock pairs.
*
* Each GeminiToolCall becomes:
* - A tool_use block (id, name, input = args)
* - Optionally a tool_result block if a result is present (tool_use_id, content, is_error)
*/
export function toolCallsToContentBlocks(toolCalls: GeminiToolCall[]): ContentBlock[] {
const blocks: ContentBlock[] = [];
for (const tc of toolCalls) {
// tool_use block
blocks.push({
type: 'tool_use',
id: tc.id,
name: tc.name,
input: tc.args,
});
// tool_result block — only if result data is present
if (tc.result !== undefined && tc.result !== null) {
const isError = tc.status === 'error' || tc.status === 'cancelled';
// Extract the output or error string from the function response structure
let resultContent = '';
if (Array.isArray(tc.result) && tc.result.length > 0) {
const firstResult = tc.result[0] as Record<string, unknown> | null;
const functionResponse = firstResult?.functionResponse as Record<string, unknown> | undefined;
const response = functionResponse?.response as Record<string, unknown> | undefined;
if (isError) {
resultContent = typeof response?.error === 'string'
? response.error
: JSON.stringify(response?.error ?? '');
} else {
resultContent = typeof response?.output === 'string'
? response.output
: JSON.stringify(response?.output ?? '');
}
}
blocks.push({
type: 'tool_result',
tool_use_id: tc.id,
content: resultContent,
is_error: isError,
});
}
}
return blocks;
}