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>
|
- **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 编排)
|
### 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 通过**。
|
- **状态**: `[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);
|
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 */
|
||||||
#toolbar {
|
#toolbar {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ export class TabApp {
|
|||||||
private readonly projects: ProjectsPanel
|
private readonly projects: ProjectsPanel
|
||||||
private homeView: HomeView = 'sessions'
|
private homeView: HomeView = 'sessions'
|
||||||
private readonly segControl: HTMLElement
|
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) {
|
constructor(paneHost: HTMLElement, tabBar: HTMLElement) {
|
||||||
this.paneHost = paneHost
|
this.paneHost = paneHost
|
||||||
@@ -104,7 +109,9 @@ export class TabApp {
|
|||||||
* When no tab is open: show the seg control and the selected sub-panel.
|
* When no tab is open: show the seg control and the selected sub-panel.
|
||||||
*/
|
*/
|
||||||
private updateHomeView(): void {
|
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.
|
// Sync active-class on segmented control buttons.
|
||||||
const btns = this.segControl.querySelectorAll<HTMLButtonElement>('.home-seg-btn')
|
const btns = this.segControl.querySelectorAll<HTMLButtonElement>('.home-seg-btn')
|
||||||
@@ -112,8 +119,12 @@ export class TabApp {
|
|||||||
const view: HomeView = i === 0 ? 'sessions' : 'projects'
|
const view: HomeView = i === 0 ? 'sessions' : 'projects'
|
||||||
btn.classList.toggle('active', view === this.homeView)
|
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.segControl.style.display = 'flex'
|
||||||
this.launcher.setVisible(this.homeView === 'sessions')
|
this.launcher.setVisible(this.homeView === 'sessions')
|
||||||
this.projects.setVisible(this.homeView === 'projects')
|
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 {
|
private saveHomeView(): void {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(HOME_VIEW_KEY, this.homeView)
|
localStorage.setItem(HOME_VIEW_KEY, this.homeView)
|
||||||
@@ -310,11 +334,13 @@ export class TabApp {
|
|||||||
activate(i: number): void {
|
activate(i: number): void {
|
||||||
if (i < 0 || i >= this.tabs.length) return
|
if (i < 0 || i >= this.tabs.length) return
|
||||||
this.maybeAskNotify() // first switch is a user gesture — request notif permission
|
this.maybeAskNotify() // first switch is a user gesture — request notif permission
|
||||||
|
this.homeForced = false // showing a terminal dismisses any home overlay
|
||||||
this.activeIndex = i
|
this.activeIndex = i
|
||||||
const entry = this.tabs[i]
|
const entry = this.tabs[i]
|
||||||
if (entry) entry.hasActivity = false // viewing clears the unread dot
|
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, idx) => (idx === i ? t.session.show() : t.session.hide()))
|
||||||
this.tabs.forEach((t) => this.refreshTab(t)) // in-place class/text update, no rebuild
|
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.updateApprovalBar()
|
||||||
this.persist()
|
this.persist()
|
||||||
}
|
}
|
||||||
@@ -327,6 +353,7 @@ export class TabApp {
|
|||||||
if (this.tabs.length === 0) {
|
if (this.tabs.length === 0) {
|
||||||
// v0.5: closing the last tab returns to the home screen — no auto-blank tab.
|
// v0.5: closing the last tab returns to the home screen — no auto-blank tab.
|
||||||
this.activeIndex = -1
|
this.activeIndex = -1
|
||||||
|
this.homeForced = false // back to the natural "no tabs → home" state
|
||||||
this.persist()
|
this.persist()
|
||||||
this.rebuild() // updateHomeView() (in rebuild) shows the chooser
|
this.rebuild() // updateHomeView() (in rebuild) shows the chooser
|
||||||
this.updateApprovalBar()
|
this.updateApprovalBar()
|
||||||
@@ -459,6 +486,17 @@ export class TabApp {
|
|||||||
private rebuild(): void {
|
private rebuild(): void {
|
||||||
this.tabBar.replaceChildren()
|
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) => {
|
this.tabs.forEach((entry, idx) => {
|
||||||
const tabEl = document.createElement('div')
|
const tabEl = document.createElement('div')
|
||||||
entry.el = tabEl
|
entry.el = tabEl
|
||||||
|
|||||||
@@ -463,3 +463,51 @@ describe('TabApp — P6 openProject + home segmented control', () => {
|
|||||||
expect(app.snapshot()[0]!.title).toBe('bare-repo')
|
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