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
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, anddesktop/src/pages/Settings.tsx: Settings UI state and controls for H5 enable/token URL management. - Modify
desktop/src/i18n/locales/en.tsanddesktop/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, anddesktop/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.jsonin anh5Accessobject. -
Use
crypto.randomBytes(32).toString('base64url')withh5_prefix for raw tokens. -
Hash with SHA-256 and store
tokenHash, not raw token. -
Preview format: first 7 chars, ellipsis, last 4 chars.
-
Normalize
allowedOriginsto validhttp://orhttps://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-accessPOST /api/h5-access/enablePOST /api/h5-access/disablePOST /api/h5-access/regeneratePUT /api/h5-accessPOST /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, andasset://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 includeAuthorization: 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
AppShellmobile 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
sidebarOpenbehavior 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.mtsif 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:
- Run Task 1 locally or with one executor. It defines server service/API contracts.
- After Task 1 lands, dispatch one executor for Task 2.
- 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
settingsStoreandAppShellcarefully. - Run Task 5 after Task 4 because both touch
AppShell. - 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.