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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "prism-terminal",
"productName": "Prism Terminal",
"version": "0.28.0",
"version": "0.28.1",
"description": "A tabbed Windows terminal for AI CLIs.",
"main": "./out/main/index.js",
"author": "Max",
Expand Down
13 changes: 12 additions & 1 deletion src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TabStrip } from './components/TabStrip'
import TitleBar, { TitleButtons } from './components/TitleBar'
import { useTitleBarMode } from './lib/titleBarPrefs'
import EmptyState from './components/EmptyState'
import type { SettingsPage } from './components/Settings'
import { Dialog } from './components/Dialog'
import {
addTab,
Expand Down Expand Up @@ -159,6 +160,16 @@ export default function App(): JSX.Element {
const active = tabs.find((t) => t.id === activeId) ?? null
// A placeholder has no shell yet (#106): nothing that needs one may see it.
const activeShell = active && active.kind !== 'settings' && !active.pending ? active : null
// The Settings tab keeps its page while it stays open (#123; owner,
// 2026-10-04: "if i go from settings to another tab then to settings again
// it should be the same tab it was on ... unless i close the settings tab
// and reopen"). The page unmounts behind another tab, so App holds it, and
// closing the tab is what forgets it.
const [settingsPage, setSettingsPage] = useState<SettingsPage>('general')
const settingsOpen = tabs.some((t) => t.kind === 'settings')
// Adjusted while rendering, React's own pattern for state that follows other
// state: no effect, so no frame painted with the stale page.
if (!settingsOpen && settingsPage !== 'general') setSettingsPage('general')
// Tabs still coming back to an agent wear a ring in the strip (#106): the
// placeholders, then the sessions resuming until their agent has drawn.
const resumingNow = useSyncExternalStore(onResumingChange, resumingIds)
Expand Down Expand Up @@ -670,7 +681,7 @@ export default function App(): JSX.Element {
{active?.kind === 'settings' && (
<div className="h-full min-h-0 w-full bg-[var(--p-bg)]">
<Suspense fallback={null}>
<Settings />
<Settings page={settingsPage} onPage={setSettingsPage} />
</Suspense>
</div>
)}
Expand Down
19 changes: 13 additions & 6 deletions src/renderer/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ function AppearanceTab(): JSX.Element {

/* ---------- page shell ---------- */

type TabId = 'general' | 'appearance' | 'dictation'
export type SettingsPage = 'general' | 'appearance' | 'dictation'
type TabId = SettingsPage

const Ico = ({ d }: { d: string }): JSX.Element => (
<svg
Expand Down Expand Up @@ -419,12 +420,18 @@ const UI_FONT = '"Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif'

/**
* The Settings page. It rides the tab strip as a tab of its own, so it FILLS
* whatever App mounts it in rather than fixing itself over the window, and it
* takes no props: there is nothing to close (the tab's X does that) and every
* setting is a store it writes.
* whatever App mounts it in rather than fixing itself over the window. There
* is nothing to close (the tab's X does that) and every setting is a store it
* writes. The page it shows is App's (#123): this component unmounts whenever
* another tab is in front, and coming back must find the page that was left.
*/
export default function Settings(): JSX.Element {
const [tab, setTab] = useState<TabId>('general')
export default function Settings({
page: tab,
onPage: setTab
}: {
page: SettingsPage
onPage: (page: SettingsPage) => void
}): JSX.Element {
const active = TABS.find((t) => t.id === tab) ?? TABS[0]
return (
<div
Expand Down
25 changes: 25 additions & 0 deletions tools/e2e/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,31 @@ const scenarios = {
await closeApp(app)
},

// SETTINGS KEEPS ITS PAGE WHILE ITS TAB STAYS OPEN (#123; owner,
// 2026-10-04): another tab in front and back finds the page that was left;
// closing the Settings tab and opening it again starts on General.
async settingsPage(ok) {
const w = world()
const { app, page } = await launch(w, { args: [w.alpha] })
ok(await until(async () => (await tabLabels(page)).length === 1), 'one tab open')
const current = () =>
page.evaluate(() => document.querySelector('[data-settings-tab][aria-current="page"]')?.getAttribute('data-settings-tab') ?? null)
const settingsTab = page.locator('[data-tab]', { hasText: 'Settings' })
await page.locator('[data-title-settings]').click()
ok(await until(async () => (await current()) === 'general'), 'Settings opens on General')
await page.locator('[data-settings-tab="dictation"]').click()
ok(await until(async () => (await current()) === 'dictation'), 'Dictation picked')
await page.locator('[data-tab]').first().click()
ok(await until(async () => (await current()) === null), 'the shell is in front, Settings unmounted')
await settingsTab.click()
ok(await until(async () => (await current()) === 'dictation'), `back on Settings it is still Dictation (${await current()})`)
await settingsTab.locator('[data-tab-close]').click({ force: true })
ok(await until(async () => (await settingsTab.count()) === 0), 'the Settings tab closed')
await page.locator('[data-title-settings]').click()
ok(await until(async () => (await current()) === 'general'), `opened again it starts on General (${await current()})`)
await closeApp(app)
},

// LAUNCH WITH RESTORED AGENT TABS (#106; spec
// docs/superpowers/specs/2026-09-30-launch-skeleton-design.md; owner,
// 2026-09-30: "you see the no tab screen (false, there are three tabs
Expand Down
Loading