Skip to content
Merged
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
12 changes: 12 additions & 0 deletions packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type { ProjectAvatarVariant }
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
const DEFAULT_SIDEBAR_WIDTH = 344
const DEFAULT_FILE_TREE_WIDTH = 200
const DEFAULT_PANEL_COLUMN_WIDTH = 400
const DEFAULT_SESSION_WIDTH = 600
const DEFAULT_TERMINAL_HEIGHT = 280
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
Expand Down Expand Up @@ -269,6 +270,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
width: DEFAULT_FILE_TREE_WIDTH,
tab: "changes" as "changes" | "all",
},
panelColumn: {
width: DEFAULT_PANEL_COLUMN_WIDTH,
},
session: {
width: DEFAULT_SESSION_WIDTH,
},
Expand Down Expand Up @@ -642,6 +646,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("review", "diffStyle", diffStyle)
},
},
// the session side panel's tabs column (vault + file tabs) — its OWN
// width, so the panel never flex-fills the window (Kate 2026-07-27)
panelColumn: {
width: createMemo(() => store.panelColumn?.width ?? DEFAULT_PANEL_COLUMN_WIDTH),
resize(width: number) {
setStore("panelColumn", { width })
},
},
fileTree: {
opened: createMemo(() => store.fileTree?.opened ?? true),
width: createMemo(() => store.fileTree?.width ?? DEFAULT_FILE_TREE_WIDTH),
Expand Down
19 changes: 1 addition & 18 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ export default function Page() {
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
const sessionPanelWidth = createMemo(() => {
if (!desktopSidePanelOpen()) return "100%"
if (desktopReviewOpen()) return `${layout.session.width()}px`
if (desktopReviewOpen()) return `calc(100% - ${layout.panelColumn.width()}px)`
return `calc(100% - ${layout.fileTree.width()}px)`
})
const centered = createMemo(() => isDesktop() && !desktopReviewOpen())
Expand Down Expand Up @@ -1838,23 +1838,6 @@ export default function Page() {
<Show when={params.id || !newSessionDesign()}>{composerRegion("dock")}</Show>
</div>

<Show when={desktopReviewOpen()}>
<div onPointerDown={() => size.start()}>
<ResizeHandle
classList={{
"-right-1": settings.general.newLayoutDesigns(),
}}
direction="horizontal"
size={layout.session.width()}
min={450}
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
onResize={(width) => {
size.touch()
layout.session.resize(width)
}}
/>
</div>
</Show>
</div>

<SessionSidePanel size={size} />
Expand Down
75 changes: 60 additions & 15 deletions packages/app/src/pages/session/session-side-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js"
import { For, Match, Show, Switch, createEffect, createMemo, on, onCleanup, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { createMediaQuery } from "@solid-primitives/media"
import { Tabs } from "@opencode-ai/ui/tabs"
Expand Down Expand Up @@ -63,9 +63,11 @@ export function SessionSidePanel(props: { size: Sizing }) {
}),
)
const open = createMemo(() => tabsOpen() || fileOpen())
// the tabs column owns its width (never flex-fills the window) and can be
// dragged much narrower — Kate 2026-07-27
const panelWidth = createMemo(() => {
if (!open()) return "0px"
if (tabsOpen()) return "auto"
if (tabsOpen()) return `${layout.panelColumn.width()}px`
return `${layout.fileTree.width()}px`
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
Expand Down Expand Up @@ -107,16 +109,38 @@ export function SessionSidePanel(props: { size: Sizing }) {
// command, and context-tree deep-links open the store; inside a session THIS
// is the host, so mirror store state into a "vault" tab (and back on close)
const vaultOpen = createMemo(() => vaultPanel.opened())
createEffect(() => {
if (!isDesktop()) return
if (vaultPanel.opened()) {
if (!view().reviewPanel.opened()) view().reviewPanel.open()
tabs().open("vault")
tabs().setActive("vault")
} else {
tabs().close("vault")
}
})
// on() scopes tracking to the STORE signal alone — the tab reads/writes in
// the callback are untracked. Tracking them looped: tabs().open() writes
// the same store the effect would re-read, and Solid spins the effect
// until the stack blows (found via Playwright pageerror stack).
createEffect(
on(
() => vaultPanel.opened(),
(openNow) => {
if (!isDesktop()) return
if (openNow) {
if (!view().reviewPanel.opened()) view().reviewPanel.open()
if (!tabs().all().includes("vault")) tabs().open("vault")
if (tabs().active() !== "vault") tabs().setActive("vault")
} else if (tabs().all().includes("vault")) {
tabs().close("vault")
}
},
),
)
// column closed (panel toggle) → the store must follow, or the titlebar
// button's next press toggles an invisible state and "does nothing"
createEffect(
on(
tabsOpen,
(openNow, wasOpen) => {
if (wasOpen && !openNow && vaultPanel.opened()) vaultPanel.close()
// a column with nothing to show fills with the vault by default
if (!wasOpen && openNow && tabState.activeTab() === "empty") vaultPanel.open()
},
{ defer: true },
),
)

const tabState = createSessionTabs({
tabs,
Expand Down Expand Up @@ -187,10 +211,24 @@ export function SessionSidePanel(props: { size: Sizing }) {
"transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
"rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": settings.general.newLayoutDesigns(),
"flex-1": tabsOpen(),
}}
style={{ width: panelWidth() }}
>
<Show when={tabsOpen()}>
<div onPointerDown={() => props.size.start()}>
<ResizeHandle
direction="horizontal"
edge="start"
size={layout.panelColumn.width()}
min={240}
max={typeof window === "undefined" ? 900 : window.innerWidth * 0.6}
onResize={(width) => {
props.size.touch()
layout.panelColumn.resize(width)
}}
/>
</div>
</Show>
<Show when={open()}>
<div
class="size-full flex"
Expand Down Expand Up @@ -231,12 +269,19 @@ export function SessionSidePanel(props: { size: Sizing }) {
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => vaultPanel.close()}
onClick={() => {
vaultPanel.close()
// nothing else to show → the column goes too
if (openedTabs().length === 0 && !contextOpen()) view().reviewPanel.close()
}}
aria-label={language.t("amicode.vault.close")}
/>
}
hideCloseButton
onMiddleClick={() => vaultPanel.close()}
onMiddleClick={() => {
vaultPanel.close()
if (openedTabs().length === 0 && !contextOpen()) view().reviewPanel.close()
}}
>
<div>{language.t("amicode.vault.title")}</div>
</Tabs.Trigger>
Expand Down
21 changes: 21 additions & 0 deletions packages/ui/src/amicode/entity-rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ interface RailPart {

const RUN_POLL_MS = 2500

// Pending chips are placeholders, not buttons — say WHY they aren't clickable
// (Kate 2026-07-27: an unexplained dead chip reads as a bug).
function pendingHint(kind: string): string {
switch (kind) {
case "pulse":
return "No pulse banked yet — a completed run produces one"
case "device_session":
return "No device session yet — appears when a pulse targets hardware"
case "run":
return "No run yet — solve the formulation to create one"
case "formulation":
return "No formulation yet — amico writes one from the problem"
case "system":
return "No system picked yet"
default:
return "Not created yet"
}
}

// One line-icon per entity kind (glyphs live in the shared family — icon.tsx).
function chipIcon(kind: string) {
switch (kind) {
Expand Down Expand Up @@ -235,6 +254,8 @@ export function AmicodeEntityRail(props: {
data-slot="amicode-rail-chip"
data-stage={chip.kind}
data-pending="true"
title={pendingHint(chip.kind)}
aria-label={`${chip.label} — ${pendingHint(chip.kind)}`}
>
<Icon name={chipIcon(chip.kind)} size="small" />
{chip.label}
Expand Down
Loading