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
+46
View File
@@ -0,0 +1,46 @@
/** Centralized localStorage key constants. All keys use the `clawtap:` prefix. */
export const STORAGE = {
TOKEN: 'clawtap:token',
ADAPTER: 'clawtap:adapter',
PROJECT_DIR: 'clawtap:projectDir',
DRAFT: 'clawtap:draft',
INSTALL_DISMISSED: 'clawtap:install-dismissed',
adapterPrefs: (id: string) => `clawtap:adapterPrefs:${id}` as const,
} as const;
/** One-time migration from old key names. Runs once before app mount. */
export function migrateStorageKeys(): void {
// Rename simple keys
for (const [oldKey, newKey] of [
['token', STORAGE.TOKEN],
['selectedProjectDir', STORAGE.PROJECT_DIR],
] as const) {
const val = localStorage.getItem(oldKey);
if (val !== null && localStorage.getItem(newKey) === null) {
localStorage.setItem(newKey, val);
localStorage.removeItem(oldKey);
}
}
// Migrate old global model/permissionMode/effort into per-adapter prefs
// Check both old bare keys AND clawtap:-prefixed keys (from intermediate migration)
const oldModel = localStorage.getItem('selectedModel') || localStorage.getItem('clawtap:model');
const oldMode = localStorage.getItem('permissionMode') || localStorage.getItem('clawtap:permissionMode');
const oldEffort = localStorage.getItem('effort') || localStorage.getItem('clawtap:effort');
if (oldModel || oldMode || oldEffort) {
const adapter = localStorage.getItem(STORAGE.ADAPTER) || 'claude';
const prefsKey = STORAGE.adapterPrefs(adapter);
let prefs: Record<string, string> = {};
try { prefs = JSON.parse(localStorage.getItem(prefsKey) || '{}'); } catch {}
if (oldModel && !prefs.model) prefs.model = oldModel;
if (oldMode && !prefs.permissionMode) prefs.permissionMode = oldMode;
if (oldEffort && !prefs.effort) prefs.effort = oldEffort;
localStorage.setItem(prefsKey, JSON.stringify(prefs));
localStorage.removeItem('selectedModel');
localStorage.removeItem('permissionMode');
localStorage.removeItem('effort');
localStorage.removeItem('clawtap:model');
localStorage.removeItem('clawtap:permissionMode');
localStorage.removeItem('clawtap:effort');
}
}