feat(v0.6): add Home (⌂) button to reach Sessions/Projects from a session
Once a project/session tab was open, the home chooser (Sessions/Projects segmented control) was hidden and only reachable by closing every tab — no way back to the Projects view to open another project. Add a ⌂ Home button to the tab bar that overlays the chooser on top of the open tabs (homeForced flag; updateHomeView shows home when no tab OR forced, hiding the terminal panes). Clicking ⌂ again, clicking a tab (activate resets homeForced), or opening another session/project dismisses the overlay. Closing the last tab resets the flag. The button highlights while active. Tests: +3 (tabs.test.ts). Full suite 415 green; both typechecks clean; verified in-browser (open tab → ⌂ → Projects panel reachable → ⌂ → terminal).
This commit is contained in:
@@ -146,6 +146,15 @@
|
||||
- **commit**: <hash 或 N/A>
|
||||
======================================================= -->
|
||||
|
||||
### 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 通过**。
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<HTMLButtonElement>('.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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user