feat(desktop): reload a tab from its right-click menu
Right-click any tab and pick Reload: the pane's content remounts in place — effects re-run, state resets, measurements are retaken — while the tab keeps its slot and every other tab is untouched. A per-pane epoch atom keys the contribution inside the zone body, so reload never rewrites the layout tree. Both tab menus offer it: the zone strip menu (tool panels, the file tree, a fresh draft's main tab) and the session tab menu (tiles + the loaded main tab).
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
closeAllTreeTabs,
|
||||
closeOtherTreeTabs,
|
||||
closeTreeTabsToRight,
|
||||
reloadTreePane,
|
||||
treeTabCloseTargets
|
||||
} from '@/components/pane-shell/tree/store'
|
||||
import {
|
||||
@@ -239,12 +240,24 @@ function useSessionActions({
|
||||
})
|
||||
]
|
||||
|
||||
// TAB — close verbs that act on the strip (tabs only; a row isn't a tab).
|
||||
// TAB — verbs that act on the strip (tabs only; a row isn't a tab).
|
||||
const closeTargets = surface === 'tab' && tabPaneId ? treeTabCloseTargets(tabPaneId) : null
|
||||
|
||||
const tabCloseItems: ActionItemSpec[] =
|
||||
const tabItems: ActionItemSpec[] =
|
||||
surface === 'tab'
|
||||
? [
|
||||
...(tabPaneId
|
||||
? [
|
||||
spec({
|
||||
icon: 'refresh',
|
||||
label: t.zones.reload,
|
||||
onSelect: () => {
|
||||
triggerHaptic('selection')
|
||||
reloadTreePane(tabPaneId)
|
||||
}
|
||||
})
|
||||
]
|
||||
: []),
|
||||
...(onClose
|
||||
? [
|
||||
spec({
|
||||
@@ -342,10 +355,10 @@ function useSessionActions({
|
||||
/>
|
||||
<kit.Separator />
|
||||
{workItems.map(item => renderActionItem(kit, item))}
|
||||
{tabCloseItems.length > 0 && (
|
||||
{tabItems.length > 0 && (
|
||||
<>
|
||||
<kit.Separator />
|
||||
{tabCloseItems.map(item => renderActionItem(kit, item))}
|
||||
{tabItems.map(item => renderActionItem(kit, item))}
|
||||
</>
|
||||
)}
|
||||
<kit.Separator />
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Right-click a tab -> Reload remounts THAT pane's content: its epoch (the
|
||||
// React key the zone renderer hands the contribution) advances, and no other
|
||||
// pane's does. The layout tree itself must never move.
|
||||
|
||||
describe('reloadTreePane', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('advances only the reloaded pane epoch and leaves the tree alone', async () => {
|
||||
const tree = await import('@/components/pane-shell/tree/store')
|
||||
const model = await import('@/components/pane-shell/tree/model')
|
||||
|
||||
tree.declareDefaultTree(model.group(['workspace', 'files'], { active: 'workspace', id: 'grp-main' }))
|
||||
|
||||
const before = tree.$layoutTree.get()
|
||||
|
||||
tree.reloadTreePane('workspace')
|
||||
tree.reloadTreePane('workspace')
|
||||
|
||||
expect(tree.$treePaneEpochs.get().workspace).toBe(2)
|
||||
expect(tree.$treePaneEpochs.get().files).toBeUndefined()
|
||||
expect(tree.$layoutTree.get()).toBe(before)
|
||||
})
|
||||
})
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
$newSessionTabAction,
|
||||
$panesWithCloser,
|
||||
$treeDragging,
|
||||
$treePaneEpochs,
|
||||
activateTreePane,
|
||||
closeAllTreeTabs,
|
||||
closeOtherTreeTabs,
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
isCollapsePane,
|
||||
isSessionStripPane,
|
||||
noteActiveTreeGroup,
|
||||
reloadTreePane,
|
||||
restoreTreePane,
|
||||
SESSION_TILE_DRAG,
|
||||
setTreeGroupHeaderHidden,
|
||||
@@ -96,6 +98,12 @@ function ZoneMenu({
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderActionItem(kit, {
|
||||
icon: 'refresh',
|
||||
label: t.zones.reload,
|
||||
onSelect: () => reloadTreePane(targetPane())
|
||||
})}
|
||||
<kit.Separator />
|
||||
{paneId !== undefined &&
|
||||
renderActionItem(kit, {
|
||||
icon: 'close',
|
||||
@@ -178,6 +186,9 @@ export function TreeGroup({
|
||||
const narrow = useStore($narrowViewport)
|
||||
const newSessionTabAction = useStore($newSessionTabAction)
|
||||
const panesWithCloser = useStore($panesWithCloser)
|
||||
// Reload epochs: only an explicit tab-menu Reload writes here, so this
|
||||
// subscription costs nothing on a normal render.
|
||||
const paneEpochs = useStore($treePaneEpochs)
|
||||
|
||||
const paneFor = (id: string) => panes.find(p => p.id === id)
|
||||
|
||||
@@ -557,9 +568,14 @@ export function TreeGroup({
|
||||
// can gate its hot (per-token) subscriptions while hidden;
|
||||
// the group id identifies the ZONE it lives in, for state
|
||||
// that is per-zone rather than per-tab (composer pop-out).
|
||||
// The reload epoch keys the CONTENT, not this layer: a
|
||||
// Reload remounts the contribution (effects re-run, state
|
||||
// resets) while the layer — and every other tab — stays.
|
||||
<PaneGroupContext.Provider value={node.id}>
|
||||
<PaneVisibleContext.Provider value={isActive}>
|
||||
<ContribBoundary id={pane.id}>{pane.render()}</ContribBoundary>
|
||||
<ContribBoundary id={pane.id} key={paneEpochs[paneId] ?? 0}>
|
||||
{pane.render()}
|
||||
</ContribBoundary>
|
||||
</PaneVisibleContext.Provider>
|
||||
</PaneGroupContext.Provider>
|
||||
) : (
|
||||
|
||||
@@ -450,6 +450,22 @@ export function treeTabCloseTargets(paneId: string): { all: number; others: numb
|
||||
return { all: others.length + (isUncloseablePane(paneId) ? 0 : 1), others: others.length, right: right.length }
|
||||
}
|
||||
|
||||
/**
|
||||
* RELOAD — a pane's remount counter, the tab menu's Reload (browser parity:
|
||||
* right-click a tab, reload what's in it). The zone renderer keys a pane's
|
||||
* body layer on its epoch, so bumping it unmounts the contribution and mounts
|
||||
* it fresh — data effects re-run, measurements are retaken — while the layout
|
||||
* tree, the tab's position, and every other tab stay exactly as they were.
|
||||
* Absent until a pane is first reloaded (no key churn on a normal boot).
|
||||
*/
|
||||
export const $treePaneEpochs = atom<Readonly<Record<string, number>>>({})
|
||||
|
||||
export function reloadTreePane(paneId: string): void {
|
||||
const epochs = $treePaneEpochs.get()
|
||||
|
||||
$treePaneEpochs.set({ ...epochs, [paneId]: (epochs[paneId] ?? 0) + 1 })
|
||||
}
|
||||
|
||||
/** Close a tab the way its kind expects: a tool panel leaves the strip (and
|
||||
* syncs its toggle), everything else routes through its owning Close. */
|
||||
export function closeTabPane(paneId: string) {
|
||||
|
||||
@@ -2236,6 +2236,7 @@ export const ar = defineLocale({
|
||||
closeRunningBody:
|
||||
'هذه المحادثة ما زالت تعمل (أو تنتظر إدخالك). إغلاق التبويب يخفيها فقط — ستحتفظ الجلسة بتقدمها ويمكن إعادة فتحها من الشريط الجانبي.',
|
||||
closeRunningConfirm: 'إغلاق التبويب',
|
||||
reload: 'إعادة التحميل',
|
||||
closeOthers: 'إغلاق الأخرى',
|
||||
closeToRight: 'إغلاق ما على اليمين',
|
||||
closeAll: 'إغلاق الكل',
|
||||
|
||||
@@ -2663,6 +2663,7 @@ export const en: Translations = {
|
||||
closeRunningBody:
|
||||
'This chat is still working (or waiting on your input). Closing the tab hides it — the session keeps its progress and can be reopened from the sidebar.',
|
||||
closeRunningConfirm: 'Close tab',
|
||||
reload: 'Reload',
|
||||
closeOthers: 'Close others',
|
||||
closeToRight: 'Close to the right',
|
||||
closeAll: 'Close all',
|
||||
|
||||
@@ -2489,6 +2489,7 @@ export const ja = defineLocale({
|
||||
hideHeader: 'ヘッダーを隠す',
|
||||
minimize: '最小化',
|
||||
restore: '復元',
|
||||
reload: '再読み込み',
|
||||
closeOthers: '他を閉じる',
|
||||
closeToRight: '右側を閉じる',
|
||||
closeAll: 'すべて閉じる',
|
||||
|
||||
@@ -2262,6 +2262,7 @@ export interface Translations {
|
||||
closeRunningTitle: string
|
||||
closeRunningBody: string
|
||||
closeRunningConfirm: string
|
||||
reload: string
|
||||
closeOthers: string
|
||||
closeToRight: string
|
||||
closeAll: string
|
||||
|
||||
@@ -2409,6 +2409,7 @@ export const zhHant = defineLocale({
|
||||
hideHeader: '隱藏標題列',
|
||||
minimize: '最小化',
|
||||
restore: '還原',
|
||||
reload: '重新載入',
|
||||
closeOthers: '關閉其他',
|
||||
closeToRight: '關閉右側',
|
||||
closeAll: '全部關閉',
|
||||
|
||||
@@ -2841,6 +2841,7 @@ export const zh: Translations = {
|
||||
closeRunningTitle: '关闭正在运行的标签?',
|
||||
closeRunningBody: '此对话仍在运行(或正在等待你的输入)。关闭标签只会隐藏它——会话将保留进度,可从侧边栏重新打开。',
|
||||
closeRunningConfirm: '关闭标签',
|
||||
reload: '重新加载',
|
||||
closeOthers: '关闭其他',
|
||||
closeToRight: '关闭右侧',
|
||||
closeAll: '全部关闭',
|
||||
|
||||
Reference in New Issue
Block a user