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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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