cc-haha/desktop/src/stores/browserPanelStore.ts
程序员阿江(Relakkes) 8e5e86353a fix(desktop): simplify workbench open affordances
Previewable changed-file rows no longer show a redundant workspace chevron when the row already has an Open With control. The unified Workbench now owns the Browser entry point, and opening Browser mode creates a blank address bar instead of navigating to a hard-coded localhost port.

Constraint: Keep explicit preview links and localhost output cards able to open the browser directly with their target URL.
Rejected: Keep the top toolbar browser button | it duplicated the Workbench Browser tab and silently opened localhost:5173.
Rejected: Use localhost:5173 as the blank browser default | it makes arbitrary Browser mode look tied to a dev server.
Confidence: high
Scope-risk: moderate
Tested: cd desktop && /Users/nanmi/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node ./node_modules/typescript/bin/tsc --noEmit
Tested: cd desktop && /Users/nanmi/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node ./node_modules/vitest/vitest.mjs run src/components/chat/CurrentTurnChangeCard.test.tsx src/stores/browserPanelStore.test.ts src/components/browser/BrowserAddressBar.test.tsx src/components/workbench/WorkbenchPanel.test.tsx src/components/browser/BrowserSurface.test.tsx src/components/layout/TabBar.test.tsx --pool=forks --poolOptions.forks.singleFork=true
Tested: git diff --check -- desktop/src/components/chat/CurrentTurnChangeCard.tsx desktop/src/components/chat/CurrentTurnChangeCard.test.tsx desktop/src/components/browser/BrowserAddressBar.tsx desktop/src/components/browser/BrowserAddressBar.test.tsx desktop/src/components/browser/BrowserSurface.tsx desktop/src/components/browser/BrowserSurface.test.tsx desktop/src/components/layout/TabBar.tsx desktop/src/components/layout/TabBar.test.tsx desktop/src/components/workbench/WorkbenchPanel.tsx desktop/src/components/workbench/WorkbenchPanel.test.tsx desktop/src/stores/browserPanelStore.ts desktop/src/stores/browserPanelStore.test.ts
Not-tested: Full desktop check:desktop gate.
2026-05-31 23:18:20 +08:00

103 lines
4.0 KiB
TypeScript

import { create } from 'zustand'
import { useWorkspacePanelStore } from './workspacePanelStore'
export type BrowserSessionState = {
isOpen: boolean
url: string
title: string
history: string[]
historyIndex: number
loading: boolean
pickerActive: boolean
canGoBack: boolean
canGoForward: boolean
}
type BrowserPanelState = {
bySession: Record<string, BrowserSessionState>
open: (sessionId: string, url: string) => void
ensureBlank: (sessionId: string) => void
navigate: (sessionId: string, url: string) => void
goBack: (sessionId: string) => void
goForward: (sessionId: string) => void
setLoading: (sessionId: string, loading: boolean) => void
setPicker: (sessionId: string, active: boolean) => void
close: (sessionId: string) => void
setNavigated: (sessionId: string, url: string, title: string) => void
setReady: (sessionId: string) => void
}
const empty = (url = ''): BrowserSessionState => ({
isOpen: true,
url,
title: '',
history: url ? [url] : [],
historyIndex: url ? 0 : -1,
loading: false,
pickerActive: false,
canGoBack: false,
canGoForward: false,
})
const withNav = (s: BrowserSessionState): BrowserSessionState => ({
...s,
url: s.history[s.historyIndex] ?? s.url,
canGoBack: s.historyIndex > 0,
canGoForward: s.historyIndex < s.history.length - 1,
})
export const useBrowserPanelStore = create<BrowserPanelState>((set) => ({
bySession: {},
open: (sessionId, url) => {
set((st) => ({
bySession: { ...st.bySession, [sessionId]: { ...empty(url), loading: true } },
}))
// The browser is one mode of the unified workbench: opening a previewable
// link / localhost url surfaces the workbench in BROWSER mode. Import is
// one-directional (workspacePanelStore never imports this store), so no cycle.
const workspacePanel = useWorkspacePanelStore.getState()
workspacePanel.openPanel(sessionId)
workspacePanel.setMode(sessionId, 'browser')
},
ensureBlank: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]
if (cur) {
return { bySession: { ...st.bySession, [sessionId]: { ...cur, isOpen: true } } }
}
return { bySession: { ...st.bySession, [sessionId]: empty() } }
}),
navigate: (sessionId, url) => set((st) => {
const cur = st.bySession[sessionId] ?? empty(url)
const history = [...cur.history.slice(0, Math.max(0, cur.historyIndex + 1)), url]
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, isOpen: true, loading: true, history, historyIndex: history.length - 1 }) } }
}),
goBack: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur || cur.historyIndex <= 0) return st
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, historyIndex: cur.historyIndex - 1 }) } }
}),
goForward: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur || cur.historyIndex >= cur.history.length - 1) return st
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, historyIndex: cur.historyIndex + 1 }) } }
}),
setLoading: (sessionId, loading) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, loading } } }
}),
setPicker: (sessionId, active) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, pickerActive: active } } }
}),
close: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, isOpen: false, pickerActive: false } } }
}),
setNavigated: (sessionId, url, title) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, url, title, loading: false } } }
}),
setReady: (sessionId) => set((st) => {
const cur = st.bySession[sessionId]; if (!cur) return st
return { bySession: { ...st.bySession, [sessionId]: { ...cur, loading: false } } }
}),
}))