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
+108
View File
@@ -0,0 +1,108 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { api } from '../lib/api';
import { STORAGE } from '../lib/storage-keys';
export function useSessions() {
const [allSessions, setAllSessions] = useState<any[]>([]);
const [selectedProjectDir, setSelectedProjectDir] = useState<string | null>(
() => localStorage.getItem(STORAGE.PROJECT_DIR)
);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'projects' | 'active'>('projects');
const [activeSessions, setActiveSessions] = useState<any[]>([]);
const fetchAll = useCallback(async () => {
setLoading(true);
try {
const sessionsData = await api.sessions(undefined, 200);
setAllSessions(sessionsData);
} catch (err) {
console.error('Failed to fetch sessions:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchAll();
}, [fetchAll]);
const fetchActiveSessions = useCallback(async () => {
try {
const data = await api.activeSessions();
// Skip setState if data unchanged — prevents re-render every 10s poll
setActiveSessions(prev =>
JSON.stringify(prev) === JSON.stringify(data) ? prev : data
);
} catch (err) {
console.error('Failed to fetch active sessions:', err);
}
}, []);
// Poll every 10s when Active tab is selected
useEffect(() => {
if (activeTab !== 'active') return;
fetchActiveSessions();
const interval = setInterval(fetchActiveSessions, 3000);
return () => clearInterval(interval);
}, [activeTab, fetchActiveSessions]);
// Fetch once on mount for green dots in project view
useEffect(() => {
fetchActiveSessions();
}, [fetchActiveSessions]);
const activeSessionIds = useMemo(() => {
const ids = new Set<string>();
for (const s of activeSessions) {
if (s.sessionId) ids.add(s.sessionId);
}
return ids;
}, [activeSessions]);
const projects = useMemo(() => {
const cwds = new Set<string>();
for (const s of allSessions) {
if (s.cwd) cwds.add(s.cwd);
}
return [...cwds].sort();
}, [allSessions]);
const sessionCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const s of allSessions) {
const dir = s.cwd || '';
counts[dir] = (counts[dir] || 0) + 1;
}
return counts;
}, [allSessions]);
const filteredSessions = useMemo(() => {
if (!selectedProjectDir) return [];
return allSessions.filter((s) => s.cwd === selectedProjectDir);
}, [allSessions, selectedProjectDir]);
const selectProject = useCallback((dir: string | null) => {
setSelectedProjectDir(dir);
if (dir) {
localStorage.setItem(STORAGE.PROJECT_DIR, dir);
} else {
localStorage.removeItem(STORAGE.PROJECT_DIR);
}
}, []);
return {
sessions: filteredSessions,
projects,
selectedProjectDir,
selectProject,
sessionCounts,
loading,
refresh: fetchAll,
activeTab,
setActiveTab,
activeSessions,
activeSessionIds,
refreshActive: fetchActiveSessions,
};
}