diff --git a/docs/PROGRESS_LOG.md b/docs/PROGRESS_LOG.md index 9662d4f..0c94370 100644 --- a/docs/PROGRESS_LOG.md +++ b/docs/PROGRESS_LOG.md @@ -146,6 +146,15 @@ - **commit**: ======================================================= --> +### 2026-06-30 · v0.6 UX 修复 — 会话内无法返回 Projects 首页 + +- **状态**: `[x]` DONE。**415 测试全绿**(+3)· 双 typecheck 干净 · `build:web` OK · 真浏览器验证通过。 +- **问题**(用户反馈): 打开某个项目进入终端 tab 后,首页的 Sessions/Projects 段控被隐藏,只能**关掉所有 tab** 才能回到选择器再开下一个项目——没有"返回 Projects"的入口。 +- **修复**(`public/tabs.ts` + `public/style.css`): 标签栏新增 **⌂ Home 按钮**;点击把首页选择器(Sessions/Projects 段控 + 对应面板)**叠加显示在当前 tab 之上**(`homeForced` 标志:`updateHomeView` 改为 `tabs.length===0 || homeForced` 时显示并隐藏所有终端 pane)。再点 ⌂ 或点任一 tab(`activate` 重置 `homeForced`)即回到终端;从叠加层开新会话/项目也会自动收起。关最后一个 tab 时复位 `homeForced`。⌂ 叠加时高亮(`.tab-home.active`,Amber)。 +- **测试**(`test/tabs.test.ts` +3): ⌂ 叠加首页再切回;叠加层切到 Projects 并开第二个项目后自动收起;0 tab 时 ⌂ 为 no-op。 +- **验证**: `npx tsc -p tsconfig.json/.web.json` 干净;`npx vitest run` 415/415;真浏览器:开 shell tab→段控隐藏→点 ⌂→段控+面板叠加显示、⌂ 高亮→切 Projects 面板可见→再点 ⌂→回到终端。 +- **commit**: (本次提交) + ### 2026-06-30 · v0.6 Project Manager(P1–P6,多 agent Workflow 编排) - **状态**: `[x]` DONE(分支 `v0.6-projects`)。**398 测试全绿** · 双 typecheck 干净 · `build:web` OK · 覆盖率 90.98/81.95/89.28/93.16(≥80×4)· **真浏览器 smoke 通过**。 diff --git a/public/style.css b/public/style.css index 700d93d..ccd9193 100644 --- a/public/style.css +++ b/public/style.css @@ -207,6 +207,26 @@ body { background: var(--surface-2); } +/* v0.6: Home (⌂) button — opens the Sessions/Projects chooser over open tabs. */ +.tab-home { + background: none; + border: none; + color: var(--text-dim); + cursor: pointer; + font-size: 17px; + padding: 0 10px; + flex: none; + border-radius: 7px; +} +.tab-home:hover { + color: #fff; + background: var(--surface-2); +} +.tab-home.active { + color: var(--on-accent, #1a1408); + background: var(--accent); +} + /* toolbar */ #toolbar { flex: 0 0 auto; diff --git a/public/tabs.ts b/public/tabs.ts index d5b52a5..c15f80c 100644 --- a/public/tabs.ts +++ b/public/tabs.ts @@ -59,6 +59,11 @@ export class TabApp { private readonly projects: ProjectsPanel private homeView: HomeView = 'sessions' private readonly segControl: HTMLElement + // When true, the home chooser is overlaid on top of the open tabs (so you can + // jump back to Sessions/Projects without closing anything). Reset whenever a + // tab is activated. Irrelevant while no tab is open (home shows regardless). + private homeForced = false + private homeBtn: HTMLButtonElement | null = null constructor(paneHost: HTMLElement, tabBar: HTMLElement) { this.paneHost = paneHost @@ -104,7 +109,9 @@ export class TabApp { * When no tab is open: show the seg control and the selected sub-panel. */ private updateHomeView(): void { - const empty = this.tabs.length === 0 + // Home shows when there is no tab OR the user explicitly forced it (the ⌂ + // button), letting you reach Sessions/Projects without closing a session. + const showHome = this.tabs.length === 0 || this.homeForced // Sync active-class on segmented control buttons. const btns = this.segControl.querySelectorAll('.home-seg-btn') @@ -112,8 +119,12 @@ export class TabApp { const view: HomeView = i === 0 ? 'sessions' : 'projects' btn.classList.toggle('active', view === this.homeView) }) + // The ⌂ button is "pressed" only while overlaying home on top of open tabs. + this.homeBtn?.classList.toggle('active', this.homeForced && this.tabs.length > 0) - if (empty) { + if (showHome) { + // Overlay home: hide every terminal pane so the chooser is on top. + for (const t of this.tabs) t.session.hide() this.segControl.style.display = 'flex' this.launcher.setVisible(this.homeView === 'sessions') this.projects.setVisible(this.homeView === 'projects') @@ -124,6 +135,19 @@ export class TabApp { } } + /** Toggle the home chooser overlay over the open tabs (the ⌂ button). No-op + * when no tab is open (home is already showing). */ + private toggleHome(): void { + if (this.tabs.length === 0) return + this.homeForced = !this.homeForced + if (this.homeForced) { + this.updateHomeView() + } else { + // Return to the active terminal (activate re-shows its pane + hides home). + this.activate(this.activeIndex) + } + } + private saveHomeView(): void { try { localStorage.setItem(HOME_VIEW_KEY, this.homeView) @@ -310,11 +334,13 @@ export class TabApp { activate(i: number): void { if (i < 0 || i >= this.tabs.length) return this.maybeAskNotify() // first switch is a user gesture — request notif permission + this.homeForced = false // showing a terminal dismisses any home overlay this.activeIndex = i const entry = this.tabs[i] if (entry) entry.hasActivity = false // viewing clears the unread dot this.tabs.forEach((t, idx) => (idx === i ? t.session.show() : t.session.hide())) this.tabs.forEach((t) => this.refreshTab(t)) // in-place class/text update, no rebuild + this.updateHomeView() // hide the seg control + home panels now a tab is active this.updateApprovalBar() this.persist() } @@ -327,6 +353,7 @@ export class TabApp { if (this.tabs.length === 0) { // v0.5: closing the last tab returns to the home screen — no auto-blank tab. this.activeIndex = -1 + this.homeForced = false // back to the natural "no tabs → home" state this.persist() this.rebuild() // updateHomeView() (in rebuild) shows the chooser this.updateApprovalBar() @@ -459,6 +486,17 @@ export class TabApp { private rebuild(): void { this.tabBar.replaceChildren() + // Home (⌂): overlay the Sessions/Projects chooser over the open tabs so you + // can start another session/project without closing the current one. + const home = document.createElement('button') + home.className = 'tab-home' + home.textContent = '⌂' + home.title = 'Home — Sessions & Projects' + home.setAttribute('aria-label', 'Home — Sessions & Projects') + home.addEventListener('click', () => this.toggleHome()) + this.tabBar.appendChild(home) + this.homeBtn = home + this.tabs.forEach((entry, idx) => { const tabEl = document.createElement('div') entry.el = tabEl diff --git a/test/tabs.test.ts b/test/tabs.test.ts index bb2e225..f96bbed 100644 --- a/test/tabs.test.ts +++ b/test/tabs.test.ts @@ -463,3 +463,51 @@ describe('TabApp — P6 openProject + home segmented control', () => { expect(app.snapshot()[0]!.title).toBe('bare-repo') }) }) + +describe('TabApp — Home (⌂) button: reach the chooser from an open session', () => { + it('overlays the home chooser over an open tab, then toggles back', () => { + const { paneHost, tabBar } = makeHosts() + const app = new TabApp(paneHost, tabBar) + app.newTab() + const seg = paneHost.querySelector('.home-seg') as HTMLElement + const homeBtn = tabBar.querySelector('.tab-home') as HTMLButtonElement + expect(homeBtn).not.toBeNull() + expect(seg.style.display).toBe('none') // terminal owns the screen + + homeBtn.click() // overlay the home chooser + expect(seg.style.display).toBe('flex') + expect(launcherVisible.value).toBe(true) // default sessions view + expect(homeBtn.classList.contains('active')).toBe(true) + + homeBtn.click() // back to the terminal + expect(seg.style.display).toBe('none') + expect(launcherVisible.value).toBe(false) + expect(homeBtn.classList.contains('active')).toBe(false) + }) + + it('can switch to Projects from the overlay and open another project', () => { + const { paneHost, tabBar } = makeHosts() + const app = new TabApp(paneHost, tabBar) + app.openProject('/p/one', 'one') + + ;(tabBar.querySelector('.tab-home') as HTMLButtonElement).click() // overlay home + ;(paneHost.querySelectorAll('.home-seg-btn')[1] as HTMLButtonElement).click() // Projects + expect(projectsVisible.value).toBe(true) + + app.openProject('/p/two', 'two') // opening dismisses the overlay + expect(app.snapshot()).toHaveLength(2) + const seg = paneHost.querySelector('.home-seg') as HTMLElement + expect(seg.style.display).toBe('none') + expect(projectsVisible.value).toBe(false) + expect((tabBar.querySelector('.tab-home') as HTMLButtonElement).classList.contains('active')).toBe(false) + }) + + it('⌂ is a no-op when no tab is open (home already shown)', () => { + const { paneHost, tabBar } = makeHosts() + new TabApp(paneHost, tabBar) + const seg = paneHost.querySelector('.home-seg') as HTMLElement + expect(seg.style.display).toBe('flex') + ;(tabBar.querySelector('.tab-home') as HTMLButtonElement).click() + expect(seg.style.display).toBe('flex') // still shown, no crash + }) +})