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
+81
View File
@@ -0,0 +1,81 @@
import { WS } from './ws-types';
export type WsStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
type MessageHandler = (msg: any) => void;
type StatusHandler = (status: WsStatus) => void;
export class WsClient {
private ws: WebSocket | null = null;
private url: string;
private onMessage: MessageHandler;
private onStatus: StatusHandler;
private reconnectDelay = 1000;
private maxReconnectDelay = 30000;
private shouldReconnect = true;
private activeSessionId: string | null = null;
private activeAdapter: string | null = null;
constructor(token: string, onMessage: MessageHandler, onStatus: StatusHandler) {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
this.url = `${proto}://${location.host}/ws?token=${encodeURIComponent(token)}`;
this.onMessage = onMessage;
this.onStatus = onStatus;
}
connect() {
this.shouldReconnect = true;
this.onStatus('connecting');
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectDelay = 1000;
this.onStatus('connected');
if (this.activeSessionId) {
this.send({ type: WS.RECONNECT, sessionId: this.activeSessionId, adapter: this.activeAdapter });
}
};
this.ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === WS.SESSION_CREATED) {
this.activeSessionId = msg.sessionId;
}
this.onMessage(msg);
} catch {}
};
this.ws.onclose = () => {
this.onStatus('disconnected');
if (this.shouldReconnect) {
this.onStatus('reconnecting');
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
}
};
this.ws.onerror = () => {
this.ws?.close();
};
}
send(msg: any) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(msg));
}
}
setActiveSession(sessionId: string | null, adapter?: string) {
this.activeSessionId = sessionId;
this.activeAdapter = adapter || null;
}
disconnect() {
this.shouldReconnect = false;
this.ws?.close();
this.ws = null;
}
}