Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/tui/src/context/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
})

return {
// Freeze startup identity even if managed-service reconnect changes the transport URL.
server: serverIdentity(props.url ?? "http://localhost", service !== undefined),
get api() {
return api
},
Expand All @@ -59,3 +61,14 @@ export const { use: useClient, provider: ClientProvider } = createSimpleContext(
}
},
})

export function serverIdentity(url: string, managed = false) {
if (managed) return "local"
const value = new URL(url)
value.username = ""
value.password = ""
value.search = ""
value.hash = ""
value.pathname = value.pathname.replace(/\/+$/, "") || "/"
return value.toString()
}
24 changes: 15 additions & 9 deletions packages/tui/src/context/session-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ type TabsState = {
}

type PersistedState = {
global: TabsState
cwd: Record<string, TabsState>
servers?: Record<string, { global: TabsState; cwd: Record<string, TabsState> }>
// The managed local server continues using the legacy fields.
global?: TabsState
cwd?: Record<string, TabsState>
}

type ScrollAnchor = {
Expand Down Expand Up @@ -68,10 +70,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
// Keyed reconcile keeps tab object identity across reorders, so strip rows move instead of
// mutating in place, which per-row animations and drag state depend on.
const [store, updateStore] = storage.store<PersistedState>("tabs", {
initial: {
global: empty(),
cwd: {},
},
initial: { servers: {} },
key: "sessionID",
})
const fallback = empty()
Expand Down Expand Up @@ -99,13 +98,20 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
})

function state() {
if (config.tabs.scope === "cwd") return store.cwd[paths.cwd] ?? fallback
return store.global
const server = client.server === "local" ? store : store.servers?.[client.server]
if (!server) return fallback
if (config.tabs.scope === "cwd") return server.cwd?.[paths.cwd] ?? fallback
return server.global ?? fallback
}

function update(mutation: (draft: TabsState) => void) {
const scope = config.tabs.scope
void updateStore((draft) => mutation(scope === "cwd" ? (draft.cwd[paths.cwd] ??= empty()) : draft.global)).catch(
void updateStore((draft) => {
const server =
client.server === "local" ? draft : ((draft.servers ??= {})[client.server] ??= { global: empty(), cwd: {} })
server.cwd ??= {}
mutation(scope === "cwd" ? (server.cwd[paths.cwd] ??= empty()) : (server.global ??= empty()))
}).catch(
// Failed writes lose only tab layout, but silence would hide tabs resetting every launch.
(error) => console.error("Failed to persist session tabs", error),
)
Expand Down
36 changes: 25 additions & 11 deletions packages/tui/src/context/session-terminals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { useEvent } from "./event"
import { useStorage } from "./storage"

type SessionTerminalsState = {
sessions: Record<string, string | null>
servers?: Record<string, { sessions: Record<string, string | null> }>
// The managed local server continues using the legacy fields.
sessions?: Record<string, string | null>
}

export const { use: useSessionTerminals, provider: SessionTerminalsProvider } = createSimpleContext({
Expand All @@ -21,29 +23,41 @@ export const { use: useSessionTerminals, provider: SessionTerminalsProvider } =
const [focus, setFocus] = createSignal<string>()
const storage = useStorage()
const [store, update] = storage.store<SessionTerminalsState>("session-terminal-selection", {
initial: { sessions: {} },
})
const [terminals, updateTerminals] = storage.memory<Record<string, PersistentPtyInfo[]>>("session-terminals", {
initial: {},
initial: { servers: {} },
})
const [terminals, updateTerminals] = storage.memory<Record<string, PersistentPtyInfo[]>>(
`session-terminals:${client.server}`,
{
initial: {},
},
)
const selected = () => (client.server === "local" ? store.sessions : store.servers?.[client.server]?.sessions)

const refresh = async (sessionID: string) => {
if (!terminals[sessionID]) updateTerminals((draft) => (draft[sessionID] = []))
const result = await client.api.experimental.persistentPty.list({ sessionID })
updateTerminals((draft) => (draft[sessionID] = result))
const selected = store.sessions[sessionID]
if (!selected || result.some((terminal) => terminal.id === selected)) return
const current = selected()?.[sessionID]
if (!current || result.some((terminal) => terminal.id === current)) return
await update((draft) => {
if (draft.sessions[sessionID] !== selected) return
draft.sessions[sessionID] = null
const sessions = client.server === "local" ? draft.sessions : draft.servers?.[client.server]?.sessions
if (!sessions || sessions[sessionID] !== current) return
sessions[sessionID] = null
})
}

const selectTerminal = async (sessionID: string, ptyID: string | null) => {
if (ptyID !== null && !terminals[sessionID]?.some((terminal) => terminal.id === ptyID)) return
setFocus(ptyID ?? undefined)
await update((draft) => {
draft.sessions[sessionID] = ptyID
if (client.server === "local") {
draft.sessions ??= {}
draft.sessions[sessionID] = ptyID
return
}
draft.servers ??= {}
const server = (draft.servers[client.server] ??= { sessions: {} })
server.sessions[sessionID] = ptyID
})
}

Expand All @@ -70,7 +84,7 @@ export const { use: useSessionTerminals, provider: SessionTerminalsProvider } =
get(sessionID: string) {
return {
terminals: terminals[sessionID] ?? [],
selectedTerminalID: store.sessions[sessionID] ?? null,
selectedTerminalID: selected()?.[sessionID] ?? null,
}
},
refresh,
Expand Down
Loading
Loading