Spaces:
Running
Running
Hermes Agent commited on
Commit ·
4e89d90
1
Parent(s): 81fbcdd
Clean up all legacy OmniRoute and OpenCode references across configuration, gateway, reverse proxy, index landing, and mobile app directory
Browse files- README.md +6 -8
- entrypoint.sh +8 -24
- gateway/utils.py +5 -9
- index.html +14 -29
- mobile/app.json +0 -22
- mobile/app/(tabs)/_layout.tsx +0 -69
- mobile/app/(tabs)/files.tsx +0 -153
- mobile/app/(tabs)/index.tsx +0 -140
- mobile/app/(tabs)/search.tsx +0 -105
- mobile/app/(tabs)/sessions.tsx +0 -128
- mobile/app/(tabs)/terminal.tsx +0 -37
- mobile/app/_layout.tsx +0 -27
- mobile/app/session/[id].tsx +0 -271
- mobile/app/settings.tsx +0 -281
- mobile/components/MarkdownRenderer.tsx +0 -134
- mobile/components/PermissionCard.tsx +0 -113
- mobile/components/QuestionCard.tsx +0 -176
- mobile/components/ToolTimeline.tsx +0 -165
- mobile/constants/themes.ts +0 -132
- mobile/constants/types.ts +0 -404
- mobile/hooks/useApi.ts +0 -322
- mobile/hooks/useConnection.ts +0 -65
- mobile/hooks/useSSE.ts +0 -156
- mobile/lib/api.ts +0 -434
- mobile/lib/connection-manager.ts +0 -285
- mobile/lib/query-provider.tsx +0 -17
- mobile/lib/storage.ts +0 -134
- mobile/package-lock.json +0 -0
- mobile/package.json +0 -38
- mobile/tests/integration.test.ts +0 -251
- mobile/tsconfig.json +0 -15
- nginx.conf +1 -71
README.md
CHANGED
|
@@ -8,14 +8,12 @@ app_port: 4096
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
| 12 |
|
| 13 |
-
This Hugging Face Space hosts
|
| 14 |
|
| 15 |
-
##
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
`https://jishnupg-opencode-cli.hf.space` (or your corresponding Space URL)
|
| 21 |
-
3. Set your **Auth Type** and credentials if you configured `OPENCODE_SERVER_USERNAME`/`OPENCODE_SERVER_PASSWORD` secrets.
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Multi-Service Container Hub
|
| 12 |
|
| 13 |
+
This Hugging Face Space hosts a multi-service container hub on port 4096.
|
| 14 |
|
| 15 |
+
## Services
|
| 16 |
|
| 17 |
+
1. **Open WebUI** (`/`): Feature-rich AI chat interface.
|
| 18 |
+
2. **Jellyfin Media Server** (`/jellyfin/`): Media streaming server.
|
| 19 |
+
3. **Telegram Stream Proxy** (`/tg-stream/`): High-speed MTProto video range streamer for Telegram Drive.
|
|
|
|
|
|
entrypoint.sh
CHANGED
|
@@ -10,20 +10,9 @@ git config --global --add safe.directory '*' 2>/dev/null || true
|
|
| 10 |
|
| 11 |
# /data is the persistent HF dataset bucket mount
|
| 12 |
echo "[INIT] Setting up /data directories..."
|
| 13 |
-
mkdir -p /data/share/opencode 2>/dev/null || echo "[WARN] Could not create /data/share/opencode"
|
| 14 |
-
mkdir -p /data/config/opencode 2>/dev/null || echo "[WARN] Could not create /data/config/opencode"
|
| 15 |
-
mkdir -p /data/cache/opencode 2>/dev/null || echo "[WARN] Could not create /data/cache/opencode"
|
| 16 |
-
mkdir -p /data/state/opencode 2>/dev/null || echo "[WARN] Could not create /data/state/opencode"
|
| 17 |
mkdir -p /data/open-webui 2>/dev/null || echo "[WARN] Could not create /data/open-webui"
|
| 18 |
-
mkdir -p /data/omniroute 2>/dev/null || echo "[WARN] Could not create /data/omniroute"
|
| 19 |
mkdir -p /data/jellyfin/data /data/jellyfin/config /data/jellyfin/cache /data/jellyfin/log /data/jellyfin/media/Movies /data/jellyfin/media/TVShows 2>/dev/null || true
|
| 20 |
|
| 21 |
-
# Symlink OmniRoute data directory to /data/omniroute for 100% persistence
|
| 22 |
-
if [ ! -L "/root/.omniroute" ]; then
|
| 23 |
-
rm -rf /root/.omniroute 2>/dev/null || true
|
| 24 |
-
ln -sf /data/omniroute /root/.omniroute
|
| 25 |
-
fi
|
| 26 |
-
|
| 27 |
# Start Telegram Direct Range Stream Proxy in background
|
| 28 |
echo "[INIT] Starting Telegram Direct Stream Proxy on port 8080..."
|
| 29 |
python3 /tg_streamer.py &
|
|
@@ -54,16 +43,15 @@ chmod -R 777 /root/.cache /data/cache 2>/dev/null || true
|
|
| 54 |
sleep 2
|
| 55 |
|
| 56 |
# Pre-configure & Start Open WebUI on port 8098 (Mounted at / via FastAPI proxy)
|
| 57 |
-
echo "[INIT] Starting Open WebUI on port 8098
|
| 58 |
if command -v open-webui >/dev/null 2>&1; then
|
| 59 |
-
# Tell Open WebUI its public-facing URL
|
| 60 |
-
export WEBUI_URL="https://jishnupg-opencode-cli.hf.space"
|
| 61 |
-
export OPENAI_API_BASE_URL="
|
| 62 |
-
export OPENAI_API_KEY="
|
| 63 |
export WEBUI_SECRET_KEY="${WEBUI_SECRET_KEY:-opencode_webui_jwt_secret_2026}"
|
| 64 |
-
export ENABLE_OLLAMA_API="false"
|
| 65 |
-
export ENABLE_OPENAI_API="true"
|
| 66 |
-
# Ensure OmniRoute is NOT registered as an OpenAPI tool server (fixes 500 ContentTypeError crash)
|
| 67 |
export TOOL_SERVERS=""
|
| 68 |
export OPENAPI_TOOL_SERVERS=""
|
| 69 |
export PORT=8098
|
|
@@ -73,7 +61,7 @@ if command -v open-webui >/dev/null 2>&1; then
|
|
| 73 |
rm -rf /root/.open-webui 2>/dev/null || true
|
| 74 |
ln -sf /data/open-webui /root/.open-webui
|
| 75 |
fi
|
| 76 |
-
# Sanitize any stale
|
| 77 |
python3 -c "
|
| 78 |
import sqlite3, os
|
| 79 |
db_p = '/data/open-webui/webui.db'
|
|
@@ -104,10 +92,6 @@ fi
|
|
| 104 |
echo "[DISK] /data usage:"
|
| 105 |
df -h /data 2>/dev/null || echo "[WARN] Could not check /data disk space"
|
| 106 |
|
| 107 |
-
# Remove stale SQLite WAL/lock files to prevent "database is locked" on startup
|
| 108 |
-
rm -f /data/omniroute/*.sqlite-wal /data/omniroute/*.sqlite-shm /data/omniroute/*.lock 2>/dev/null || true
|
| 109 |
-
rm -f /root/.omniroute/*.sqlite-wal /root/.omniroute/*.sqlite-shm /root/.omniroute/*.lock 2>/dev/null || true
|
| 110 |
-
|
| 111 |
echo "============================================"
|
| 112 |
echo "=== Launching FastAPI Gateway Proxy on Port 4096 ==="
|
| 113 |
echo "============================================"
|
|
|
|
| 10 |
|
| 11 |
# /data is the persistent HF dataset bucket mount
|
| 12 |
echo "[INIT] Setting up /data directories..."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
mkdir -p /data/open-webui 2>/dev/null || echo "[WARN] Could not create /data/open-webui"
|
|
|
|
| 14 |
mkdir -p /data/jellyfin/data /data/jellyfin/config /data/jellyfin/cache /data/jellyfin/log /data/jellyfin/media/Movies /data/jellyfin/media/TVShows 2>/dev/null || true
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# Start Telegram Direct Range Stream Proxy in background
|
| 17 |
echo "[INIT] Starting Telegram Direct Stream Proxy on port 8080..."
|
| 18 |
python3 /tg_streamer.py &
|
|
|
|
| 43 |
sleep 2
|
| 44 |
|
| 45 |
# Pre-configure & Start Open WebUI on port 8098 (Mounted at / via FastAPI proxy)
|
| 46 |
+
echo "[INIT] Starting Open WebUI on port 8098..."
|
| 47 |
if command -v open-webui >/dev/null 2>&1; then
|
| 48 |
+
# Tell Open WebUI its public-facing URL
|
| 49 |
+
export WEBUI_URL="${WEBUI_URL:-https://jishnupg-opencode-cli.hf.space}"
|
| 50 |
+
export OPENAI_API_BASE_URL="${OPENAI_API_BASE_URL:-https://api.openai.com/v1}"
|
| 51 |
+
export OPENAI_API_KEY="${OPENAI_API_KEY:-}"
|
| 52 |
export WEBUI_SECRET_KEY="${WEBUI_SECRET_KEY:-opencode_webui_jwt_secret_2026}"
|
| 53 |
+
export ENABLE_OLLAMA_API="${ENABLE_OLLAMA_API:-false}"
|
| 54 |
+
export ENABLE_OPENAI_API="${ENABLE_OPENAI_API:-true}"
|
|
|
|
| 55 |
export TOOL_SERVERS=""
|
| 56 |
export OPENAPI_TOOL_SERVERS=""
|
| 57 |
export PORT=8098
|
|
|
|
| 61 |
rm -rf /root/.open-webui 2>/dev/null || true
|
| 62 |
ln -sf /data/open-webui /root/.open-webui
|
| 63 |
fi
|
| 64 |
+
# Sanitize any stale tool server entries in webui.db
|
| 65 |
python3 -c "
|
| 66 |
import sqlite3, os
|
| 67 |
db_p = '/data/open-webui/webui.db'
|
|
|
|
| 92 |
echo "[DISK] /data usage:"
|
| 93 |
df -h /data 2>/dev/null || echo "[WARN] Could not check /data disk space"
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
echo "============================================"
|
| 96 |
echo "=== Launching FastAPI Gateway Proxy on Port 4096 ==="
|
| 97 |
echo "============================================"
|
gateway/utils.py
CHANGED
|
@@ -20,8 +20,6 @@ logger = logging.getLogger("GatewayUtils")
|
|
| 20 |
|
| 21 |
# Port definitions
|
| 22 |
WEBUI_PORT = int(os.environ.get("WEBUI_PORT", 8098))
|
| 23 |
-
OMNIROUTE_PORT = int(os.environ.get("OMNIROUTE_PORT", 20128))
|
| 24 |
-
OPENCODE_PORT = int(os.environ.get("OPENCODE_PORT", 4097))
|
| 25 |
JELLYFIN_PORT = int(os.environ.get("JELLYFIN_PORT", 8096))
|
| 26 |
TG_PORT = int(os.environ.get("TG_PORT", 8080))
|
| 27 |
GATEWAY_PORT = int(os.environ.get("PORT", 4096))
|
|
@@ -29,16 +27,14 @@ GATEWAY_PORT = int(os.environ.get("PORT", 4096))
|
|
| 29 |
PUBLIC_HOST = os.environ.get("PUBLIC_HOST", "jishnupg-opencode-cli.hf.space")
|
| 30 |
PUBLIC_ORIGIN = f"https://{PUBLIC_HOST}"
|
| 31 |
|
| 32 |
-
_INTERNAL_PORTS = {GATEWAY_PORT, WEBUI_PORT,
|
| 33 |
_PORT_STRIP_RE = re.compile(r"(https?://[^/:]+):(" + "|".join(str(p) for p in _INTERNAL_PORTS) + r")")
|
| 34 |
|
| 35 |
_PORT_PREFIX_MAP = {
|
| 36 |
-
GATEWAY_PORT:
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
JELLYFIN_PORT: "/jellyfin",
|
| 41 |
-
TG_PORT: "/tg-stream",
|
| 42 |
}
|
| 43 |
|
| 44 |
# Shared Async HTTPX Client Instance
|
|
|
|
| 20 |
|
| 21 |
# Port definitions
|
| 22 |
WEBUI_PORT = int(os.environ.get("WEBUI_PORT", 8098))
|
|
|
|
|
|
|
| 23 |
JELLYFIN_PORT = int(os.environ.get("JELLYFIN_PORT", 8096))
|
| 24 |
TG_PORT = int(os.environ.get("TG_PORT", 8080))
|
| 25 |
GATEWAY_PORT = int(os.environ.get("PORT", 4096))
|
|
|
|
| 27 |
PUBLIC_HOST = os.environ.get("PUBLIC_HOST", "jishnupg-opencode-cli.hf.space")
|
| 28 |
PUBLIC_ORIGIN = f"https://{PUBLIC_HOST}"
|
| 29 |
|
| 30 |
+
_INTERNAL_PORTS = {GATEWAY_PORT, WEBUI_PORT, JELLYFIN_PORT, TG_PORT}
|
| 31 |
_PORT_STRIP_RE = re.compile(r"(https?://[^/:]+):(" + "|".join(str(p) for p in _INTERNAL_PORTS) + r")")
|
| 32 |
|
| 33 |
_PORT_PREFIX_MAP = {
|
| 34 |
+
GATEWAY_PORT: "",
|
| 35 |
+
WEBUI_PORT: "/openwebui",
|
| 36 |
+
JELLYFIN_PORT: "/jellyfin",
|
| 37 |
+
TG_PORT: "/tg-stream",
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
# Shared Async HTTPX Client Instance
|
index.html
CHANGED
|
@@ -150,62 +150,47 @@
|
|
| 150 |
|
| 151 |
<div class="grid">
|
| 152 |
<!-- Open WebUI Card -->
|
| 153 |
-
<a href="/
|
| 154 |
<div>
|
| 155 |
<div class="card-header">
|
| 156 |
<span class="icon">🤖</span>
|
| 157 |
<span class="badge">Online</span>
|
| 158 |
</div>
|
| 159 |
<h2>Open WebUI</h2>
|
| 160 |
-
<p>Feature-rich AI chat interface connected to
|
| 161 |
</div>
|
| 162 |
<div class="card-footer">
|
| 163 |
Launch Open WebUI →
|
| 164 |
</div>
|
| 165 |
</a>
|
| 166 |
|
| 167 |
-
<!--
|
| 168 |
-
<a href="/
|
| 169 |
-
<div>
|
| 170 |
-
<div class="card-header">
|
| 171 |
-
<span class="icon">⚡</span>
|
| 172 |
-
<span class="badge">Online</span>
|
| 173 |
-
</div>
|
| 174 |
-
<h2>OmniRoute AI Gateway</h2>
|
| 175 |
-
<p>AI model router dashboard and multi-provider management.</p>
|
| 176 |
-
</div>
|
| 177 |
-
<div class="card-footer">
|
| 178 |
-
Launch OmniRoute →
|
| 179 |
-
</div>
|
| 180 |
-
</a>
|
| 181 |
-
|
| 182 |
-
<!-- OpenCode Server Card -->
|
| 183 |
-
<a href="/server/" class="card">
|
| 184 |
<div>
|
| 185 |
<div class="card-header">
|
| 186 |
-
<span class="icon">
|
| 187 |
<span class="badge">Online</span>
|
| 188 |
</div>
|
| 189 |
-
<h2>
|
| 190 |
-
<p>
|
| 191 |
</div>
|
| 192 |
<div class="card-footer">
|
| 193 |
-
Launch
|
| 194 |
</div>
|
| 195 |
</a>
|
| 196 |
|
| 197 |
-
<!--
|
| 198 |
-
<a href="/
|
| 199 |
<div>
|
| 200 |
<div class="card-header">
|
| 201 |
-
<span class="icon">
|
| 202 |
<span class="badge">Online</span>
|
| 203 |
</div>
|
| 204 |
-
<h2>
|
| 205 |
-
<p>
|
| 206 |
</div>
|
| 207 |
<div class="card-footer">
|
| 208 |
-
|
| 209 |
</div>
|
| 210 |
</a>
|
| 211 |
</div>
|
|
|
|
| 150 |
|
| 151 |
<div class="grid">
|
| 152 |
<!-- Open WebUI Card -->
|
| 153 |
+
<a href="/" class="card">
|
| 154 |
<div>
|
| 155 |
<div class="card-header">
|
| 156 |
<span class="icon">🤖</span>
|
| 157 |
<span class="badge">Online</span>
|
| 158 |
</div>
|
| 159 |
<h2>Open WebUI</h2>
|
| 160 |
+
<p>Feature-rich AI chat interface connected to AI model endpoints.</p>
|
| 161 |
</div>
|
| 162 |
<div class="card-footer">
|
| 163 |
Launch Open WebUI →
|
| 164 |
</div>
|
| 165 |
</a>
|
| 166 |
|
| 167 |
+
<!-- Jellyfin Card -->
|
| 168 |
+
<a href="/jellyfin/" class="card">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
<div>
|
| 170 |
<div class="card-header">
|
| 171 |
+
<span class="icon">🎬</span>
|
| 172 |
<span class="badge">Online</span>
|
| 173 |
</div>
|
| 174 |
+
<h2>Jellyfin Media Server</h2>
|
| 175 |
+
<p>Stream movies, TV shows, and Telegram Drive media files.</p>
|
| 176 |
</div>
|
| 177 |
<div class="card-footer">
|
| 178 |
+
Launch Jellyfin →
|
| 179 |
</div>
|
| 180 |
</a>
|
| 181 |
|
| 182 |
+
<!-- TG Streamer Card -->
|
| 183 |
+
<a href="/tg-stream/health" class="card">
|
| 184 |
<div>
|
| 185 |
<div class="card-header">
|
| 186 |
+
<span class="icon">📡</span>
|
| 187 |
<span class="badge">Online</span>
|
| 188 |
</div>
|
| 189 |
+
<h2>Telegram Streamer</h2>
|
| 190 |
+
<p>High-speed MTProto 5G range streaming proxy for Telegram Drive.</p>
|
| 191 |
</div>
|
| 192 |
<div class="card-footer">
|
| 193 |
+
Check Status →
|
| 194 |
</div>
|
| 195 |
</a>
|
| 196 |
</div>
|
mobile/app.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"expo": {
|
| 3 |
-
"name": "OpenCode Mobile",
|
| 4 |
-
"slug": "opencode-mobile",
|
| 5 |
-
"version": "1.0.0",
|
| 6 |
-
"orientation": "portrait",
|
| 7 |
-
"scheme": "opencode",
|
| 8 |
-
"platforms": ["android"],
|
| 9 |
-
"android": {
|
| 10 |
-
"package": "com.opencode.mobile",
|
| 11 |
-
"adaptiveIcon": {
|
| 12 |
-
"backgroundColor": "#1a1b26"
|
| 13 |
-
}
|
| 14 |
-
},
|
| 15 |
-
"plugins": [
|
| 16 |
-
"expo-asset",
|
| 17 |
-
"expo-font",
|
| 18 |
-
"expo-router",
|
| 19 |
-
"expo-secure-store"
|
| 20 |
-
]
|
| 21 |
-
}
|
| 22 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/(tabs)/_layout.tsx
DELETED
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
import { Tabs } from "expo-router";
|
| 2 |
-
import { Home, MessageSquare, FolderOpen, Search, Terminal } from "lucide-react-native";
|
| 3 |
-
import { getTheme } from "../../lib/storage";
|
| 4 |
-
import { themes } from "../../constants/themes";
|
| 5 |
-
|
| 6 |
-
export default function TabLayout() {
|
| 7 |
-
const theme = themes[getTheme()];
|
| 8 |
-
|
| 9 |
-
const icons = {
|
| 10 |
-
index: Home,
|
| 11 |
-
sessions: MessageSquare,
|
| 12 |
-
files: FolderOpen,
|
| 13 |
-
search: Search,
|
| 14 |
-
terminal: Terminal,
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
-
return (
|
| 18 |
-
<Tabs
|
| 19 |
-
screenOptions={{
|
| 20 |
-
headerShown: false,
|
| 21 |
-
tabBarStyle: {
|
| 22 |
-
backgroundColor: theme.colors.bgSecondary,
|
| 23 |
-
borderTopColor: theme.colors.border,
|
| 24 |
-
height: 60,
|
| 25 |
-
paddingBottom: 8,
|
| 26 |
-
paddingTop: 8,
|
| 27 |
-
},
|
| 28 |
-
tabBarActiveTintColor: theme.colors.primary,
|
| 29 |
-
tabBarInactiveTintColor: theme.colors.textMuted,
|
| 30 |
-
}}
|
| 31 |
-
>
|
| 32 |
-
<Tabs.Screen
|
| 33 |
-
name="index"
|
| 34 |
-
options={{
|
| 35 |
-
title: "Home",
|
| 36 |
-
tabBarIcon: ({ color, size }) => <icons.index size={size} color={color} />,
|
| 37 |
-
}}
|
| 38 |
-
/>
|
| 39 |
-
<Tabs.Screen
|
| 40 |
-
name="sessions"
|
| 41 |
-
options={{
|
| 42 |
-
title: "Sessions",
|
| 43 |
-
tabBarIcon: ({ color, size }) => <icons.sessions size={size} color={color} />,
|
| 44 |
-
}}
|
| 45 |
-
/>
|
| 46 |
-
<Tabs.Screen
|
| 47 |
-
name="files"
|
| 48 |
-
options={{
|
| 49 |
-
title: "Files",
|
| 50 |
-
tabBarIcon: ({ color, size }) => <icons.files size={size} color={color} />,
|
| 51 |
-
}}
|
| 52 |
-
/>
|
| 53 |
-
<Tabs.Screen
|
| 54 |
-
name="search"
|
| 55 |
-
options={{
|
| 56 |
-
title: "Search",
|
| 57 |
-
tabBarIcon: ({ color, size }) => <icons.search size={size} color={color} />,
|
| 58 |
-
}}
|
| 59 |
-
/>
|
| 60 |
-
<Tabs.Screen
|
| 61 |
-
name="terminal"
|
| 62 |
-
options={{
|
| 63 |
-
title: "Terminal",
|
| 64 |
-
tabBarIcon: ({ color, size }) => <icons.terminal size={size} color={color} />,
|
| 65 |
-
}}
|
| 66 |
-
/>
|
| 67 |
-
</Tabs>
|
| 68 |
-
);
|
| 69 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/(tabs)/files.tsx
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
import { View, Text, TextInput, TouchableOpacity, FlatList, StyleSheet } from "react-native";
|
| 2 |
-
import { useState } from "react";
|
| 3 |
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
| 4 |
-
import { Search, ChevronRight, Folder, File, ArrowLeft } from "lucide-react-native";
|
| 5 |
-
import { useListFiles } from "../../hooks/useApi";
|
| 6 |
-
import { getTheme } from "../../lib/storage";
|
| 7 |
-
import { themes } from "../../constants/themes";
|
| 8 |
-
|
| 9 |
-
export default function FilesScreen() {
|
| 10 |
-
const insets = useSafeAreaInsets();
|
| 11 |
-
const theme = themes[getTheme()];
|
| 12 |
-
const [currentDir, setCurrentDir] = useState(".");
|
| 13 |
-
const [search, setSearch] = useState("");
|
| 14 |
-
const { data: files, isLoading } = useListFiles(currentDir);
|
| 15 |
-
|
| 16 |
-
const filtered = (files || []).filter(
|
| 17 |
-
(f) => !search || f.name.toLowerCase().includes(search.toLowerCase())
|
| 18 |
-
);
|
| 19 |
-
|
| 20 |
-
const dirs = currentDir === "." ? [] : currentDir.split("/");
|
| 21 |
-
const goBack = () => {
|
| 22 |
-
if (dirs.length <= 1) {
|
| 23 |
-
setCurrentDir(".");
|
| 24 |
-
} else {
|
| 25 |
-
setCurrentDir(dirs.slice(0, -1).join("/"));
|
| 26 |
-
}
|
| 27 |
-
};
|
| 28 |
-
|
| 29 |
-
return (
|
| 30 |
-
<View style={[styles.container, { backgroundColor: theme.colors.bg }]}>
|
| 31 |
-
<View style={[styles.header, { paddingTop: insets.top + 12 }]}>
|
| 32 |
-
{currentDir !== "." && (
|
| 33 |
-
<TouchableOpacity onPress={goBack} style={styles.backBtn}>
|
| 34 |
-
<ArrowLeft size={20} color={theme.colors.text} />
|
| 35 |
-
</TouchableOpacity>
|
| 36 |
-
)}
|
| 37 |
-
<Text style={[styles.title, { color: theme.colors.text }]} numberOfLines={1}>
|
| 38 |
-
{currentDir === "." ? "Files" : dirs[dirs.length - 1]}
|
| 39 |
-
</Text>
|
| 40 |
-
</View>
|
| 41 |
-
|
| 42 |
-
<View style={styles.breadcrumb}>
|
| 43 |
-
{dirs.map((d, i) => (
|
| 44 |
-
<TouchableOpacity
|
| 45 |
-
key={i}
|
| 46 |
-
onPress={() => setCurrentDir(dirs.slice(0, i + 1).join("/"))}
|
| 47 |
-
style={styles.breadcrumbItem}
|
| 48 |
-
>
|
| 49 |
-
<Text style={[styles.breadcrumbText, { color: theme.colors.primary }]}>{d}</Text>
|
| 50 |
-
<ChevronRight size={12} color={theme.colors.textMuted} />
|
| 51 |
-
</TouchableOpacity>
|
| 52 |
-
))}
|
| 53 |
-
{dirs.length === 0 && (
|
| 54 |
-
<Text style={[styles.breadcrumbText, { color: theme.colors.textMuted }]}>root</Text>
|
| 55 |
-
)}
|
| 56 |
-
</View>
|
| 57 |
-
|
| 58 |
-
<View style={[styles.searchBar, { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}>
|
| 59 |
-
<Search size={16} color={theme.colors.textMuted} />
|
| 60 |
-
<TextInput
|
| 61 |
-
style={[styles.searchInput, { color: theme.colors.text }]}
|
| 62 |
-
placeholder="Filter files..."
|
| 63 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 64 |
-
value={search}
|
| 65 |
-
onChangeText={setSearch}
|
| 66 |
-
/>
|
| 67 |
-
</View>
|
| 68 |
-
|
| 69 |
-
<FlatList
|
| 70 |
-
data={filtered}
|
| 71 |
-
keyExtractor={(item) => item.path}
|
| 72 |
-
contentContainerStyle={{ paddingBottom: insets.bottom + 100, paddingHorizontal: 20 }}
|
| 73 |
-
showsVerticalScrollIndicator={false}
|
| 74 |
-
ListEmptyComponent={
|
| 75 |
-
<View style={styles.empty}>
|
| 76 |
-
<Folder size={40} color={theme.colors.textMuted} />
|
| 77 |
-
<Text style={[styles.emptyText, { color: theme.colors.textMuted }]}>
|
| 78 |
-
{isLoading ? "Loading..." : "Empty"}
|
| 79 |
-
</Text>
|
| 80 |
-
</View>
|
| 81 |
-
}
|
| 82 |
-
renderItem={({ item }) => (
|
| 83 |
-
<TouchableOpacity
|
| 84 |
-
style={[styles.fileItem, { backgroundColor: theme.colors.surface }]}
|
| 85 |
-
onPress={() => {
|
| 86 |
-
if (item.type === "directory") {
|
| 87 |
-
setCurrentDir(item.path);
|
| 88 |
-
}
|
| 89 |
-
}}
|
| 90 |
-
>
|
| 91 |
-
{item.type === "directory" ? (
|
| 92 |
-
<Folder size={18} color={theme.colors.warning} />
|
| 93 |
-
) : (
|
| 94 |
-
<File size={18} color={theme.colors.textMuted} />
|
| 95 |
-
)}
|
| 96 |
-
<Text style={[styles.fileName, { color: theme.colors.text }]} numberOfLines={1}>
|
| 97 |
-
{item.name}
|
| 98 |
-
</Text>
|
| 99 |
-
{item.type === "directory" && (
|
| 100 |
-
<ChevronRight size={16} color={theme.colors.textMuted} />
|
| 101 |
-
)}
|
| 102 |
-
</TouchableOpacity>
|
| 103 |
-
)}
|
| 104 |
-
/>
|
| 105 |
-
</View>
|
| 106 |
-
);
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
const styles = StyleSheet.create({
|
| 110 |
-
container: { flex: 1 },
|
| 111 |
-
header: {
|
| 112 |
-
flexDirection: "row",
|
| 113 |
-
alignItems: "center",
|
| 114 |
-
gap: 10,
|
| 115 |
-
paddingHorizontal: 20,
|
| 116 |
-
paddingBottom: 12,
|
| 117 |
-
},
|
| 118 |
-
backBtn: { padding: 4 },
|
| 119 |
-
title: { fontSize: 28, fontWeight: "700", letterSpacing: -0.5, flex: 1 },
|
| 120 |
-
breadcrumb: {
|
| 121 |
-
flexDirection: "row",
|
| 122 |
-
alignItems: "center",
|
| 123 |
-
flexWrap: "wrap",
|
| 124 |
-
paddingHorizontal: 20,
|
| 125 |
-
marginBottom: 12,
|
| 126 |
-
gap: 4,
|
| 127 |
-
},
|
| 128 |
-
breadcrumbItem: { flexDirection: "row", alignItems: "center", gap: 4 },
|
| 129 |
-
breadcrumbText: { fontSize: 13 },
|
| 130 |
-
searchBar: {
|
| 131 |
-
flexDirection: "row",
|
| 132 |
-
alignItems: "center",
|
| 133 |
-
gap: 8,
|
| 134 |
-
marginHorizontal: 20,
|
| 135 |
-
marginBottom: 16,
|
| 136 |
-
paddingHorizontal: 14,
|
| 137 |
-
paddingVertical: 10,
|
| 138 |
-
borderRadius: 10,
|
| 139 |
-
borderWidth: 1,
|
| 140 |
-
},
|
| 141 |
-
searchInput: { flex: 1, fontSize: 14, padding: 0 },
|
| 142 |
-
fileItem: {
|
| 143 |
-
flexDirection: "row",
|
| 144 |
-
alignItems: "center",
|
| 145 |
-
gap: 10,
|
| 146 |
-
padding: 12,
|
| 147 |
-
borderRadius: 10,
|
| 148 |
-
marginBottom: 4,
|
| 149 |
-
},
|
| 150 |
-
fileName: { flex: 1, fontSize: 14 },
|
| 151 |
-
empty: { alignItems: "center", marginTop: 80, gap: 12 },
|
| 152 |
-
emptyText: { fontSize: 14 },
|
| 153 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/(tabs)/index.tsx
DELETED
|
@@ -1,140 +0,0 @@
|
|
| 1 |
-
import { View, Text, TouchableOpacity, ScrollView, StyleSheet } from "react-native";
|
| 2 |
-
import { useRouter } from "expo-router";
|
| 3 |
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
| 4 |
-
import { Plus, Wifi, WifiOff, Settings } from "lucide-react-native";
|
| 5 |
-
import { useSessions, useCreateSession } from "../../hooks/useApi";
|
| 6 |
-
import { useConnection } from "../../hooks/useConnection";
|
| 7 |
-
import { getTheme } from "../../lib/storage";
|
| 8 |
-
import { themes } from "../../constants/themes";
|
| 9 |
-
|
| 10 |
-
export default function HomeScreen() {
|
| 11 |
-
const router = useRouter();
|
| 12 |
-
const insets = useSafeAreaInsets();
|
| 13 |
-
const theme = themes[getTheme()];
|
| 14 |
-
const { isConnected, state: connState } = useConnection();
|
| 15 |
-
const { data: sessions } = useSessions();
|
| 16 |
-
const createSession = useCreateSession();
|
| 17 |
-
|
| 18 |
-
const handleNewSession = async () => {
|
| 19 |
-
try {
|
| 20 |
-
const session = await createSession.mutateAsync({ title: "New Chat", directory: "default" });
|
| 21 |
-
router.push(`/session/${session.id}`);
|
| 22 |
-
} catch {}
|
| 23 |
-
};
|
| 24 |
-
|
| 25 |
-
const recentSessions = (sessions || []).slice(0, 5);
|
| 26 |
-
|
| 27 |
-
return (
|
| 28 |
-
<View style={[styles.container, { backgroundColor: theme.colors.bg }]}>
|
| 29 |
-
<View style={[styles.header, { paddingTop: insets.top + 12 }]}>
|
| 30 |
-
<Text style={[styles.title, { color: theme.colors.text }]}>OpenCode</Text>
|
| 31 |
-
<TouchableOpacity onPress={() => router.push("/settings")}>
|
| 32 |
-
<Settings size={22} color={theme.colors.textMuted} />
|
| 33 |
-
</TouchableOpacity>
|
| 34 |
-
</View>
|
| 35 |
-
|
| 36 |
-
<ScrollView
|
| 37 |
-
style={styles.scroll}
|
| 38 |
-
contentContainerStyle={{ paddingBottom: insets.bottom + 100 }}
|
| 39 |
-
showsVerticalScrollIndicator={false}
|
| 40 |
-
>
|
| 41 |
-
<View
|
| 42 |
-
style={[
|
| 43 |
-
styles.statusCard,
|
| 44 |
-
{
|
| 45 |
-
backgroundColor: isConnected ? theme.colors.bgTertiary : theme.colors.surface,
|
| 46 |
-
borderColor: isConnected ? theme.colors.success : theme.colors.danger,
|
| 47 |
-
},
|
| 48 |
-
]}
|
| 49 |
-
>
|
| 50 |
-
{isConnected ? (
|
| 51 |
-
<Wifi size={18} color={theme.colors.success} />
|
| 52 |
-
) : (
|
| 53 |
-
<WifiOff size={18} color={theme.colors.danger} />
|
| 54 |
-
)}
|
| 55 |
-
<Text style={[styles.statusText, { color: theme.colors.text }]}>
|
| 56 |
-
{isConnected ? "Connected" : connState === "reconnecting" ? "Reconnecting..." : "Disconnected"}
|
| 57 |
-
</Text>
|
| 58 |
-
</View>
|
| 59 |
-
|
| 60 |
-
<TouchableOpacity
|
| 61 |
-
style={[styles.newSessionBtn, { backgroundColor: theme.colors.primary }]}
|
| 62 |
-
onPress={handleNewSession}
|
| 63 |
-
activeOpacity={0.8}
|
| 64 |
-
>
|
| 65 |
-
<Plus size={20} color={theme.colors.primaryText} />
|
| 66 |
-
<Text style={[styles.newSessionText, { color: theme.colors.primaryText }]}>
|
| 67 |
-
New Session
|
| 68 |
-
</Text>
|
| 69 |
-
</TouchableOpacity>
|
| 70 |
-
|
| 71 |
-
{recentSessions.length > 0 && (
|
| 72 |
-
<View style={styles.section}>
|
| 73 |
-
<Text style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}>
|
| 74 |
-
Recent Sessions
|
| 75 |
-
</Text>
|
| 76 |
-
{recentSessions.map((s) => (
|
| 77 |
-
<TouchableOpacity
|
| 78 |
-
key={s.id}
|
| 79 |
-
style={[styles.sessionItem, { backgroundColor: theme.colors.surface }]}
|
| 80 |
-
onPress={() => router.push(`/session/${s.id}`)}
|
| 81 |
-
>
|
| 82 |
-
<Text style={[styles.sessionTitle, { color: theme.colors.text }]} numberOfLines={1}>
|
| 83 |
-
{s.title || "Untitled"}
|
| 84 |
-
</Text>
|
| 85 |
-
<Text style={[styles.sessionTime, { color: theme.colors.textMuted }]}>
|
| 86 |
-
{new Date(s.updatedAt).toLocaleDateString()}
|
| 87 |
-
</Text>
|
| 88 |
-
</TouchableOpacity>
|
| 89 |
-
))}
|
| 90 |
-
</View>
|
| 91 |
-
)}
|
| 92 |
-
</ScrollView>
|
| 93 |
-
</View>
|
| 94 |
-
);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
const styles = StyleSheet.create({
|
| 98 |
-
container: { flex: 1 },
|
| 99 |
-
header: {
|
| 100 |
-
flexDirection: "row",
|
| 101 |
-
justifyContent: "space-between",
|
| 102 |
-
alignItems: "center",
|
| 103 |
-
paddingHorizontal: 20,
|
| 104 |
-
paddingBottom: 12,
|
| 105 |
-
},
|
| 106 |
-
title: { fontSize: 28, fontWeight: "700", letterSpacing: -0.5 },
|
| 107 |
-
scroll: { flex: 1, paddingHorizontal: 20 },
|
| 108 |
-
statusCard: {
|
| 109 |
-
flexDirection: "row",
|
| 110 |
-
alignItems: "center",
|
| 111 |
-
gap: 10,
|
| 112 |
-
padding: 14,
|
| 113 |
-
borderRadius: 12,
|
| 114 |
-
marginBottom: 20,
|
| 115 |
-
borderWidth: 1,
|
| 116 |
-
},
|
| 117 |
-
statusText: { fontSize: 14, fontWeight: "500" },
|
| 118 |
-
newSessionBtn: {
|
| 119 |
-
flexDirection: "row",
|
| 120 |
-
alignItems: "center",
|
| 121 |
-
justifyContent: "center",
|
| 122 |
-
gap: 8,
|
| 123 |
-
padding: 16,
|
| 124 |
-
borderRadius: 14,
|
| 125 |
-
marginBottom: 28,
|
| 126 |
-
},
|
| 127 |
-
newSessionText: { fontSize: 16, fontWeight: "600" },
|
| 128 |
-
section: { marginBottom: 24 },
|
| 129 |
-
sectionTitle: { fontSize: 13, fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 10 },
|
| 130 |
-
sessionItem: {
|
| 131 |
-
flexDirection: "row",
|
| 132 |
-
justifyContent: "space-between",
|
| 133 |
-
alignItems: "center",
|
| 134 |
-
padding: 14,
|
| 135 |
-
borderRadius: 10,
|
| 136 |
-
marginBottom: 6,
|
| 137 |
-
},
|
| 138 |
-
sessionTitle: { fontSize: 15, fontWeight: "500", flex: 1, marginRight: 10 },
|
| 139 |
-
sessionTime: { fontSize: 12 },
|
| 140 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/(tabs)/search.tsx
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
import { View, Text, TextInput, FlatList, StyleSheet } from "react-native";
|
| 2 |
-
import { useState } from "react";
|
| 3 |
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
| 4 |
-
import { Search as SearchIcon } from "lucide-react-native";
|
| 5 |
-
import { useFindFiles } from "../../hooks/useApi";
|
| 6 |
-
import { getTheme } from "../../lib/storage";
|
| 7 |
-
import { themes } from "../../constants/themes";
|
| 8 |
-
|
| 9 |
-
export default function SearchScreen() {
|
| 10 |
-
const insets = useSafeAreaInsets();
|
| 11 |
-
const theme = themes[getTheme()];
|
| 12 |
-
const [query, setQuery] = useState("");
|
| 13 |
-
const { data: results, isLoading } = useFindFiles(query);
|
| 14 |
-
|
| 15 |
-
return (
|
| 16 |
-
<View style={[styles.container, { backgroundColor: theme.colors.bg }]}>
|
| 17 |
-
<View style={[styles.header, { paddingTop: insets.top + 12 }]}>
|
| 18 |
-
<Text style={[styles.title, { color: theme.colors.text }]}>Search</Text>
|
| 19 |
-
</View>
|
| 20 |
-
|
| 21 |
-
<View style={[styles.searchBar, { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}>
|
| 22 |
-
<SearchIcon size={16} color={theme.colors.textMuted} />
|
| 23 |
-
<TextInput
|
| 24 |
-
style={[styles.searchInput, { color: theme.colors.text }]}
|
| 25 |
-
placeholder="Search files, code, text..."
|
| 26 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 27 |
-
value={query}
|
| 28 |
-
onChangeText={setQuery}
|
| 29 |
-
autoFocus
|
| 30 |
-
/>
|
| 31 |
-
</View>
|
| 32 |
-
|
| 33 |
-
<FlatList
|
| 34 |
-
data={results || []}
|
| 35 |
-
keyExtractor={(_, i) => String(i)}
|
| 36 |
-
contentContainerStyle={{ paddingBottom: insets.bottom + 100, paddingHorizontal: 20 }}
|
| 37 |
-
showsVerticalScrollIndicator={false}
|
| 38 |
-
ListEmptyComponent={
|
| 39 |
-
<View style={styles.empty}>
|
| 40 |
-
<SearchIcon size={40} color={theme.colors.textMuted} />
|
| 41 |
-
<Text style={[styles.emptyText, { color: theme.colors.textMuted }]}>
|
| 42 |
-
{query.length === 0
|
| 43 |
-
? "Type to search"
|
| 44 |
-
: isLoading
|
| 45 |
-
? "Searching..."
|
| 46 |
-
: "No results"}
|
| 47 |
-
</Text>
|
| 48 |
-
</View>
|
| 49 |
-
}
|
| 50 |
-
renderItem={({ item }) => (
|
| 51 |
-
<View style={[styles.resultCard, { backgroundColor: theme.colors.surface }]}>
|
| 52 |
-
<View style={styles.resultHeader}>
|
| 53 |
-
<Text style={[styles.resultFile, { color: theme.colors.primary }]} numberOfLines={1}>
|
| 54 |
-
{item.file}
|
| 55 |
-
</Text>
|
| 56 |
-
<Text style={[styles.resultLine, { color: theme.colors.textMuted }]}>
|
| 57 |
-
L{item.line}
|
| 58 |
-
</Text>
|
| 59 |
-
</View>
|
| 60 |
-
<Text style={[styles.resultMatch, { color: theme.colors.text }]} numberOfLines={2}>
|
| 61 |
-
{item.context}
|
| 62 |
-
</Text>
|
| 63 |
-
</View>
|
| 64 |
-
)}
|
| 65 |
-
/>
|
| 66 |
-
</View>
|
| 67 |
-
);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
const styles = StyleSheet.create({
|
| 71 |
-
container: { flex: 1 },
|
| 72 |
-
header: {
|
| 73 |
-
paddingHorizontal: 20,
|
| 74 |
-
paddingBottom: 12,
|
| 75 |
-
},
|
| 76 |
-
title: { fontSize: 28, fontWeight: "700", letterSpacing: -0.5 },
|
| 77 |
-
searchBar: {
|
| 78 |
-
flexDirection: "row",
|
| 79 |
-
alignItems: "center",
|
| 80 |
-
gap: 8,
|
| 81 |
-
marginHorizontal: 20,
|
| 82 |
-
marginBottom: 16,
|
| 83 |
-
paddingHorizontal: 14,
|
| 84 |
-
paddingVertical: 10,
|
| 85 |
-
borderRadius: 10,
|
| 86 |
-
borderWidth: 1,
|
| 87 |
-
},
|
| 88 |
-
searchInput: { flex: 1, fontSize: 14, padding: 0 },
|
| 89 |
-
resultCard: {
|
| 90 |
-
padding: 12,
|
| 91 |
-
borderRadius: 10,
|
| 92 |
-
marginBottom: 6,
|
| 93 |
-
},
|
| 94 |
-
resultHeader: {
|
| 95 |
-
flexDirection: "row",
|
| 96 |
-
justifyContent: "space-between",
|
| 97 |
-
alignItems: "center",
|
| 98 |
-
marginBottom: 4,
|
| 99 |
-
},
|
| 100 |
-
resultFile: { fontSize: 13, fontWeight: "500", flex: 1, marginRight: 8 },
|
| 101 |
-
resultLine: { fontSize: 12 },
|
| 102 |
-
resultMatch: { fontSize: 13 },
|
| 103 |
-
empty: { alignItems: "center", marginTop: 80, gap: 12 },
|
| 104 |
-
emptyText: { fontSize: 14 },
|
| 105 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/(tabs)/sessions.tsx
DELETED
|
@@ -1,128 +0,0 @@
|
|
| 1 |
-
import { View, Text, TextInput, TouchableOpacity, FlatList, Alert, StyleSheet } from "react-native";
|
| 2 |
-
import { useRouter } from "expo-router";
|
| 3 |
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
| 4 |
-
import { Plus, Search, Trash2, MessageSquare } from "lucide-react-native";
|
| 5 |
-
import { useState } from "react";
|
| 6 |
-
import { useSessions, useCreateSession, useDeleteSession } from "../../hooks/useApi";
|
| 7 |
-
import { getTheme } from "../../lib/storage";
|
| 8 |
-
import { themes } from "../../constants/themes";
|
| 9 |
-
|
| 10 |
-
export default function SessionsScreen() {
|
| 11 |
-
const router = useRouter();
|
| 12 |
-
const insets = useSafeAreaInsets();
|
| 13 |
-
const theme = themes[getTheme()];
|
| 14 |
-
const { data: sessions, isLoading } = useSessions();
|
| 15 |
-
const createSession = useCreateSession();
|
| 16 |
-
const deleteSession = useDeleteSession();
|
| 17 |
-
const [search, setSearch] = useState("");
|
| 18 |
-
|
| 19 |
-
const filtered = (sessions || []).filter(
|
| 20 |
-
(s) => !search || (s.title || "").toLowerCase().includes(search.toLowerCase())
|
| 21 |
-
);
|
| 22 |
-
|
| 23 |
-
const handleCreate = async () => {
|
| 24 |
-
try {
|
| 25 |
-
const session = await createSession.mutateAsync({ title: "New Chat", directory: "default" });
|
| 26 |
-
router.push(`/session/${session.id}`);
|
| 27 |
-
} catch {}
|
| 28 |
-
};
|
| 29 |
-
|
| 30 |
-
const handleDelete = (id: string, title: string) => {
|
| 31 |
-
Alert.alert("Delete Session", `Delete "${title || "Untitled"}"?`, [
|
| 32 |
-
{ text: "Cancel", style: "cancel" },
|
| 33 |
-
{ text: "Delete", style: "destructive", onPress: () => deleteSession.mutateAsync(id) },
|
| 34 |
-
]);
|
| 35 |
-
};
|
| 36 |
-
|
| 37 |
-
return (
|
| 38 |
-
<View style={[styles.container, { backgroundColor: theme.colors.bg }]}>
|
| 39 |
-
<View style={[styles.header, { paddingTop: insets.top + 12 }]}>
|
| 40 |
-
<Text style={[styles.title, { color: theme.colors.text }]}>Sessions</Text>
|
| 41 |
-
<TouchableOpacity onPress={handleCreate}>
|
| 42 |
-
<Plus size={22} color={theme.colors.primary} />
|
| 43 |
-
</TouchableOpacity>
|
| 44 |
-
</View>
|
| 45 |
-
|
| 46 |
-
<View style={[styles.searchBar, { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}>
|
| 47 |
-
<Search size={16} color={theme.colors.textMuted} />
|
| 48 |
-
<TextInput
|
| 49 |
-
style={[styles.searchInput, { color: theme.colors.text }]}
|
| 50 |
-
placeholder="Search sessions..."
|
| 51 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 52 |
-
value={search}
|
| 53 |
-
onChangeText={setSearch}
|
| 54 |
-
/>
|
| 55 |
-
</View>
|
| 56 |
-
|
| 57 |
-
<FlatList
|
| 58 |
-
data={filtered}
|
| 59 |
-
keyExtractor={(item) => item.id}
|
| 60 |
-
contentContainerStyle={{ paddingBottom: insets.bottom + 100, paddingHorizontal: 20 }}
|
| 61 |
-
showsVerticalScrollIndicator={false}
|
| 62 |
-
ListEmptyComponent={
|
| 63 |
-
<View style={styles.empty}>
|
| 64 |
-
<MessageSquare size={40} color={theme.colors.textMuted} />
|
| 65 |
-
<Text style={[styles.emptyText, { color: theme.colors.textMuted }]}>
|
| 66 |
-
{isLoading ? "Loading..." : "No sessions yet"}
|
| 67 |
-
</Text>
|
| 68 |
-
</View>
|
| 69 |
-
}
|
| 70 |
-
renderItem={({ item }) => (
|
| 71 |
-
<TouchableOpacity
|
| 72 |
-
style={[styles.sessionCard, { backgroundColor: theme.colors.surface }]}
|
| 73 |
-
onPress={() => router.push(`/session/${item.id}`)}
|
| 74 |
-
>
|
| 75 |
-
<View style={styles.sessionInfo}>
|
| 76 |
-
<Text style={[styles.sessionTitle, { color: theme.colors.text }]} numberOfLines={1}>
|
| 77 |
-
{item.title || "Untitled"}
|
| 78 |
-
</Text>
|
| 79 |
-
<Text style={[styles.sessionDate, { color: theme.colors.textMuted }]}>
|
| 80 |
-
{new Date(item.updatedAt).toLocaleString()}
|
| 81 |
-
</Text>
|
| 82 |
-
</View>
|
| 83 |
-
<TouchableOpacity onPress={() => handleDelete(item.id, item.title)}>
|
| 84 |
-
<Trash2 size={16} color={theme.colors.textMuted} />
|
| 85 |
-
</TouchableOpacity>
|
| 86 |
-
</TouchableOpacity>
|
| 87 |
-
)}
|
| 88 |
-
/>
|
| 89 |
-
</View>
|
| 90 |
-
);
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
const styles = StyleSheet.create({
|
| 94 |
-
container: { flex: 1 },
|
| 95 |
-
header: {
|
| 96 |
-
flexDirection: "row",
|
| 97 |
-
justifyContent: "space-between",
|
| 98 |
-
alignItems: "center",
|
| 99 |
-
paddingHorizontal: 20,
|
| 100 |
-
paddingBottom: 12,
|
| 101 |
-
},
|
| 102 |
-
title: { fontSize: 28, fontWeight: "700", letterSpacing: -0.5 },
|
| 103 |
-
searchBar: {
|
| 104 |
-
flexDirection: "row",
|
| 105 |
-
alignItems: "center",
|
| 106 |
-
gap: 8,
|
| 107 |
-
marginHorizontal: 20,
|
| 108 |
-
marginBottom: 16,
|
| 109 |
-
paddingHorizontal: 14,
|
| 110 |
-
paddingVertical: 10,
|
| 111 |
-
borderRadius: 10,
|
| 112 |
-
borderWidth: 1,
|
| 113 |
-
},
|
| 114 |
-
searchInput: { flex: 1, fontSize: 14, padding: 0 },
|
| 115 |
-
sessionCard: {
|
| 116 |
-
flexDirection: "row",
|
| 117 |
-
alignItems: "center",
|
| 118 |
-
justifyContent: "space-between",
|
| 119 |
-
padding: 14,
|
| 120 |
-
borderRadius: 10,
|
| 121 |
-
marginBottom: 6,
|
| 122 |
-
},
|
| 123 |
-
sessionInfo: { flex: 1, marginRight: 12 },
|
| 124 |
-
sessionTitle: { fontSize: 15, fontWeight: "500" },
|
| 125 |
-
sessionDate: { fontSize: 12, marginTop: 2 },
|
| 126 |
-
empty: { alignItems: "center", marginTop: 80, gap: 12 },
|
| 127 |
-
emptyText: { fontSize: 14 },
|
| 128 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/(tabs)/terminal.tsx
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
import { View, Text, StyleSheet } from "react-native";
|
| 2 |
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
| 3 |
-
import { Terminal } from "lucide-react-native";
|
| 4 |
-
import { getTheme } from "../../lib/storage";
|
| 5 |
-
import { themes } from "../../constants/themes";
|
| 6 |
-
|
| 7 |
-
export default function TerminalScreen() {
|
| 8 |
-
const insets = useSafeAreaInsets();
|
| 9 |
-
const theme = themes[getTheme()];
|
| 10 |
-
|
| 11 |
-
return (
|
| 12 |
-
<View style={[styles.container, { backgroundColor: theme.colors.bg }]}>
|
| 13 |
-
<View style={[styles.header, { paddingTop: insets.top + 12 }]}>
|
| 14 |
-
<Text style={[styles.title, { color: theme.colors.text }]}>Terminal</Text>
|
| 15 |
-
</View>
|
| 16 |
-
<View style={styles.empty}>
|
| 17 |
-
<Terminal size={48} color={theme.colors.textMuted} />
|
| 18 |
-
<Text style={[styles.emptyTitle, { color: theme.colors.text }]}>Terminal</Text>
|
| 19 |
-
<Text style={[styles.emptyText, { color: theme.colors.textMuted }]}>
|
| 20 |
-
Terminal sessions will appear here.
|
| 21 |
-
</Text>
|
| 22 |
-
</View>
|
| 23 |
-
</View>
|
| 24 |
-
);
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
const styles = StyleSheet.create({
|
| 28 |
-
container: { flex: 1 },
|
| 29 |
-
header: {
|
| 30 |
-
paddingHorizontal: 20,
|
| 31 |
-
paddingBottom: 12,
|
| 32 |
-
},
|
| 33 |
-
title: { fontSize: 28, fontWeight: "700", letterSpacing: -0.5 },
|
| 34 |
-
empty: { flex: 1, alignItems: "center", justifyContent: "center", gap: 12 },
|
| 35 |
-
emptyTitle: { fontSize: 18, fontWeight: "600" },
|
| 36 |
-
emptyText: { fontSize: 14 },
|
| 37 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/_layout.tsx
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import { useEffect } from "react";
|
| 2 |
-
import { Stack } from "expo-router";
|
| 3 |
-
import { StatusBar } from "expo-status-bar";
|
| 4 |
-
import { QueryProvider } from "../lib/query-provider";
|
| 5 |
-
import { getTheme } from "../lib/storage";
|
| 6 |
-
import { themes } from "../constants/themes";
|
| 7 |
-
|
| 8 |
-
export default function RootLayout() {
|
| 9 |
-
const theme = themes[getTheme()];
|
| 10 |
-
|
| 11 |
-
useEffect(() => {
|
| 12 |
-
const { SystemUI } = require("expo-system-ui");
|
| 13 |
-
SystemUI.setBackgroundColorAsync(theme.colors.bg);
|
| 14 |
-
}, []);
|
| 15 |
-
|
| 16 |
-
return (
|
| 17 |
-
<QueryProvider>
|
| 18 |
-
<StatusBar style="light" />
|
| 19 |
-
<Stack
|
| 20 |
-
screenOptions={{
|
| 21 |
-
headerShown: false,
|
| 22 |
-
contentStyle: { backgroundColor: theme.colors.bg },
|
| 23 |
-
}}
|
| 24 |
-
/>
|
| 25 |
-
</QueryProvider>
|
| 26 |
-
);
|
| 27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/session/[id].tsx
DELETED
|
@@ -1,271 +0,0 @@
|
|
| 1 |
-
import { View, Text, TextInput, TouchableOpacity, FlatList, KeyboardAvoidingView, Platform, StyleSheet } from "react-native";
|
| 2 |
-
import { useLocalSearchParams, useRouter } from "expo-router";
|
| 3 |
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
| 4 |
-
import { useState, useEffect, useRef } from "react";
|
| 5 |
-
import { ArrowLeft, Send, Loader, AlertTriangle } from "lucide-react-native";
|
| 6 |
-
import { useMessages, useSession, useSendMessage, useReplyPermission, useReplyQuestion } from "../../hooks/useApi";
|
| 7 |
-
import { useSSE, getMessagesFromEvents } from "../../hooks/useSSE";
|
| 8 |
-
import { MarkdownRenderer } from "../../components/MarkdownRenderer";
|
| 9 |
-
import { ToolTimeline } from "../../components/ToolTimeline";
|
| 10 |
-
import { PermissionCard } from "../../components/PermissionCard";
|
| 11 |
-
import { QuestionCard } from "../../components/QuestionCard";
|
| 12 |
-
import { Message, MessagePart, ToolPart, PermissionRequest, QuestionRequest } from "../../constants/types";
|
| 13 |
-
import { getTheme } from "../../lib/storage";
|
| 14 |
-
import { themes } from "../../constants/themes";
|
| 15 |
-
|
| 16 |
-
export default function ChatScreen() {
|
| 17 |
-
const { id } = useLocalSearchParams<{ id: string }>();
|
| 18 |
-
const router = useRouter();
|
| 19 |
-
const insets = useSafeAreaInsets();
|
| 20 |
-
const theme = themes[getTheme()];
|
| 21 |
-
const [input, setInput] = useState("");
|
| 22 |
-
const flatListRef = useRef<FlatList>(null);
|
| 23 |
-
|
| 24 |
-
const { data: session } = useSession(id || "");
|
| 25 |
-
const { data: serverMessages } = useMessages(id || "");
|
| 26 |
-
const { connect, disconnect, streaming, events, error } = useSSE(id);
|
| 27 |
-
const sendMessage = useSendMessage();
|
| 28 |
-
const replyPermission = useReplyPermission();
|
| 29 |
-
const replyQuestion = useReplyQuestion();
|
| 30 |
-
|
| 31 |
-
// Build messages from server + SSE events
|
| 32 |
-
const sseMessages = getMessagesFromEvents(events);
|
| 33 |
-
const allMessages: Message[] = [...(serverMessages || []), ...sseMessages];
|
| 34 |
-
|
| 35 |
-
// Deduplicate by id
|
| 36 |
-
const seen = new Set<string>();
|
| 37 |
-
const uniqueMessages = allMessages.filter((m) => {
|
| 38 |
-
if (seen.has(m.id)) return false;
|
| 39 |
-
seen.add(m.id);
|
| 40 |
-
return true;
|
| 41 |
-
});
|
| 42 |
-
|
| 43 |
-
// Collect pending permissions and questions
|
| 44 |
-
const pendingPermissions: PermissionRequest[] = [];
|
| 45 |
-
const pendingQuestions: QuestionRequest[] = [];
|
| 46 |
-
for (const ev of events) {
|
| 47 |
-
if (ev.type === "permission.requested") {
|
| 48 |
-
const perm = ev.properties as PermissionRequest;
|
| 49 |
-
if (perm.state === "pending") pendingPermissions.push(perm);
|
| 50 |
-
}
|
| 51 |
-
if (ev.type === "question.asked") {
|
| 52 |
-
const q = ev.properties as QuestionRequest;
|
| 53 |
-
if (q.state === "pending") pendingQuestions.push(q);
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
useEffect(() => {
|
| 58 |
-
connect();
|
| 59 |
-
return () => disconnect();
|
| 60 |
-
}, [id]);
|
| 61 |
-
|
| 62 |
-
const handleSend = () => {
|
| 63 |
-
if (!input.trim() || !id) return;
|
| 64 |
-
sendMessage.mutate({ sessionId: id, content: input.trim() });
|
| 65 |
-
setInput("");
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
useEffect(() => {
|
| 69 |
-
if (uniqueMessages.length > 0 || events.length > 0) {
|
| 70 |
-
setTimeout(() => flatListRef.current?.scrollToEnd({ animated: true }), 100);
|
| 71 |
-
}
|
| 72 |
-
}, [uniqueMessages.length, events.length]);
|
| 73 |
-
|
| 74 |
-
const renderPart = (part: MessagePart, index: number) => {
|
| 75 |
-
if (part.type === "text" && part.text) {
|
| 76 |
-
return <MarkdownRenderer key={index} content={part.text} />;
|
| 77 |
-
}
|
| 78 |
-
if (part.type === "tool") {
|
| 79 |
-
const toolPart = part as ToolPart;
|
| 80 |
-
return (
|
| 81 |
-
<ToolTimeline
|
| 82 |
-
key={toolPart.callID || index}
|
| 83 |
-
parts={[toolPart]}
|
| 84 |
-
/>
|
| 85 |
-
);
|
| 86 |
-
}
|
| 87 |
-
if (part.type === "reasoning") {
|
| 88 |
-
return (
|
| 89 |
-
<View key={index} style={[styles.reasoning, { backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border }]}>
|
| 90 |
-
<Text style={[styles.reasoningLabel, { color: theme.colors.textMuted }]}>Thinking...</Text>
|
| 91 |
-
<Text style={[styles.reasoningText, { color: theme.colors.textMuted }]} numberOfLines={3}>
|
| 92 |
-
{part.text}
|
| 93 |
-
</Text>
|
| 94 |
-
</View>
|
| 95 |
-
);
|
| 96 |
-
}
|
| 97 |
-
return null;
|
| 98 |
-
};
|
| 99 |
-
|
| 100 |
-
const renderMessage = (item: Message) => {
|
| 101 |
-
const isUser = item.role === "user";
|
| 102 |
-
|
| 103 |
-
// Group consecutive tool parts
|
| 104 |
-
const toolParts: ToolPart[] = [];
|
| 105 |
-
const otherParts: MessagePart[] = [];
|
| 106 |
-
for (const part of item.parts) {
|
| 107 |
-
if (part.type === "tool") toolParts.push(part as ToolPart);
|
| 108 |
-
else otherParts.push(part);
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
return (
|
| 112 |
-
<View
|
| 113 |
-
style={[
|
| 114 |
-
styles.bubble,
|
| 115 |
-
isUser
|
| 116 |
-
? { backgroundColor: theme.colors.primary, alignSelf: "flex-end", maxWidth: "85%" }
|
| 117 |
-
: { backgroundColor: theme.colors.surface, alignSelf: "flex-start", maxWidth: "90%" },
|
| 118 |
-
]}
|
| 119 |
-
>
|
| 120 |
-
{otherParts.map((part, i) => renderPart(part, i))}
|
| 121 |
-
{toolParts.length > 0 && <ToolTimeline parts={toolParts} />}
|
| 122 |
-
</View>
|
| 123 |
-
);
|
| 124 |
-
};
|
| 125 |
-
|
| 126 |
-
return (
|
| 127 |
-
<KeyboardAvoidingView
|
| 128 |
-
style={[styles.container, { backgroundColor: theme.colors.bg }]}
|
| 129 |
-
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
| 130 |
-
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 0}
|
| 131 |
-
>
|
| 132 |
-
<View style={[styles.header, { paddingTop: insets.top + 8, backgroundColor: theme.colors.bgSecondary, borderBottomColor: theme.colors.border }]}>
|
| 133 |
-
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
| 134 |
-
<ArrowLeft size={20} color={theme.colors.text} />
|
| 135 |
-
</TouchableOpacity>
|
| 136 |
-
<Text style={[styles.headerTitle, { color: theme.colors.text }]} numberOfLines={1}>
|
| 137 |
-
{session?.title || "Chat"}
|
| 138 |
-
</Text>
|
| 139 |
-
</View>
|
| 140 |
-
|
| 141 |
-
<FlatList
|
| 142 |
-
ref={flatListRef}
|
| 143 |
-
data={uniqueMessages}
|
| 144 |
-
keyExtractor={(item) => item.id}
|
| 145 |
-
contentContainerStyle={{ padding: 20, paddingBottom: 20, gap: 12 }}
|
| 146 |
-
showsVerticalScrollIndicator={false}
|
| 147 |
-
onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })}
|
| 148 |
-
renderItem={({ item }) => renderMessage(item)}
|
| 149 |
-
ListFooterComponent={
|
| 150 |
-
<>
|
| 151 |
-
{/* Pending permissions */}
|
| 152 |
-
{pendingPermissions.map((perm) => (
|
| 153 |
-
<PermissionCard
|
| 154 |
-
key={perm.id}
|
| 155 |
-
request={perm}
|
| 156 |
-
onAllow={() => replyPermission.mutate({ sessionId: id!, requestId: perm.id, allow: true })}
|
| 157 |
-
onDeny={() => replyPermission.mutate({ sessionId: id!, requestId: perm.id, allow: false })}
|
| 158 |
-
/>
|
| 159 |
-
))}
|
| 160 |
-
|
| 161 |
-
{/* Pending questions */}
|
| 162 |
-
{pendingQuestions.map((q) => (
|
| 163 |
-
<QuestionCard
|
| 164 |
-
key={q.id}
|
| 165 |
-
request={q}
|
| 166 |
-
onReply={(answer) => replyQuestion.mutate({ sessionId: id!, requestId: q.id, answer })}
|
| 167 |
-
/>
|
| 168 |
-
))}
|
| 169 |
-
|
| 170 |
-
{/* Streaming indicator */}
|
| 171 |
-
{streaming && (
|
| 172 |
-
<View style={[styles.bubble, { backgroundColor: theme.colors.surface, alignSelf: "flex-start" }]}>
|
| 173 |
-
<View style={styles.typing}>
|
| 174 |
-
<Loader size={14} color={theme.colors.textMuted} />
|
| 175 |
-
<Text style={[styles.typingText, { color: theme.colors.textMuted }]}>Thinking...</Text>
|
| 176 |
-
</View>
|
| 177 |
-
</View>
|
| 178 |
-
)}
|
| 179 |
-
</>
|
| 180 |
-
}
|
| 181 |
-
/>
|
| 182 |
-
|
| 183 |
-
{error && (
|
| 184 |
-
<View style={[styles.errorBar, { backgroundColor: theme.colors.danger }]}>
|
| 185 |
-
<AlertTriangle size={14} color="#fff" />
|
| 186 |
-
<Text style={[styles.errorText, { color: "#fff" }]}>{error}</Text>
|
| 187 |
-
</View>
|
| 188 |
-
)}
|
| 189 |
-
|
| 190 |
-
<View style={[styles.inputBar, { borderTopColor: theme.colors.border, backgroundColor: theme.colors.bgSecondary, paddingBottom: insets.bottom + 8 }]}>
|
| 191 |
-
<TextInput
|
| 192 |
-
style={[styles.textInput, { color: theme.colors.text, backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}
|
| 193 |
-
placeholder="Type a message..."
|
| 194 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 195 |
-
value={input}
|
| 196 |
-
onChangeText={setInput}
|
| 197 |
-
onSubmitEditing={handleSend}
|
| 198 |
-
multiline
|
| 199 |
-
maxLength={4000}
|
| 200 |
-
/>
|
| 201 |
-
<TouchableOpacity
|
| 202 |
-
style={[styles.sendBtn, { backgroundColor: input.trim() ? theme.colors.primary : theme.colors.surface }]}
|
| 203 |
-
onPress={handleSend}
|
| 204 |
-
disabled={!input.trim() || streaming}
|
| 205 |
-
>
|
| 206 |
-
<Send size={18} color={input.trim() ? theme.colors.primaryText : theme.colors.textMuted} />
|
| 207 |
-
</TouchableOpacity>
|
| 208 |
-
</View>
|
| 209 |
-
</KeyboardAvoidingView>
|
| 210 |
-
);
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
const styles = StyleSheet.create({
|
| 214 |
-
container: { flex: 1 },
|
| 215 |
-
header: {
|
| 216 |
-
flexDirection: "row",
|
| 217 |
-
alignItems: "center",
|
| 218 |
-
gap: 10,
|
| 219 |
-
paddingHorizontal: 16,
|
| 220 |
-
paddingBottom: 10,
|
| 221 |
-
borderBottomWidth: 1,
|
| 222 |
-
},
|
| 223 |
-
backBtn: { padding: 4 },
|
| 224 |
-
headerTitle: { fontSize: 17, fontWeight: "600", flex: 1 },
|
| 225 |
-
bubble: {
|
| 226 |
-
padding: 14,
|
| 227 |
-
borderRadius: 14,
|
| 228 |
-
},
|
| 229 |
-
reasoning: {
|
| 230 |
-
padding: 10,
|
| 231 |
-
borderRadius: 8,
|
| 232 |
-
borderWidth: 1,
|
| 233 |
-
marginTop: 4,
|
| 234 |
-
},
|
| 235 |
-
reasoningLabel: { fontSize: 11, fontWeight: "600", textTransform: "uppercase", marginBottom: 4 },
|
| 236 |
-
reasoningText: { fontSize: 12, lineHeight: 18, fontStyle: "italic" },
|
| 237 |
-
typing: { flexDirection: "row", alignItems: "center", gap: 6 },
|
| 238 |
-
typingText: { fontSize: 14 },
|
| 239 |
-
errorBar: {
|
| 240 |
-
flexDirection: "row",
|
| 241 |
-
alignItems: "center",
|
| 242 |
-
gap: 8,
|
| 243 |
-
paddingHorizontal: 16,
|
| 244 |
-
paddingVertical: 8,
|
| 245 |
-
},
|
| 246 |
-
errorText: { fontSize: 13, flex: 1 },
|
| 247 |
-
inputBar: {
|
| 248 |
-
flexDirection: "row",
|
| 249 |
-
alignItems: "flex-end",
|
| 250 |
-
gap: 8,
|
| 251 |
-
paddingHorizontal: 16,
|
| 252 |
-
paddingTop: 10,
|
| 253 |
-
borderTopWidth: 1,
|
| 254 |
-
},
|
| 255 |
-
textInput: {
|
| 256 |
-
flex: 1,
|
| 257 |
-
fontSize: 15,
|
| 258 |
-
paddingHorizontal: 14,
|
| 259 |
-
paddingVertical: 10,
|
| 260 |
-
borderRadius: 12,
|
| 261 |
-
borderWidth: 1,
|
| 262 |
-
maxHeight: 120,
|
| 263 |
-
},
|
| 264 |
-
sendBtn: {
|
| 265 |
-
width: 40,
|
| 266 |
-
height: 40,
|
| 267 |
-
borderRadius: 12,
|
| 268 |
-
alignItems: "center",
|
| 269 |
-
justifyContent: "center",
|
| 270 |
-
},
|
| 271 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/app/settings.tsx
DELETED
|
@@ -1,281 +0,0 @@
|
|
| 1 |
-
import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet } from "react-native";
|
| 2 |
-
import { useRouter } from "expo-router";
|
| 3 |
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
| 4 |
-
import { useState, useEffect } from "react";
|
| 5 |
-
import { ArrowLeft, Check, Shield, Globe, Key, Lock } from "lucide-react-native";
|
| 6 |
-
import { getHealth } from "../lib/api";
|
| 7 |
-
import {
|
| 8 |
-
getServerUrl,
|
| 9 |
-
setServerUrl as persistUrl,
|
| 10 |
-
getAuthType,
|
| 11 |
-
setAuthType,
|
| 12 |
-
getAuthValue,
|
| 13 |
-
setAuthValue,
|
| 14 |
-
clearAuth,
|
| 15 |
-
getTheme,
|
| 16 |
-
setTheme as persistTheme,
|
| 17 |
-
} from "../lib/storage";
|
| 18 |
-
import type { AuthType } from "../lib/storage";
|
| 19 |
-
import { themes } from "../constants/themes";
|
| 20 |
-
import { ThemeName } from "../constants/types";
|
| 21 |
-
|
| 22 |
-
const themeNames = Object.keys(themes) as ThemeName[];
|
| 23 |
-
|
| 24 |
-
const authTypes: { type: AuthType; label: string; description: string; icon: typeof Shield }[] = [
|
| 25 |
-
{ type: "none", label: "None", description: "No authentication", icon: Globe },
|
| 26 |
-
{ type: "basic", label: "Basic Auth", description: "Username & password", icon: Lock },
|
| 27 |
-
{ type: "bearer", label: "Bearer Token", description: "Authorization header", icon: Key },
|
| 28 |
-
{ type: "apikey", label: "API Key", description: "X-API-Key header", icon: Shield },
|
| 29 |
-
];
|
| 30 |
-
|
| 31 |
-
export default function SettingsScreen() {
|
| 32 |
-
const router = useRouter();
|
| 33 |
-
const insets = useSafeAreaInsets();
|
| 34 |
-
const currentTheme = getTheme();
|
| 35 |
-
const theme = themes[currentTheme];
|
| 36 |
-
|
| 37 |
-
const [serverInput, setServerInput] = useState(getServerUrl());
|
| 38 |
-
const [authType, setAuthTypeState] = useState<AuthType>(getAuthType());
|
| 39 |
-
const [authValue, setAuthValueState] = useState(getAuthValue());
|
| 40 |
-
const [connected, setConnected] = useState<boolean | null>(null);
|
| 41 |
-
const [serverVersion, setServerVersion] = useState<string | null>(null);
|
| 42 |
-
|
| 43 |
-
// Basic auth split
|
| 44 |
-
const [basicUser, setBasicUser] = useState(() => {
|
| 45 |
-
const val = getAuthValue();
|
| 46 |
-
return getAuthType() === "basic" && val.includes(":") ? val.split(":")[0] : "";
|
| 47 |
-
});
|
| 48 |
-
const [basicPass, setBasicPass] = useState(() => {
|
| 49 |
-
const val = getAuthValue();
|
| 50 |
-
return getAuthType() === "basic" && val.includes(":") ? val.split(":").slice(1).join(":") : "";
|
| 51 |
-
});
|
| 52 |
-
|
| 53 |
-
const testConnection = async () => {
|
| 54 |
-
persistUrl(serverInput);
|
| 55 |
-
|
| 56 |
-
// Save auth
|
| 57 |
-
if (authType === "basic") {
|
| 58 |
-
setAuthValue(`${basicUser}:${basicPass}`);
|
| 59 |
-
} else {
|
| 60 |
-
setAuthValue(authValue);
|
| 61 |
-
}
|
| 62 |
-
setAuthType(authType);
|
| 63 |
-
|
| 64 |
-
try {
|
| 65 |
-
const health = await getHealth();
|
| 66 |
-
setConnected(health.healthy);
|
| 67 |
-
setServerVersion(health.version || null);
|
| 68 |
-
} catch {
|
| 69 |
-
setConnected(false);
|
| 70 |
-
setServerVersion(null);
|
| 71 |
-
}
|
| 72 |
-
};
|
| 73 |
-
|
| 74 |
-
const handleThemeSelect = (name: ThemeName) => {
|
| 75 |
-
persistTheme(name);
|
| 76 |
-
};
|
| 77 |
-
|
| 78 |
-
return (
|
| 79 |
-
<View style={[styles.container, { backgroundColor: theme.colors.bg }]}>
|
| 80 |
-
<View style={[styles.header, { paddingTop: insets.top + 12, borderBottomColor: theme.colors.border }]}>
|
| 81 |
-
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
| 82 |
-
<ArrowLeft size={20} color={theme.colors.text} />
|
| 83 |
-
</TouchableOpacity>
|
| 84 |
-
<Text style={[styles.title, { color: theme.colors.text }]}>Settings</Text>
|
| 85 |
-
</View>
|
| 86 |
-
|
| 87 |
-
<ScrollView
|
| 88 |
-
contentContainerStyle={{ padding: 20, paddingBottom: insets.bottom + 40 }}
|
| 89 |
-
showsVerticalScrollIndicator={false}
|
| 90 |
-
>
|
| 91 |
-
{/* Server */}
|
| 92 |
-
<Text style={[styles.sectionLabel, { color: theme.colors.textSecondary }]}>Server</Text>
|
| 93 |
-
<View style={[styles.card, { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}>
|
| 94 |
-
<Text style={[styles.label, { color: theme.colors.textMuted }]}>Server URL</Text>
|
| 95 |
-
<TextInput
|
| 96 |
-
style={[styles.input, { color: theme.colors.text, backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border }]}
|
| 97 |
-
value={serverInput}
|
| 98 |
-
onChangeText={setServerInput}
|
| 99 |
-
placeholder="http://127.0.0.1:4096"
|
| 100 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 101 |
-
autoCapitalize="none"
|
| 102 |
-
autoCorrect={false}
|
| 103 |
-
/>
|
| 104 |
-
|
| 105 |
-
{/* Auth Type */}
|
| 106 |
-
<Text style={[styles.label, { color: theme.colors.textMuted, marginTop: 8 }]}>Authentication</Text>
|
| 107 |
-
<View style={styles.authGrid}>
|
| 108 |
-
{authTypes.map((a) => {
|
| 109 |
-
const Icon = a.icon;
|
| 110 |
-
const selected = authType === a.type;
|
| 111 |
-
return (
|
| 112 |
-
<TouchableOpacity
|
| 113 |
-
key={a.type}
|
| 114 |
-
style={[
|
| 115 |
-
styles.authCard,
|
| 116 |
-
{
|
| 117 |
-
backgroundColor: selected ? theme.colors.bgTertiary : theme.colors.bg,
|
| 118 |
-
borderColor: selected ? theme.colors.primary : theme.colors.border,
|
| 119 |
-
},
|
| 120 |
-
]}
|
| 121 |
-
onPress={() => setAuthTypeState(a.type)}
|
| 122 |
-
>
|
| 123 |
-
<Icon size={16} color={selected ? theme.colors.primary : theme.colors.textMuted} />
|
| 124 |
-
<Text style={[styles.authLabel, { color: selected ? theme.colors.text : theme.colors.textMuted }]}>
|
| 125 |
-
{a.label}
|
| 126 |
-
</Text>
|
| 127 |
-
</TouchableOpacity>
|
| 128 |
-
);
|
| 129 |
-
})}
|
| 130 |
-
</View>
|
| 131 |
-
|
| 132 |
-
{/* Auth Value Input */}
|
| 133 |
-
{authType === "basic" && (
|
| 134 |
-
<View style={{ gap: 8, marginTop: 8 }}>
|
| 135 |
-
<TextInput
|
| 136 |
-
style={[styles.input, { color: theme.colors.text, backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border }]}
|
| 137 |
-
value={basicUser}
|
| 138 |
-
onChangeText={setBasicUser}
|
| 139 |
-
placeholder="Username"
|
| 140 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 141 |
-
autoCapitalize="none"
|
| 142 |
-
/>
|
| 143 |
-
<TextInput
|
| 144 |
-
style={[styles.input, { color: theme.colors.text, backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border }]}
|
| 145 |
-
value={basicPass}
|
| 146 |
-
onChangeText={setBasicPass}
|
| 147 |
-
placeholder="Password"
|
| 148 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 149 |
-
secureTextEntry
|
| 150 |
-
/>
|
| 151 |
-
</View>
|
| 152 |
-
)}
|
| 153 |
-
{authType === "bearer" && (
|
| 154 |
-
<TextInput
|
| 155 |
-
style={[styles.input, { color: theme.colors.text, backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border, marginTop: 8 }]}
|
| 156 |
-
value={authValue}
|
| 157 |
-
onChangeText={setAuthValueState}
|
| 158 |
-
placeholder="Bearer token"
|
| 159 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 160 |
-
autoCapitalize="none"
|
| 161 |
-
secureTextEntry
|
| 162 |
-
/>
|
| 163 |
-
)}
|
| 164 |
-
{authType === "apikey" && (
|
| 165 |
-
<TextInput
|
| 166 |
-
style={[styles.input, { color: theme.colors.text, backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border, marginTop: 8 }]}
|
| 167 |
-
value={authValue}
|
| 168 |
-
onChangeText={setAuthValueState}
|
| 169 |
-
placeholder="API key"
|
| 170 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 171 |
-
autoCapitalize="none"
|
| 172 |
-
secureTextEntry
|
| 173 |
-
/>
|
| 174 |
-
)}
|
| 175 |
-
|
| 176 |
-
<TouchableOpacity style={[styles.btn, { backgroundColor: theme.colors.primary }]} onPress={testConnection}>
|
| 177 |
-
<Text style={[styles.btnText, { color: theme.colors.primaryText }]}>Test Connection</Text>
|
| 178 |
-
</TouchableOpacity>
|
| 179 |
-
{connected !== null && (
|
| 180 |
-
<Text style={[styles.status, { color: connected ? theme.colors.success : theme.colors.danger }]}>
|
| 181 |
-
{connected ? `Connected${serverVersion ? ` (v${serverVersion})` : ""}` : "Failed to connect"}
|
| 182 |
-
</Text>
|
| 183 |
-
)}
|
| 184 |
-
</View>
|
| 185 |
-
|
| 186 |
-
{/* Theme */}
|
| 187 |
-
<Text style={[styles.sectionLabel, { color: theme.colors.textSecondary, marginTop: 28 }]}>Theme</Text>
|
| 188 |
-
<View style={styles.themeGrid}>
|
| 189 |
-
{themeNames.map((name) => {
|
| 190 |
-
const t = themes[name];
|
| 191 |
-
const selected = currentTheme === name;
|
| 192 |
-
return (
|
| 193 |
-
<TouchableOpacity
|
| 194 |
-
key={name}
|
| 195 |
-
style={[
|
| 196 |
-
styles.themeCard,
|
| 197 |
-
{
|
| 198 |
-
backgroundColor: t.colors.bg,
|
| 199 |
-
borderColor: selected ? theme.colors.primary : t.colors.border,
|
| 200 |
-
borderWidth: selected ? 2 : 1,
|
| 201 |
-
},
|
| 202 |
-
]}
|
| 203 |
-
onPress={() => handleThemeSelect(name)}
|
| 204 |
-
>
|
| 205 |
-
<View style={styles.themePreview}>
|
| 206 |
-
<View style={[styles.dot, { backgroundColor: t.colors.primary }]} />
|
| 207 |
-
<View style={[styles.dot, { backgroundColor: t.colors.secondary }]} />
|
| 208 |
-
<View style={[styles.dot, { backgroundColor: t.colors.danger }]} />
|
| 209 |
-
</View>
|
| 210 |
-
<Text style={[styles.themeName, { color: t.colors.text }]}>{t.label}</Text>
|
| 211 |
-
{selected && <Check size={14} color={theme.colors.primary} />}
|
| 212 |
-
</TouchableOpacity>
|
| 213 |
-
);
|
| 214 |
-
})}
|
| 215 |
-
</View>
|
| 216 |
-
|
| 217 |
-
{/* About */}
|
| 218 |
-
<Text style={[styles.sectionLabel, { color: theme.colors.textSecondary, marginTop: 28 }]}>About</Text>
|
| 219 |
-
<View style={[styles.card, { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}>
|
| 220 |
-
<Text style={[styles.aboutText, { color: theme.colors.text }]}>OpenCode Mobile</Text>
|
| 221 |
-
<Text style={[styles.aboutVersion, { color: theme.colors.textMuted }]}>v1.0.0</Text>
|
| 222 |
-
</View>
|
| 223 |
-
</ScrollView>
|
| 224 |
-
</View>
|
| 225 |
-
);
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
const styles = StyleSheet.create({
|
| 229 |
-
container: { flex: 1 },
|
| 230 |
-
header: {
|
| 231 |
-
flexDirection: "row",
|
| 232 |
-
alignItems: "center",
|
| 233 |
-
gap: 10,
|
| 234 |
-
paddingHorizontal: 20,
|
| 235 |
-
paddingBottom: 12,
|
| 236 |
-
borderBottomWidth: 1,
|
| 237 |
-
},
|
| 238 |
-
backBtn: { padding: 4 },
|
| 239 |
-
title: { fontSize: 28, fontWeight: "700", letterSpacing: -0.5 },
|
| 240 |
-
sectionLabel: { fontSize: 13, fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 10 },
|
| 241 |
-
card: { padding: 16, borderRadius: 12, borderWidth: 1, gap: 10 },
|
| 242 |
-
label: { fontSize: 13, fontWeight: "500" },
|
| 243 |
-
input: {
|
| 244 |
-
fontSize: 14,
|
| 245 |
-
paddingHorizontal: 12,
|
| 246 |
-
paddingVertical: 10,
|
| 247 |
-
borderRadius: 8,
|
| 248 |
-
borderWidth: 1,
|
| 249 |
-
},
|
| 250 |
-
btn: { paddingVertical: 12, borderRadius: 10, alignItems: "center", marginTop: 4 },
|
| 251 |
-
btnText: { fontSize: 14, fontWeight: "600" },
|
| 252 |
-
status: { fontSize: 13, textAlign: "center" },
|
| 253 |
-
authGrid: {
|
| 254 |
-
flexDirection: "row",
|
| 255 |
-
flexWrap: "wrap",
|
| 256 |
-
gap: 8,
|
| 257 |
-
},
|
| 258 |
-
authCard: {
|
| 259 |
-
flexDirection: "row",
|
| 260 |
-
alignItems: "center",
|
| 261 |
-
gap: 6,
|
| 262 |
-
paddingHorizontal: 12,
|
| 263 |
-
paddingVertical: 8,
|
| 264 |
-
borderRadius: 8,
|
| 265 |
-
borderWidth: 1,
|
| 266 |
-
},
|
| 267 |
-
authLabel: { fontSize: 13, fontWeight: "500" },
|
| 268 |
-
themeGrid: { gap: 8 },
|
| 269 |
-
themeCard: {
|
| 270 |
-
flexDirection: "row",
|
| 271 |
-
alignItems: "center",
|
| 272 |
-
gap: 12,
|
| 273 |
-
padding: 14,
|
| 274 |
-
borderRadius: 10,
|
| 275 |
-
},
|
| 276 |
-
themePreview: { flexDirection: "row", gap: 4 },
|
| 277 |
-
dot: { width: 10, height: 10, borderRadius: 5 },
|
| 278 |
-
themeName: { flex: 1, fontSize: 14, fontWeight: "500" },
|
| 279 |
-
aboutText: { fontSize: 15, fontWeight: "600" },
|
| 280 |
-
aboutVersion: { fontSize: 13 },
|
| 281 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/components/MarkdownRenderer.tsx
DELETED
|
@@ -1,134 +0,0 @@
|
|
| 1 |
-
import React from "react";
|
| 2 |
-
import { View, Text, StyleSheet, ScrollView } from "react-native";
|
| 3 |
-
import Markdown from "react-native-markdown-display";
|
| 4 |
-
import { themes } from "../constants/themes";
|
| 5 |
-
import { getTheme } from "../lib/storage";
|
| 6 |
-
|
| 7 |
-
interface MarkdownRendererProps {
|
| 8 |
-
content: string;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
const codeStyle = (theme: { colors: Record<string, string> }) =>
|
| 12 |
-
StyleSheet.create({
|
| 13 |
-
body: {},
|
| 14 |
-
heading1: { fontSize: 22, fontWeight: "700", color: theme.colors.text, marginTop: 16, marginBottom: 8 },
|
| 15 |
-
heading2: { fontSize: 19, fontWeight: "700", color: theme.colors.text, marginTop: 14, marginBottom: 6 },
|
| 16 |
-
heading3: { fontSize: 16, fontWeight: "600", color: theme.colors.text, marginTop: 12, marginBottom: 4 },
|
| 17 |
-
paragraph: { fontSize: 15, lineHeight: 22, color: theme.colors.text, marginBottom: 8 },
|
| 18 |
-
link: { color: theme.colors.primary, textDecorationLine: "none" },
|
| 19 |
-
blockquote: {
|
| 20 |
-
borderLeftWidth: 3,
|
| 21 |
-
borderLeftColor: theme.colors.primary,
|
| 22 |
-
paddingLeft: 12,
|
| 23 |
-
marginLeft: 0,
|
| 24 |
-
marginVertical: 8,
|
| 25 |
-
opacity: 0.8,
|
| 26 |
-
},
|
| 27 |
-
code_inline: {
|
| 28 |
-
fontFamily: "monospace",
|
| 29 |
-
fontSize: 13,
|
| 30 |
-
color: theme.colors.accent,
|
| 31 |
-
backgroundColor: theme.colors.bgTertiary,
|
| 32 |
-
paddingHorizontal: 5,
|
| 33 |
-
paddingVertical: 2,
|
| 34 |
-
borderRadius: 4,
|
| 35 |
-
},
|
| 36 |
-
code_block: {
|
| 37 |
-
fontFamily: "monospace",
|
| 38 |
-
fontSize: 13,
|
| 39 |
-
color: theme.colors.text,
|
| 40 |
-
backgroundColor: theme.colors.bgTertiary,
|
| 41 |
-
padding: 12,
|
| 42 |
-
borderRadius: 8,
|
| 43 |
-
marginVertical: 8,
|
| 44 |
-
borderWidth: 1,
|
| 45 |
-
borderColor: theme.colors.border,
|
| 46 |
-
},
|
| 47 |
-
fence: {
|
| 48 |
-
fontFamily: "monospace",
|
| 49 |
-
fontSize: 13,
|
| 50 |
-
color: theme.colors.text,
|
| 51 |
-
backgroundColor: theme.colors.bgTertiary,
|
| 52 |
-
padding: 12,
|
| 53 |
-
borderRadius: 8,
|
| 54 |
-
marginVertical: 8,
|
| 55 |
-
borderWidth: 1,
|
| 56 |
-
borderColor: theme.colors.border,
|
| 57 |
-
},
|
| 58 |
-
list_item: { fontSize: 15, lineHeight: 22, color: theme.colors.text, marginBottom: 4 },
|
| 59 |
-
bullet_list: { marginVertical: 8 },
|
| 60 |
-
ordered_list: { marginVertical: 8 },
|
| 61 |
-
table: { borderWidth: 1, borderColor: theme.colors.border, marginVertical: 8 },
|
| 62 |
-
table_header_cell: {
|
| 63 |
-
backgroundColor: theme.colors.bgTertiary,
|
| 64 |
-
padding: 8,
|
| 65 |
-
borderBottomWidth: 1,
|
| 66 |
-
borderBottomColor: theme.colors.border,
|
| 67 |
-
},
|
| 68 |
-
table_cell: { padding: 8, borderBottomWidth: 1, borderBottomColor: theme.colors.border },
|
| 69 |
-
strong: { fontWeight: "700" },
|
| 70 |
-
em: { fontStyle: "italic" },
|
| 71 |
-
hr: { height: 1, backgroundColor: theme.colors.border, marginVertical: 16 },
|
| 72 |
-
image: { marginVertical: 8 },
|
| 73 |
-
});
|
| 74 |
-
|
| 75 |
-
export function MarkdownRenderer({ content }: MarkdownRendererProps) {
|
| 76 |
-
const theme = themes[getTheme()];
|
| 77 |
-
const styles = codeStyle(theme);
|
| 78 |
-
|
| 79 |
-
return (
|
| 80 |
-
<Markdown style={styles}>{content}</Markdown>
|
| 81 |
-
);
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
// ── Code block with language label ──
|
| 85 |
-
|
| 86 |
-
interface CodeBlockProps {
|
| 87 |
-
language?: string;
|
| 88 |
-
children: string;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
export function CodeBlock({ language, children }: CodeBlockProps) {
|
| 92 |
-
const theme = themes[getTheme()];
|
| 93 |
-
|
| 94 |
-
return (
|
| 95 |
-
<View style={[codeStyles.container, { backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border }]}>
|
| 96 |
-
{language ? (
|
| 97 |
-
<View style={[codeStyles.header, { borderBottomColor: theme.colors.border }]}>
|
| 98 |
-
<Text style={[codeStyles.language, { color: theme.colors.textMuted }]}>{language}</Text>
|
| 99 |
-
</View>
|
| 100 |
-
) : null}
|
| 101 |
-
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
| 102 |
-
<Text style={[codeStyles.code, { color: theme.colors.text }]} selectable>
|
| 103 |
-
{children}
|
| 104 |
-
</Text>
|
| 105 |
-
</ScrollView>
|
| 106 |
-
</View>
|
| 107 |
-
);
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
const codeStyles = StyleSheet.create({
|
| 111 |
-
container: {
|
| 112 |
-
borderRadius: 8,
|
| 113 |
-
borderWidth: 1,
|
| 114 |
-
marginVertical: 8,
|
| 115 |
-
overflow: "hidden",
|
| 116 |
-
},
|
| 117 |
-
header: {
|
| 118 |
-
paddingHorizontal: 12,
|
| 119 |
-
paddingVertical: 6,
|
| 120 |
-
borderBottomWidth: 1,
|
| 121 |
-
},
|
| 122 |
-
language: {
|
| 123 |
-
fontSize: 11,
|
| 124 |
-
fontWeight: "600",
|
| 125 |
-
textTransform: "uppercase",
|
| 126 |
-
letterSpacing: 0.5,
|
| 127 |
-
},
|
| 128 |
-
code: {
|
| 129 |
-
fontFamily: "monospace",
|
| 130 |
-
fontSize: 13,
|
| 131 |
-
lineHeight: 20,
|
| 132 |
-
padding: 12,
|
| 133 |
-
},
|
| 134 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/components/PermissionCard.tsx
DELETED
|
@@ -1,113 +0,0 @@
|
|
| 1 |
-
import React from "react";
|
| 2 |
-
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
| 3 |
-
import { Shield, Check, X } from "lucide-react-native";
|
| 4 |
-
import { themes } from "../constants/themes";
|
| 5 |
-
import { getTheme } from "../lib/storage";
|
| 6 |
-
import type { PermissionRequest } from "../constants/types";
|
| 7 |
-
|
| 8 |
-
interface PermissionCardProps {
|
| 9 |
-
request: PermissionRequest;
|
| 10 |
-
onAllow: () => void;
|
| 11 |
-
onDeny: () => void;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
export function PermissionCard({ request, onAllow, onDeny }: PermissionCardProps) {
|
| 15 |
-
const theme = themes[getTheme()];
|
| 16 |
-
|
| 17 |
-
return (
|
| 18 |
-
<View style={[styles.card, { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}>
|
| 19 |
-
<View style={styles.header}>
|
| 20 |
-
<Shield size={16} color={theme.colors.warning} />
|
| 21 |
-
<Text style={[styles.title, { color: theme.colors.text }]}>Permission Required</Text>
|
| 22 |
-
</View>
|
| 23 |
-
|
| 24 |
-
<Text style={[styles.permission, { color: theme.colors.textSecondary }]}>
|
| 25 |
-
{request.permission}
|
| 26 |
-
</Text>
|
| 27 |
-
|
| 28 |
-
{request.description ? (
|
| 29 |
-
<Text style={[styles.description, { color: theme.colors.textMuted }]}>
|
| 30 |
-
{request.description}
|
| 31 |
-
</Text>
|
| 32 |
-
) : null}
|
| 33 |
-
|
| 34 |
-
<View style={styles.actions}>
|
| 35 |
-
<TouchableOpacity
|
| 36 |
-
style={[styles.denyBtn, { backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border }]}
|
| 37 |
-
onPress={onDeny}
|
| 38 |
-
activeOpacity={0.7}
|
| 39 |
-
>
|
| 40 |
-
<X size={16} color={theme.colors.danger} />
|
| 41 |
-
<Text style={[styles.denyText, { color: theme.colors.danger }]}>Deny</Text>
|
| 42 |
-
</TouchableOpacity>
|
| 43 |
-
|
| 44 |
-
<TouchableOpacity
|
| 45 |
-
style={[styles.allowBtn, { backgroundColor: theme.colors.success }]}
|
| 46 |
-
onPress={onAllow}
|
| 47 |
-
activeOpacity={0.7}
|
| 48 |
-
>
|
| 49 |
-
<Check size={16} color="#fff" />
|
| 50 |
-
<Text style={[styles.allowText, { color: "#fff" }]}>Allow</Text>
|
| 51 |
-
</TouchableOpacity>
|
| 52 |
-
</View>
|
| 53 |
-
</View>
|
| 54 |
-
);
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
const styles = StyleSheet.create({
|
| 58 |
-
card: {
|
| 59 |
-
padding: 16,
|
| 60 |
-
borderRadius: 12,
|
| 61 |
-
borderWidth: 1,
|
| 62 |
-
gap: 10,
|
| 63 |
-
},
|
| 64 |
-
header: {
|
| 65 |
-
flexDirection: "row",
|
| 66 |
-
alignItems: "center",
|
| 67 |
-
gap: 8,
|
| 68 |
-
},
|
| 69 |
-
title: {
|
| 70 |
-
fontSize: 15,
|
| 71 |
-
fontWeight: "600",
|
| 72 |
-
},
|
| 73 |
-
permission: {
|
| 74 |
-
fontSize: 14,
|
| 75 |
-
fontWeight: "500",
|
| 76 |
-
},
|
| 77 |
-
description: {
|
| 78 |
-
fontSize: 13,
|
| 79 |
-
lineHeight: 18,
|
| 80 |
-
},
|
| 81 |
-
actions: {
|
| 82 |
-
flexDirection: "row",
|
| 83 |
-
gap: 10,
|
| 84 |
-
marginTop: 4,
|
| 85 |
-
},
|
| 86 |
-
denyBtn: {
|
| 87 |
-
flex: 1,
|
| 88 |
-
flexDirection: "row",
|
| 89 |
-
alignItems: "center",
|
| 90 |
-
justifyContent: "center",
|
| 91 |
-
gap: 6,
|
| 92 |
-
paddingVertical: 10,
|
| 93 |
-
borderRadius: 8,
|
| 94 |
-
borderWidth: 1,
|
| 95 |
-
},
|
| 96 |
-
denyText: {
|
| 97 |
-
fontSize: 14,
|
| 98 |
-
fontWeight: "600",
|
| 99 |
-
},
|
| 100 |
-
allowBtn: {
|
| 101 |
-
flex: 1,
|
| 102 |
-
flexDirection: "row",
|
| 103 |
-
alignItems: "center",
|
| 104 |
-
justifyContent: "center",
|
| 105 |
-
gap: 6,
|
| 106 |
-
paddingVertical: 10,
|
| 107 |
-
borderRadius: 8,
|
| 108 |
-
},
|
| 109 |
-
allowText: {
|
| 110 |
-
fontSize: 14,
|
| 111 |
-
fontWeight: "600",
|
| 112 |
-
},
|
| 113 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/components/QuestionCard.tsx
DELETED
|
@@ -1,176 +0,0 @@
|
|
| 1 |
-
import React, { useState } from "react";
|
| 2 |
-
import { View, Text, TouchableOpacity, TextInput, StyleSheet } from "react-native";
|
| 3 |
-
import { HelpCircle, Send } from "lucide-react-native";
|
| 4 |
-
import { themes } from "../constants/themes";
|
| 5 |
-
import { getTheme } from "../lib/storage";
|
| 6 |
-
import type { QuestionRequest } from "../constants/types";
|
| 7 |
-
|
| 8 |
-
interface QuestionCardProps {
|
| 9 |
-
request: QuestionRequest;
|
| 10 |
-
onReply: (answer: string) => void;
|
| 11 |
-
onReject?: () => void;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
export function QuestionCard({ request, onReply, onReject }: QuestionCardProps) {
|
| 15 |
-
const theme = themes[getTheme()];
|
| 16 |
-
const [selected, setSelected] = useState<string | null>(null);
|
| 17 |
-
const [customAnswer, setCustomAnswer] = useState("");
|
| 18 |
-
|
| 19 |
-
const handleSubmit = () => {
|
| 20 |
-
if (selected) {
|
| 21 |
-
onReply(selected);
|
| 22 |
-
} else if (customAnswer.trim()) {
|
| 23 |
-
onReply(customAnswer.trim());
|
| 24 |
-
}
|
| 25 |
-
};
|
| 26 |
-
|
| 27 |
-
const hasOptions = request.options && request.options.length > 0;
|
| 28 |
-
|
| 29 |
-
return (
|
| 30 |
-
<View style={[styles.card, { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }]}>
|
| 31 |
-
<View style={styles.header}>
|
| 32 |
-
<HelpCircle size={16} color={theme.colors.primary} />
|
| 33 |
-
<Text style={[styles.title, { color: theme.colors.text }]}>Question</Text>
|
| 34 |
-
</View>
|
| 35 |
-
|
| 36 |
-
<Text style={[styles.question, { color: theme.colors.textSecondary }]}>
|
| 37 |
-
{request.question}
|
| 38 |
-
</Text>
|
| 39 |
-
|
| 40 |
-
{hasOptions ? (
|
| 41 |
-
<View style={styles.options}>
|
| 42 |
-
{request.options!.map((option) => {
|
| 43 |
-
const isSelected = selected === option;
|
| 44 |
-
return (
|
| 45 |
-
<TouchableOpacity
|
| 46 |
-
key={option}
|
| 47 |
-
style={[
|
| 48 |
-
styles.option,
|
| 49 |
-
{
|
| 50 |
-
backgroundColor: isSelected ? theme.colors.bgTertiary : theme.colors.bg,
|
| 51 |
-
borderColor: isSelected ? theme.colors.primary : theme.colors.border,
|
| 52 |
-
},
|
| 53 |
-
]}
|
| 54 |
-
onPress={() => setSelected(option)}
|
| 55 |
-
activeOpacity={0.7}
|
| 56 |
-
>
|
| 57 |
-
<View
|
| 58 |
-
style={[
|
| 59 |
-
styles.radio,
|
| 60 |
-
{
|
| 61 |
-
borderColor: isSelected ? theme.colors.primary : theme.colors.textMuted,
|
| 62 |
-
backgroundColor: isSelected ? theme.colors.primary : "transparent",
|
| 63 |
-
},
|
| 64 |
-
]}
|
| 65 |
-
/>
|
| 66 |
-
<Text
|
| 67 |
-
style={[
|
| 68 |
-
styles.optionText,
|
| 69 |
-
{ color: isSelected ? theme.colors.text : theme.colors.textSecondary },
|
| 70 |
-
]}
|
| 71 |
-
>
|
| 72 |
-
{option}
|
| 73 |
-
</Text>
|
| 74 |
-
</TouchableOpacity>
|
| 75 |
-
);
|
| 76 |
-
})}
|
| 77 |
-
</View>
|
| 78 |
-
) : (
|
| 79 |
-
<TextInput
|
| 80 |
-
style={[styles.input, { color: theme.colors.text, backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border }]}
|
| 81 |
-
value={customAnswer}
|
| 82 |
-
onChangeText={setCustomAnswer}
|
| 83 |
-
placeholder="Type your answer..."
|
| 84 |
-
placeholderTextColor={theme.colors.textMuted}
|
| 85 |
-
multiline
|
| 86 |
-
/>
|
| 87 |
-
)}
|
| 88 |
-
|
| 89 |
-
<TouchableOpacity
|
| 90 |
-
style={[
|
| 91 |
-
styles.submitBtn,
|
| 92 |
-
{
|
| 93 |
-
backgroundColor: selected || customAnswer.trim() ? theme.colors.primary : theme.colors.bgTertiary,
|
| 94 |
-
},
|
| 95 |
-
]}
|
| 96 |
-
onPress={handleSubmit}
|
| 97 |
-
disabled={!selected && !customAnswer.trim()}
|
| 98 |
-
activeOpacity={0.7}
|
| 99 |
-
>
|
| 100 |
-
<Send size={14} color={selected || customAnswer.trim() ? theme.colors.primaryText : theme.colors.textMuted} />
|
| 101 |
-
<Text
|
| 102 |
-
style={[
|
| 103 |
-
styles.submitText,
|
| 104 |
-
{ color: selected || customAnswer.trim() ? theme.colors.primaryText : theme.colors.textMuted },
|
| 105 |
-
]}
|
| 106 |
-
>
|
| 107 |
-
Submit
|
| 108 |
-
</Text>
|
| 109 |
-
</TouchableOpacity>
|
| 110 |
-
</View>
|
| 111 |
-
);
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
const styles = StyleSheet.create({
|
| 115 |
-
card: {
|
| 116 |
-
padding: 16,
|
| 117 |
-
borderRadius: 12,
|
| 118 |
-
borderWidth: 1,
|
| 119 |
-
gap: 10,
|
| 120 |
-
},
|
| 121 |
-
header: {
|
| 122 |
-
flexDirection: "row",
|
| 123 |
-
alignItems: "center",
|
| 124 |
-
gap: 8,
|
| 125 |
-
},
|
| 126 |
-
title: {
|
| 127 |
-
fontSize: 15,
|
| 128 |
-
fontWeight: "600",
|
| 129 |
-
},
|
| 130 |
-
question: {
|
| 131 |
-
fontSize: 14,
|
| 132 |
-
lineHeight: 20,
|
| 133 |
-
},
|
| 134 |
-
options: {
|
| 135 |
-
gap: 6,
|
| 136 |
-
},
|
| 137 |
-
option: {
|
| 138 |
-
flexDirection: "row",
|
| 139 |
-
alignItems: "center",
|
| 140 |
-
gap: 10,
|
| 141 |
-
padding: 12,
|
| 142 |
-
borderRadius: 8,
|
| 143 |
-
borderWidth: 1,
|
| 144 |
-
},
|
| 145 |
-
radio: {
|
| 146 |
-
width: 16,
|
| 147 |
-
height: 16,
|
| 148 |
-
borderRadius: 8,
|
| 149 |
-
borderWidth: 2,
|
| 150 |
-
},
|
| 151 |
-
optionText: {
|
| 152 |
-
fontSize: 14,
|
| 153 |
-
flex: 1,
|
| 154 |
-
},
|
| 155 |
-
input: {
|
| 156 |
-
fontSize: 14,
|
| 157 |
-
paddingHorizontal: 12,
|
| 158 |
-
paddingVertical: 10,
|
| 159 |
-
borderRadius: 8,
|
| 160 |
-
borderWidth: 1,
|
| 161 |
-
minHeight: 44,
|
| 162 |
-
},
|
| 163 |
-
submitBtn: {
|
| 164 |
-
flexDirection: "row",
|
| 165 |
-
alignItems: "center",
|
| 166 |
-
justifyContent: "center",
|
| 167 |
-
gap: 6,
|
| 168 |
-
paddingVertical: 10,
|
| 169 |
-
borderRadius: 8,
|
| 170 |
-
marginTop: 4,
|
| 171 |
-
},
|
| 172 |
-
submitText: {
|
| 173 |
-
fontSize: 14,
|
| 174 |
-
fontWeight: "600",
|
| 175 |
-
},
|
| 176 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/components/ToolTimeline.tsx
DELETED
|
@@ -1,165 +0,0 @@
|
|
| 1 |
-
import React from "react";
|
| 2 |
-
import { View, Text, StyleSheet } from "react-native";
|
| 3 |
-
import { Loader, Check, X, FileText, Search, Terminal, Globe, Wrench } from "lucide-react-native";
|
| 4 |
-
import { themes } from "../constants/themes";
|
| 5 |
-
import { getTheme } from "../lib/storage";
|
| 6 |
-
import type { ToolPart } from "../constants/types";
|
| 7 |
-
|
| 8 |
-
interface ToolTimelineProps {
|
| 9 |
-
parts: ToolPart[];
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const toolIcons: Record<string, typeof Wrench> = {
|
| 13 |
-
read: FileText,
|
| 14 |
-
write: FileText,
|
| 15 |
-
edit: FileText,
|
| 16 |
-
glob: Search,
|
| 17 |
-
grep: Search,
|
| 18 |
-
bash: Terminal,
|
| 19 |
-
webfetch: Globe,
|
| 20 |
-
websearch: Globe,
|
| 21 |
-
};
|
| 22 |
-
|
| 23 |
-
const toolLabels: Record<string, string> = {
|
| 24 |
-
read: "Reading file",
|
| 25 |
-
write: "Writing file",
|
| 26 |
-
edit: "Editing file",
|
| 27 |
-
glob: "Searching files",
|
| 28 |
-
grep: "Searching content",
|
| 29 |
-
bash: "Running command",
|
| 30 |
-
webfetch: "Fetching URL",
|
| 31 |
-
websearch: "Searching web",
|
| 32 |
-
};
|
| 33 |
-
|
| 34 |
-
export function ToolTimeline({ parts }: ToolTimelineProps) {
|
| 35 |
-
const theme = themes[getTheme()];
|
| 36 |
-
|
| 37 |
-
if (parts.length === 0) return null;
|
| 38 |
-
|
| 39 |
-
return (
|
| 40 |
-
<View style={styles.container}>
|
| 41 |
-
{parts.map((part, index) => {
|
| 42 |
-
const Icon = toolIcons[part.tool] || Wrench;
|
| 43 |
-
const label = toolLabels[part.tool] || part.tool;
|
| 44 |
-
const isRunning = part.state === "call";
|
| 45 |
-
const isError = part.state === "error";
|
| 46 |
-
const isDone = part.state === "result";
|
| 47 |
-
|
| 48 |
-
return (
|
| 49 |
-
<View key={part.callID || index} style={styles.row}>
|
| 50 |
-
{/* Status indicator */}
|
| 51 |
-
<View
|
| 52 |
-
style={[
|
| 53 |
-
styles.indicator,
|
| 54 |
-
{
|
| 55 |
-
backgroundColor: isRunning
|
| 56 |
-
? theme.colors.warning
|
| 57 |
-
: isError
|
| 58 |
-
? theme.colors.danger
|
| 59 |
-
: theme.colors.success,
|
| 60 |
-
},
|
| 61 |
-
]}
|
| 62 |
-
/>
|
| 63 |
-
|
| 64 |
-
{/* Content */}
|
| 65 |
-
<View style={styles.content}>
|
| 66 |
-
<View style={styles.header}>
|
| 67 |
-
<Icon
|
| 68 |
-
size={14}
|
| 69 |
-
color={
|
| 70 |
-
isRunning
|
| 71 |
-
? theme.colors.warning
|
| 72 |
-
: isError
|
| 73 |
-
? theme.colors.danger
|
| 74 |
-
: theme.colors.success
|
| 75 |
-
}
|
| 76 |
-
/>
|
| 77 |
-
<Text
|
| 78 |
-
style={[
|
| 79 |
-
styles.label,
|
| 80 |
-
{
|
| 81 |
-
color: isRunning
|
| 82 |
-
? theme.colors.text
|
| 83 |
-
: theme.colors.textMuted,
|
| 84 |
-
},
|
| 85 |
-
]}
|
| 86 |
-
numberOfLines={1}
|
| 87 |
-
>
|
| 88 |
-
{label}
|
| 89 |
-
</Text>
|
| 90 |
-
|
| 91 |
-
{isRunning && <Loader size={12} color={theme.colors.warning} />}
|
| 92 |
-
{isDone && <Check size={12} color={theme.colors.success} />}
|
| 93 |
-
{isError && <X size={12} color={theme.colors.danger} />}
|
| 94 |
-
</View>
|
| 95 |
-
|
| 96 |
-
{/* Tool title (if provided) */}
|
| 97 |
-
{part.title ? (
|
| 98 |
-
<Text
|
| 99 |
-
style={[styles.title, { color: theme.colors.textSecondary }]}
|
| 100 |
-
numberOfLines={1}
|
| 101 |
-
>
|
| 102 |
-
{part.title}
|
| 103 |
-
</Text>
|
| 104 |
-
) : null}
|
| 105 |
-
|
| 106 |
-
{/* Show truncated input for certain tools */}
|
| 107 |
-
{part.input && (part.tool === "bash" || part.tool === "webfetch") && (
|
| 108 |
-
<Text
|
| 109 |
-
style={[styles.detail, { color: theme.colors.textMuted }]}
|
| 110 |
-
numberOfLines={1}
|
| 111 |
-
>
|
| 112 |
-
{part.tool === "bash"
|
| 113 |
-
? `$ ${part.input.command || JSON.stringify(part.input).slice(0, 60)}`
|
| 114 |
-
: String(part.input.url || "").slice(0, 80)}
|
| 115 |
-
</Text>
|
| 116 |
-
)}
|
| 117 |
-
</View>
|
| 118 |
-
</View>
|
| 119 |
-
);
|
| 120 |
-
})}
|
| 121 |
-
</View>
|
| 122 |
-
);
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
const styles = StyleSheet.create({
|
| 126 |
-
container: {
|
| 127 |
-
gap: 2,
|
| 128 |
-
marginVertical: 4,
|
| 129 |
-
},
|
| 130 |
-
row: {
|
| 131 |
-
flexDirection: "row",
|
| 132 |
-
alignItems: "flex-start",
|
| 133 |
-
gap: 10,
|
| 134 |
-
},
|
| 135 |
-
indicator: {
|
| 136 |
-
width: 6,
|
| 137 |
-
height: 6,
|
| 138 |
-
borderRadius: 3,
|
| 139 |
-
marginTop: 6,
|
| 140 |
-
},
|
| 141 |
-
content: {
|
| 142 |
-
flex: 1,
|
| 143 |
-
gap: 2,
|
| 144 |
-
},
|
| 145 |
-
header: {
|
| 146 |
-
flexDirection: "row",
|
| 147 |
-
alignItems: "center",
|
| 148 |
-
gap: 6,
|
| 149 |
-
},
|
| 150 |
-
label: {
|
| 151 |
-
fontSize: 13,
|
| 152 |
-
fontWeight: "500",
|
| 153 |
-
flex: 1,
|
| 154 |
-
},
|
| 155 |
-
title: {
|
| 156 |
-
fontSize: 12,
|
| 157 |
-
marginLeft: 20,
|
| 158 |
-
},
|
| 159 |
-
detail: {
|
| 160 |
-
fontSize: 12,
|
| 161 |
-
fontFamily: "monospace",
|
| 162 |
-
marginLeft: 20,
|
| 163 |
-
opacity: 0.7,
|
| 164 |
-
},
|
| 165 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/constants/themes.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
| 1 |
-
import { Theme, ThemeName } from "./types";
|
| 2 |
-
|
| 3 |
-
export const themes: Record<ThemeName, Theme> = {
|
| 4 |
-
tokyo: {
|
| 5 |
-
name: "tokyo",
|
| 6 |
-
label: "Tokyo Night",
|
| 7 |
-
colors: {
|
| 8 |
-
bg: "#1a1b26",
|
| 9 |
-
bgSecondary: "#16161e",
|
| 10 |
-
bgTertiary: "#24283b",
|
| 11 |
-
surface: "#292e42",
|
| 12 |
-
border: "#3b4261",
|
| 13 |
-
text: "#c0caf5",
|
| 14 |
-
textSecondary: "#a9b1d6",
|
| 15 |
-
textMuted: "#565f89",
|
| 16 |
-
primary: "#7aa2f7",
|
| 17 |
-
primaryText: "#1a1b26",
|
| 18 |
-
secondary: "#bb9af7",
|
| 19 |
-
danger: "#f7768e",
|
| 20 |
-
success: "#9ece6a",
|
| 21 |
-
warning: "#e0af68",
|
| 22 |
-
accent: "#7dcfff",
|
| 23 |
-
},
|
| 24 |
-
},
|
| 25 |
-
dracula: {
|
| 26 |
-
name: "dracula",
|
| 27 |
-
label: "Dracula",
|
| 28 |
-
colors: {
|
| 29 |
-
bg: "#282a36",
|
| 30 |
-
bgSecondary: "#21222c",
|
| 31 |
-
bgTertiary: "#343746",
|
| 32 |
-
surface: "#44475a",
|
| 33 |
-
border: "#6272a4",
|
| 34 |
-
text: "#f8f8f2",
|
| 35 |
-
textSecondary: "#bd93f9",
|
| 36 |
-
textMuted: "#6272a4",
|
| 37 |
-
primary: "#bd93f9",
|
| 38 |
-
primaryText: "#282a36",
|
| 39 |
-
secondary: "#ff79c6",
|
| 40 |
-
danger: "#ff5555",
|
| 41 |
-
success: "#50fa7b",
|
| 42 |
-
warning: "#f1fa8c",
|
| 43 |
-
accent: "#8be9fd",
|
| 44 |
-
},
|
| 45 |
-
},
|
| 46 |
-
nord: {
|
| 47 |
-
name: "nord",
|
| 48 |
-
label: "Nord",
|
| 49 |
-
colors: {
|
| 50 |
-
bg: "#2e3440",
|
| 51 |
-
bgSecondary: "#3b4252",
|
| 52 |
-
bgTertiary: "#434c5e",
|
| 53 |
-
surface: "#4c566a",
|
| 54 |
-
border: "#4c566a",
|
| 55 |
-
text: "#eceff4",
|
| 56 |
-
textSecondary: "#d8dee9",
|
| 57 |
-
textMuted: "#7b88a1",
|
| 58 |
-
primary: "#88c0d0",
|
| 59 |
-
primaryText: "#2e3440",
|
| 60 |
-
secondary: "#b48ead",
|
| 61 |
-
danger: "#bf616a",
|
| 62 |
-
success: "#a3be8c",
|
| 63 |
-
warning: "#ebcb8b",
|
| 64 |
-
accent: "#8fbcbb",
|
| 65 |
-
},
|
| 66 |
-
},
|
| 67 |
-
gruvbox: {
|
| 68 |
-
name: "gruvbox",
|
| 69 |
-
label: "Gruvbox",
|
| 70 |
-
colors: {
|
| 71 |
-
bg: "#282828",
|
| 72 |
-
bgSecondary: "#1d2021",
|
| 73 |
-
bgTertiary: "#3c3836",
|
| 74 |
-
surface: "#504945",
|
| 75 |
-
border: "#665c54",
|
| 76 |
-
text: "#ebdbb2",
|
| 77 |
-
textSecondary: "#d5c4a1",
|
| 78 |
-
textMuted: "#928374",
|
| 79 |
-
primary: "#d79921",
|
| 80 |
-
primaryText: "#282828",
|
| 81 |
-
secondary: "#b8bb26",
|
| 82 |
-
danger: "#cc241d",
|
| 83 |
-
success: "#98971a",
|
| 84 |
-
warning: "#d65d0e",
|
| 85 |
-
accent: "#83a598",
|
| 86 |
-
},
|
| 87 |
-
},
|
| 88 |
-
onedark: {
|
| 89 |
-
name: "onedark",
|
| 90 |
-
label: "One Dark",
|
| 91 |
-
colors: {
|
| 92 |
-
bg: "#282c34",
|
| 93 |
-
bgSecondary: "#21252b",
|
| 94 |
-
bgTertiary: "#333842",
|
| 95 |
-
surface: "#3e4451",
|
| 96 |
-
border: "#4b5263",
|
| 97 |
-
text: "#abb2bf",
|
| 98 |
-
textSecondary: "#c8ccd4",
|
| 99 |
-
textMuted: "#636d83",
|
| 100 |
-
primary: "#61afef",
|
| 101 |
-
primaryText: "#282c34",
|
| 102 |
-
secondary: "#c678dd",
|
| 103 |
-
danger: "#e06c75",
|
| 104 |
-
success: "#98c379",
|
| 105 |
-
warning: "#e5c07b",
|
| 106 |
-
accent: "#56b6c2",
|
| 107 |
-
},
|
| 108 |
-
},
|
| 109 |
-
catppuccin: {
|
| 110 |
-
name: "catppuccin",
|
| 111 |
-
label: "Catppuccin Mocha",
|
| 112 |
-
colors: {
|
| 113 |
-
bg: "#1e1e2e",
|
| 114 |
-
bgSecondary: "#181825",
|
| 115 |
-
bgTertiary: "#313244",
|
| 116 |
-
surface: "#45475a",
|
| 117 |
-
border: "#585b70",
|
| 118 |
-
text: "#cdd6f4",
|
| 119 |
-
textSecondary: "#bac2de",
|
| 120 |
-
textMuted: "#6c7086",
|
| 121 |
-
primary: "#89b4fa",
|
| 122 |
-
primaryText: "#1e1e2e",
|
| 123 |
-
secondary: "#cba6f7",
|
| 124 |
-
danger: "#f38ba8",
|
| 125 |
-
success: "#a6e3a1",
|
| 126 |
-
warning: "#f9e2af",
|
| 127 |
-
accent: "#94e2d5",
|
| 128 |
-
},
|
| 129 |
-
},
|
| 130 |
-
};
|
| 131 |
-
|
| 132 |
-
export const defaultTheme: ThemeName = "tokyo";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/constants/types.ts
DELETED
|
@@ -1,404 +0,0 @@
|
|
| 1 |
-
// Types derived from real OpenCode serve OpenAPI spec (v1.18.3)
|
| 2 |
-
// Endpoint prefix: /api/
|
| 3 |
-
|
| 4 |
-
// ── Health ──
|
| 5 |
-
export interface HealthResponse {
|
| 6 |
-
healthy: boolean;
|
| 7 |
-
version: string;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
// ── Config ──
|
| 11 |
-
export interface OpenCodeConfig {
|
| 12 |
-
$schema?: string;
|
| 13 |
-
theme?: string;
|
| 14 |
-
provider?: Record<string, ProviderConfig>;
|
| 15 |
-
model?: Record<string, string>;
|
| 16 |
-
command?: Record<string, string>;
|
| 17 |
-
plugin?: Record<string, unknown>;
|
| 18 |
-
mcp?: Record<string, unknown>;
|
| 19 |
-
share?: unknown;
|
| 20 |
-
experimental?: unknown;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
export interface ProviderConfig {
|
| 24 |
-
api?: string;
|
| 25 |
-
apiKey?: string;
|
| 26 |
-
models?: Record<string, unknown>;
|
| 27 |
-
options?: Record<string, unknown>;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
// ── Provider ──
|
| 31 |
-
export interface Provider {
|
| 32 |
-
id: string;
|
| 33 |
-
name: string;
|
| 34 |
-
api?: string;
|
| 35 |
-
auth?: ProviderAuth;
|
| 36 |
-
models?: Model[];
|
| 37 |
-
env?: Record<string, string>;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
export interface ProviderAuth {
|
| 41 |
-
type: "api_key" | "oauth";
|
| 42 |
-
api_key?: { env?: string; header?: string };
|
| 43 |
-
oauth?: { authorize_url: string; token_url: string; scopes: string[] };
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
// ── Model ──
|
| 47 |
-
export interface Model {
|
| 48 |
-
id: string;
|
| 49 |
-
name: string;
|
| 50 |
-
providerID: string;
|
| 51 |
-
context?: number;
|
| 52 |
-
maxTokens?: number;
|
| 53 |
-
reasoning?: boolean;
|
| 54 |
-
config?: Record<string, unknown>;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
// ── Session ──
|
| 58 |
-
export interface Session {
|
| 59 |
-
id: string;
|
| 60 |
-
parentID?: string;
|
| 61 |
-
title: string;
|
| 62 |
-
agent?: string;
|
| 63 |
-
model?: string;
|
| 64 |
-
status: "idle" | "busy" | "error";
|
| 65 |
-
directory: string;
|
| 66 |
-
createdAt: string;
|
| 67 |
-
updatedAt: string;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
export interface SessionStatus {
|
| 71 |
-
busy: boolean;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
/** Paginated list response from /api/session */
|
| 75 |
-
export interface SessionListResponse {
|
| 76 |
-
data: Session[];
|
| 77 |
-
cursor?: {
|
| 78 |
-
previous?: string;
|
| 79 |
-
next?: string;
|
| 80 |
-
};
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
/** Model list response from /api/model */
|
| 84 |
-
export interface ModelListResponse {
|
| 85 |
-
location?: {
|
| 86 |
-
directory?: string;
|
| 87 |
-
project?: { id: string; directory: string };
|
| 88 |
-
};
|
| 89 |
-
data: Model[];
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
// ── Message ──
|
| 93 |
-
export interface Message {
|
| 94 |
-
id: string;
|
| 95 |
-
sessionID: string;
|
| 96 |
-
role: "user" | "assistant" | "tool" | "system";
|
| 97 |
-
parts: MessagePart[];
|
| 98 |
-
createdAt: string;
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
export type MessagePart =
|
| 102 |
-
| TextPart
|
| 103 |
-
| ToolPart
|
| 104 |
-
| StepPart
|
| 105 |
-
| ReasoningPart
|
| 106 |
-
| SourcePart;
|
| 107 |
-
|
| 108 |
-
export interface TextPart {
|
| 109 |
-
type: "text";
|
| 110 |
-
text: string;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
export interface ToolPart {
|
| 114 |
-
type: "tool";
|
| 115 |
-
tool: string;
|
| 116 |
-
state: "call" | "result" | "error";
|
| 117 |
-
callID: string;
|
| 118 |
-
input?: Record<string, unknown>;
|
| 119 |
-
output?: string;
|
| 120 |
-
title?: string;
|
| 121 |
-
metadata?: Record<string, unknown>;
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
export interface StepPart {
|
| 125 |
-
type: "step";
|
| 126 |
-
name: string;
|
| 127 |
-
state: "pending" | "running" | "completed" | "error";
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
export interface ReasoningPart {
|
| 131 |
-
type: "reasoning";
|
| 132 |
-
text: string;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
export interface SourcePart {
|
| 136 |
-
type: "source";
|
| 137 |
-
source: string;
|
| 138 |
-
title?: string;
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
// ── Events (SSE) ──
|
| 142 |
-
export type ServerEvent =
|
| 143 |
-
| SessionCreatedEvent
|
| 144 |
-
| SessionUpdatedEvent
|
| 145 |
-
| SessionDeletedEvent
|
| 146 |
-
| MessageCreatedEvent
|
| 147 |
-
| MessageUpdatedEvent
|
| 148 |
-
| MessagePartUpdatedEvent
|
| 149 |
-
| MessagePartDeletedEvent
|
| 150 |
-
| PermissionRequestedEvent
|
| 151 |
-
| PermissionUpdatedEvent
|
| 152 |
-
| QuestionAskedEvent
|
| 153 |
-
| QuestionAnsweredEvent
|
| 154 |
-
| FileChangedEvent
|
| 155 |
-
| TodoUpdatedEvent
|
| 156 |
-
| SyncEvent
|
| 157 |
-
| ErrorEvent;
|
| 158 |
-
|
| 159 |
-
export interface SessionCreatedEvent {
|
| 160 |
-
type: "session.created";
|
| 161 |
-
properties: Session;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
export interface SessionUpdatedEvent {
|
| 165 |
-
type: "session.updated";
|
| 166 |
-
properties: Partial<Session> & { id: string };
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
export interface SessionDeletedEvent {
|
| 170 |
-
type: "session.deleted";
|
| 171 |
-
properties: { id: string };
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
export interface MessageCreatedEvent {
|
| 175 |
-
type: "message.created";
|
| 176 |
-
properties: Message;
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
export interface MessageUpdatedEvent {
|
| 180 |
-
type: "message.updated";
|
| 181 |
-
properties: Partial<Message> & { id: string };
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
export interface MessagePartUpdatedEvent {
|
| 185 |
-
type: "message.part.updated";
|
| 186 |
-
properties: {
|
| 187 |
-
messageID: string;
|
| 188 |
-
part: MessagePart;
|
| 189 |
-
sessionID: string;
|
| 190 |
-
};
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
export interface MessagePartDeletedEvent {
|
| 194 |
-
type: "message.part.deleted";
|
| 195 |
-
properties: {
|
| 196 |
-
messageID: string;
|
| 197 |
-
partID: string;
|
| 198 |
-
sessionID: string;
|
| 199 |
-
};
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
export interface PermissionRequestedEvent {
|
| 203 |
-
type: "permission.requested";
|
| 204 |
-
properties: PermissionRequest;
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
export interface PermissionUpdatedEvent {
|
| 208 |
-
type: "permission.updated";
|
| 209 |
-
properties: PermissionRequest;
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
export interface QuestionAskedEvent {
|
| 213 |
-
type: "question.asked";
|
| 214 |
-
properties: QuestionRequest;
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
export interface QuestionAnsweredEvent {
|
| 218 |
-
type: "question.answered";
|
| 219 |
-
properties: QuestionRequest;
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
export interface FileChangedEvent {
|
| 223 |
-
type: "file.changed";
|
| 224 |
-
properties: { path: string; status: string };
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
export interface TodoUpdatedEvent {
|
| 228 |
-
type: "todo.updated";
|
| 229 |
-
properties: { sessionID: string; todos: Todo[] };
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
export interface SyncEvent {
|
| 233 |
-
type: "sync";
|
| 234 |
-
properties: Record<string, unknown>;
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
export interface ErrorEvent {
|
| 238 |
-
type: "error";
|
| 239 |
-
properties: { message: string; code?: string };
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
// ── Permission ──
|
| 243 |
-
export interface PermissionRequest {
|
| 244 |
-
id: string;
|
| 245 |
-
sessionID: string;
|
| 246 |
-
messageID?: string;
|
| 247 |
-
partID?: string;
|
| 248 |
-
permission: string;
|
| 249 |
-
description?: string;
|
| 250 |
-
state: "pending" | "approved" | "denied";
|
| 251 |
-
createdAt: string;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
export interface SavedPermission {
|
| 255 |
-
id: string;
|
| 256 |
-
permission: string;
|
| 257 |
-
pattern?: string;
|
| 258 |
-
allow: boolean;
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
// ── Question ──
|
| 262 |
-
export interface QuestionRequest {
|
| 263 |
-
id: string;
|
| 264 |
-
sessionID: string;
|
| 265 |
-
question: string;
|
| 266 |
-
options?: string[];
|
| 267 |
-
state: "pending" | "answered" | "rejected";
|
| 268 |
-
answer?: string;
|
| 269 |
-
createdAt: string;
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
// ── Todo ──
|
| 273 |
-
export interface Todo {
|
| 274 |
-
id: string;
|
| 275 |
-
content: string;
|
| 276 |
-
status: "pending" | "in_progress" | "completed";
|
| 277 |
-
metadata?: Record<string, unknown>;
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
// ── File ──
|
| 281 |
-
export interface FileEntry {
|
| 282 |
-
name: string;
|
| 283 |
-
path: string;
|
| 284 |
-
type: "file" | "directory";
|
| 285 |
-
size?: number;
|
| 286 |
-
modified?: string;
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
export interface FileContent {
|
| 290 |
-
content: string;
|
| 291 |
-
encoding?: string;
|
| 292 |
-
}
|
| 293 |
-
|
| 294 |
-
export interface FileStatus {
|
| 295 |
-
path: string;
|
| 296 |
-
status: string;
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
// ── Find/Search ──
|
| 300 |
-
export interface FindResult {
|
| 301 |
-
file: string;
|
| 302 |
-
line: number;
|
| 303 |
-
column: number;
|
| 304 |
-
match: string;
|
| 305 |
-
context: string;
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
export interface SymbolResult {
|
| 309 |
-
name: string;
|
| 310 |
-
kind: string;
|
| 311 |
-
file: string;
|
| 312 |
-
line: number;
|
| 313 |
-
column: number;
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
// ── PTY ──
|
| 317 |
-
export interface PTY {
|
| 318 |
-
id: string;
|
| 319 |
-
shell: string;
|
| 320 |
-
cwd: string;
|
| 321 |
-
pid?: number;
|
| 322 |
-
createdAt: string;
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
// ── Command ──
|
| 326 |
-
export interface Command {
|
| 327 |
-
name: string;
|
| 328 |
-
description?: string;
|
| 329 |
-
category?: string;
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
// ── Skill ──
|
| 333 |
-
export interface Skill {
|
| 334 |
-
name: string;
|
| 335 |
-
description?: string;
|
| 336 |
-
location?: string;
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
// ── Agent ──
|
| 340 |
-
export interface Agent {
|
| 341 |
-
name: string;
|
| 342 |
-
description?: string;
|
| 343 |
-
tools?: string[];
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
// ── Diff ──
|
| 347 |
-
export interface DiffEntry {
|
| 348 |
-
path: string;
|
| 349 |
-
status: string;
|
| 350 |
-
additions: number;
|
| 351 |
-
deletions: number;
|
| 352 |
-
hunks?: DiffHunk[];
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
export interface DiffHunk {
|
| 356 |
-
oldStart: number;
|
| 357 |
-
newStart: number;
|
| 358 |
-
oldLines: number;
|
| 359 |
-
newLines: number;
|
| 360 |
-
content: string;
|
| 361 |
-
}
|
| 362 |
-
|
| 363 |
-
// ── VCS ──
|
| 364 |
-
export interface VCSStatus {
|
| 365 |
-
branch: string;
|
| 366 |
-
dirty: boolean;
|
| 367 |
-
ahead: number;
|
| 368 |
-
behind: number;
|
| 369 |
-
}
|
| 370 |
-
|
| 371 |
-
// ── Context helpers ──
|
| 372 |
-
export interface SessionContext {
|
| 373 |
-
files: string[];
|
| 374 |
-
capabilities: string[];
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
export interface SessionHistory {
|
| 378 |
-
messages: Message[];
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
// ── Theme ──
|
| 382 |
-
export type ThemeName = "tokyo" | "dracula" | "nord" | "gruvbox" | "onedark" | "catppuccin";
|
| 383 |
-
|
| 384 |
-
export interface Theme {
|
| 385 |
-
name: ThemeName;
|
| 386 |
-
label: string;
|
| 387 |
-
colors: {
|
| 388 |
-
bg: string;
|
| 389 |
-
bgSecondary: string;
|
| 390 |
-
bgTertiary: string;
|
| 391 |
-
surface: string;
|
| 392 |
-
border: string;
|
| 393 |
-
text: string;
|
| 394 |
-
textSecondary: string;
|
| 395 |
-
textMuted: string;
|
| 396 |
-
primary: string;
|
| 397 |
-
primaryText: string;
|
| 398 |
-
secondary: string;
|
| 399 |
-
danger: string;
|
| 400 |
-
success: string;
|
| 401 |
-
warning: string;
|
| 402 |
-
accent: string;
|
| 403 |
-
};
|
| 404 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/hooks/useApi.ts
DELETED
|
@@ -1,322 +0,0 @@
|
|
| 1 |
-
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
| 2 |
-
import * as api from "../lib/api";
|
| 3 |
-
|
| 4 |
-
// ── Health ──
|
| 5 |
-
|
| 6 |
-
export function useHealth() {
|
| 7 |
-
return useQuery({
|
| 8 |
-
queryKey: ["health"],
|
| 9 |
-
queryFn: api.getHealth,
|
| 10 |
-
refetchInterval: 10000,
|
| 11 |
-
retry: 3,
|
| 12 |
-
});
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
// ── Config ──
|
| 16 |
-
|
| 17 |
-
export function useConfig() {
|
| 18 |
-
return useQuery({
|
| 19 |
-
queryKey: ["config"],
|
| 20 |
-
queryFn: api.getConfig,
|
| 21 |
-
});
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
export function usePatchConfig() {
|
| 25 |
-
const qc = useQueryClient();
|
| 26 |
-
return useMutation({
|
| 27 |
-
mutationFn: api.patchConfig,
|
| 28 |
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["config"] }),
|
| 29 |
-
});
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
// ── Sessions ──
|
| 33 |
-
|
| 34 |
-
export function useSessions() {
|
| 35 |
-
return useQuery({
|
| 36 |
-
queryKey: ["sessions"],
|
| 37 |
-
queryFn: api.getSessions,
|
| 38 |
-
});
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
export function useActiveSessions() {
|
| 42 |
-
return useQuery({
|
| 43 |
-
queryKey: ["sessions", "active"],
|
| 44 |
-
queryFn: api.getActiveSessions,
|
| 45 |
-
});
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
export function useSession(id: string) {
|
| 49 |
-
return useQuery({
|
| 50 |
-
queryKey: ["session", id],
|
| 51 |
-
queryFn: () => api.getSession(id),
|
| 52 |
-
enabled: !!id,
|
| 53 |
-
});
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
export function useCreateSession() {
|
| 57 |
-
const qc = useQueryClient();
|
| 58 |
-
return useMutation({
|
| 59 |
-
mutationFn: (options?: { title?: string; agent?: string; model?: string; directory?: string }) =>
|
| 60 |
-
api.createSession(options),
|
| 61 |
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["sessions"] }),
|
| 62 |
-
});
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
export function useDeleteSession() {
|
| 66 |
-
const qc = useQueryClient();
|
| 67 |
-
return useMutation({
|
| 68 |
-
mutationFn: (id: string) => api.deleteSession(id),
|
| 69 |
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["sessions"] }),
|
| 70 |
-
});
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
export function usePatchSession() {
|
| 74 |
-
const qc = useQueryClient();
|
| 75 |
-
return useMutation({
|
| 76 |
-
mutationFn: ({ id, data }: { id: string; data: { title?: string; agent?: string; model?: string } }) =>
|
| 77 |
-
api.patchSession(id, data),
|
| 78 |
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["sessions"] }),
|
| 79 |
-
});
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
export function useSessionTodos(sessionId: string) {
|
| 83 |
-
return useQuery({
|
| 84 |
-
queryKey: ["todos", sessionId],
|
| 85 |
-
queryFn: () => api.getSessionTodos(sessionId),
|
| 86 |
-
enabled: !!sessionId,
|
| 87 |
-
});
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
// ── Messages ──
|
| 91 |
-
|
| 92 |
-
export function useMessages(sessionId: string) {
|
| 93 |
-
return useQuery({
|
| 94 |
-
queryKey: ["messages", sessionId],
|
| 95 |
-
queryFn: () => api.getMessages(sessionId),
|
| 96 |
-
enabled: !!sessionId,
|
| 97 |
-
});
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
export function useSendMessage() {
|
| 101 |
-
const qc = useQueryClient();
|
| 102 |
-
return useMutation({
|
| 103 |
-
mutationFn: ({ sessionId, content }: { sessionId: string; content: string }) =>
|
| 104 |
-
api.sendMessage(sessionId, content),
|
| 105 |
-
onSuccess: (_, vars) => {
|
| 106 |
-
qc.invalidateQueries({ queryKey: ["messages", vars.sessionId] });
|
| 107 |
-
qc.invalidateQueries({ queryKey: ["sessions"] });
|
| 108 |
-
},
|
| 109 |
-
});
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
export function useAbortSession() {
|
| 113 |
-
return useMutation({
|
| 114 |
-
mutationFn: (sessionId: string) => api.abortSession(sessionId),
|
| 115 |
-
});
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
export function useCompactSession() {
|
| 119 |
-
const qc = useQueryClient();
|
| 120 |
-
return useMutation({
|
| 121 |
-
mutationFn: (sessionId: string) => api.compactSession(sessionId),
|
| 122 |
-
onSuccess: (_, sessionId) => {
|
| 123 |
-
qc.invalidateQueries({ queryKey: ["messages", sessionId] });
|
| 124 |
-
},
|
| 125 |
-
});
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
export function useShareSession() {
|
| 129 |
-
const qc = useQueryClient();
|
| 130 |
-
return useMutation({
|
| 131 |
-
mutationFn: (sessionId: string) => api.shareSession(sessionId),
|
| 132 |
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["sessions"] }),
|
| 133 |
-
});
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
export function useForkSession() {
|
| 137 |
-
const qc = useQueryClient();
|
| 138 |
-
return useMutation({
|
| 139 |
-
mutationFn: (sessionId: string) => api.forkSession(sessionId),
|
| 140 |
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["sessions"] }),
|
| 141 |
-
});
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
// ── Session diff / revert ──
|
| 145 |
-
|
| 146 |
-
export function useSessionDiff(sessionId: string) {
|
| 147 |
-
return useQuery({
|
| 148 |
-
queryKey: ["diff", sessionId],
|
| 149 |
-
queryFn: () => api.getSessionDiff(sessionId),
|
| 150 |
-
enabled: !!sessionId,
|
| 151 |
-
});
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
// ── Permissions ──
|
| 155 |
-
|
| 156 |
-
export function usePermissionRequests() {
|
| 157 |
-
return useQuery({
|
| 158 |
-
queryKey: ["permissions", "requests"],
|
| 159 |
-
queryFn: api.getPermissionRequests,
|
| 160 |
-
refetchInterval: 5000,
|
| 161 |
-
});
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
export function useSavedPermissions() {
|
| 165 |
-
return useQuery({
|
| 166 |
-
queryKey: ["permissions", "saved"],
|
| 167 |
-
queryFn: api.getSavedPermissions,
|
| 168 |
-
});
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
export function useReplyPermission() {
|
| 172 |
-
const qc = useQueryClient();
|
| 173 |
-
return useMutation({
|
| 174 |
-
mutationFn: ({
|
| 175 |
-
sessionId,
|
| 176 |
-
requestId,
|
| 177 |
-
allow,
|
| 178 |
-
}: {
|
| 179 |
-
sessionId: string;
|
| 180 |
-
requestId: string;
|
| 181 |
-
allow: boolean;
|
| 182 |
-
}) => api.replyPermission(sessionId, requestId, allow),
|
| 183 |
-
onSuccess: () => {
|
| 184 |
-
qc.invalidateQueries({ queryKey: ["permissions"] });
|
| 185 |
-
},
|
| 186 |
-
});
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
// ── Questions ──
|
| 190 |
-
|
| 191 |
-
export function useQuestionRequests() {
|
| 192 |
-
return useQuery({
|
| 193 |
-
queryKey: ["questions", "requests"],
|
| 194 |
-
queryFn: api.getQuestionRequests,
|
| 195 |
-
refetchInterval: 5000,
|
| 196 |
-
});
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
export function useReplyQuestion() {
|
| 200 |
-
const qc = useQueryClient();
|
| 201 |
-
return useMutation({
|
| 202 |
-
mutationFn: ({
|
| 203 |
-
sessionId,
|
| 204 |
-
requestId,
|
| 205 |
-
answer,
|
| 206 |
-
}: {
|
| 207 |
-
sessionId: string;
|
| 208 |
-
requestId: string;
|
| 209 |
-
answer: string;
|
| 210 |
-
}) => api.replyQuestion(sessionId, requestId, answer),
|
| 211 |
-
onSuccess: () => {
|
| 212 |
-
qc.invalidateQueries({ queryKey: ["questions"] });
|
| 213 |
-
},
|
| 214 |
-
});
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
// ── Providers / Models ──
|
| 218 |
-
|
| 219 |
-
export function useProviders() {
|
| 220 |
-
return useQuery({
|
| 221 |
-
queryKey: ["providers"],
|
| 222 |
-
queryFn: api.getProviders,
|
| 223 |
-
});
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
export function useModels() {
|
| 227 |
-
return useQuery({
|
| 228 |
-
queryKey: ["models"],
|
| 229 |
-
queryFn: api.getModels,
|
| 230 |
-
});
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
// ── Agent / Command / Skill ──
|
| 234 |
-
|
| 235 |
-
export function useAgents() {
|
| 236 |
-
return useQuery({
|
| 237 |
-
queryKey: ["agents"],
|
| 238 |
-
queryFn: api.getAgents,
|
| 239 |
-
});
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
export function useCommands() {
|
| 243 |
-
return useQuery({
|
| 244 |
-
queryKey: ["commands"],
|
| 245 |
-
queryFn: api.getCommands,
|
| 246 |
-
});
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
export function useSkills() {
|
| 250 |
-
return useQuery({
|
| 251 |
-
queryKey: ["skills"],
|
| 252 |
-
queryFn: api.getSkills,
|
| 253 |
-
});
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
// ── Files ──
|
| 257 |
-
|
| 258 |
-
export function useListFiles(dir: string = ".") {
|
| 259 |
-
return useQuery({
|
| 260 |
-
queryKey: ["files", dir],
|
| 261 |
-
queryFn: () => api.listFiles(dir),
|
| 262 |
-
});
|
| 263 |
-
}
|
| 264 |
-
|
| 265 |
-
export function useFileContent(path: string) {
|
| 266 |
-
return useQuery({
|
| 267 |
-
queryKey: ["fileContent", path],
|
| 268 |
-
queryFn: () => api.readFile(path),
|
| 269 |
-
enabled: !!path,
|
| 270 |
-
});
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
export function useFindFiles(query: string, dir?: string) {
|
| 274 |
-
return useQuery({
|
| 275 |
-
queryKey: ["findFiles", query, dir],
|
| 276 |
-
queryFn: () => api.findFiles(query, dir),
|
| 277 |
-
enabled: query.length > 0,
|
| 278 |
-
});
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
export function useFindSymbols(query: string) {
|
| 282 |
-
return useQuery({
|
| 283 |
-
queryKey: ["findSymbols", query],
|
| 284 |
-
queryFn: () => api.findSymbols(query),
|
| 285 |
-
enabled: query.length > 0,
|
| 286 |
-
});
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
// ── VCS ──
|
| 290 |
-
|
| 291 |
-
export function useVCSStatus() {
|
| 292 |
-
return useQuery({
|
| 293 |
-
queryKey: ["vcs"],
|
| 294 |
-
queryFn: api.getVCSStatus,
|
| 295 |
-
refetchInterval: 15000,
|
| 296 |
-
});
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
// ── PTY ──
|
| 300 |
-
|
| 301 |
-
export function usePTYs() {
|
| 302 |
-
return useQuery({
|
| 303 |
-
queryKey: ["pty"],
|
| 304 |
-
queryFn: api.getPTYs,
|
| 305 |
-
});
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
export function useCreatePTY() {
|
| 309 |
-
const qc = useQueryClient();
|
| 310 |
-
return useMutation({
|
| 311 |
-
mutationFn: (shell?: string) => api.createPTY(shell),
|
| 312 |
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["pty"] }),
|
| 313 |
-
});
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
export function useDeletePTY() {
|
| 317 |
-
const qc = useQueryClient();
|
| 318 |
-
return useMutation({
|
| 319 |
-
mutationFn: (id: string) => api.deletePTY(id),
|
| 320 |
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["pty"] }),
|
| 321 |
-
});
|
| 322 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/hooks/useConnection.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
import { useState, useEffect, useCallback, useRef } from "react";
|
| 2 |
-
import {
|
| 3 |
-
getConnectionManager,
|
| 4 |
-
type ConnectionState,
|
| 5 |
-
} from "../lib/connection-manager";
|
| 6 |
-
import type { ServerEvent } from "../constants/types";
|
| 7 |
-
|
| 8 |
-
export interface UseConnectionReturn {
|
| 9 |
-
state: ConnectionState;
|
| 10 |
-
connect: (sessionId?: string) => void;
|
| 11 |
-
disconnect: () => void;
|
| 12 |
-
switchSession: (sessionId: string | null) => void;
|
| 13 |
-
isConnected: boolean;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export function useConnection(sessionId?: string): UseConnectionReturn {
|
| 17 |
-
const [state, setState] = useState<ConnectionState>("disconnected");
|
| 18 |
-
const manager = useRef(getConnectionManager());
|
| 19 |
-
|
| 20 |
-
useEffect(() => {
|
| 21 |
-
const unsub = manager.current.onStateChange(setState);
|
| 22 |
-
return unsub;
|
| 23 |
-
}, []);
|
| 24 |
-
|
| 25 |
-
// Auto-connect on mount
|
| 26 |
-
useEffect(() => {
|
| 27 |
-
manager.current.connect(sessionId);
|
| 28 |
-
return () => {
|
| 29 |
-
// Don't disconnect on unmount — keep connection alive
|
| 30 |
-
};
|
| 31 |
-
}, [sessionId]);
|
| 32 |
-
|
| 33 |
-
const connect = useCallback(
|
| 34 |
-
(sid?: string) => manager.current.connect(sid ?? sessionId),
|
| 35 |
-
[sessionId]
|
| 36 |
-
);
|
| 37 |
-
|
| 38 |
-
const disconnect = useCallback(() => manager.current.disconnect(), []);
|
| 39 |
-
|
| 40 |
-
const switchSession = useCallback(
|
| 41 |
-
(sid: string | null) => manager.current.switchSession(sid),
|
| 42 |
-
[]
|
| 43 |
-
);
|
| 44 |
-
|
| 45 |
-
return {
|
| 46 |
-
state,
|
| 47 |
-
connect,
|
| 48 |
-
disconnect,
|
| 49 |
-
switchSession,
|
| 50 |
-
isConnected: state === "connected",
|
| 51 |
-
};
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
export function useConnectionEvents(
|
| 55 |
-
onEvent: (event: ServerEvent) => void
|
| 56 |
-
) {
|
| 57 |
-
const callbackRef = useRef(onEvent);
|
| 58 |
-
callbackRef.current = onEvent;
|
| 59 |
-
|
| 60 |
-
useEffect(() => {
|
| 61 |
-
const manager = getConnectionManager();
|
| 62 |
-
const unsub = manager.onEvent((event) => callbackRef.current(event));
|
| 63 |
-
return unsub;
|
| 64 |
-
}, []);
|
| 65 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/hooks/useSSE.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
| 1 |
-
import { useState, useCallback, useRef, useEffect } from "react";
|
| 2 |
-
import { subscribeEvents, parseSSELines } from "../lib/api";
|
| 3 |
-
import type { ServerEvent, Message, MessagePart } from "../constants/types";
|
| 4 |
-
|
| 5 |
-
export interface SSEState {
|
| 6 |
-
connected: boolean;
|
| 7 |
-
streaming: boolean;
|
| 8 |
-
error: string | null;
|
| 9 |
-
events: ServerEvent[];
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
export function useSSE(sessionId?: string) {
|
| 13 |
-
const [state, setState] = useState<SSEState>({
|
| 14 |
-
connected: false,
|
| 15 |
-
streaming: false,
|
| 16 |
-
error: null,
|
| 17 |
-
events: [],
|
| 18 |
-
});
|
| 19 |
-
|
| 20 |
-
const abortRef = useRef<AbortController | null>(null);
|
| 21 |
-
const bufferRef = useRef("");
|
| 22 |
-
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
| 23 |
-
|
| 24 |
-
const connect = useCallback(async () => {
|
| 25 |
-
setState((s) => ({ ...s, error: null }));
|
| 26 |
-
|
| 27 |
-
try {
|
| 28 |
-
const response = await subscribeEvents(sessionId);
|
| 29 |
-
abortRef.current = new AbortController();
|
| 30 |
-
setState((s) => ({ ...s, connected: true, streaming: true }));
|
| 31 |
-
|
| 32 |
-
const reader = response.body?.getReader();
|
| 33 |
-
if (!reader) throw new Error("No readable stream");
|
| 34 |
-
|
| 35 |
-
const decoder = new TextDecoder();
|
| 36 |
-
|
| 37 |
-
while (true) {
|
| 38 |
-
const { done, value } = await reader.read();
|
| 39 |
-
if (done) break;
|
| 40 |
-
|
| 41 |
-
bufferRef.current += decoder.decode(value, { stream: true });
|
| 42 |
-
const { events, remaining } = parseSSELines(bufferRef.current);
|
| 43 |
-
bufferRef.current = remaining;
|
| 44 |
-
|
| 45 |
-
if (events.length > 0) {
|
| 46 |
-
setState((s) => ({
|
| 47 |
-
...s,
|
| 48 |
-
events: [...s.events, ...events],
|
| 49 |
-
}));
|
| 50 |
-
}
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
setState((s) => ({ ...s, connected: false, streaming: false }));
|
| 54 |
-
} catch (err: unknown) {
|
| 55 |
-
if (err instanceof Error && err.name !== "AbortError") {
|
| 56 |
-
setState((s) => ({
|
| 57 |
-
...s,
|
| 58 |
-
connected: false,
|
| 59 |
-
streaming: false,
|
| 60 |
-
error: err.message,
|
| 61 |
-
}));
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
}, [sessionId]);
|
| 65 |
-
|
| 66 |
-
const disconnect = useCallback(() => {
|
| 67 |
-
abortRef.current?.abort();
|
| 68 |
-
if (reconnectTimer.current) {
|
| 69 |
-
clearTimeout(reconnectTimer.current);
|
| 70 |
-
}
|
| 71 |
-
setState({ connected: false, streaming: false, error: null, events: [] });
|
| 72 |
-
}, []);
|
| 73 |
-
|
| 74 |
-
const clearEvents = useCallback(() => {
|
| 75 |
-
setState((s) => ({ ...s, events: [] }));
|
| 76 |
-
}, []);
|
| 77 |
-
|
| 78 |
-
// Auto-reconnect on disconnect
|
| 79 |
-
useEffect(() => {
|
| 80 |
-
if (!state.connected && !state.streaming && sessionId) {
|
| 81 |
-
reconnectTimer.current = setTimeout(() => {
|
| 82 |
-
connect();
|
| 83 |
-
}, 3000);
|
| 84 |
-
}
|
| 85 |
-
return () => {
|
| 86 |
-
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
|
| 87 |
-
};
|
| 88 |
-
}, [state.connected, state.streaming, sessionId, connect]);
|
| 89 |
-
|
| 90 |
-
// Cleanup on unmount
|
| 91 |
-
useEffect(() => {
|
| 92 |
-
return () => {
|
| 93 |
-
abortRef.current?.abort();
|
| 94 |
-
};
|
| 95 |
-
}, []);
|
| 96 |
-
|
| 97 |
-
return {
|
| 98 |
-
...state,
|
| 99 |
-
connect,
|
| 100 |
-
disconnect,
|
| 101 |
-
clearEvents,
|
| 102 |
-
};
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
// ── Derived helpers ──
|
| 106 |
-
|
| 107 |
-
/** Extract the latest assistant message from events */
|
| 108 |
-
export function getLatestAssistantMessage(events: ServerEvent[]): Message | null {
|
| 109 |
-
for (let i = events.length - 1; i >= 0; i--) {
|
| 110 |
-
const ev = events[i];
|
| 111 |
-
if (ev.type === "message.created" || ev.type === "message.updated") {
|
| 112 |
-
const msg = ev.properties as Message;
|
| 113 |
-
if (msg.role === "assistant") return msg;
|
| 114 |
-
}
|
| 115 |
-
}
|
| 116 |
-
return null;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
/** Extract all messages from events in order */
|
| 120 |
-
export function getMessagesFromEvents(events: ServerEvent[]): Message[] {
|
| 121 |
-
const map = new Map<string, Message>();
|
| 122 |
-
|
| 123 |
-
for (const ev of events) {
|
| 124 |
-
if (ev.type === "message.created") {
|
| 125 |
-
const msg = ev.properties as Message;
|
| 126 |
-
map.set(msg.id, msg);
|
| 127 |
-
} else if (ev.type === "message.updated") {
|
| 128 |
-
const partial = ev.properties as Partial<Message> & { id: string };
|
| 129 |
-
const existing = map.get(partial.id);
|
| 130 |
-
if (existing) {
|
| 131 |
-
map.set(partial.id, { ...existing, ...partial });
|
| 132 |
-
}
|
| 133 |
-
} else if (ev.type === "message.part.updated") {
|
| 134 |
-
const { messageID, part } = ev.properties as {
|
| 135 |
-
messageID: string;
|
| 136 |
-
part: MessagePart;
|
| 137 |
-
sessionID: string;
|
| 138 |
-
};
|
| 139 |
-
const existing = map.get(messageID);
|
| 140 |
-
if (existing) {
|
| 141 |
-
const partIdx = existing.parts.findIndex(
|
| 142 |
-
(p) => JSON.stringify(p) === JSON.stringify(part)
|
| 143 |
-
);
|
| 144 |
-
const newParts = [...existing.parts];
|
| 145 |
-
if (partIdx >= 0) {
|
| 146 |
-
newParts[partIdx] = part;
|
| 147 |
-
} else {
|
| 148 |
-
newParts.push(part);
|
| 149 |
-
}
|
| 150 |
-
map.set(messageID, { ...existing, parts: newParts });
|
| 151 |
-
}
|
| 152 |
-
}
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
return Array.from(map.values());
|
| 156 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/lib/api.ts
DELETED
|
@@ -1,434 +0,0 @@
|
|
| 1 |
-
import { getServerUrl, getAuthHeader, setServerUrl as persistServerUrl, setCredentials, clearAuth, setAuthType, setAuthValue } from "./storage";
|
| 2 |
-
import type { AuthType } from "./storage";
|
| 3 |
-
import type {
|
| 4 |
-
HealthResponse,
|
| 5 |
-
Session,
|
| 6 |
-
SessionStatus,
|
| 7 |
-
SessionListResponse,
|
| 8 |
-
Message,
|
| 9 |
-
MessagePart,
|
| 10 |
-
OpenCodeConfig,
|
| 11 |
-
Provider,
|
| 12 |
-
Model,
|
| 13 |
-
ModelListResponse,
|
| 14 |
-
FileEntry,
|
| 15 |
-
FileContent,
|
| 16 |
-
FindResult,
|
| 17 |
-
SymbolResult,
|
| 18 |
-
PTY,
|
| 19 |
-
Command,
|
| 20 |
-
Skill,
|
| 21 |
-
Agent,
|
| 22 |
-
DiffEntry,
|
| 23 |
-
VCSStatus,
|
| 24 |
-
PermissionRequest,
|
| 25 |
-
SavedPermission,
|
| 26 |
-
QuestionRequest,
|
| 27 |
-
Todo,
|
| 28 |
-
ServerEvent,
|
| 29 |
-
} from "../constants/types";
|
| 30 |
-
|
| 31 |
-
// ── Base request helper ──
|
| 32 |
-
|
| 33 |
-
async function request<T>(
|
| 34 |
-
path: string,
|
| 35 |
-
options?: RequestInit
|
| 36 |
-
): Promise<T> {
|
| 37 |
-
const base = getServerUrl();
|
| 38 |
-
const authHeaders = getAuthHeader();
|
| 39 |
-
const headers: Record<string, string> = {
|
| 40 |
-
"Content-Type": "application/json",
|
| 41 |
-
...authHeaders,
|
| 42 |
-
...((options?.headers as Record<string, string>) || {}),
|
| 43 |
-
};
|
| 44 |
-
|
| 45 |
-
const res = await fetch(`${base}${path}`, {
|
| 46 |
-
...options,
|
| 47 |
-
headers,
|
| 48 |
-
});
|
| 49 |
-
|
| 50 |
-
if (!res.ok) {
|
| 51 |
-
const text = await res.text();
|
| 52 |
-
throw new Error(text || `HTTP ${res.status} ${res.statusText}`);
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
const contentType = res.headers.get("content-type") || "";
|
| 56 |
-
if (contentType.includes("application/json")) {
|
| 57 |
-
return res.json();
|
| 58 |
-
}
|
| 59 |
-
return res.text() as unknown as T;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
/** Extract data array from { location, data } or { data } response */
|
| 63 |
-
function extractData<T>(response: { data?: T[] } | T[]): T[] {
|
| 64 |
-
if (Array.isArray(response)) return response;
|
| 65 |
-
return response.data || [];
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
// ── Server URL / Credentials ──
|
| 69 |
-
|
| 70 |
-
export function setServerUrl(url: string) {
|
| 71 |
-
persistServerUrl(url);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
export function setServerCredentials(username: string, password: string) {
|
| 75 |
-
setCredentials(username, password);
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
// ── Health ──
|
| 79 |
-
|
| 80 |
-
export function getHealth(): Promise<HealthResponse> {
|
| 81 |
-
return request("/api/health");
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
// ── Config ── (root endpoint, not /api/)
|
| 85 |
-
|
| 86 |
-
export function getConfig(): Promise<OpenCodeConfig> {
|
| 87 |
-
return request("/config");
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
export function patchConfig(config: Partial<OpenCodeConfig>): Promise<void> {
|
| 91 |
-
return request("/config", { method: "PATCH", body: JSON.stringify(config) });
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
// ── Sessions ──
|
| 95 |
-
|
| 96 |
-
export function getSessions(): Promise<Session[]> {
|
| 97 |
-
return request("/session");
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
export function getActiveSessions(): Promise<Session[]> {
|
| 101 |
-
return request("/session/active");
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
export function getSession(id: string): Promise<Session> {
|
| 105 |
-
return request(`/session/${id}`);
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
/** Get session via /api/ — returns {data: Session} */
|
| 109 |
-
export function getSessionApi(id: string): Promise<Session> {
|
| 110 |
-
return request<{ data: Session }>(`/api/session/${id}`).then((r) => r.data);
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
export function createSession(options?: {
|
| 114 |
-
title?: string;
|
| 115 |
-
agent?: string;
|
| 116 |
-
model?: string;
|
| 117 |
-
directory?: string;
|
| 118 |
-
}): Promise<Session> {
|
| 119 |
-
return request("/session", {
|
| 120 |
-
method: "POST",
|
| 121 |
-
body: JSON.stringify(options || {}),
|
| 122 |
-
});
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
export function deleteSession(id: string): Promise<void> {
|
| 126 |
-
return request(`/session/${id}`, { method: "DELETE" });
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
export function patchSession(
|
| 130 |
-
id: string,
|
| 131 |
-
data: { title?: string; agent?: string; model?: string }
|
| 132 |
-
): Promise<Session> {
|
| 133 |
-
return request(`/session/${id}`, {
|
| 134 |
-
method: "PATCH",
|
| 135 |
-
body: JSON.stringify(data),
|
| 136 |
-
});
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
export function getSessionStatus(id: string): Promise<SessionStatus> {
|
| 140 |
-
return request(`/session/${id}`);
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
export function getSessionChildren(id: string): Promise<Session[]> {
|
| 144 |
-
return request<Session[]>(`/session/${id}/children`).catch(() => []);
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
export function getSessionTodos(id: string): Promise<Todo[]> {
|
| 148 |
-
return request(`/session/${id}/todo`);
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
// ── Messages ──
|
| 152 |
-
|
| 153 |
-
export function getMessages(sessionId: string): Promise<Message[]> {
|
| 154 |
-
return request(`/session/${sessionId}/message`);
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
export function getMessage(sessionId: string, messageId: string): Promise<Message> {
|
| 158 |
-
return request(`/session/${sessionId}/message/${messageId}`);
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
export function deleteMessage(sessionId: string, messageId: string): Promise<void> {
|
| 162 |
-
return request(`/session/${sessionId}/message/${messageId}`, {
|
| 163 |
-
method: "DELETE",
|
| 164 |
-
});
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
// ── Send message (triggers SSE response) ──
|
| 168 |
-
|
| 169 |
-
export interface MessageResponse {
|
| 170 |
-
info: {
|
| 171 |
-
id: string;
|
| 172 |
-
parentID?: string;
|
| 173 |
-
role: string;
|
| 174 |
-
sessionID: string;
|
| 175 |
-
modelID?: string;
|
| 176 |
-
providerID?: string;
|
| 177 |
-
time?: { created: number; completed?: number };
|
| 178 |
-
finish?: string;
|
| 179 |
-
};
|
| 180 |
-
parts: MessagePart[];
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
export function sendMessage(
|
| 184 |
-
sessionId: string,
|
| 185 |
-
content: string
|
| 186 |
-
): Promise<MessageResponse> {
|
| 187 |
-
return request(`/session/${sessionId}/message`, {
|
| 188 |
-
method: "POST",
|
| 189 |
-
body: JSON.stringify({
|
| 190 |
-
parts: [{ type: "text", text: content }],
|
| 191 |
-
}),
|
| 192 |
-
});
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
// ── Session actions ──
|
| 196 |
-
|
| 197 |
-
export function abortSession(sessionId: string): Promise<void> {
|
| 198 |
-
return request(`/session/${sessionId}/abort`, { method: "POST" });
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
export function initSession(sessionId: string): Promise<void> {
|
| 202 |
-
return request(`/session/${sessionId}/init`, { method: "POST" });
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
export function compactSession(sessionId: string): Promise<void> {
|
| 206 |
-
return request(`/session/${sessionId}/compact`, { method: "POST" });
|
| 207 |
-
}
|
| 208 |
-
|
| 209 |
-
export function summarizeSession(sessionId: string): Promise<void> {
|
| 210 |
-
return request(`/session/${sessionId}/summarize`, { method: "POST" });
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
export function shareSession(sessionId: string): Promise<{ url?: string }> {
|
| 214 |
-
return request(`/session/${sessionId}/share`, { method: "POST" });
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
export function forkSession(sessionId: string): Promise<Session> {
|
| 218 |
-
return request(`/session/${sessionId}/fork`, { method: "POST" });
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
// ── Session diff / revert ──
|
| 222 |
-
|
| 223 |
-
export function getSessionDiff(sessionId: string): Promise<DiffEntry[]> {
|
| 224 |
-
return request(`/session/${sessionId}/diff`);
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
export function revertSessionStage(sessionId: string): Promise<void> {
|
| 228 |
-
return request(`/session/${sessionId}/revert`, { method: "POST" });
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
export function revertSessionClear(sessionId: string): Promise<void> {
|
| 232 |
-
return request(`/session/${sessionId}/unrevert`, { method: "POST" });
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
// ── Session history / context ──
|
| 236 |
-
|
| 237 |
-
export function getSessionHistory(sessionId: string): Promise<{ messages: Message[] }> {
|
| 238 |
-
return request(`/session/${sessionId}/history`);
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
export function getSessionContext(sessionId: string): Promise<{ files: string[]; capabilities: string[] }> {
|
| 242 |
-
return request(`/session/${sessionId}/context`);
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
// ── Permissions ──
|
| 246 |
-
|
| 247 |
-
export function getPermissionRequests(): Promise<PermissionRequest[]> {
|
| 248 |
-
return request("/permission");
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
export function getSavedPermissions(): Promise<SavedPermission[]> {
|
| 252 |
-
return request("/permission/saved");
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
-
export function deleteSavedPermission(id: string): Promise<void> {
|
| 256 |
-
return request(`/permission/saved/${id}`, { method: "DELETE" });
|
| 257 |
-
}
|
| 258 |
-
|
| 259 |
-
export function replyPermission(
|
| 260 |
-
sessionId: string,
|
| 261 |
-
requestId: string,
|
| 262 |
-
allow: boolean
|
| 263 |
-
): Promise<void> {
|
| 264 |
-
return request(`/session/${sessionId}/permissions/${requestId}`, {
|
| 265 |
-
method: "POST",
|
| 266 |
-
body: JSON.stringify({ allow }),
|
| 267 |
-
});
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
// ── Questions ──
|
| 271 |
-
|
| 272 |
-
export function getQuestionRequests(): Promise<QuestionRequest[]> {
|
| 273 |
-
return request("/question");
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
export function getSessionQuestions(sessionId: string): Promise<QuestionRequest[]> {
|
| 277 |
-
return request(`/session/${sessionId}/question`);
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
export function replyQuestion(
|
| 281 |
-
sessionId: string,
|
| 282 |
-
requestId: string,
|
| 283 |
-
answer: string
|
| 284 |
-
): Promise<void> {
|
| 285 |
-
return request(`/question/${requestId}/reply`, {
|
| 286 |
-
method: "POST",
|
| 287 |
-
body: JSON.stringify({ answer }),
|
| 288 |
-
});
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
export function rejectQuestion(
|
| 292 |
-
sessionId: string,
|
| 293 |
-
requestId: string
|
| 294 |
-
): Promise<void> {
|
| 295 |
-
return request(`/question/${requestId}/reject`, {
|
| 296 |
-
method: "POST",
|
| 297 |
-
});
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
// ── Providers ──
|
| 301 |
-
|
| 302 |
-
export function getProviders(): Promise<Provider[]> {
|
| 303 |
-
return request<{ data: Provider[] }>("/api/provider").then((r) => r.data);
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
export function getProvider(id: string): Promise<Provider> {
|
| 307 |
-
return request(`/api/provider/${id}`);
|
| 308 |
-
}
|
| 309 |
-
|
| 310 |
-
// ── Models ──
|
| 311 |
-
|
| 312 |
-
export function getModels(): Promise<Model[]> {
|
| 313 |
-
return request<{ data: Model[] }>("/api/model").then((r) => r.data);
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
// ── Agent / Command / Skill ──
|
| 317 |
-
|
| 318 |
-
export function getAgents(): Promise<Agent[]> {
|
| 319 |
-
return request<{ data: Agent[] }>("/api/agent").then((r) => r.data);
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
export function getCommands(): Promise<Command[]> {
|
| 323 |
-
return request<{ data: Command[] }>("/api/command").then((r) => r.data);
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
export function getSkills(): Promise<Skill[]> {
|
| 327 |
-
return request<{ data: Skill[] }>("/api/skill").then((r) => r.data);
|
| 328 |
-
}
|
| 329 |
-
|
| 330 |
-
// ── Files ──
|
| 331 |
-
|
| 332 |
-
export function listFiles(dir: string = "."): Promise<FileEntry[]> {
|
| 333 |
-
return request<{ data: FileEntry[] }>(`/api/fs/list?path=${encodeURIComponent(dir)}`).then((r) => r.data);
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
export function readFile(filePath: string): Promise<FileContent> {
|
| 337 |
-
return request(`/file/content?path=${encodeURIComponent(filePath)}`);
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
export function findFiles(query: string, dir?: string): Promise<FindResult[]> {
|
| 341 |
-
const params = new URLSearchParams({ query });
|
| 342 |
-
if (dir) params.set("path", dir);
|
| 343 |
-
return request(`/find/file?${params.toString()}`);
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
export function findSymbols(query: string): Promise<SymbolResult[]> {
|
| 347 |
-
return request(`/find/symbol?query=${encodeURIComponent(query)}`);
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
// ── VCS ──
|
| 351 |
-
|
| 352 |
-
export function getVCSStatus(): Promise<VCSStatus> {
|
| 353 |
-
return request("/api/vcs/status");
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
// ── PTY ──
|
| 357 |
-
|
| 358 |
-
export function getPTYs(): Promise<PTY[]> {
|
| 359 |
-
return request<{ data: PTY[] }>("/api/pty").then((r) => r.data);
|
| 360 |
-
}
|
| 361 |
-
|
| 362 |
-
export function createPTY(shell?: string): Promise<PTY> {
|
| 363 |
-
return request("/api/pty", {
|
| 364 |
-
method: "POST",
|
| 365 |
-
body: JSON.stringify({ shell }),
|
| 366 |
-
});
|
| 367 |
-
}
|
| 368 |
-
|
| 369 |
-
export function getPTY(id: string): Promise<PTY> {
|
| 370 |
-
return request(`/api/pty/${id}`);
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
export function deletePTY(id: string): Promise<void> {
|
| 374 |
-
return request(`/api/pty/${id}`, { method: "DELETE" });
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
export function getPTYConnectToken(id: string): Promise<{ token: string }> {
|
| 378 |
-
return request(`/api/pty/${id}/connect-token`, { method: "POST" });
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
// ── SSE Event Stream (returns raw Response for manual parsing) ──
|
| 382 |
-
|
| 383 |
-
export async function subscribeEvents(
|
| 384 |
-
sessionId?: string
|
| 385 |
-
): Promise<Response> {
|
| 386 |
-
const base = getServerUrl();
|
| 387 |
-
const authHeaders = getAuthHeader();
|
| 388 |
-
const url = sessionId
|
| 389 |
-
? `${base}/session/${sessionId}/event`
|
| 390 |
-
: `${base}/event`;
|
| 391 |
-
|
| 392 |
-
const res = await fetch(url, {
|
| 393 |
-
headers: { Accept: "text/event-stream", ...authHeaders },
|
| 394 |
-
});
|
| 395 |
-
|
| 396 |
-
if (!res.ok) {
|
| 397 |
-
throw new Error(`SSE connection failed: ${res.status}`);
|
| 398 |
-
}
|
| 399 |
-
|
| 400 |
-
return res;
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
// ── SSE Parser helper ──
|
| 404 |
-
|
| 405 |
-
export function parseSSELines(
|
| 406 |
-
buffer: string
|
| 407 |
-
): { events: ServerEvent[]; remaining: string } {
|
| 408 |
-
const events: ServerEvent[] = [];
|
| 409 |
-
const lines = buffer.split("\n");
|
| 410 |
-
const remaining = lines.pop() || "";
|
| 411 |
-
|
| 412 |
-
let eventType = "";
|
| 413 |
-
let eventData = "";
|
| 414 |
-
|
| 415 |
-
for (const line of lines) {
|
| 416 |
-
if (line.startsWith("event: ")) {
|
| 417 |
-
eventType = line.slice(7).trim();
|
| 418 |
-
} else if (line.startsWith("data: ")) {
|
| 419 |
-
eventData = line.slice(6);
|
| 420 |
-
} else if (line === "") {
|
| 421 |
-
if (eventType && eventData) {
|
| 422 |
-
try {
|
| 423 |
-
events.push(JSON.parse(eventData) as ServerEvent);
|
| 424 |
-
} catch {
|
| 425 |
-
// skip malformed
|
| 426 |
-
}
|
| 427 |
-
}
|
| 428 |
-
eventType = "";
|
| 429 |
-
eventData = "";
|
| 430 |
-
}
|
| 431 |
-
}
|
| 432 |
-
|
| 433 |
-
return { events, remaining };
|
| 434 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/lib/connection-manager.ts
DELETED
|
@@ -1,285 +0,0 @@
|
|
| 1 |
-
import { getHealth, subscribeEvents, parseSSELines } from "./api";
|
| 2 |
-
import { getServerUrl, getCredentials } from "./storage";
|
| 3 |
-
import type { ServerEvent } from "../constants/types";
|
| 4 |
-
|
| 5 |
-
export type ConnectionState =
|
| 6 |
-
| "disconnected"
|
| 7 |
-
| "connecting"
|
| 8 |
-
| "connected"
|
| 9 |
-
| "reconnecting"
|
| 10 |
-
| "error";
|
| 11 |
-
|
| 12 |
-
export type ConnectionListener = (state: ConnectionState) => void;
|
| 13 |
-
export type EventListener = (event: ServerEvent) => void;
|
| 14 |
-
|
| 15 |
-
interface ConnectionManagerOptions {
|
| 16 |
-
/** Max reconnect attempts before giving up (default: Infinity) */
|
| 17 |
-
maxRetries?: number;
|
| 18 |
-
/** Base delay in ms for exponential backoff (default: 1000) */
|
| 19 |
-
baseDelay?: number;
|
| 20 |
-
/** Max delay in ms (default: 30000) */
|
| 21 |
-
maxDelay?: number;
|
| 22 |
-
/** Health check interval in ms (default: 15000) */
|
| 23 |
-
heartbeatInterval?: number;
|
| 24 |
-
/** Timeout for health check in ms (default: 5000) */
|
| 25 |
-
healthTimeout?: number;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
export class ConnectionManager {
|
| 29 |
-
private state: ConnectionState = "disconnected";
|
| 30 |
-
private retryCount = 0;
|
| 31 |
-
private maxRetries: number;
|
| 32 |
-
private baseDelay: number;
|
| 33 |
-
private maxDelay: number;
|
| 34 |
-
private heartbeatMs: number;
|
| 35 |
-
private healthTimeout: number;
|
| 36 |
-
|
| 37 |
-
private stateListeners: Set<ConnectionListener> = new Set();
|
| 38 |
-
private eventListeners: Set<EventListener> = new Set();
|
| 39 |
-
|
| 40 |
-
private heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
| 41 |
-
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
| 42 |
-
private abortController: AbortController | null = null;
|
| 43 |
-
private sseBuffer = "";
|
| 44 |
-
|
| 45 |
-
/** Currently active SSE session subscription (null = global events) */
|
| 46 |
-
private activeSessionId: string | null = null;
|
| 47 |
-
|
| 48 |
-
constructor(options?: ConnectionManagerOptions) {
|
| 49 |
-
this.maxRetries = options?.maxRetries ?? Infinity;
|
| 50 |
-
this.baseDelay = options?.baseDelay ?? 1000;
|
| 51 |
-
this.maxDelay = options?.maxDelay ?? 30000;
|
| 52 |
-
this.heartbeatMs = options?.heartbeatInterval ?? 15000;
|
| 53 |
-
this.healthTimeout = options?.healthTimeout ?? 5000;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
// ── State ──
|
| 57 |
-
|
| 58 |
-
getState(): ConnectionState {
|
| 59 |
-
return this.state;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
isConnected(): boolean {
|
| 63 |
-
return this.state === "connected";
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
// ── Listeners ──
|
| 67 |
-
|
| 68 |
-
onStateChange(listener: ConnectionListener): () => void {
|
| 69 |
-
this.stateListeners.add(listener);
|
| 70 |
-
return () => this.stateListeners.delete(listener);
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
onEvent(listener: EventListener): () => void {
|
| 74 |
-
this.eventListeners.add(listener);
|
| 75 |
-
return () => this.eventListeners.delete(listener);
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
private setState(newState: ConnectionState) {
|
| 79 |
-
if (this.state === newState) return;
|
| 80 |
-
this.state = newState;
|
| 81 |
-
this.stateListeners.forEach((fn) => fn(newState));
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
private emitEvent(event: ServerEvent) {
|
| 85 |
-
this.eventListeners.forEach((fn) => fn(event));
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
// ── Connect ──
|
| 89 |
-
|
| 90 |
-
async connect(sessionId?: string) {
|
| 91 |
-
if (this.state === "connected" || this.state === "connecting") return;
|
| 92 |
-
|
| 93 |
-
this.activeSessionId = sessionId ?? null;
|
| 94 |
-
this.setState("connecting");
|
| 95 |
-
|
| 96 |
-
try {
|
| 97 |
-
await this.checkHealth();
|
| 98 |
-
this.retryCount = 0;
|
| 99 |
-
this.setState("connected");
|
| 100 |
-
this.startHeartbeat();
|
| 101 |
-
this.connectSSE();
|
| 102 |
-
} catch {
|
| 103 |
-
this.setState("error");
|
| 104 |
-
this.scheduleReconnect();
|
| 105 |
-
}
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
disconnect() {
|
| 109 |
-
this.stopHeartbeat();
|
| 110 |
-
this.stopReconnect();
|
| 111 |
-
this.abortController?.abort();
|
| 112 |
-
this.abortController = null;
|
| 113 |
-
this.sseBuffer = "";
|
| 114 |
-
this.retryCount = 0;
|
| 115 |
-
this.setState("disconnected");
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
// ── Health check ──
|
| 119 |
-
|
| 120 |
-
private async checkHealth(): Promise<void> {
|
| 121 |
-
const controller = new AbortController();
|
| 122 |
-
const timeout = setTimeout(() => controller.abort(), this.healthTimeout);
|
| 123 |
-
|
| 124 |
-
try {
|
| 125 |
-
const result = await Promise.race([
|
| 126 |
-
getHealth(),
|
| 127 |
-
new Promise<never>((_, reject) => {
|
| 128 |
-
controller.signal.addEventListener("abort", () =>
|
| 129 |
-
reject(new Error("Health check timeout"))
|
| 130 |
-
);
|
| 131 |
-
}),
|
| 132 |
-
]);
|
| 133 |
-
|
| 134 |
-
clearTimeout(timeout);
|
| 135 |
-
|
| 136 |
-
if (!result.healthy) {
|
| 137 |
-
throw new Error("Server reports unhealthy");
|
| 138 |
-
}
|
| 139 |
-
} catch (err) {
|
| 140 |
-
clearTimeout(timeout);
|
| 141 |
-
throw err;
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
// ── Heartbeat ──
|
| 146 |
-
|
| 147 |
-
private startHeartbeat() {
|
| 148 |
-
this.stopHeartbeat();
|
| 149 |
-
this.heartbeatTimer = setInterval(async () => {
|
| 150 |
-
try {
|
| 151 |
-
await this.checkHealth();
|
| 152 |
-
// If we were in error state, recover
|
| 153 |
-
if (this.state === "error") {
|
| 154 |
-
this.retryCount = 0;
|
| 155 |
-
this.setState("connected");
|
| 156 |
-
this.connectSSE();
|
| 157 |
-
}
|
| 158 |
-
} catch {
|
| 159 |
-
if (this.state === "connected") {
|
| 160 |
-
this.setState("error");
|
| 161 |
-
this.scheduleReconnect();
|
| 162 |
-
}
|
| 163 |
-
}
|
| 164 |
-
}, this.heartbeatMs);
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
private stopHeartbeat() {
|
| 168 |
-
if (this.heartbeatTimer) {
|
| 169 |
-
clearInterval(this.heartbeatTimer);
|
| 170 |
-
this.heartbeatTimer = null;
|
| 171 |
-
}
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
// ── SSE ──
|
| 175 |
-
|
| 176 |
-
private connectSSE() {
|
| 177 |
-
this.abortController?.abort();
|
| 178 |
-
this.abortController = new AbortController();
|
| 179 |
-
this.sseBuffer = "";
|
| 180 |
-
|
| 181 |
-
const doConnect = async () => {
|
| 182 |
-
try {
|
| 183 |
-
const response = await subscribeEvents(this.activeSessionId ?? undefined);
|
| 184 |
-
const reader = response.body?.getReader();
|
| 185 |
-
if (!reader) throw new Error("No readable stream");
|
| 186 |
-
|
| 187 |
-
const decoder = new TextDecoder();
|
| 188 |
-
|
| 189 |
-
while (true) {
|
| 190 |
-
if (this.abortController?.signal.aborted) break;
|
| 191 |
-
|
| 192 |
-
const { done, value } = await reader.read();
|
| 193 |
-
if (done) break;
|
| 194 |
-
|
| 195 |
-
this.sseBuffer += decoder.decode(value, { stream: true });
|
| 196 |
-
const { events, remaining } = parseSSELines(this.sseBuffer);
|
| 197 |
-
this.sseBuffer = remaining;
|
| 198 |
-
|
| 199 |
-
for (const event of events) {
|
| 200 |
-
this.emitEvent(event);
|
| 201 |
-
}
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
// SSE ended normally — try reconnect
|
| 205 |
-
if (this.state === "connected") {
|
| 206 |
-
this.setState("error");
|
| 207 |
-
this.scheduleReconnect();
|
| 208 |
-
}
|
| 209 |
-
} catch (err) {
|
| 210 |
-
if (
|
| 211 |
-
this.abortController?.signal.aborted ||
|
| 212 |
-
this.state === "disconnected"
|
| 213 |
-
) {
|
| 214 |
-
return;
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
console.warn("[ConnectionManager] SSE error:", err);
|
| 218 |
-
if (this.state === "connected") {
|
| 219 |
-
this.setState("error");
|
| 220 |
-
this.scheduleReconnect();
|
| 221 |
-
}
|
| 222 |
-
}
|
| 223 |
-
};
|
| 224 |
-
|
| 225 |
-
doConnect();
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
// ── Reconnect with exponential backoff ──
|
| 229 |
-
|
| 230 |
-
private scheduleReconnect() {
|
| 231 |
-
if (this.retryCount >= this.maxRetries) {
|
| 232 |
-
this.setState("disconnected");
|
| 233 |
-
return;
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
this.setState("reconnecting");
|
| 237 |
-
|
| 238 |
-
const delay = Math.min(
|
| 239 |
-
this.baseDelay * Math.pow(2, this.retryCount),
|
| 240 |
-
this.maxDelay
|
| 241 |
-
);
|
| 242 |
-
this.retryCount++;
|
| 243 |
-
|
| 244 |
-
this.reconnectTimer = setTimeout(() => {
|
| 245 |
-
this.connectSSE();
|
| 246 |
-
// Also check health
|
| 247 |
-
this.checkHealth()
|
| 248 |
-
.then(() => {
|
| 249 |
-
this.retryCount = 0;
|
| 250 |
-
this.setState("connected");
|
| 251 |
-
})
|
| 252 |
-
.catch(() => {
|
| 253 |
-
// Will retry again
|
| 254 |
-
});
|
| 255 |
-
}, delay);
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
private stopReconnect() {
|
| 259 |
-
if (this.reconnectTimer) {
|
| 260 |
-
clearTimeout(this.reconnectTimer);
|
| 261 |
-
this.reconnectTimer = null;
|
| 262 |
-
}
|
| 263 |
-
}
|
| 264 |
-
|
| 265 |
-
// ── Switch session subscription ──
|
| 266 |
-
|
| 267 |
-
switchSession(sessionId: string | null) {
|
| 268 |
-
if (this.activeSessionId === sessionId) return;
|
| 269 |
-
this.activeSessionId = sessionId;
|
| 270 |
-
if (this.state === "connected") {
|
| 271 |
-
this.connectSSE();
|
| 272 |
-
}
|
| 273 |
-
}
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
// ── Singleton ──
|
| 277 |
-
|
| 278 |
-
let instance: ConnectionManager | null = null;
|
| 279 |
-
|
| 280 |
-
export function getConnectionManager(options?: ConnectionManagerOptions): ConnectionManager {
|
| 281 |
-
if (!instance) {
|
| 282 |
-
instance = new ConnectionManager(options);
|
| 283 |
-
}
|
| 284 |
-
return instance;
|
| 285 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/lib/query-provider.tsx
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
import React from "react";
|
| 2 |
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
| 3 |
-
|
| 4 |
-
const queryClient = new QueryClient({
|
| 5 |
-
defaultOptions: {
|
| 6 |
-
queries: {
|
| 7 |
-
staleTime: 5000,
|
| 8 |
-
refetchOnWindowFocus: false,
|
| 9 |
-
},
|
| 10 |
-
},
|
| 11 |
-
});
|
| 12 |
-
|
| 13 |
-
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
| 14 |
-
return (
|
| 15 |
-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
| 16 |
-
);
|
| 17 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/lib/storage.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
| 1 |
-
import AsyncStorage from "@react-native-async-storage/async-storage";
|
| 2 |
-
import { ThemeName } from "../constants/types";
|
| 3 |
-
|
| 4 |
-
export const storageKeys = {
|
| 5 |
-
serverUrl: "opencode_server_url",
|
| 6 |
-
authType: "opencode_auth_type",
|
| 7 |
-
authValue: "opencode_auth_value",
|
| 8 |
-
theme: "opencode_theme",
|
| 9 |
-
lastSessionId: "opencode_last_session_id",
|
| 10 |
-
} as const;
|
| 11 |
-
|
| 12 |
-
export type AuthType = "none" | "basic" | "bearer" | "apikey";
|
| 13 |
-
|
| 14 |
-
const cache: Record<string, string | null> = {};
|
| 15 |
-
|
| 16 |
-
async function get(key: string): Promise<string | null> {
|
| 17 |
-
if (cache[key] !== undefined) return cache[key];
|
| 18 |
-
const val = await AsyncStorage.getItem(key);
|
| 19 |
-
cache[key] = val;
|
| 20 |
-
return val;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
async function set(key: string, value: string): Promise<void> {
|
| 24 |
-
cache[key] = value;
|
| 25 |
-
await AsyncStorage.setItem(key, value);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
async function del(key: string): Promise<void> {
|
| 29 |
-
cache[key] = null;
|
| 30 |
-
await AsyncStorage.removeItem(key);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
function getSync(key: string): string | null {
|
| 34 |
-
if (cache[key] !== undefined) return cache[key];
|
| 35 |
-
return null;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
// Preload all keys on module load
|
| 39 |
-
const _loaded = (async () => {
|
| 40 |
-
const entries = await AsyncStorage.multiGet(Object.values(storageKeys));
|
| 41 |
-
for (const [k, v] of entries) {
|
| 42 |
-
cache[k] = v;
|
| 43 |
-
}
|
| 44 |
-
})();
|
| 45 |
-
|
| 46 |
-
// ── Server URL ──
|
| 47 |
-
|
| 48 |
-
export function getServerUrl(): string {
|
| 49 |
-
return getSync(storageKeys.serverUrl) || "http://127.0.0.1:4096";
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
export async function setServerUrl(url: string): Promise<void> {
|
| 53 |
-
await set(storageKeys.serverUrl, url);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
// ── Auth ──
|
| 57 |
-
|
| 58 |
-
export function getAuthType(): AuthType {
|
| 59 |
-
return (getSync(storageKeys.authType) as AuthType) || "none";
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
export async function setAuthType(type: AuthType): Promise<void> {
|
| 63 |
-
await set(storageKeys.authType, type);
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
export function getAuthValue(): string {
|
| 67 |
-
return getSync(storageKeys.authValue) || "";
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
export async function setAuthValue(value: string): Promise<void> {
|
| 71 |
-
await set(storageKeys.authValue, value);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
export function getCredentials(): { username: string; password: string } {
|
| 75 |
-
const val = getAuthValue();
|
| 76 |
-
if (getAuthType() === "basic" && val.includes(":")) {
|
| 77 |
-
const [username, ...rest] = val.split(":");
|
| 78 |
-
return { username, password: rest.join(":") };
|
| 79 |
-
}
|
| 80 |
-
return { username: "", password: "" };
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
export async function setCredentials(username: string, password: string): Promise<void> {
|
| 84 |
-
await setAuthType("basic");
|
| 85 |
-
await setAuthValue(`${username}:${password}`);
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
export async function clearAuth(): Promise<void> {
|
| 89 |
-
await del(storageKeys.authType);
|
| 90 |
-
await del(storageKeys.authValue);
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
export function getAuthHeader(): Record<string, string> {
|
| 94 |
-
const type = getAuthType();
|
| 95 |
-
const value = getAuthValue();
|
| 96 |
-
|
| 97 |
-
if (!value) return {};
|
| 98 |
-
|
| 99 |
-
switch (type) {
|
| 100 |
-
case "basic": {
|
| 101 |
-
const creds = getCredentials();
|
| 102 |
-
if (creds.username) {
|
| 103 |
-
return { Authorization: `Basic ${btoa(`${creds.username}:${creds.password}`)}` };
|
| 104 |
-
}
|
| 105 |
-
return {};
|
| 106 |
-
}
|
| 107 |
-
case "bearer":
|
| 108 |
-
return { Authorization: `Bearer ${value}` };
|
| 109 |
-
case "apikey":
|
| 110 |
-
return { "X-API-Key": value };
|
| 111 |
-
default:
|
| 112 |
-
return {};
|
| 113 |
-
}
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
// ── Theme ──
|
| 117 |
-
|
| 118 |
-
export function getTheme(): ThemeName {
|
| 119 |
-
return (getSync(storageKeys.theme) as ThemeName) || "tokyo";
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
export async function setTheme(theme: ThemeName): Promise<void> {
|
| 123 |
-
await set(storageKeys.theme, theme);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
// ── Last session ──
|
| 127 |
-
|
| 128 |
-
export function getLastSessionId(): string | undefined {
|
| 129 |
-
return getSync(storageKeys.lastSessionId) || undefined;
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
export async function setLastSessionId(id: string): Promise<void> {
|
| 133 |
-
await set(storageKeys.lastSessionId, id);
|
| 134 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/package-lock.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
mobile/package.json
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "opencode-mobile",
|
| 3 |
-
"version": "1.0.0",
|
| 4 |
-
"main": "expo-router/entry",
|
| 5 |
-
"scripts": {
|
| 6 |
-
"start": "expo start",
|
| 7 |
-
"android": "expo start --android",
|
| 8 |
-
"ios": "expo start --ios",
|
| 9 |
-
"web": "expo start --web"
|
| 10 |
-
},
|
| 11 |
-
"dependencies": {
|
| 12 |
-
"@react-native-async-storage/async-storage": "2.1.2",
|
| 13 |
-
"@tanstack/react-query": "^5.60.6",
|
| 14 |
-
"expo": "~53.0.27",
|
| 15 |
-
"expo-asset": "~11.1.7",
|
| 16 |
-
"expo-constants": "~17.1.8",
|
| 17 |
-
"expo-font": "~13.3.2",
|
| 18 |
-
"expo-linking": "~7.1.7",
|
| 19 |
-
"expo-router": "~5.1.11",
|
| 20 |
-
"expo-secure-store": "~14.2.4",
|
| 21 |
-
"expo-splash-screen": "~0.30.10",
|
| 22 |
-
"expo-status-bar": "~2.2.3",
|
| 23 |
-
"expo-system-ui": "~5.0.11",
|
| 24 |
-
"lucide-react-native": "^0.460.0",
|
| 25 |
-
"react": "19.0.0",
|
| 26 |
-
"react-native": "0.79.6",
|
| 27 |
-
"react-native-gesture-handler": "~2.24.0",
|
| 28 |
-
"react-native-markdown-display": "^7.0.2",
|
| 29 |
-
"react-native-reanimated": "~3.17.4",
|
| 30 |
-
"react-native-safe-area-context": "5.4.0",
|
| 31 |
-
"react-native-screens": "~4.11.1",
|
| 32 |
-
"react-native-svg": "15.11.2"
|
| 33 |
-
},
|
| 34 |
-
"devDependencies": {
|
| 35 |
-
"@types/react": "~19.0.10",
|
| 36 |
-
"typescript": "~5.8.3"
|
| 37 |
-
}
|
| 38 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/tests/integration.test.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* End-to-end integration test for OpenCode Mobile client
|
| 3 |
-
* Run against a live opencode serve instance
|
| 4 |
-
*
|
| 5 |
-
* Usage: npx tsx mobile/tests/integration.test.ts
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
const BASE = "http://127.0.0.1:4096";
|
| 9 |
-
|
| 10 |
-
interface TestResult {
|
| 11 |
-
name: string;
|
| 12 |
-
passed: boolean;
|
| 13 |
-
error?: string;
|
| 14 |
-
duration: number;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
const results: TestResult[] = [];
|
| 18 |
-
|
| 19 |
-
async function test(name: string, fn: () => Promise<void>) {
|
| 20 |
-
const start = Date.now();
|
| 21 |
-
try {
|
| 22 |
-
await fn();
|
| 23 |
-
results.push({ name, passed: true, duration: Date.now() - start });
|
| 24 |
-
console.log(` ✓ ${name} (${Date.now() - start}ms)`);
|
| 25 |
-
} catch (err: any) {
|
| 26 |
-
results.push({
|
| 27 |
-
name,
|
| 28 |
-
passed: false,
|
| 29 |
-
error: err.message,
|
| 30 |
-
duration: Date.now() - start,
|
| 31 |
-
});
|
| 32 |
-
console.log(` ✗ ${name}: ${err.message}`);
|
| 33 |
-
}
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
function assert(condition: boolean, msg: string) {
|
| 37 |
-
if (!condition) throw new Error(msg);
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
async function api<T = any>(path: string, opts?: RequestInit): Promise<T> {
|
| 41 |
-
const res = await fetch(`${BASE}${path}`, {
|
| 42 |
-
headers: { "Content-Type": "application/json", ...opts?.headers },
|
| 43 |
-
...opts,
|
| 44 |
-
});
|
| 45 |
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 46 |
-
return res.json();
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
// ── Tests ──
|
| 50 |
-
|
| 51 |
-
async function run() {
|
| 52 |
-
console.log("\n=== OpenCode Integration Tests ===\n");
|
| 53 |
-
|
| 54 |
-
let sessionId: string;
|
| 55 |
-
|
| 56 |
-
// 1. Health
|
| 57 |
-
await test("GET /api/health", async () => {
|
| 58 |
-
const data = await api("/api/health");
|
| 59 |
-
assert(data.healthy === true, "not healthy");
|
| 60 |
-
});
|
| 61 |
-
|
| 62 |
-
// 2. Config (root endpoint — /api/config returns HTML)
|
| 63 |
-
await test("GET /config", async () => {
|
| 64 |
-
const data = await api("/config");
|
| 65 |
-
assert(typeof data === "object", "config is not object");
|
| 66 |
-
});
|
| 67 |
-
|
| 68 |
-
// 3. List sessions (root returns array)
|
| 69 |
-
await test("GET /session (list)", async () => {
|
| 70 |
-
const data = await api("/session");
|
| 71 |
-
assert(Array.isArray(data), "not array");
|
| 72 |
-
});
|
| 73 |
-
|
| 74 |
-
// 4. List sessions via /api/ (returns {data, cursor})
|
| 75 |
-
await test("GET /api/session (list)", async () => {
|
| 76 |
-
const data = await api("/api/session");
|
| 77 |
-
assert(Array.isArray(data.data), "data is not array");
|
| 78 |
-
});
|
| 79 |
-
|
| 80 |
-
// 5. Create session
|
| 81 |
-
await test("POST /session (create)", async () => {
|
| 82 |
-
const data = await api("/session", {
|
| 83 |
-
method: "POST",
|
| 84 |
-
body: JSON.stringify({ title: "Integration Test" }),
|
| 85 |
-
});
|
| 86 |
-
assert(data.id, "no id returned");
|
| 87 |
-
sessionId = data.id;
|
| 88 |
-
});
|
| 89 |
-
|
| 90 |
-
// 6. Get session (root)
|
| 91 |
-
await test("GET /session/{id}", async () => {
|
| 92 |
-
const data = await api(`/session/${sessionId}`);
|
| 93 |
-
assert(data.id === sessionId, "id mismatch");
|
| 94 |
-
});
|
| 95 |
-
|
| 96 |
-
// 7. Get session via /api/ (returns {data: Session})
|
| 97 |
-
await test("GET /api/session/{id}", async () => {
|
| 98 |
-
const data = await api(`/api/session/${sessionId}`);
|
| 99 |
-
assert(data.data?.id === sessionId, "id mismatch in data");
|
| 100 |
-
});
|
| 101 |
-
|
| 102 |
-
// 8. List messages (empty)
|
| 103 |
-
await test("GET /session/{id}/message (empty)", async () => {
|
| 104 |
-
const data = await api(`/session/${sessionId}/message`);
|
| 105 |
-
assert(Array.isArray(data), "not array");
|
| 106 |
-
});
|
| 107 |
-
|
| 108 |
-
// 9. Send message (requires {parts: [{type:'text', text:'...'}]})
|
| 109 |
-
await test("POST /session/{id}/message (send)", async () => {
|
| 110 |
-
const data = await api(`/session/${sessionId}/message`, {
|
| 111 |
-
method: "POST",
|
| 112 |
-
body: JSON.stringify({
|
| 113 |
-
parts: [{ type: "text", text: "Say exactly: integration test ok" }],
|
| 114 |
-
}),
|
| 115 |
-
});
|
| 116 |
-
assert(data.info || data.parts, "unexpected response shape");
|
| 117 |
-
});
|
| 118 |
-
|
| 119 |
-
// 10. SSE event stream (global endpoint)
|
| 120 |
-
await test("GET /event (SSE)", async () => {
|
| 121 |
-
const controller = new AbortController();
|
| 122 |
-
const timeout = setTimeout(() => controller.abort(), 4000);
|
| 123 |
-
|
| 124 |
-
const res = await fetch(`${BASE}/event`, {
|
| 125 |
-
headers: { Accept: "text/event-stream" },
|
| 126 |
-
signal: controller.signal,
|
| 127 |
-
});
|
| 128 |
-
|
| 129 |
-
clearTimeout(timeout);
|
| 130 |
-
assert(res.ok, `HTTP ${res.status}`);
|
| 131 |
-
|
| 132 |
-
const reader = res.body!.getReader();
|
| 133 |
-
const decoder = new TextDecoder();
|
| 134 |
-
let buffer = "";
|
| 135 |
-
let eventCount = 0;
|
| 136 |
-
|
| 137 |
-
const readTimeout = setTimeout(() => reader.cancel(), 3000);
|
| 138 |
-
|
| 139 |
-
while (eventCount < 1) {
|
| 140 |
-
const { done, value } = await reader.read();
|
| 141 |
-
if (done) break;
|
| 142 |
-
buffer += decoder.decode(value, { stream: true });
|
| 143 |
-
const lines = buffer.split("\n");
|
| 144 |
-
buffer = lines.pop() || "";
|
| 145 |
-
for (const line of lines) {
|
| 146 |
-
if (line.startsWith("data: ") || line.startsWith("event: ")) eventCount++;
|
| 147 |
-
}
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
clearTimeout(readTimeout);
|
| 151 |
-
reader.cancel();
|
| 152 |
-
assert(eventCount > 0, `no events (got ${eventCount})`);
|
| 153 |
-
});
|
| 154 |
-
|
| 155 |
-
// 11. Providers
|
| 156 |
-
await test("GET /api/provider", async () => {
|
| 157 |
-
const data = await api("/api/provider");
|
| 158 |
-
assert(Array.isArray(data.data), "data is not array");
|
| 159 |
-
});
|
| 160 |
-
|
| 161 |
-
// 12. Models
|
| 162 |
-
await test("GET /api/model", async () => {
|
| 163 |
-
const data = await api("/api/model");
|
| 164 |
-
assert(Array.isArray(data.data), "data is not array");
|
| 165 |
-
});
|
| 166 |
-
|
| 167 |
-
// 13. Agents
|
| 168 |
-
await test("GET /api/agent", async () => {
|
| 169 |
-
const data = await api("/api/agent");
|
| 170 |
-
assert(Array.isArray(data.data), "data is not array");
|
| 171 |
-
});
|
| 172 |
-
|
| 173 |
-
// 14. Commands
|
| 174 |
-
await test("GET /api/command", async () => {
|
| 175 |
-
const data = await api("/api/command");
|
| 176 |
-
assert(Array.isArray(data.data), "data is not array");
|
| 177 |
-
});
|
| 178 |
-
|
| 179 |
-
// 15. Skills
|
| 180 |
-
await test("GET /api/skill", async () => {
|
| 181 |
-
const data = await api("/api/skill");
|
| 182 |
-
assert(Array.isArray(data.data), "data is not array");
|
| 183 |
-
});
|
| 184 |
-
|
| 185 |
-
// 16. File list
|
| 186 |
-
await test("GET /api/fs/list", async () => {
|
| 187 |
-
const data = await api("/api/fs/list?path=.");
|
| 188 |
-
assert(Array.isArray(data.data), "data is not array");
|
| 189 |
-
assert(data.data.length > 0, "empty file list");
|
| 190 |
-
});
|
| 191 |
-
|
| 192 |
-
// 17. File content (root endpoint)
|
| 193 |
-
await test("GET /file/content", async () => {
|
| 194 |
-
const data = await api("/file/content?path=package.json");
|
| 195 |
-
assert(data.content || typeof data === "string", "no content");
|
| 196 |
-
});
|
| 197 |
-
|
| 198 |
-
// 18. Find files (root endpoint)
|
| 199 |
-
await test("GET /find/file", async () => {
|
| 200 |
-
const data = await api("/find/file?query=package");
|
| 201 |
-
assert(Array.isArray(data), "not array");
|
| 202 |
-
});
|
| 203 |
-
|
| 204 |
-
// 19. PTY list
|
| 205 |
-
await test("GET /api/pty", async () => {
|
| 206 |
-
const data = await api("/api/pty");
|
| 207 |
-
assert(Array.isArray(data.data), "data is not array");
|
| 208 |
-
});
|
| 209 |
-
|
| 210 |
-
// 20. VCS status
|
| 211 |
-
await test("GET /vcs/status", async () => {
|
| 212 |
-
const data = await api("/vcs/status");
|
| 213 |
-
assert(typeof data === "object", "not object");
|
| 214 |
-
});
|
| 215 |
-
|
| 216 |
-
// 21. Delete session
|
| 217 |
-
await test("DELETE /session/{id}", async () => {
|
| 218 |
-
await api(`/session/${sessionId}`, { method: "DELETE" });
|
| 219 |
-
});
|
| 220 |
-
|
| 221 |
-
// 22. Verify deleted
|
| 222 |
-
await test("GET /session/{id} after delete", async () => {
|
| 223 |
-
try {
|
| 224 |
-
await api(`/session/${sessionId}`);
|
| 225 |
-
assert(false, "should have thrown");
|
| 226 |
-
} catch {
|
| 227 |
-
// Expected
|
| 228 |
-
}
|
| 229 |
-
});
|
| 230 |
-
|
| 231 |
-
// Summary
|
| 232 |
-
console.log("\n=== Results ===\n");
|
| 233 |
-
const passed = results.filter((r) => r.passed).length;
|
| 234 |
-
const failed = results.filter((r) => !r.passed).length;
|
| 235 |
-
console.log(` ${passed} passed, ${failed} failed out of ${results.length} tests`);
|
| 236 |
-
|
| 237 |
-
if (failed > 0) {
|
| 238 |
-
console.log("\n Failed tests:");
|
| 239 |
-
results
|
| 240 |
-
.filter((r) => !r.passed)
|
| 241 |
-
.forEach((r) => console.log(` - ${r.name}: ${r.error}`));
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
console.log("");
|
| 245 |
-
process.exit(failed > 0 ? 1 : 0);
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
run().catch((err) => {
|
| 249 |
-
console.error("Fatal:", err);
|
| 250 |
-
process.exit(1);
|
| 251 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mobile/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"extends": "expo/tsconfig.base",
|
| 3 |
-
"compilerOptions": {
|
| 4 |
-
"strict": true,
|
| 5 |
-
"paths": {
|
| 6 |
-
"@/*": [
|
| 7 |
-
"./*"
|
| 8 |
-
]
|
| 9 |
-
}
|
| 10 |
-
},
|
| 11 |
-
"include": [
|
| 12 |
-
"**/*.ts",
|
| 13 |
-
"**/*.tsx"
|
| 14 |
-
]
|
| 15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nginx.conf
CHANGED
|
@@ -27,77 +27,7 @@ http {
|
|
| 27 |
proxy_redirect ~^https?://[^/]+:4096/(.*)$ https://jishnupg-opencode-cli.hf.space/$1;
|
| 28 |
|
| 29 |
# -------------------------------------------------------
|
| 30 |
-
# 1.
|
| 31 |
-
# -------------------------------------------------------
|
| 32 |
-
location /omniroute/ {
|
| 33 |
-
proxy_pass http://127.0.0.1:20128/;
|
| 34 |
-
proxy_set_header Upgrade $http_upgrade;
|
| 35 |
-
proxy_set_header Connection "upgrade";
|
| 36 |
-
proxy_redirect /login /omniroute/login;
|
| 37 |
-
proxy_redirect /dashboard /omniroute/dashboard;
|
| 38 |
-
proxy_set_header Accept-Encoding "";
|
| 39 |
-
sub_filter_types text/javascript application/javascript application/json;
|
| 40 |
-
sub_filter 'href="/' 'href="/omniroute/';
|
| 41 |
-
sub_filter 'src="/' 'src="/omniroute/';
|
| 42 |
-
sub_filter 'action="/' 'action="/omniroute/';
|
| 43 |
-
sub_filter '="/_next/' '="/omniroute/_next/';
|
| 44 |
-
sub_filter '"/_next/' '"/omniroute/_next/';
|
| 45 |
-
sub_filter '="/assets/' '="/assets/';
|
| 46 |
-
sub_filter_once off;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
location = /omniroute {
|
| 50 |
-
return 301 /omniroute/;
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
location /_next/ {
|
| 54 |
-
proxy_pass http://127.0.0.1:20128/_next/;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
location /assets/ {
|
| 58 |
-
proxy_pass http://127.0.0.1:20128/assets/;
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
location /v1/ {
|
| 62 |
-
proxy_pass http://127.0.0.1:20128/v1/;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
# -------------------------------------------------------
|
| 66 |
-
# 2. OPENCODE CLI SERVER (/server, /session, /project, etc.)
|
| 67 |
-
# -------------------------------------------------------
|
| 68 |
-
location /server/ {
|
| 69 |
-
proxy_pass http://127.0.0.1:4097/;
|
| 70 |
-
proxy_set_header Upgrade $http_upgrade;
|
| 71 |
-
proxy_set_header Connection "upgrade";
|
| 72 |
-
proxy_set_header Accept-Encoding "";
|
| 73 |
-
sub_filter_types text/javascript application/javascript;
|
| 74 |
-
sub_filter 'href="/' 'href="/server/';
|
| 75 |
-
sub_filter 'src="/' 'src="/server/';
|
| 76 |
-
sub_filter_once off;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
location = /server {
|
| 80 |
-
return 301 /server/;
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
location /session { proxy_pass http://127.0.0.1:4097; }
|
| 84 |
-
location /project { proxy_pass http://127.0.0.1:4097; }
|
| 85 |
-
location /config { proxy_pass http://127.0.0.1:4097; }
|
| 86 |
-
location /permission { proxy_pass http://127.0.0.1:4097; }
|
| 87 |
-
location /question { proxy_pass http://127.0.0.1:4097; }
|
| 88 |
-
location /file { proxy_pass http://127.0.0.1:4097; }
|
| 89 |
-
location /find { proxy_pass http://127.0.0.1:4097; }
|
| 90 |
-
location /events { proxy_pass http://127.0.0.1:4097; proxy_buffering off; }
|
| 91 |
-
location /event { proxy_pass http://127.0.0.1:4097; proxy_buffering off; }
|
| 92 |
-
|
| 93 |
-
location /ws {
|
| 94 |
-
proxy_pass http://127.0.0.1:4097;
|
| 95 |
-
proxy_set_header Upgrade $http_upgrade;
|
| 96 |
-
proxy_set_header Connection "upgrade";
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
# -------------------------------------------------------
|
| 100 |
-
# 3. JELLYFIN MEDIA SERVER (/jellyfin)
|
| 101 |
# -------------------------------------------------------
|
| 102 |
location /jellyfin/ {
|
| 103 |
proxy_pass http://127.0.0.1:8096/;
|
|
|
|
| 27 |
proxy_redirect ~^https?://[^/]+:4096/(.*)$ https://jishnupg-opencode-cli.hf.space/$1;
|
| 28 |
|
| 29 |
# -------------------------------------------------------
|
| 30 |
+
# 1. JELLYFIN MEDIA SERVER (/jellyfin)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# -------------------------------------------------------
|
| 32 |
location /jellyfin/ {
|
| 33 |
proxy_pass http://127.0.0.1:8096/;
|