feat: 任务管理页二级开关「有新后台任务时自动展开」(autoOpenTasks)
- 新 pref autoOpenTasks(默认开):当前会话出现新后台任务(任意新任务 id, 非仅首个)时自动展开侧边栏并打开任务管理页,与 autoOpenSubagent 并列 - 注册为 subagent tab 的声明式二级开关(设置页齿轮弹窗,key 在 PrefsSchema) - Sidebar 新增独立基线 effect(镜像子代理自动展开;刷新不触发、开关关闭/类型 禁用不触发);detectNewTask 纯函数(settling/同 id/他会话不触发) - 测试:detectNewTask 用例、prefs 解析断言、builtins 开关清单、schema 默认值 断言;全量 371 通过
This commit is contained in:
@@ -41,10 +41,10 @@ describe('built-in tab registrations', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('the subagent tab declares its auto-open related setting', () => {
|
||||
it('the subagent tab declares its auto-open related settings', () => {
|
||||
const { service } = setup()
|
||||
const toggles = service.getTab('subagent')?.settings?.toggles ?? []
|
||||
expect(toggles.map(t => t.key)).toEqual(['autoOpenSubagent'])
|
||||
expect(toggles.map(t => t.key)).toEqual(['autoOpenSubagent', 'autoOpenTasks'])
|
||||
})
|
||||
|
||||
it('the terminal tab declares the model terminal-tools related setting', () => {
|
||||
|
||||
@@ -45,6 +45,8 @@ describe('dsh-better-sidebar plugin export shape', () => {
|
||||
expect(resolved.openByDefault).toBe(true)
|
||||
expect(resolved.defaultWidthPercent).toBe(30)
|
||||
expect(resolved.autoOpenSubagent).toBe(true)
|
||||
// A new background task auto-opens the Tasks page too.
|
||||
expect(resolved.autoOpenTasks).toBe(true)
|
||||
// The terminal tools default OFF (the feature is dormant until the user
|
||||
// enables it in the side card settings).
|
||||
expect(resolved.agentTerminalTools).toBe(false)
|
||||
@@ -57,6 +59,6 @@ describe('dsh-better-sidebar plugin export shape', () => {
|
||||
const overridden = (PrefsSchema as unknown as {
|
||||
(input: Record<string, unknown> | undefined): Record<string, unknown>
|
||||
})({ openByDefault: false, defaultWidthPercent: 45 })
|
||||
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenTasks: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -514,6 +514,7 @@ describe('side card settings routes', () => {
|
||||
openByDefault: true,
|
||||
defaultWidthPercent: 30,
|
||||
autoOpenSubagent: true,
|
||||
autoOpenTasks: true,
|
||||
agentTerminalTools: false,
|
||||
bottomPanelAutoTerminal: true,
|
||||
interceptOpenPath: true,
|
||||
|
||||
@@ -225,9 +225,9 @@ describe('SubagentView background tasks', () => {
|
||||
)
|
||||
const row = container.querySelector('button[aria-label*="unread cmd"]') as HTMLButtonElement
|
||||
await act(async () => { row.click() })
|
||||
// read:false → the pane explains the model has not read it (never the
|
||||
// model's cursor, so there is nothing to steal yet).
|
||||
expect(container.textContent).toContain('模型尚未读取该任务的输出')
|
||||
// read:false → the pane explains the output awaits the model's task_output
|
||||
// (never the model's cursor, so there is nothing to steal yet).
|
||||
expect(container.textContent).toContain('等待模型读取该任务的输出')
|
||||
unmount()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
collectTreeTasks,
|
||||
detectNewTask,
|
||||
formatTaskDuration,
|
||||
isTaskLive,
|
||||
orderTasks,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
taskStatusLabel,
|
||||
treeSessionIds,
|
||||
} from '../src/client/subagent-tasks.ts'
|
||||
import type { SidebarSessionSummary, SidebarTaskStatus, SidebarTaskView } from '../src/context-types.ts'
|
||||
import type { SidebarSessionList, SidebarSessionSummary, SidebarTaskStatus, SidebarTaskView } from '../src/context-types.ts'
|
||||
|
||||
/** The translator stub: renders duration templates like the real locale copy. */
|
||||
const templates: Record<string, string> = {
|
||||
@@ -131,3 +132,34 @@ describe('status presentation helpers', () => {
|
||||
expect(formatTaskDuration(-5, t)).toBe('0秒')
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectNewTask', () => {
|
||||
const list = (tasksBySession: Record<string, SidebarTaskView[]>): SidebarSessionList => ({
|
||||
current: 'root',
|
||||
byId: { root: { id: 'root', displayTitle: 'root' } },
|
||||
subagentsByParent: {},
|
||||
tasksBySession: tasksBySession,
|
||||
})
|
||||
|
||||
it('fires on EVERY new task id for the session (not just the first)', () => {
|
||||
expect(detectNewTask(list({}), list({ root: [task('bash-1')] }), 'root')).toBe(true)
|
||||
expect(detectNewTask(
|
||||
list({ root: [task('bash-1')] }),
|
||||
list({ root: [task('bash-1'), task('bash-2')] }),
|
||||
'root',
|
||||
)).toBe(true)
|
||||
})
|
||||
|
||||
it('stays quiet on settling, same ids, other sessions, or an absent mirror', () => {
|
||||
// Settling only mutates status, never adds ids.
|
||||
expect(detectNewTask(
|
||||
list({ root: [task('bash-1')] }),
|
||||
list({ root: [task('bash-1', { status: 'completed', finishedAt: 2_000 })] }),
|
||||
'root',
|
||||
)).toBe(false)
|
||||
expect(detectNewTask(list({ root: [task('bash-1')] }), list({ root: [task('bash-1')] }), 'root')).toBe(false)
|
||||
// Tasks owned by another session do not trigger the current one.
|
||||
expect(detectNewTask(list({}), list({ child: [task('bash-1')] }), 'root')).toBe(false)
|
||||
expect(detectNewTask(list({}), list({}), 'root')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
+12
-4
@@ -1150,6 +1150,7 @@ describe('side card preferences', () => {
|
||||
openByDefault: false,
|
||||
defaultWidthPercent: 60,
|
||||
autoOpenSubagent: false,
|
||||
autoOpenTasks: true,
|
||||
agentTerminalTools: true,
|
||||
bottomPanelAutoTerminal: true,
|
||||
interceptOpenPath: true,
|
||||
@@ -1168,6 +1169,7 @@ describe('side card preferences', () => {
|
||||
openByDefault: true,
|
||||
defaultWidthPercent: 33,
|
||||
autoOpenSubagent: true,
|
||||
autoOpenTasks: true,
|
||||
agentTerminalTools: false,
|
||||
bottomPanelAutoTerminal: true,
|
||||
interceptOpenPath: true,
|
||||
@@ -1186,6 +1188,7 @@ describe('side card preferences', () => {
|
||||
openByDefault: false,
|
||||
defaultWidthPercent: 40,
|
||||
autoOpenSubagent: true,
|
||||
autoOpenTasks: true,
|
||||
agentTerminalTools: false,
|
||||
bottomPanelAutoTerminal: true,
|
||||
interceptOpenPath: true,
|
||||
@@ -1205,6 +1208,11 @@ describe('side card preferences', () => {
|
||||
.toBe(false)
|
||||
expect((await loadPrefs(wire({ openByDefault: true, defaultWidthPercent: 40, agentTerminalTools: true }))).agentTerminalTools)
|
||||
.toBe(true)
|
||||
// The task auto-open is ON by default; only an explicit false turns it off.
|
||||
expect((await loadPrefs(wire({ openByDefault: true, defaultWidthPercent: 40, autoOpenTasks: 1 }))).autoOpenTasks)
|
||||
.toBe(true)
|
||||
expect((await loadPrefs(wire({ openByDefault: true, defaultWidthPercent: 40, autoOpenTasks: false }))).autoOpenTasks)
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('defaults interceptOpenPath to true; only an explicit false turns the takeover off', async () => {
|
||||
@@ -1234,9 +1242,9 @@ describe('side card preferences', () => {
|
||||
const store = createSidebarStore()
|
||||
// Node environment: no window → the width falls back to PANEL_DEFAULT,
|
||||
// while the open flag still follows the preference.
|
||||
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenTasks: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||
store.setSession('fresh-session')
|
||||
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenTasks: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||
const snapshot = store.getSnapshot()
|
||||
expect(snapshot.sessionId).toBe('fresh-session')
|
||||
expect(snapshot.state?.panelOpen).toBe(false)
|
||||
@@ -1272,7 +1280,7 @@ describe('side card preferences', () => {
|
||||
|
||||
it('skips the default explorer tab when the explorer type is disabled', () => {
|
||||
const store = createSidebarStore()
|
||||
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: { explorer: false }, viewersEnabled: {} })
|
||||
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenTasks: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: { explorer: false }, viewersEnabled: {} })
|
||||
store.setSession('no-explorer')
|
||||
const state = store.getSnapshot().state!
|
||||
const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs)
|
||||
@@ -1280,7 +1288,7 @@ describe('side card preferences', () => {
|
||||
expect(state.splits.kind).toBe('leaf')
|
||||
// Re-enabling seeds the explorer tab again.
|
||||
const openStore = createSidebarStore()
|
||||
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenTasks: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, tabsEnabled: {}, viewersEnabled: {} })
|
||||
openStore.setSession('with-explorer')
|
||||
const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
|
||||
expect(openTabs.map(tab => tab.type)).toEqual(['explorer'])
|
||||
|
||||
Reference in New Issue
Block a user