Merge dev into main #22
@@ -25,7 +25,39 @@ jobs:
|
|||||||
git remote add origin http://gitea:3000/jimmy/nas-tools.git
|
git remote add origin http://gitea:3000/jimmy/nas-tools.git
|
||||||
git fetch --depth 1 origin "$GITHUB_SHA"
|
git fetch --depth 1 origin "$GITHUB_SHA"
|
||||||
git checkout --detach FETCH_HEAD
|
git checkout --detach FETCH_HEAD
|
||||||
|
- name: Warm mirror cache for base images
|
||||||
|
run: |
|
||||||
|
set -u
|
||||||
|
mirror_host="127.0.0.1:5501"
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
if curl -fsS "http://${mirror_host}/v2/" >/dev/null; then
|
||||||
|
echo "Registry mirror is healthy at ${mirror_host}"
|
||||||
|
else
|
||||||
|
echo "Warning: registry mirror health check failed at ${mirror_host}; continuing with normal pull path"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: curl is unavailable; skipping explicit mirror health check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
prewarm_base_image() {
|
||||||
|
image_ref="$1"
|
||||||
|
mirror_ref="${mirror_host}/library/${image_ref}"
|
||||||
|
echo "Attempting mirror pre-pull: ${mirror_ref}"
|
||||||
|
if docker pull "${mirror_ref}"; then
|
||||||
|
docker tag "${mirror_ref}" "${image_ref}"
|
||||||
|
echo "Mirror pre-pull succeeded for ${image_ref}"
|
||||||
|
else
|
||||||
|
echo "Warning: mirror pre-pull failed for ${image_ref}; continuing with normal pull during build"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prewarm_base_image "node:20-alpine"
|
||||||
|
prewarm_base_image "python:3.12-slim"
|
||||||
|
|
||||||
- name: Build dev image
|
- name: Build dev image
|
||||||
run: DOCKER_BUILDKIT=0 docker build --cache-from nas-dashboard-dev:latest -t nas-dashboard-dev:latest dashboard/
|
run: DOCKER_BUILDKIT=0 docker build --cache-from nas-dashboard-dev:latest -t nas-dashboard-dev:latest dashboard/
|
||||||
|
- name: Sync runtime compose file
|
||||||
|
run: cp dashboard/docker-compose.dev.yml /nas-dashboard/docker-compose.dev.yml
|
||||||
- name: Deploy dev
|
- name: Deploy dev
|
||||||
run: docker compose -f /nas-dashboard/docker-compose.dev.yml up -d --pull never
|
run: docker compose -f /nas-dashboard/docker-compose.dev.yml up -d --pull never
|
||||||
|
|||||||
@@ -25,7 +25,39 @@ jobs:
|
|||||||
git remote add origin http://gitea:3000/jimmy/nas-tools.git
|
git remote add origin http://gitea:3000/jimmy/nas-tools.git
|
||||||
git fetch --depth 1 origin "$GITHUB_SHA"
|
git fetch --depth 1 origin "$GITHUB_SHA"
|
||||||
git checkout --detach FETCH_HEAD
|
git checkout --detach FETCH_HEAD
|
||||||
|
- name: Warm mirror cache for base images
|
||||||
|
run: |
|
||||||
|
set -u
|
||||||
|
mirror_host="127.0.0.1:5501"
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
if curl -fsS "http://${mirror_host}/v2/" >/dev/null; then
|
||||||
|
echo "Registry mirror is healthy at ${mirror_host}"
|
||||||
|
else
|
||||||
|
echo "Warning: registry mirror health check failed at ${mirror_host}; continuing with normal pull path"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: curl is unavailable; skipping explicit mirror health check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
prewarm_base_image() {
|
||||||
|
image_ref="$1"
|
||||||
|
mirror_ref="${mirror_host}/library/${image_ref}"
|
||||||
|
echo "Attempting mirror pre-pull: ${mirror_ref}"
|
||||||
|
if docker pull "${mirror_ref}"; then
|
||||||
|
docker tag "${mirror_ref}" "${image_ref}"
|
||||||
|
echo "Mirror pre-pull succeeded for ${image_ref}"
|
||||||
|
else
|
||||||
|
echo "Warning: mirror pre-pull failed for ${image_ref}; continuing with normal pull during build"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prewarm_base_image "node:20-alpine"
|
||||||
|
prewarm_base_image "python:3.12-slim"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: DOCKER_BUILDKIT=0 docker build -t nas-dashboard:latest dashboard/
|
run: DOCKER_BUILDKIT=0 docker build -t nas-dashboard:latest dashboard/
|
||||||
|
- name: Sync runtime compose file
|
||||||
|
run: cp dashboard/docker-compose.yml /nas-dashboard/docker-compose.yml
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: docker compose -f /nas-dashboard/docker-compose.yml up -d --pull never
|
run: docker compose -f /nas-dashboard/docker-compose.yml up -d --pull never
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Environment Handoff: Plan + Blockers (2026-03-03)
|
||||||
|
|
||||||
|
## Scope completed in this environment
|
||||||
|
|
||||||
|
- Reviewed the last 24h code changes focused on new dashboard services and images:
|
||||||
|
- LiteLLM
|
||||||
|
- CC Connect
|
||||||
|
- Info Engine
|
||||||
|
- Sidebar/service wiring
|
||||||
|
- Implemented one concrete fix:
|
||||||
|
- `dashboard/docker-compose.yml`
|
||||||
|
- Set `LITELLM_URL=http://litellm:4005`
|
||||||
|
- Added dashboard service to external network `nas-dashboard_internal`
|
||||||
|
- Declared `nas-dashboard_internal` under top-level `networks` as `external: true`
|
||||||
|
|
||||||
|
## Current git state summary
|
||||||
|
|
||||||
|
- Local branch context encountered during work:
|
||||||
|
- `dev` had two local commits ahead of remote at one point:
|
||||||
|
- `c29bda7` (includes `dashboard/docker-compose.yml` + `dashboard/frontend/src/routes/Terminal.svelte`)
|
||||||
|
- `da88513` (`dashboard/backend/routers/terminal.py`)
|
||||||
|
- `main` was behind `origin/main` and was fast-forwarded locally during troubleshooting.
|
||||||
|
- Final push/merge sequence was not fully completed in this environment due runtime/interaction blockers (below).
|
||||||
|
|
||||||
|
## Blockers in this environment
|
||||||
|
|
||||||
|
1. **No Docker runtime in this shell**
|
||||||
|
- `docker: command not found`
|
||||||
|
- Impact: cannot run compose/runtime verification (`docker compose config`, container health checks, end-to-end service validation).
|
||||||
|
|
||||||
|
2. **Limited/fragile git remote auth flow in this session**
|
||||||
|
- Token push via URL works for some commands, but interactive/approval interruptions occurred during follow-up operations.
|
||||||
|
- Impact: full unattended sequence (`push dev -> validate -> merge main -> push main`) was interrupted and must be re-executed in a more capable/stable environment.
|
||||||
|
|
||||||
|
3. **Branch divergence handling required care**
|
||||||
|
- `main` and `dev` divergence prevented `--ff-only` merge in one attempted sequence.
|
||||||
|
- Impact: merge strategy should be re-run intentionally after confirming remote heads in target environment.
|
||||||
|
|
||||||
|
## Recommended next steps (for stronger environment)
|
||||||
|
|
||||||
|
1. **Sync and inspect branch heads**
|
||||||
|
- Fetch all refs and inspect divergence between `origin/main` and `origin/dev`.
|
||||||
|
- Confirm whether commits `c29bda7` and `da88513` are already on remote `dev`.
|
||||||
|
|
||||||
|
2. **Validate compose and runtime behavior on NAS-capable host**
|
||||||
|
- Validate `dashboard/docker-compose.yml` syntax.
|
||||||
|
- Confirm network exists:
|
||||||
|
- `nas-dashboard_internal`
|
||||||
|
- Verify dashboard container can resolve and reach `litellm:4005` on shared network.
|
||||||
|
|
||||||
|
3. **Functional verification from dashboard backend**
|
||||||
|
- Check `/api/litellm/health` returns `up` or expected `auth_required`.
|
||||||
|
- Check CC Connect health/start/stop endpoints against actual container name.
|
||||||
|
- Check Info Engine endpoints (`/api/info-engine/items`, `/api/info-engine/health`) with mounted DB path.
|
||||||
|
|
||||||
|
4. **Complete git delivery flow**
|
||||||
|
- Push finalized `dev`.
|
||||||
|
- Merge `dev -> main` using the repo’s preferred strategy.
|
||||||
|
- Push `main`.
|
||||||
|
- Confirm CI/deploy workflows trigger as expected.
|
||||||
|
|
||||||
|
## Files directly touched for the LiteLLM networking fix
|
||||||
|
|
||||||
|
- `dashboard/docker-compose.yml`
|
||||||
|
|
||||||
|
## Notes for next Claude instance
|
||||||
|
|
||||||
|
- This repo’s remote is Gitea at:
|
||||||
|
- `http://localhost:3300/jimmy/nas-tools.git`
|
||||||
|
- External UI URL from project context:
|
||||||
|
- `https://git.jimmygan.com`
|
||||||
|
- Prior user preference in this thread:
|
||||||
|
- Keep CC Connect sidebar label lowercase.
|
||||||
@@ -30,6 +30,10 @@ VPS_SSH_KEY_PATH = os.environ.get("VPS_SSH_KEY_PATH", "/app/ssh/vps_id_ed25519")
|
|||||||
CADDY_VPS_SSH_HOST = os.environ.get("CADDY_VPS_SSH_HOST", "161.33.182.109")
|
CADDY_VPS_SSH_HOST = os.environ.get("CADDY_VPS_SSH_HOST", "161.33.182.109")
|
||||||
CADDY_VPS_SSH_USER = os.environ.get("CADDY_VPS_SSH_USER", "ubuntu")
|
CADDY_VPS_SSH_USER = os.environ.get("CADDY_VPS_SSH_USER", "ubuntu")
|
||||||
|
|
||||||
|
MAC_SSH_HOST = os.environ.get("MAC_SSH_HOST", "192.168.31.22")
|
||||||
|
MAC_SSH_USER = os.environ.get("MAC_SSH_USER", "jimmyg")
|
||||||
|
MAC_SSH_KEY_PATH = os.environ.get("MAC_SSH_KEY_PATH", "/app/ssh/mac_id_ed25519")
|
||||||
|
|
||||||
# Telegram notifications
|
# Telegram notifications
|
||||||
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||||
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import config
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_SESSIONS = 5
|
MAX_SESSIONS = 5
|
||||||
IDLE_TIMEOUT = 30 * 60 # 30 minutes
|
|
||||||
_active_sessions: set = set()
|
_active_sessions: set = set()
|
||||||
|
|
||||||
HOSTS = {
|
HOSTS = {
|
||||||
"nas": {"host": config.SSH_HOST, "user": config.SSH_USER, "key": config.SSH_KEY_PATH},
|
"nas": {"host": config.SSH_HOST, "user": config.SSH_USER, "key": config.SSH_KEY_PATH},
|
||||||
"vps": {"host": config.VPS_SSH_HOST, "user": config.VPS_SSH_USER, "key": config.VPS_SSH_KEY_PATH},
|
"vps": {"host": config.VPS_SSH_HOST, "user": config.VPS_SSH_USER, "key": config.VPS_SSH_KEY_PATH},
|
||||||
"caddy-vps": {"host": config.CADDY_VPS_SSH_HOST, "user": config.CADDY_VPS_SSH_USER, "key": config.VPS_SSH_KEY_PATH},
|
"caddy-vps": {"host": config.CADDY_VPS_SSH_HOST, "user": config.CADDY_VPS_SSH_USER, "key": config.VPS_SSH_KEY_PATH},
|
||||||
|
"mac": {"host": config.MAC_SSH_HOST, "user": config.MAC_SSH_USER, "key": config.MAC_SSH_KEY_PATH},
|
||||||
"claude-dev": {"host": config.SSH_HOST, "user": config.SSH_USER, "key": config.SSH_KEY_PATH,
|
"claude-dev": {"host": config.SSH_HOST, "user": config.SSH_USER, "key": config.SSH_KEY_PATH,
|
||||||
"command": "/volume1/@appstore/ContainerManager/usr/bin/docker exec -it claude-dev bash"},
|
"command": "/volume1/@appstore/ContainerManager/usr/bin/docker exec -it claude-dev bash"},
|
||||||
}
|
}
|
||||||
@@ -95,18 +95,10 @@ async def ws_endpoint(websocket: WebSocket, host: str = Query("nas")):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
read_task = asyncio.create_task(read_ssh())
|
read_task = asyncio.create_task(read_ssh())
|
||||||
last_activity = time.monotonic()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
try:
|
msg = await websocket.receive()
|
||||||
msg = await asyncio.wait_for(websocket.receive(), timeout=60)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
if time.monotonic() - last_activity > IDLE_TIMEOUT:
|
|
||||||
await websocket.close(code=1000, reason="Idle timeout")
|
|
||||||
break
|
|
||||||
continue
|
|
||||||
last_activity = time.monotonic()
|
|
||||||
if "bytes" in msg and msg["bytes"]:
|
if "bytes" in msg and msg["bytes"]:
|
||||||
data = msg["bytes"]
|
data = msg["bytes"]
|
||||||
if data[0:1] == b"\x01" and len(data) == 5:
|
if data[0:1] == b"\x01" and len(data) == 5:
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ services:
|
|||||||
- SSH_USER=zjgump
|
- SSH_USER=zjgump
|
||||||
- VPS_SSH_HOST=158.101.140.85
|
- VPS_SSH_HOST=158.101.140.85
|
||||||
- VPS_SSH_USER=jimmyg
|
- VPS_SSH_USER=jimmyg
|
||||||
|
- CADDY_VPS_SSH_HOST=161.33.182.109
|
||||||
|
- CADDY_VPS_SSH_USER=ubuntu
|
||||||
|
- MAC_SSH_HOST=192.168.31.22
|
||||||
|
- MAC_SSH_USER=jimmyg
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
- SECRET_KEY=${SECRET_KEY}
|
||||||
- ADMIN_USER=jimmy
|
- ADMIN_USER=jimmy
|
||||||
- ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH}
|
- ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH}
|
||||||
@@ -27,6 +31,7 @@ services:
|
|||||||
- /volume1:/volume1
|
- /volume1:/volume1
|
||||||
- /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro
|
- /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro
|
||||||
- /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro
|
- /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro
|
||||||
|
- /volume1/docker/nas-dashboard/ssh/mac_terminal:/app/ssh/mac_id_ed25519:ro
|
||||||
- /volume1/docker/nas-dashboard/ssh/known_hosts:/app/ssh/known_hosts:ro
|
- /volume1/docker/nas-dashboard/ssh/known_hosts:/app/ssh/known_hosts:ro
|
||||||
- /volume1/docker/chat-summarizer/data:/app/data/chat-summarizer:ro
|
- /volume1/docker/chat-summarizer/data:/app/data/chat-summarizer:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ services:
|
|||||||
- SSH_USER=zjgump
|
- SSH_USER=zjgump
|
||||||
- VPS_SSH_HOST=158.101.140.85
|
- VPS_SSH_HOST=158.101.140.85
|
||||||
- VPS_SSH_USER=jimmyg
|
- VPS_SSH_USER=jimmyg
|
||||||
|
- CADDY_VPS_SSH_HOST=161.33.182.109
|
||||||
|
- CADDY_VPS_SSH_USER=ubuntu
|
||||||
|
- MAC_SSH_HOST=192.168.31.22
|
||||||
|
- MAC_SSH_USER=jimmyg
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
- SECRET_KEY=${SECRET_KEY}
|
||||||
- ADMIN_USER=jimmy
|
- ADMIN_USER=jimmy
|
||||||
- ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH}
|
- ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH}
|
||||||
@@ -44,10 +48,12 @@ services:
|
|||||||
- TELEGRAM_PROXY=${TELEGRAM_PROXY:-}
|
- TELEGRAM_PROXY=${TELEGRAM_PROXY:-}
|
||||||
- CORS_ORIGINS=https://nas.jimmygan.com
|
- CORS_ORIGINS=https://nas.jimmygan.com
|
||||||
- LITELLM_HEALTH_API_KEY=${LITELLM_HEALTH_API_KEY:-}
|
- LITELLM_HEALTH_API_KEY=${LITELLM_HEALTH_API_KEY:-}
|
||||||
|
- LITELLM_URL=http://litellm:4005
|
||||||
volumes:
|
volumes:
|
||||||
- /volume1:/volume1
|
- /volume1:/volume1
|
||||||
- /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro
|
- /volume1/docker/nas-dashboard/ssh/dashboard_terminal:/app/ssh/id_ed25519:ro
|
||||||
- /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro
|
- /volume1/docker/nas-dashboard/ssh/vps_terminal:/app/ssh/vps_id_ed25519:ro
|
||||||
|
- /volume1/docker/nas-dashboard/ssh/mac_terminal:/app/ssh/mac_id_ed25519:ro
|
||||||
- /volume1/docker/nas-dashboard/ssh/known_hosts:/app/ssh/known_hosts:ro
|
- /volume1/docker/nas-dashboard/ssh/known_hosts:/app/ssh/known_hosts:ro
|
||||||
- /volume1/docker/chat-summarizer/data:/app/data/chat-summarizer:ro
|
- /volume1/docker/chat-summarizer/data:/app/data/chat-summarizer:ro
|
||||||
- /volume1/docker/info-engine/data:/app/data/info-engine:ro
|
- /volume1/docker/info-engine/data:/app/data/info-engine:ro
|
||||||
@@ -62,6 +68,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
- gitea_gitea
|
- gitea_gitea
|
||||||
|
- nas-dashboard_internal
|
||||||
depends_on:
|
depends_on:
|
||||||
- docker-socket-proxy
|
- docker-socket-proxy
|
||||||
logging:
|
logging:
|
||||||
@@ -74,3 +81,5 @@ networks:
|
|||||||
internal:
|
internal:
|
||||||
gitea_gitea:
|
gitea_gitea:
|
||||||
external: true
|
external: true
|
||||||
|
nas-dashboard_internal:
|
||||||
|
external: true
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
{ id: "nas", label: "NAS" },
|
{ id: "nas", label: "NAS" },
|
||||||
{ id: "vps", label: "VPS" },
|
{ id: "vps", label: "VPS" },
|
||||||
{ id: "caddy-vps", label: "Caddy VPS" },
|
{ id: "caddy-vps", label: "Caddy VPS" },
|
||||||
|
{ id: "mac", label: "Mac" },
|
||||||
{ id: "claude-dev", label: "Claude Dev" },
|
{ id: "claude-dev", label: "Claude Dev" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -16,6 +17,57 @@
|
|||||||
let tabCounter = 0;
|
let tabCounter = 0;
|
||||||
const tabData = new Map();
|
const tabData = new Map();
|
||||||
|
|
||||||
|
function cleanName(name) {
|
||||||
|
return (name || "image")
|
||||||
|
.replace(/[^a-zA-Z0-9._-]/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "") || "image";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extensionFor(type) {
|
||||||
|
if (type === "image/png") return "png";
|
||||||
|
if (type === "image/jpeg") return "jpg";
|
||||||
|
if (type === "image/webp") return "webp";
|
||||||
|
if (type === "image/gif") return "gif";
|
||||||
|
return "bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileToBase64(file) {
|
||||||
|
const buf = await file.arrayBuffer();
|
||||||
|
let binary = "";
|
||||||
|
const bytes = new Uint8Array(buf);
|
||||||
|
const chunkSize = 0x8000;
|
||||||
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||||
|
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendDroppedImage(term, ws, file) {
|
||||||
|
if (!file?.type?.startsWith("image/")) return;
|
||||||
|
if (ws.readyState !== 1) return;
|
||||||
|
|
||||||
|
const ext = extensionFor(file.type);
|
||||||
|
const baseName = cleanName(file.name).replace(/\.[a-zA-Z0-9]+$/, "");
|
||||||
|
const stamp = Date.now();
|
||||||
|
const imagePath = `/tmp/claude-images/${baseName || "image"}-${stamp}.${ext}`;
|
||||||
|
const b64 = await fileToBase64(file);
|
||||||
|
|
||||||
|
const cmd = [
|
||||||
|
"python3 - <<'PY'",
|
||||||
|
"import base64, pathlib",
|
||||||
|
`p = pathlib.Path(${JSON.stringify(imagePath)})`,
|
||||||
|
"p.parent.mkdir(parents=True, exist_ok=True)",
|
||||||
|
`p.write_bytes(base64.b64decode(${JSON.stringify(b64)}))`,
|
||||||
|
`print(f'saved image -> {p}')`,
|
||||||
|
"PY",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
ws.send(new TextEncoder().encode(cmd));
|
||||||
|
term.write(`\r\n\x1b[90m[Uploaded image to ${imagePath}]\x1b[0m\r\n`);
|
||||||
|
}
|
||||||
|
|
||||||
function addTab(hostId) {
|
function addTab(hostId) {
|
||||||
const id = ++tabCounter;
|
const id = ++tabCounter;
|
||||||
const label = hosts.find((h) => h.id === hostId).label;
|
const label = hosts.find((h) => h.id === hostId).label;
|
||||||
@@ -29,6 +81,9 @@
|
|||||||
if (d) {
|
if (d) {
|
||||||
d.voice?.destroy();
|
d.voice?.destroy();
|
||||||
d.ro?.disconnect();
|
d.ro?.disconnect();
|
||||||
|
d.term?.element?.removeEventListener("dragover", d.handleDragOver);
|
||||||
|
d.term?.element?.removeEventListener("drop", d.handleDrop);
|
||||||
|
d.term?.textarea?.removeEventListener("paste", d.handlePaste);
|
||||||
d.ws?.close();
|
d.ws?.close();
|
||||||
d.term?.dispose();
|
d.term?.dispose();
|
||||||
tabData.delete(id);
|
tabData.delete(id);
|
||||||
@@ -89,17 +144,52 @@
|
|||||||
ws.onerror = () => updateTab(tabId, { connected: false, error: "Connection failed" });
|
ws.onerror = () => updateTab(tabId, { connected: false, error: "Connection failed" });
|
||||||
term.onData((data) => ws.readyState === 1 && ws.send(new TextEncoder().encode(data)));
|
term.onData((data) => ws.readyState === 1 && ws.send(new TextEncoder().encode(data)));
|
||||||
|
|
||||||
|
const handleDragOver = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (activeTab !== tabId) return;
|
||||||
|
const files = [...(e.dataTransfer?.files || [])].filter((f) => f.type?.startsWith("image/"));
|
||||||
|
for (const file of files) {
|
||||||
|
await sendDroppedImage(term, ws, file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = async (e) => {
|
||||||
|
if (activeTab !== tabId) return;
|
||||||
|
const items = [...(e.clipboardData?.items || [])]
|
||||||
|
.filter((it) => it.kind === "file" && it.type?.startsWith("image/"));
|
||||||
|
for (const item of items) {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file) await sendDroppedImage(term, ws, file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
term.element?.addEventListener("dragover", handleDragOver);
|
||||||
|
term.element?.addEventListener("drop", handleDrop);
|
||||||
|
term.textarea?.addEventListener("paste", handlePaste);
|
||||||
|
|
||||||
const ro = new ResizeObserver(() => { fit.fit(); sendSize(); });
|
const ro = new ResizeObserver(() => { fit.fit(); sendSize(); });
|
||||||
ro.observe(el);
|
ro.observe(el);
|
||||||
const voice = new VoiceSession(() => { voiceTick++; });
|
const voice = new VoiceSession(() => { voiceTick++; });
|
||||||
voice.attach(term, ws);
|
voice.attach(term, ws);
|
||||||
tabData.set(tabId, { ws, term, ro, el, voice });
|
tabData.set(tabId, { ws, term, ro, el, voice, handleDragOver, handleDrop, handlePaste });
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
function activeVoice() { return voiceTick >= 0 && tabData.get(activeTab)?.voice; }
|
function activeVoice() { return voiceTick >= 0 && tabData.get(activeTab)?.voice; }
|
||||||
|
|
||||||
onDestroy(() => tabData.forEach((d) => { d.voice?.destroy(); d.ro?.disconnect(); d.ws?.close(); d.term?.dispose(); }));
|
onDestroy(() => tabData.forEach((d) => {
|
||||||
|
d.voice?.destroy();
|
||||||
|
d.ro?.disconnect();
|
||||||
|
d.term?.element?.removeEventListener("dragover", d.handleDragOver);
|
||||||
|
d.term?.element?.removeEventListener("drop", d.handleDrop);
|
||||||
|
d.term?.textarea?.removeEventListener("paste", d.handlePaste);
|
||||||
|
d.ws?.close();
|
||||||
|
d.term?.dispose();
|
||||||
|
}));
|
||||||
</script>
|
</script>
|
||||||
<div class={activeVoice()?.voiceMode ? 'flex flex-col h-[calc(100vh-4rem)] overflow-hidden gap-1' : 'space-y-2'}>
|
<div class={activeVoice()?.voiceMode ? 'flex flex-col h-[calc(100vh-4rem)] overflow-hidden gap-1' : 'space-y-2'}>
|
||||||
{#if !activeVoice()?.voiceMode}
|
{#if !activeVoice()?.voiceMode}
|
||||||
@@ -160,6 +250,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-surface-500">
|
||||||
|
Tip: drag & drop or paste images into the active terminal tab to upload them to <code class="text-surface-400">/tmp/claude-images</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
{#if !tabs.length}
|
{#if !tabs.length}
|
||||||
<div class="flex items-center justify-center h-[calc(100vh-12rem)] text-surface-500 text-sm">
|
<div class="flex items-center justify-center h-[calc(100vh-12rem)] text-surface-500 text-sm">
|
||||||
Click + to open a terminal
|
Click + to open a terminal
|
||||||
|
|||||||
Reference in New Issue
Block a user