mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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.
103 lines
4.0 KiB
TypeScript
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 } } }
|
|
}),
|
|
}))
|