cc-haha/docs/superpowers/plans/2026-05-09-h5-access.md
程序员阿江(Relakkes) 5f6547450f Plan H5 access implementation lanes
The implementation needs to land through isolated service, auth, runtime, settings, and mobile-shell tasks so subagents can work without colliding with the desktop default path.

Constraint: Follow the approved opt-in H5 design and keep Tauri/local desktop behavior unchanged
Constraint: Use subagent-driven execution for implementation work
Rejected: One broad implementation pass | too much shared-file risk around AppShell and auth middleware
Confidence: high
Scope-risk: moderate
Directive: Execute Task 4 before Task 5 because both touch AppShell
Tested: Plan self-review for spec coverage, placeholders, and type naming consistency
Not-tested: Runtime behavior; this commit is implementation planning only
2026-05-09 23:15:09 +08:00

28 KiB

H5 Access Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add opt-in personal/team H5 browser access with a generated token, controlled origin handling, authenticated REST/WebSocket access, and a mobile-safe chat shell without disrupting the default desktop/Tauri flow.

Architecture: Store H5 access configuration under the existing cc-haha managed settings file, expose a narrow H5 settings API for the desktop Settings UI, and validate the generated token in the shared server auth path when remote access is required. The browser H5 runtime reuses the current Vite SPA with a connection screen, central REST/WebSocket auth injection, and responsive chat layout adjustments.

Tech Stack: Bun server, TypeScript, React 18, Zustand, Vite, Vitest, Testing Library, existing REST/WebSocket server.


File Structure

  • Create src/server/services/h5AccessService.ts: H5 settings persistence, token generation, token hashing, sanitized response shaping, token validation.
  • Create src/server/api/h5-access.ts: H5 Settings API endpoints for status, enable, disable, regenerate, update, and verify.
  • Modify src/server/router.ts: route /api/h5-access.
  • Modify src/server/middleware/auth.ts: accept either existing Anthropic bearer auth or enabled H5 token auth when auth is required.
  • Modify src/server/middleware/cors.ts: make allowed origins configurable through H5 settings and keep localhost/Tauri defaults.
  • Modify src/server/index.ts: use async CORS/auth helpers for REST, proxy, and client WebSocket upgrades.
  • Add/modify server tests under src/server/__tests__/.
  • Create desktop/src/api/h5Access.ts: typed client for H5 settings endpoints.
  • Modify desktop/src/api/client.ts: auth token storage/injection helpers for browser H5 mode.
  • Modify desktop/src/api/websocket.ts: http -> ws, https -> wss, and token query injection.
  • Modify desktop/src/lib/desktopRuntime.ts: read/write H5 browser connection state and distinguish startup health from authenticated verification.
  • Create desktop/src/components/layout/H5ConnectionView.tsx: browser-only connection form.
  • Modify desktop/src/components/layout/AppShell.tsx: show H5 connection view on browser auth startup failure; keep Tauri startup unchanged.
  • Modify desktop/src/stores/settingsStore.ts, desktop/src/types/settings.ts, and desktop/src/pages/Settings.tsx: Settings UI state and controls for H5 enable/token URL management.
  • Modify desktop/src/i18n/locales/en.ts and desktop/src/i18n/locales/zh.ts: new H5 labels.
  • Create desktop/src/hooks/useMobileViewport.ts: shared mobile breakpoint hook.
  • Modify desktop/src/theme/globals.css, desktop/src/components/layout/AppShell.tsx, desktop/src/components/layout/Sidebar.tsx, desktop/src/pages/ActiveSession.tsx, desktop/src/components/chat/ChatInput.tsx, and desktop/src/components/controls/PermissionModeSelector.tsx: mobile shell and touch-target improvements.
  • Add/modify desktop tests under desktop/src/**.
  • Create docs/desktop/h5-access.md: concise user setup notes for LAN/reverse proxy mode.

Task 1: Server H5 Settings Service And API

Files:

  • Create: src/server/services/h5AccessService.ts

  • Create: src/server/api/h5-access.ts

  • Modify: src/server/router.ts

  • Test: src/server/__tests__/h5-access-service.test.ts

  • Test: src/server/__tests__/h5-access-api.test.ts

  • Step 1: Write service tests first

Add tests covering default disabled state, token generation, hash-only persistence, token regeneration invalidating old tokens, and unknown field preservation.

// src/server/__tests__/h5-access-service.test.ts
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
import * as fs from 'fs/promises'
import * as os from 'os'
import * as path from 'path'
import { H5AccessService } from '../services/h5AccessService.js'

let tmpDir: string

beforeEach(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'h5-access-service-'))
  process.env.CLAUDE_CONFIG_DIR = tmpDir
})

afterEach(async () => {
  delete process.env.CLAUDE_CONFIG_DIR
  await fs.rm(tmpDir, { recursive: true, force: true })
})

describe('H5AccessService', () => {
  it('defaults to disabled without creating settings', async () => {
    const service = new H5AccessService()
    await expect(service.getSettings()).resolves.toMatchObject({
      enabled: false,
      tokenPreview: null,
      allowedOrigins: [],
      publicBaseUrl: null,
    })
  })

  it('generates a raw token once and stores only a hash plus preview', async () => {
    const service = new H5AccessService()
    const result = await service.enable()
    expect(result.token).toMatch(/^h5_[A-Za-z0-9_-]{43,}$/)
    expect(result.settings.enabled).toBe(true)
    expect(result.settings.tokenPreview).toMatch(/^h5_.+\\.{3}.+$/)
    expect(await service.validateToken(result.token)).toBe(true)

    const raw = await fs.readFile(path.join(tmpDir, 'cc-haha', 'settings.json'), 'utf-8')
    expect(raw).toContain('h5Access')
    expect(raw).not.toContain(result.token)
  })

  it('preserves unrelated managed settings fields', async () => {
    await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
    await fs.writeFile(
      path.join(tmpDir, 'cc-haha', 'settings.json'),
      JSON.stringify({ env: { ANTHROPIC_MODEL: 'keep-me' }, futureField: { keep: true } }),
      'utf-8',
    )

    const service = new H5AccessService()
    await service.enable()
    const saved = JSON.parse(await fs.readFile(path.join(tmpDir, 'cc-haha', 'settings.json'), 'utf-8'))
    expect(saved.env.ANTHROPIC_MODEL).toBe('keep-me')
    expect(saved.futureField).toEqual({ keep: true })
  })

  it('regenerates token and rejects the previous token', async () => {
    const service = new H5AccessService()
    const first = await service.enable()
    const second = await service.regenerateToken()
    expect(second.token).not.toBe(first.token)
    expect(await service.validateToken(first.token)).toBe(false)
    expect(await service.validateToken(second.token)).toBe(true)
  })
})
  • Step 2: Run service tests and verify they fail

Run: bun test src/server/__tests__/h5-access-service.test.ts

Expected: FAIL because H5AccessService does not exist.

  • Step 3: Implement H5AccessService

Create the service with these exports and behavior:

export type H5AccessSettings = {
  enabled: boolean
  tokenPreview: string | null
  allowedOrigins: string[]
  publicBaseUrl: string | null
}

export type H5AccessEnableResult = {
  settings: H5AccessSettings
  token: string
}

export class H5AccessService {
  async getSettings(): Promise<H5AccessSettings>
  async enable(): Promise<H5AccessEnableResult>
  async disable(): Promise<H5AccessSettings>
  async regenerateToken(): Promise<H5AccessEnableResult>
  async updateSettings(input: {
    allowedOrigins?: string[]
    publicBaseUrl?: string | null
  }): Promise<H5AccessSettings>
  async validateToken(token: string | null | undefined): Promise<boolean>
  async isOriginAllowed(origin: string | null | undefined): Promise<boolean>
}

Implementation details:

  • Use process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude').

  • Persist under cc-haha/settings.json in an h5Access object.

  • Use crypto.randomBytes(32).toString('base64url') with h5_ prefix for raw tokens.

  • Hash with SHA-256 and store tokenHash, not raw token.

  • Preview format: first 7 chars, ellipsis, last 4 chars.

  • Normalize allowedOrigins to valid http:// or https:// origins only, no path, no wildcard.

  • Preserve unknown fields by reading current JSON, replacing only h5Access, and writing back.

  • Step 4: Run service tests and verify they pass

Run: bun test src/server/__tests__/h5-access-service.test.ts

Expected: PASS.

  • Step 5: Write API tests

Add route tests for sanitized status, enable returning raw token once, verify accepting bearer token, and disable rejecting the old token.

// src/server/__tests__/h5-access-api.test.ts
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
import * as fs from 'fs/promises'
import * as os from 'os'
import * as path from 'path'
import { handleApiRequest } from '../router.js'

let tmpDir: string

async function api(method: string, pathname: string, body?: unknown, token?: string) {
  const headers: Record<string, string> = { 'Content-Type': 'application/json' }
  if (token) headers.Authorization = `Bearer ${token}`
  return handleApiRequest(
    new Request(`http://localhost${pathname}`, {
      method,
      headers,
      body: body === undefined ? undefined : JSON.stringify(body),
    }),
    new URL(`http://localhost${pathname}`),
  )
}

beforeEach(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'h5-access-api-'))
  process.env.CLAUDE_CONFIG_DIR = tmpDir
})

afterEach(async () => {
  delete process.env.CLAUDE_CONFIG_DIR
  await fs.rm(tmpDir, { recursive: true, force: true })
})

describe('/api/h5-access', () => {
  it('enables access and returns raw token only in mutation response', async () => {
    const enabled = await api('POST', '/api/h5-access/enable')
    expect(enabled.status).toBe(200)
    const payload = await enabled.json()
    expect(payload.token).toMatch(/^h5_/)
    expect(payload.settings.enabled).toBe(true)

    const status = await api('GET', '/api/h5-access')
    const statusPayload = await status.json()
    expect(statusPayload.token).toBeUndefined()
    expect(statusPayload.settings.tokenPreview).toBe(payload.settings.tokenPreview)
  })

  it('verifies a good token and rejects a bad token', async () => {
    const enabled = await api('POST', '/api/h5-access/enable')
    const { token } = await enabled.json()

    expect((await api('POST', '/api/h5-access/verify', undefined, token)).status).toBe(200)
    expect((await api('POST', '/api/h5-access/verify', undefined, 'bad-token')).status).toBe(401)
  })
})
  • Step 6: Implement API route and router case

src/server/api/h5-access.ts handles:

  • GET /api/h5-access
  • POST /api/h5-access/enable
  • POST /api/h5-access/disable
  • POST /api/h5-access/regenerate
  • PUT /api/h5-access
  • POST /api/h5-access/verify

Add case 'h5-access': return handleH5AccessApi(req, url, segments) to src/server/router.ts.

  • Step 7: Run API tests

Run: bun test src/server/__tests__/h5-access-service.test.ts src/server/__tests__/h5-access-api.test.ts

Expected: PASS.

  • Step 8: Commit Task 1
git add src/server/services/h5AccessService.ts src/server/api/h5-access.ts src/server/router.ts src/server/__tests__/h5-access-service.test.ts src/server/__tests__/h5-access-api.test.ts
git commit -m "Add managed H5 access settings service"

Use Lore trailers in the commit body: Constraint, Rejected, Confidence, Scope-risk, Tested, and Not-tested.

Task 2: Server Auth, CORS, And WebSocket Integration

Files:

  • Modify: src/server/middleware/auth.ts

  • Modify: src/server/middleware/cors.ts

  • Modify: src/server/index.ts

  • Test: src/server/__tests__/h5-access-auth.test.ts

  • Test: existing src/server/__tests__/e2e/full-flow.test.ts

  • Step 1: Write auth/CORS tests

Create tests that start the server bound to 0.0.0.0 on a random port and assert:

  • H5 disabled + no Anthropic key rejects /api/status.
  • H5 enabled + wrong token rejects /api/status.
  • H5 enabled + correct token allows /api/status.
  • CORS rejects unlisted origins.
  • CORS allows configured origins and includes Vary: Origin.
  • Client WebSocket upgrade rejects missing/wrong token when auth is required.
// src/server/__tests__/h5-access-auth.test.ts
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
import * as fs from 'fs/promises'
import * as os from 'os'
import * as path from 'path'
import { startServer } from '../index.js'
import { H5AccessService } from '../services/h5AccessService.js'

let tmpDir: string
let server: ReturnType<typeof startServer> | null = null

beforeEach(async () => {
  tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'h5-access-auth-'))
  process.env.CLAUDE_CONFIG_DIR = tmpDir
  delete process.env.ANTHROPIC_API_KEY
})

afterEach(async () => {
  server?.stop(true)
  server = null
  delete process.env.CLAUDE_CONFIG_DIR
  delete process.env.ANTHROPIC_API_KEY
  await fs.rm(tmpDir, { recursive: true, force: true })
})

function freePort() {
  return 46000 + Math.floor(Math.random() * 1000)
}

describe('H5 remote auth', () => {
  it('allows remote API requests with enabled H5 token', async () => {
    const service = new H5AccessService()
    const { token } = await service.enable()
    await service.updateSettings({ allowedOrigins: ['http://phone.local:1420'] })

    const port = freePort()
    server = startServer(port, '0.0.0.0')

    const ok = await fetch(`http://127.0.0.1:${port}/api/status`, {
      headers: {
        Authorization: `Bearer ${token}`,
        Origin: 'http://phone.local:1420',
      },
    })
    expect(ok.status).toBe(200)
    expect(ok.headers.get('Access-Control-Allow-Origin')).toBe('http://phone.local:1420')
    expect(ok.headers.get('Vary')).toContain('Origin')

    const bad = await fetch(`http://127.0.0.1:${port}/api/status`, {
      headers: { Authorization: 'Bearer bad-token', Origin: 'http://phone.local:1420' },
    })
    expect(bad.status).toBe(401)
  })
})
  • Step 2: Run tests and verify they fail

Run: bun test src/server/__tests__/h5-access-auth.test.ts --timeout 30000

Expected: FAIL because auth/CORS do not consult H5 settings yet.

  • Step 3: Extend auth middleware

Change auth.ts to expose an async helper:

export async function requireAuth(req: Request): Promise<Response | null> {
  const existing = validateAuth(req)
  if (existing.valid) return null

  const token = extractBearerToken(req)
  if (await new H5AccessService().validateToken(token)) return null

  return Response.json(
    { error: 'Unauthorized', message: existing.error || 'Invalid authorization token' },
    { status: 401 },
  )
}

Keep validateAuth() available for existing tests. Add extractBearerToken(req) as a small exported helper for WebSocket query-token tests if useful.

  • Step 4: Make CORS origin-aware

Change corsHeaders(origin) to async corsHeaders(origin) or add corsHeadersForOrigin(origin, h5Service) and update callers in index.ts.

Rules:

  • Always allow localhost, 127.0.0.1, tauri.localhost, tauri://localhost, and asset://localhost.

  • If H5 is enabled, allow exact origins in h5Access.allowedOrigins.

  • Never wildcard.

  • Always set Vary: Origin.

  • Step 5: Update src/server/index.ts

Update all call sites that currently use corsHeaders(origin) and requireAuth(req):

  • OPTIONS
  • /ws/*
  • /api/*
  • /proxy/*
  • /health
  • error responses in API/proxy catch blocks

For WebSocket auth, accept either Authorization header or ?token= by constructing a request-like check or by adding validateRequestToken(req, url.searchParams.get('token')).

  • Step 6: Run focused auth/server tests

Run:

bun test src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/settings.test.ts src/server/__tests__/e2e/full-flow.test.ts --timeout 30000

Expected: PASS.

  • Step 7: Commit Task 2
git add src/server/middleware/auth.ts src/server/middleware/cors.ts src/server/index.ts src/server/__tests__/h5-access-auth.test.ts
git commit -m "Protect remote H5 access with token auth"

Task 3: Desktop H5 Settings UI

Files:

  • Create: desktop/src/api/h5Access.ts

  • Modify: desktop/src/types/settings.ts

  • Modify: desktop/src/stores/settingsStore.ts

  • Modify: desktop/src/pages/Settings.tsx

  • Modify: desktop/src/i18n/locales/en.ts

  • Modify: desktop/src/i18n/locales/zh.ts

  • Test: desktop/src/__tests__/generalSettings.test.tsx

  • Step 1: Write UI/store tests first

Extend generalSettings.test.tsx with:

  • H5 section renders disabled state.
  • Enabling calls enableH5Access.
  • Regenerating token calls regenerateH5AccessToken.
  • Updating allowed origin/public URL calls updateH5AccessSettings.

Use the existing useSettingsStore.setState() pattern in that file.

  • Step 2: Add typed API client

Create desktop/src/api/h5Access.ts:

import { api } from './client'

export type H5AccessSettings = {
  enabled: boolean
  tokenPreview: string | null
  allowedOrigins: string[]
  publicBaseUrl: string | null
}

export type H5AccessStatus = { settings: H5AccessSettings }
export type H5AccessTokenResult = { settings: H5AccessSettings; token: string }

export const h5AccessApi = {
  get() {
    return api.get<H5AccessStatus>('/api/h5-access')
  },
  enable() {
    return api.post<H5AccessTokenResult>('/api/h5-access/enable')
  },
  disable() {
    return api.post<H5AccessStatus>('/api/h5-access/disable')
  },
  regenerate() {
    return api.post<H5AccessTokenResult>('/api/h5-access/regenerate')
  },
  update(input: { allowedOrigins?: string[]; publicBaseUrl?: string | null }) {
    return api.put<H5AccessStatus>('/api/h5-access', input)
  },
}
  • Step 3: Extend settings store

Add state and actions:

h5Access: H5AccessSettings
h5AccessGeneratedToken: string | null
fetchH5Access: () => Promise<void>
enableH5Access: () => Promise<void>
disableH5Access: () => Promise<void>
regenerateH5AccessToken: () => Promise<void>
updateH5AccessSettings: (input: { allowedOrigins?: string[]; publicBaseUrl?: string | null }) => Promise<void>
clearH5AccessGeneratedToken: () => void

Call fetchH5Access() inside fetchAll() alongside existing settings requests.

  • Step 4: Add Settings UI section

In GeneralSettings, add a section after System Notifications and before WebFetch:

  • checkbox Enable H5 access
  • token preview row
  • generated token one-time visible box with copy button
  • regenerate button
  • public URL input
  • allowed origins textarea or comma-separated input
  • copied URL display
  • compact safety note

Keep styling consistent with existing rounded settings panels. Do not create nested cards.

  • Step 5: Add i18n strings

Add matching English and Chinese keys under settings.general.h5....

  • Step 6: Run focused desktop tests

Run:

cd desktop && bunx vitest run src/__tests__/generalSettings.test.tsx src/stores/settingsStore.test.ts

Expected: PASS.

  • Step 7: Commit Task 3
git add desktop/src/api/h5Access.ts desktop/src/types/settings.ts desktop/src/stores/settingsStore.ts desktop/src/pages/Settings.tsx desktop/src/i18n/locales/en.ts desktop/src/i18n/locales/zh.ts desktop/src/__tests__/generalSettings.test.tsx
git commit -m "Add desktop controls for H5 access"

Task 4: Browser H5 Runtime And Auth Injection

Files:

  • Modify: desktop/src/api/client.ts

  • Modify: desktop/src/api/websocket.ts

  • Modify: desktop/src/lib/desktopRuntime.ts

  • Create: desktop/src/components/layout/H5ConnectionView.tsx

  • Modify: desktop/src/components/layout/AppShell.tsx

  • Test: desktop/src/api/client.test.ts

  • Test: desktop/src/api/websocket.test.ts

  • Test: desktop/src/components/layout/AppShell.test.tsx

  • Step 1: Write API client tests

Add tests asserting:

  • default local requests do not include Authorization.

  • setAuthToken('h5_x') causes REST requests to include Authorization: Bearer h5_x.

  • diagnostics reporting does not include token in payload.

  • Step 2: Implement auth helpers in client.ts

Add:

let authToken: string | null = null

export function setAuthToken(token: string | null) {
  authToken = token && token.trim() ? token.trim() : null
}

export function getAuthToken() {
  return authToken
}

In request(), include Authorization only when authToken is set.

  • Step 3: Write WebSocket URL tests

In websocket.test.ts, mock getBaseUrl() and getAuthToken() for:

  • http://127.0.0.1:3456 -> ws://127.0.0.1:3456/ws/session

  • https://example.com/backend -> wss://example.com/backend/ws/session

  • token adds ?token=<encoded>

  • Step 4: Implement WebSocket URL builder

Add a small exported helper in websocket.ts:

export function buildSessionWebSocketUrl(sessionId: string) {
  const base = new URL(getBaseUrl())
  base.protocol = base.protocol === 'https:' ? 'wss:' : 'ws:'
  base.pathname = `${base.pathname.replace(/\/$/, '')}/ws/${sessionId}`
  const token = getAuthToken()
  if (token) base.searchParams.set('token', token)
  return base.toString()
}

Use it in connect().

  • Step 5: Add H5 connection runtime

In desktopRuntime.ts, add localStorage keys:

const H5_SERVER_URL_KEY = 'cc-haha-h5-server-url'
const H5_TOKEN_KEY = 'cc-haha-h5-token'

Add functions:

export function isBrowserH5Runtime() {
  return !isTauriRuntime()
}

export function readStoredH5Connection() {
  return { serverUrl: localStorage.getItem(H5_SERVER_URL_KEY), token: localStorage.getItem(H5_TOKEN_KEY) }
}

export async function saveAndVerifyH5Connection(serverUrl: string, token: string) {
  setBaseUrl(serverUrl)
  setAuthToken(token)
  await api.post('/api/h5-access/verify')
  localStorage.setItem(H5_SERVER_URL_KEY, serverUrl)
  localStorage.setItem(H5_TOKEN_KEY, token)
}

Keep Tauri startup path untouched.

  • Step 6: Add H5ConnectionView

The component accepts initialServerUrl, initialError, and onConnected. It renders server URL and token inputs, calls saveAndVerifyH5Connection, and displays concise errors.

  • Step 7: Wire AppShell

In browser mode, if startup fails with 401 or no stored token, render H5ConnectionView. On successful connect, retry initializeDesktopServerUrl() and fetchSettings().

Do not render this connection view in Tauri mode.

  • Step 8: Run focused desktop runtime tests

Run:

cd desktop && bunx vitest run src/api/client.test.ts src/api/websocket.test.ts src/components/layout/AppShell.test.tsx

Expected: PASS.

  • Step 9: Commit Task 4
git add desktop/src/api/client.ts desktop/src/api/websocket.ts desktop/src/lib/desktopRuntime.ts desktop/src/components/layout/H5ConnectionView.tsx desktop/src/components/layout/AppShell.tsx desktop/src/api/client.test.ts desktop/src/api/websocket.test.ts desktop/src/components/layout/AppShell.test.tsx
git commit -m "Add authenticated H5 browser runtime"

Task 5: Mobile Chat Shell

Files:

  • Create: desktop/src/hooks/useMobileViewport.ts

  • Modify: desktop/src/theme/globals.css

  • Modify: desktop/src/components/layout/AppShell.tsx

  • Modify: desktop/src/components/layout/Sidebar.tsx

  • Modify: desktop/src/pages/ActiveSession.tsx

  • Modify: desktop/src/components/chat/ChatInput.tsx

  • Modify: desktop/src/components/controls/PermissionModeSelector.tsx

  • Test: desktop/src/components/layout/AppShell.test.tsx

  • Test: desktop/src/pages/ActiveSession.test.tsx

  • Test: desktop/src/components/chat/ChatInput.test.tsx

  • Step 1: Add viewport hook test and implementation

Create useMobileViewport.ts with matchMedia('(max-width: 767px)'), SSR-safe default false, and cleanup of listeners.

  • Step 2: Update shell CSS

Add classes in globals.css:

.app-shell {
  height: 100vh;
  height: 100dvh;
}

@media (max-width: 767px) {
  .app-shell {
    min-width: 0;
  }

  .sidebar-shell {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 60;
    width: min(86vw, 320px);
    transform: translateX(-100%);
  }

  .sidebar-shell[data-mobile-state="open"] {
    transform: translateX(0);
  }

  .mobile-touch-target {
    min-height: 44px;
    min-width: 44px;
  }
}

Preserve existing desktop sidebar behavior outside the media query.

  • Step 3: Update AppShell mobile drawer

Use useMobileViewport() to:

  • apply className="app-shell flex overflow-hidden ..."

  • add data-mobile-state

  • add a backdrop button when sidebar is open on mobile

  • keep desktop sidebarOpen behavior unchanged

  • Step 4: Relax mobile chat min-widths

In ActiveSession.tsx, replace unconditional min-w-[320px] and min-w-[360px] with responsive classes:

  • desktop keeps current min widths

  • mobile uses min-w-0 w-full

  • workspace and terminal resize handles do not render on mobile

  • Step 5: Make composer touch-safe

In ChatInput.tsx, on mobile:

  • plus button uses mobile-touch-target

  • send/stop compact button uses h-11 w-11

  • slash and plus menus use left-0 right-0 w-auto max-w-[calc(100vw-24px)]

  • keyboard shortcut footer in slash menu can hide on mobile

  • Step 6: Make permission selector mobile-safe

In PermissionModeSelector.tsx, on mobile:

  • dropdown uses fixed bottom sheet or full-width anchored sheet.

  • bypass confirm overlay removes pl-[var(--sidebar-width)].

  • dialog width becomes w-[calc(100vw-24px)] max-w-[420px].

  • Step 7: Run focused mobile tests

Run:

cd desktop && bunx vitest run src/components/layout/AppShell.test.tsx src/pages/ActiveSession.test.tsx src/components/chat/ChatInput.test.tsx

Expected: PASS.

  • Step 8: Commit Task 5
git add desktop/src/hooks/useMobileViewport.ts desktop/src/theme/globals.css desktop/src/components/layout/AppShell.tsx desktop/src/components/layout/Sidebar.tsx desktop/src/pages/ActiveSession.tsx desktop/src/components/chat/ChatInput.tsx desktop/src/components/controls/PermissionModeSelector.tsx desktop/src/components/layout/AppShell.test.tsx desktop/src/pages/ActiveSession.test.tsx desktop/src/components/chat/ChatInput.test.tsx
git commit -m "Adapt chat shell for mobile H5 access"

Task 6: H5 Documentation And Verification

Files:

  • Create: docs/desktop/h5-access.md

  • Modify: docs/.vitepress/config.mts if desktop docs sidebar needs the new page.

  • Test/verify: docs build if sidebar changes.

  • Step 1: Write user-facing setup doc

Create docs/desktop/h5-access.md with:

  • enable H5 in Settings

  • copy token

  • configure allowed origin

  • expose backend with LAN/reverse proxy

  • open H5 URL on phone

  • reset token if leaked

  • TLS recommendation for domains

  • Step 2: Add docs navigation only if desktop docs already list peer pages

Inspect docs/.vitepress/config.mts. If docs/desktop is sidebar-linked, add H5 page next to related desktop pages. If there is no desktop sidebar section, leave the page unlinked for now and mention the path in final handoff.

  • Step 3: Run docs check if navigation changed

Run: bun run check:docs

Expected: PASS. If it fails due pre-existing npm lock/dependency drift, record the blocker and do not modify lockfiles unless the failure is caused by this docs change.

  • Step 4: Run full targeted gates

Run:

bun run check:server
bun run check:desktop

Then run:

bun run verify

Expected: PASS or explicit pre-existing blocker from the quality report.

  • Step 5: Browser/mobile smoke

Start backend and frontend on isolated ports:

SERVER_PORT=3456 bun run src/server/index.ts --host 127.0.0.1
cd desktop && VITE_DESKTOP_SERVER_URL=http://127.0.0.1:3456 bun run dev -- --host 127.0.0.1 --port 1422

Use browser automation at 390x844 and 430x932 to verify:

  • H5 connection view renders in browser mode without stored token.

  • token entry reaches sessions list after server is configured.

  • session list and chat column do not overflow viewport.

  • send/stop controls are visible and touch-sized.

  • Step 6: Commit Task 6

git add docs/desktop/h5-access.md docs/.vitepress/config.mts
git commit -m "Document H5 access setup"

Only include docs/.vitepress/config.mts if it changed.

Execution Strategy

Use subagent-driven execution with sequencing to avoid shared-file conflicts:

  1. Run Task 1 locally or with one executor. It defines server service/API contracts.
  2. After Task 1 lands, dispatch one executor for Task 2.
  3. In parallel after Task 1, dispatch one executor for Task 3 and one for Task 4 only if their write sets are kept separate; coordinate shared settingsStore and AppShell carefully.
  4. Run Task 5 after Task 4 because both touch AppShell.
  5. Run Task 6 last.

Do not let multiple workers edit desktop/src/components/layout/AppShell.tsx at the same time. If parallel execution is used, Task 4 owns AppShell first; Task 5 rebases on that result.

Plan Self-Review

  • Spec coverage: H5 settings, token generation, CORS/auth, browser runtime, mobile shell, error handling, docs, and verification are covered by Tasks 1-6.
  • Scope: This remains one implementation plan because each task is independently testable and all tasks serve the single opt-in H5 access feature.
  • Ambiguity resolved: H5 config is stored in ~/.claude/cc-haha/settings.json, not the shared ~/.claude/settings.json.
  • Desktop safety: Default localhost/Tauri behavior is explicitly preserved and covered by focused desktop tests.