diff --git a/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx b/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx
index 73f741a905..57d8a9f962 100644
--- a/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx
+++ b/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx
@@ -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({
/>
{workItems.map(item => renderActionItem(kit, item))}
- {tabCloseItems.length > 0 && (
+ {tabItems.length > 0 && (
<>
- {tabCloseItems.map(item => renderActionItem(kit, item))}
+ {tabItems.map(item => renderActionItem(kit, item))}
>
)}
diff --git a/apps/desktop/src/components/pane-shell/tree/pane-reload.test.ts b/apps/desktop/src/components/pane-shell/tree/pane-reload.test.ts
new file mode 100644
index 0000000000..8c78d5d391
--- /dev/null
+++ b/apps/desktop/src/components/pane-shell/tree/pane-reload.test.ts
@@ -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)
+ })
+})
diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx
index dcf5c05840..4f7d7ada11 100644
--- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx
+++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx
@@ -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())
+ })}
+
{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.
- {pane.render()}
+
+ {pane.render()}
+
) : (
diff --git a/apps/desktop/src/components/pane-shell/tree/store.ts b/apps/desktop/src/components/pane-shell/tree/store.ts
index 42267459b9..105fdb0df9 100644
--- a/apps/desktop/src/components/pane-shell/tree/store.ts
+++ b/apps/desktop/src/components/pane-shell/tree/store.ts
@@ -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>>({})
+
+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) {
diff --git a/apps/desktop/src/i18n/ar.ts b/apps/desktop/src/i18n/ar.ts
index d71aab8858..6091c916c3 100644
--- a/apps/desktop/src/i18n/ar.ts
+++ b/apps/desktop/src/i18n/ar.ts
@@ -2236,6 +2236,7 @@ export const ar = defineLocale({
closeRunningBody:
'هذه المحادثة ما زالت تعمل (أو تنتظر إدخالك). إغلاق التبويب يخفيها فقط — ستحتفظ الجلسة بتقدمها ويمكن إعادة فتحها من الشريط الجانبي.',
closeRunningConfirm: 'إغلاق التبويب',
+ reload: 'إعادة التحميل',
closeOthers: 'إغلاق الأخرى',
closeToRight: 'إغلاق ما على اليمين',
closeAll: 'إغلاق الكل',
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index 5d72bd090e..fe764eb310 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -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',
diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts
index 13bfd18c3b..16d58834c6 100644
--- a/apps/desktop/src/i18n/ja.ts
+++ b/apps/desktop/src/i18n/ja.ts
@@ -2489,6 +2489,7 @@ export const ja = defineLocale({
hideHeader: 'ヘッダーを隠す',
minimize: '最小化',
restore: '復元',
+ reload: '再読み込み',
closeOthers: '他を閉じる',
closeToRight: '右側を閉じる',
closeAll: 'すべて閉じる',
diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts
index 79b56030d6..b82feb4001 100644
--- a/apps/desktop/src/i18n/types.ts
+++ b/apps/desktop/src/i18n/types.ts
@@ -2262,6 +2262,7 @@ export interface Translations {
closeRunningTitle: string
closeRunningBody: string
closeRunningConfirm: string
+ reload: string
closeOthers: string
closeToRight: string
closeAll: string
diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts
index 7f0fcfde88..31a0b62a53 100644
--- a/apps/desktop/src/i18n/zh-hant.ts
+++ b/apps/desktop/src/i18n/zh-hant.ts
@@ -2409,6 +2409,7 @@ export const zhHant = defineLocale({
hideHeader: '隱藏標題列',
minimize: '最小化',
restore: '還原',
+ reload: '重新載入',
closeOthers: '關閉其他',
closeToRight: '關閉右側',
closeAll: '全部關閉',
diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts
index a9ccfee506..10b4727245 100644
--- a/apps/desktop/src/i18n/zh.ts
+++ b/apps/desktop/src/i18n/zh.ts
@@ -2841,6 +2841,7 @@ export const zh: Translations = {
closeRunningTitle: '关闭正在运行的标签?',
closeRunningBody: '此对话仍在运行(或正在等待你的输入)。关闭标签只会隐藏它——会话将保留进度,可从侧边栏重新打开。',
closeRunningConfirm: '关闭标签',
+ reload: '重新加载',
closeOthers: '关闭其他',
closeToRight: '关闭右侧',
closeAll: '全部关闭',