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
10 changes: 5 additions & 5 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,13 @@ declare global {
__OPENCODE__?: {
deepLinks?: string[]
}
api?: {
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
exportDebugLogs?: () => Promise<string>
}
}
}

type TitlebarAPI = {
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
}

function QueryProvider(props: ParentProps) {
const client = new QueryClient({
defaultOptions: {
Expand Down Expand Up @@ -266,7 +266,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
<Font />
<ThemeProvider
onThemeApplied={(_, mode) => {
void window.api?.setTitlebar?.({ mode })
void (window as Window & { api?: TitlebarAPI }).api?.setTitlebar?.({ mode })
}}
>
<LanguageProvider locale={props.locale}>
Expand Down
27 changes: 27 additions & 0 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const tabs = useTabs()
const tabsStore = tabs.store
const tabsStoreActions = tabs
const sessionTabCount = () =>
tabsStore.filter((tab) => tab.type === "session" && tab.server === server.key).length
const panelToggleLabel = () =>
tabs.panels.tiled() ? language.t("command.tabs.viewAsTabs") : language.t("command.tabs.viewAsPanels")
const navigateTab = (tab: Tab) => {
const href = tabHref(tab)
if (tab.server === server.key) {
Expand Down Expand Up @@ -337,6 +341,14 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const current = currentTab()

return [
{
id: "tabs.panel.toggle",
category: language.t("command.category.view"),
title: panelToggleLabel(),
disabled: sessionTabCount() < 2,
hidden: true,
onSelect: () => tabs.panels.toggle(),
},
{
id: "tab.new",
category: "tab",
Expand Down Expand Up @@ -551,6 +563,21 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
aria-label={language.t("command.session.new")}
/>
</Show>
<Show when={sessionTabCount() > 1}>
<Tooltip placement="bottom" value={panelToggleLabel()}>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="shrink-0"
state={tabs.panels.tiled() ? "pressed" : undefined}
onClick={() => tabs.panels.toggle()}
aria-label={panelToggleLabel()}
aria-pressed={tabs.panels.tiled()}
icon={<IconV2 name={tabs.panels.tiled() ? "status-active" : "status"} />}
/>
</Tooltip>
</Show>
<div class="flex-1" />
<TitlebarV2Right state={v2RightState()} />
<Show when={windows() && !electronWindows()}>
Expand Down
26 changes: 26 additions & 0 deletions packages/app/src/context/tabs-order.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, test } from "bun:test"
import { swapTabOrder, type TabOrderItem } from "./tabs-order"

const sessionTab = (sessionId: string): TabOrderItem => ({
type: "session",
server: "sidecar",
dirBase64: "L3JlcG8=",
sessionId,
})

describe("swapTabOrder", () => {
test("swaps matching tabs by tab identity", () => {
const first = sessionTab("first")
const second = sessionTab("second")
const third = sessionTab("third")

expect(swapTabOrder([first, second, third], first, third)).toEqual([third, second, first])
})

test("keeps order when either tab is missing", () => {
const first = sessionTab("first")
const second = sessionTab("second")

expect(swapTabOrder([first], first, second)).toEqual([first])
})
})
29 changes: 29 additions & 0 deletions packages/app/src/context/tabs-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type TabOrderItem =
| {
type: "session"
server: string
dirBase64: string
sessionId: string
}
| {
type: "draft"
draftID: string
}

export const tabOrderKey = (tab: TabOrderItem) =>
tab.type === "draft" ? `draft:${tab.draftID}` : `${tab.server}\n/${tab.dirBase64}/session/${tab.sessionId}`

export function swapTabOrder<T extends TabOrderItem>(tabs: readonly T[], first: TabOrderItem, second: TabOrderItem) {
const firstKey = tabOrderKey(first)
const secondKey = tabOrderKey(second)
if (firstKey === secondKey) return tabs.slice()

const firstIndex = tabs.findIndex((tab) => tabOrderKey(tab) === firstKey)
const secondIndex = tabs.findIndex((tab) => tabOrderKey(tab) === secondKey)
if (firstIndex === -1 || secondIndex === -1) return tabs.slice()

const next = tabs.slice()
next[firstIndex] = tabs[secondIndex]!
next[secondIndex] = tabs[firstIndex]!
return next
}
19 changes: 17 additions & 2 deletions packages/app/src/context/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { usePlatform } from "./platform"
import { uuid } from "@/utils/uuid"
import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
import { swapTabOrder, tabOrderKey } from "./tabs-order"

export type SessionTab = {
type: "session"
Expand All @@ -32,7 +33,7 @@ export const draftHref = (draftID: string) => `/new-session?draftId=${encodeURIC
export const tabHref = (tab: Tab) =>
tab.type === "draft" ? draftHref(tab.draftID) : `/${tab.dirBase64}/session/${tab.sessionId}`

export const tabKey = (tab: Tab) => (tab.type === "draft" ? `draft:${tab.draftID}` : `${tab.server}\n${tabHref(tab)}`)
export const tabKey = tabOrderKey

export function sessionHasOpenTab(tabs: Tab[], server: ServerConnection.Key, session: Session) {
const dirBase64 = base64Encode(session.directory)
Expand Down Expand Up @@ -62,6 +63,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
createStore<Tab[]>([]),
)
const [panelStore, setPanelStore] = persisted(Persist.global("tabs.panels"), createStore({ tiled: false }))

const params = useParams()
const navigate = useNavigate()
Expand Down Expand Up @@ -158,6 +160,9 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}).finally(() => closing.delete(key))
if (draftID) removeDraftPersisted(draftID)
},
swapTabs: (first: Tab, second: Tab) => {
setStore((tabs) => swapTabOrder(tabs, first, second))
},
removeServer(key: ServerConnection.Key) {
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
Expand Down Expand Up @@ -211,6 +216,16 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
}

return { ...actions, store, ready }
const panels = {
tiled: () => panelStore.tiled,
setTiled(value: boolean) {
setPanelStore("tiled", value)
},
toggle() {
setPanelStore("tiled", (value) => !value)
},
}

return { ...actions, panels, store, ready }
},
})
18 changes: 17 additions & 1 deletion packages/app/src/custom-elements.d.ts
2 changes: 2 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export const dict = {
"command.session.new": "New session",
"command.file.open": "Open file",
"command.tab.close": "Close tab",
"command.tabs.viewAsPanels": "View tabs as panels",
"command.tabs.viewAsTabs": "View as tabs",
"command.context.addSelection": "Add selection to context",
"command.context.addSelection.description": "Add selected lines from the current file",
"command.input.focus": "Focus input",
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,21 @@
width: 100%;
}

[data-session-panel-grid] [data-session-panel],
[data-session-panel-grid] [data-session-panel] .scroll-view__viewport,
[data-session-panel-grid] [data-session-panel] [data-slot="session-turn-message-container"],
[data-session-panel-grid] [data-session-panel] [data-slot="session-turn-message-content"],
[data-session-panel-grid] [data-session-panel] [data-slot="session-turn-assistant-content"] {
min-width: 0;
max-width: 100%;
overflow-x: hidden;
}

[data-session-panel-grid] [data-session-panel] [data-slot="session-turn-message-content"],
[data-session-panel-grid] [data-session-panel] [data-slot="session-turn-assistant-content"] {
overflow-wrap: anywhere;
}

@container getting-started (min-width: 17rem) {
[data-component="getting-started-actions"] {
flex-direction: row;
Expand Down
Loading
Loading