From 9324598cd5e3dd1b5b0c57b2e95ed2c2d2dada23 Mon Sep 17 00:00:00 2001 From: weishu Date: Wed, 20 May 2026 17:31:25 +0800 Subject: [PATCH] chore: remove dead UI and agent entrypoints --- .gitignore | 3 + cli/src/agent/index.ts | 4 - cli/src/agent/runners/gemini.ts | 23 --- cli/src/api/socketOutbox.ts | 156 ------------------ web/src/components/MachineList.tsx | 38 ----- web/src/components/SpawnSession.tsx | 239 ---------------------------- web/src/hooks/useScrollToBottom.ts | 38 ----- 7 files changed, 3 insertions(+), 498 deletions(-) delete mode 100644 cli/src/agent/index.ts delete mode 100644 cli/src/agent/runners/gemini.ts delete mode 100644 cli/src/api/socketOutbox.ts delete mode 100644 web/src/components/MachineList.tsx delete mode 100644 web/src/components/SpawnSession.tsx delete mode 100644 web/src/hooks/useScrollToBottom.ts diff --git a/.gitignore b/.gitignore index 89164b35..530320c8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,13 @@ **/dist-exe/ **/*.tsbuildinfo hub/src/web/embeddedAssets.generated.ts +hub/src/generated/ # Downloaded tools (fetched at build time) hub/tools/tunwg/tunwg-* hub/tools/tunwg/*.exe +shared/tools/tunwg/tunwg-* +shared/tools/tunwg/*.exe # Env files (can contain secrets) **/.env diff --git a/cli/src/agent/index.ts b/cli/src/agent/index.ts deleted file mode 100644 index e0fbab26..00000000 --- a/cli/src/agent/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './types'; -export * from './AgentRegistry'; -export * from './messageConverter'; -export * from './permissionAdapter'; diff --git a/cli/src/agent/runners/gemini.ts b/cli/src/agent/runners/gemini.ts deleted file mode 100644 index 9d0c0af5..00000000 --- a/cli/src/agent/runners/gemini.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AgentRegistry } from '@/agent/AgentRegistry'; -import { AcpSdkBackend } from '@/agent/backends/acp'; - -function buildEnv(): Record { - return Object.keys(process.env).reduce((acc, key) => { - const value = process.env[key]; - if (typeof value === 'string') { - acc[key] = value; - } - return acc; - }, {} as Record); -} - -export function registerGeminiAgent(yolo: boolean): void { - const args = ['--experimental-acp']; - if (yolo) args.push('--yolo'); - - AgentRegistry.register('gemini', () => new AcpSdkBackend({ - command: 'gemini', - args, - env: buildEnv() - })); -} diff --git a/cli/src/api/socketOutbox.ts b/cli/src/api/socketOutbox.ts deleted file mode 100644 index 6ae92182..00000000 --- a/cli/src/api/socketOutbox.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { logger } from '@/ui/logger' - -const DEFAULT_OUTBOX_MAX_BYTES = resolveEnvNumber('HAPI_OUTBOX_MAX_BYTES', 16_000_000) -const DEFAULT_OUTBOX_MAX_ITEMS = resolveEnvNumber('HAPI_OUTBOX_MAX_ITEMS', 500) -const DEFAULT_OUTBOX_MAX_ITEM_BYTES = resolveEnvNumber('HAPI_OUTBOX_MAX_ITEM_BYTES', 1_000_000) -const DEFAULT_OUTBOX_MAX_AGE_MS = resolveEnvNumber('HAPI_OUTBOX_MAX_AGE_MS', 15 * 60_000, true) -const DEFAULT_DROP_LOG_INTERVAL_MS = resolveEnvNumber('HAPI_OUTBOX_DROP_LOG_INTERVAL_MS', 5_000) - -type OutboxItem = { - event: string - args: readonly unknown[] - sizeBytes: number - enqueuedAt: number -} - -type SocketOutboxOptions = { - maxBytes?: number - maxItems?: number - maxItemBytes?: number - maxAgeMs?: number - dropLogIntervalMs?: number -} - -function resolveEnvNumber(name: string, fallback: number, allowZero: boolean = false): number { - const raw = process.env[name] - if (!raw) { - return fallback - } - const parsed = Number.parseInt(raw, 10) - if (!Number.isFinite(parsed)) { - return fallback - } - if (parsed < 0) { - return fallback - } - if (parsed === 0 && !allowZero) { - return fallback - } - return parsed -} - -function estimateSizeBytes(payload: unknown): number { - try { - return Buffer.byteLength(JSON.stringify(payload), 'utf8') - } catch { - return Number.MAX_SAFE_INTEGER - } -} - -export class SocketOutbox { - private readonly maxBytes: number - private readonly maxItems: number - private readonly maxItemBytes: number - private readonly maxAgeMs: number - private readonly dropLogIntervalMs: number - private items: OutboxItem[] = [] - private queuedBytes = 0 - private droppedCount = 0 - private droppedBytes = 0 - private lastDropLogAt = 0 - private lastDropReason = 'unknown' - - constructor(options?: SocketOutboxOptions) { - this.maxBytes = options?.maxBytes ?? DEFAULT_OUTBOX_MAX_BYTES - this.maxItems = options?.maxItems ?? DEFAULT_OUTBOX_MAX_ITEMS - this.maxItemBytes = options?.maxItemBytes ?? DEFAULT_OUTBOX_MAX_ITEM_BYTES - this.maxAgeMs = options?.maxAgeMs ?? DEFAULT_OUTBOX_MAX_AGE_MS - this.dropLogIntervalMs = options?.dropLogIntervalMs ?? DEFAULT_DROP_LOG_INTERVAL_MS - } - - enqueue(event: string, args: readonly unknown[]): boolean { - if (this.maxBytes <= 0 || this.maxItems <= 0) { - this.recordDrop('outbox-disabled', 0) - return false - } - - this.dropExpired() - - const sizeBytes = estimateSizeBytes({ event, args }) - if (sizeBytes > this.maxItemBytes || sizeBytes > this.maxBytes) { - this.recordDrop('item-too-large', sizeBytes) - return false - } - - while (this.items.length >= this.maxItems || this.queuedBytes + sizeBytes > this.maxBytes) { - const removed = this.items.shift() - if (!removed) { - break - } - this.queuedBytes -= removed.sizeBytes - this.recordDrop('outbox-full', removed.sizeBytes) - } - - if (this.items.length >= this.maxItems || this.queuedBytes + sizeBytes > this.maxBytes) { - this.recordDrop('outbox-full', sizeBytes) - return false - } - - this.items.push({ - event, - args, - sizeBytes, - enqueuedAt: Date.now() - }) - this.queuedBytes += sizeBytes - return true - } - - flush(emit: (event: string, args: readonly unknown[]) => void): void { - this.dropExpired() - - if (this.items.length === 0) { - return - } - - const items = this.items - this.items = [] - this.queuedBytes = 0 - - for (const item of items) { - emit(item.event, item.args) - } - } - - private dropExpired(): void { - if (this.maxAgeMs <= 0) { - return - } - - const cutoff = Date.now() - this.maxAgeMs - while (this.items.length > 0 && this.items[0].enqueuedAt < cutoff) { - const removed = this.items.shift() - if (!removed) { - break - } - this.queuedBytes -= removed.sizeBytes - this.recordDrop('expired', removed.sizeBytes) - } - } - - private recordDrop(reason: string, bytes: number): void { - this.droppedCount += 1 - this.droppedBytes += bytes - this.lastDropReason = reason - - const now = Date.now() - if (now - this.lastDropLogAt < this.dropLogIntervalMs) { - return - } - - logger.warn(`[OUTBOX] Dropped ${this.droppedCount} items (${this.droppedBytes} bytes). reason=${this.lastDropReason}`) - this.droppedCount = 0 - this.droppedBytes = 0 - this.lastDropLogAt = now - } -} diff --git a/web/src/components/MachineList.tsx b/web/src/components/MachineList.tsx deleted file mode 100644 index 71758a2b..00000000 --- a/web/src/components/MachineList.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { Machine } from '@/types/api' -import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' - -function getMachineTitle(machine: Machine): string { - if (machine.metadata?.displayName) return machine.metadata.displayName - if (machine.metadata?.host) return machine.metadata.host - return machine.id.slice(0, 8) -} - -export function MachineList(props: { - machines: Machine[] - onSelect: (machineId: string) => void -}) { - return ( -
-
- {props.machines.length} online -
- -
- {props.machines.map((m) => ( - props.onSelect(m.id)} - > - - {getMachineTitle(m)} - - {m.metadata?.platform ? m.metadata.platform : 'Unknown platform'} - - - - ))} -
-
- ) -} diff --git a/web/src/components/SpawnSession.tsx b/web/src/components/SpawnSession.tsx deleted file mode 100644 index 407ed62d..00000000 --- a/web/src/components/SpawnSession.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { useDeferredValue, useEffect, useMemo, useState } from 'react' -import type { ApiClient } from '@/api/client' -import type { Machine } from '@/types/api' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { useMachinePathsExists } from '@/hooks/useMachinePathsExists' -import { usePlatform } from '@/hooks/usePlatform' -import { useSpawnSession } from '@/hooks/mutations/useSpawnSession' -import { formatRunnerSpawnError } from '@/utils/formatRunnerSpawnError' -import { useTranslation } from '@/lib/use-translation' - -type SessionType = 'simple' | 'worktree' - -function getMachineTitle(machine: Machine | null): string { - if (!machine) return 'Machine' - if (machine.metadata?.displayName) return machine.metadata.displayName - if (machine.metadata?.host) return machine.metadata.host - return machine.id.slice(0, 8) -} - -export function SpawnSession(props: { - api: ApiClient - machineId: string - machine: Machine | null - onSuccess: (sessionId: string) => void - onCancel: () => void -}) { - const { haptic } = usePlatform() - const { t } = useTranslation() - const [directory, setDirectory] = useState('') - const [sessionType, setSessionType] = useState('simple') - const [worktreeName, setWorktreeName] = useState('') - const [directoryCreationConfirmed, setDirectoryCreationConfirmed] = useState(false) - const [error, setError] = useState(null) - const { spawnSession, isPending, error: spawnError } = useSpawnSession(props.api) - - const machineTitle = useMemo(() => getMachineTitle(props.machine), [props.machine]) - const runnerSpawnError = useMemo( - () => formatRunnerSpawnError(props.machine), - [props.machine?.runnerState?.lastSpawnError] - ) - const trimmedDirectory = directory.trim() - const deferredDirectory = useDeferredValue(trimmedDirectory) - const pathsToCheck = useMemo( - () => deferredDirectory ? [deferredDirectory] : [], - [deferredDirectory] - ) - const { pathExistence, checkPathsExists } = useMachinePathsExists( - props.api, - props.machineId, - pathsToCheck - ) - const currentDirectoryExists = trimmedDirectory ? pathExistence[trimmedDirectory] : undefined - const needsDirectoryCreationWarning = sessionType === 'simple' && trimmedDirectory !== '' && currentDirectoryExists === false - const missingWorktreeDirectory = sessionType === 'worktree' && trimmedDirectory !== '' && currentDirectoryExists === false - const directoryStatusMessage = missingWorktreeDirectory - ? t('session.directoryMissingWorktree') - : needsDirectoryCreationWarning - ? ( - directoryCreationConfirmed - ? t('session.directoryMissingSimpleConfirm') - : t('session.directoryMissingSimple') - ) - : null - const createLabel = needsDirectoryCreationWarning && directoryCreationConfirmed - ? t('session.createAndCreateDirectory') - : t('spawn.create') - - useEffect(() => { - setDirectoryCreationConfirmed(false) - }, [props.machineId, sessionType, trimmedDirectory]) - - async function spawn() { - if (!trimmedDirectory) return - - setError(null) - try { - const existsResult = await checkPathsExists([trimmedDirectory]) - const directoryExists = existsResult[trimmedDirectory] - - if (sessionType === 'worktree' && directoryExists === false) { - haptic.notification('error') - setError(t('session.directoryMissingWorktree')) - return - } - - if (sessionType === 'simple' && directoryExists === false && !directoryCreationConfirmed) { - setDirectoryCreationConfirmed(true) - return - } - - const result = await spawnSession({ - machineId: props.machineId, - directory: trimmedDirectory, - sessionType, - worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined - }) - if (result.type === 'success') { - haptic.notification('success') - props.onSuccess(result.sessionId) - return - } - haptic.notification('error') - setError(result.message) - } catch (e) { - haptic.notification('error') - setError(e instanceof Error ? e.message : 'Failed to spawn session') - } - } - - return ( -
- - - {t('spawn.title')} - - {machineTitle} - - - -
- setDirectory(e.target.value)} - className="w-full rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] p-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--app-link)]" - /> - - {directoryStatusMessage ? ( -
- {directoryStatusMessage} -
- ) : null} - -
- -
- {(['simple', 'worktree'] as const).map((type) => ( -
- {type === 'worktree' ? ( -
- setSessionType('worktree')} - disabled={isPending} - className="mt-1 accent-[var(--app-link)]" - /> -
-
- {sessionType === 'worktree' ? ( - setWorktreeName(e.target.value)} - disabled={isPending} - className="w-full rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-60" - /> - ) : ( - - )} -
- - Create a new worktree next to the repo - -
-
- ) : ( - - )} -
- ))} -
-
- - {runnerSpawnError ? ( -
- Runner last spawn error: {runnerSpawnError} -
- ) : null} - - {(error ?? spawnError) ? ( -
- {error ?? spawnError} -
- ) : null} - -
- - -
-
-
-
-
- ) -} diff --git a/web/src/hooks/useScrollToBottom.ts b/web/src/hooks/useScrollToBottom.ts deleted file mode 100644 index 995d0d55..00000000 --- a/web/src/hooks/useScrollToBottom.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect, useRef, type RefObject } from 'react' - -export function useScrollToBottom( - deps: readonly unknown[], - options?: { thresholdPx?: number } -): RefObject { - const containerRef = useRef(null) - const stickToBottomRef = useRef(true) - - useEffect(() => { - const el = containerRef.current - if (!el) return - - const thresholdPx = options?.thresholdPx ?? 120 - - const onScroll = () => { - const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight - stickToBottomRef.current = distanceFromBottom < thresholdPx - } - - el.addEventListener('scroll', onScroll, { passive: true }) - onScroll() - - return () => { - el.removeEventListener('scroll', onScroll) - } - }, [options?.thresholdPx]) - - useEffect(() => { - const el = containerRef.current - if (!el) return - if (!stickToBottomRef.current) return - - el.scrollTo({ top: el.scrollHeight }) - }, deps) - - return containerRef -}