feat: unify execution and add workspace file imports

Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content.

Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation.

Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit.

Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
This commit is contained in:
mesalogo
2026-10-04 20:02:59 +08:00
parent 0cd5da33f4
commit 56ebd40bd8
374 changed files with 7075 additions and 6224 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ void app.whenReady().then(async () => {
const conversationId = `review-probe:${randomUUID()}`
let text = ''
try {
for await (const event of runtime.run({ requestId: randomUUID(), conversationId, workMode: 'ask', prompt },
for await (const event of runtime.run({ requestId: randomUUID(), conversationId, prompt },
AbortSignal.timeout(90000), async () => 'deny')) {
if (event.type === 'text') text += event.delta
if (event.type === 'error') throw new Error(event.message)
+1 -1
View File
@@ -36,7 +36,7 @@ app.whenReady().then(async () => {
modelProfiles: runtimeSettings.modelProfiles.map(profile => ({ id: profile.id, supportsImageInput: profile.supportsImageInput }))
} : runtimeSettings } as never,
{} as never, { clear() {}, cancelImport() {} } as never, {} as never, database,
{ clear() {} } as never, {} as never, async () => {}, undefined, undefined, undefined, undefined,
{} as never, async () => {}, undefined, undefined, undefined, undefined,
applicationSettings)
if (!live) win.webContents.on('console-message', event => console.log(event.message))
try {
+1 -1
View File
@@ -37,7 +37,7 @@ app.whenReady().then(async () => {
{} as never, { clear() {}, cancelImport() {}, getDraft: () => [] } as never,
{ snapshot: () => ({ libraries: [], sources: [], documents: [], entities: [], relations: [], evidence: [], tasks: [] }),
database: { externalStore: { listBindings: () => [] } }, external: { listInstances: () => [] } } as never, database,
{ clear() {} } as never, {} as never, async () => {}, undefined, undefined, undefined, undefined, applicationSettings,
{} as never, async () => {}, undefined, undefined, undefined, undefined, applicationSettings,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
{ getPending: async () => undefined } as never)
win.webContents.on('console-message', event => { if (event.level === 'error') console.error(event.message) })
+1 -1
View File
@@ -60,7 +60,7 @@ void app.whenReady().then(async () => {
{} as never, { clear() {}, cancelImport() {}, getDraft: () => [] } as never,
{ snapshot: () => ({ libraries: [], sources: [], documents: [], entities: [], relations: [], evidence: [], tasks: [] }),
database: { externalStore: { listBindings: () => [] } }, external: { listInstances: () => [] } } as never, database,
{ clear() {} } as never, {} as never, async () => {}, undefined, undefined, undefined, undefined, applicationSettings,
{} as never, async () => {}, undefined, undefined, undefined, undefined, applicationSettings,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
{ getPending: async () => undefined } as never)
const run = <T = unknown>(script: string): Promise<T> => win.webContents.executeJavaScript(script, true)
+1 -1
View File
@@ -38,7 +38,7 @@ app.whenReady().then(async () => {
capabilities, { clear() {}, cancelImport() {}, getDraft: () => [] } as never,
{ snapshot: () => ({ libraries: [], sources: [], documents: [], entities: [], relations: [], evidence: [], tasks: [] }),
database: { externalStore: { listBindings: () => [] } }, external: { listInstances: () => [] } } as never,
database, { clear() {} } as never, {} as never, async () => {}, undefined, browser,
database, {} as never, async () => {}, undefined, browser,
undefined, undefined, applicationSettings, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, { getPending: async () => undefined } as never)
const errors: string[] = []
+99 -8
View File
@@ -25,6 +25,11 @@ app.whenReady().then(async () => {
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
database.initialize(directory)
const project = database.listProjects()[0]!
const archivedProject = database.createProject({ name: 'Archived workspace', description: '', rootPath: directory })
const archivedConversationId = randomUUID()
database.saveLocalConversations([{ header: { id: archivedConversationId, projectId: archivedProject.id, title: 'Archived history', updatedAt: 1 },
messages: [{ id: randomUUID(), role: 'assistant', state: 'complete', createdAt: 1, content: 'Preserved archived message' }] }])
database.setProjectArchived(archivedProject.id, true)
const conversationId = randomUUID()
const resultId = randomUUID()
const html = '<html><body><a href="#end">Jump to end</a>' + '<p>Long preview text</p>'.repeat(80) +
@@ -36,6 +41,10 @@ app.whenReady().then(async () => {
snippet: 'Saved citation', rank: 1, external: { kind: 'external', provider: 'fastgpt', instanceId: randomUUID(), remoteKnowledgeBaseId: 'review' } }]
}, { id: randomUUID(), role: 'assistant', state: 'complete', createdAt: Date.now(),
content: '```html\n<html><body><h1>Plain keyboard preview</h1>' + '<p>Long plain content</p>'.repeat(80) + '</body></html>\n```' }] }])
database.saveLocalConversations(Array.from({ length: 14 }, (_, i) => ({
header: { id: randomUUID(), projectId: project.id, title: `Earlier workspace conversation with a long title ${i}`,
updatedAt: new Date(2025, 8, 2, 10, i).getTime() }, messages: []
})))
const settings = { ...defaultRuntimeSettings, workspacePath: directory, modelProfiles: [], embeddingConnections: [] }
const applicationSettings = new ApplicationSettingsStore(join(directory, 'application.json'))
await applicationSettings.update({ checkUpdatesOnStartup: false })
@@ -49,7 +58,7 @@ app.whenReady().then(async () => {
capabilities, { clear() {}, cancelImport() {}, getDraft: () => [] } as never,
{ snapshot: () => ({ libraries: [], sources: [], documents: [], entities: [], relations: [], evidence: [], tasks: [] }),
database: { externalStore: { listBindings: () => [] } }, external: { listInstances: () => [] } } as never,
database, { clear() {} } as never, {} as never, async () => {}, undefined, browser,
database, {} as never, async () => {}, undefined, browser,
undefined, undefined, applicationSettings, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, { getPending: async () => undefined } as never)
// Parsing payloads are fixtures; App, preload, window keyboard IPC and browser services are production.
@@ -161,13 +170,13 @@ app.whenReady().then(async () => {
assert(await js('document.querySelector(".project-switcher__control").matches("button") && document.querySelectorAll(".project-switcher__picker button").length === 1 && !!document.querySelector(".project-switcher__trigger .project-switcher__activity")'))
await screenshot('workspace-idle-trigger')
database.createTask({ id: randomUUID(), projectId: project.id, conversationId,
title: 'Review workspace layout', instructions: 'Layout fixture only', workMode: 'ask', status: 'running' })
title: 'Review workspace layout', instructions: 'Layout fixture only', status: 'running' })
win.webContents.send(ipcChannels.conversationsChanged)
await wait('!!document.querySelector(".project-switcher__control .project-activity__running")')
assert.equal(await js('document.querySelector(".project-switcher__control").getBoundingClientRect().height'), idleTrigger)
await screenshot('workspace-active-trigger')
const workspaceLayouts = []
for (const [width, height] of [[1280, 800], [1280, 480], [560, 640]]) {
for (const [width, height] of [[1280, 800], [1280, 480], [560, 640], [375, 480]]) {
win.setContentSize(width!, height!)
await wait(`innerWidth === ${width} && innerHeight === ${height}`)
if (width! < 900) {
@@ -181,7 +190,7 @@ app.whenReady().then(async () => {
await click('.project-switcher__activity')
await wait('!!document.querySelector(".workspace-menu")')
await js('Promise.all(document.getAnimations().filter(a => a instanceof CSSTransition).map(a => a.finished.catch(() => {})))')
const layout = await js<{ bounded: boolean; divider: boolean; compact: boolean; rows: boolean }>(`(() => {
const layout = await js<{ bounded: boolean; divider: boolean; compact: boolean; rows: boolean; gap: number; triggerHit: boolean; times: boolean; enter: boolean }>(`(() => {
const rect = selector => document.querySelector(selector).getBoundingClientRect();
const left = rect('.workspace-menu__projects'), input = rect('.workspace-menu__search');
const menu = rect('.workspace-menu'), toolbar = rect('.workspace-menu__toolbar');
@@ -189,21 +198,87 @@ app.whenReady().then(async () => {
const border = getComputedStyle(document.querySelector('.workspace-menu__projects'));
const row = document.querySelector('.workspace-menu [role="menuitemradio"]');
const parts = [...row.querySelector('span').children].map(e => e.getBoundingClientRect());
const trigger = document.querySelector('.project-switcher__control'), anchor = trigger.getBoundingClientRect();
const times = [...document.querySelectorAll('.workspace-menu__conversation time')];
const enter = rect('.workspace-menu__enter button'), right = rect('.workspace-menu__activity');
return { bounded: input.left >= left.left && input.right <= left.right && menu.left >= 16 && menu.right <= innerWidth - 16 && menu.top >= 16 && menu.bottom <= innerHeight - 16,
divider: border.borderRightWidth === '1px' && border.borderRightStyle === 'solid' && border.borderRightColor === getComputedStyle(document.querySelector('.workspace-menu__search')).borderRightColor,
compact: toolbar.height < 70 && filters.right <= action.left && Math.abs(filters.top + filters.height / 2 - action.top - action.height / 2) < 2,
rows: parts.length >= 3 && parts[0].bottom <= parts[1].top && parts[1].bottom <= parts[2].top };
rows: parts.length >= 3 && parts[0].bottom <= parts[1].top && parts[1].bottom <= parts[2].top,
gap: menu.top - anchor.bottom,
triggerHit: [anchor.top + 8, anchor.bottom - 8].every(y => trigger.contains(document.elementFromPoint(anchor.left + anchor.width / 2, y))),
enter: enter.left >= right.left && Math.abs(enter.right - right.right + 8) <= 1 && Math.abs(enter.bottom - right.bottom + 8) <= 1 && enter.top >= toolbar.bottom,
times: times.length === 11 && times.every(time => {
const button = time.closest('button'), title = button.querySelector('strong').getBoundingClientRect(), r = time.getBoundingClientRect(), status = button.querySelector('.workspace-menu__metadata small');
return time.dateTime && time.title && title.width > 0 && title.right <= r.left && r.right <= button.getBoundingClientRect().right && (!status || status.getBoundingClientRect().top >= r.bottom);
}) };
})()`)
assert.deepEqual(layout, { bounded: true, divider: true, compact: true, rows: true }, `${width}x${height} ${theme}: ${JSON.stringify(layout)}`)
assert.deepEqual(layout, { bounded: true, divider: true, compact: true, rows: true, gap: 8, triggerHit: true, times: true, enter: true }, `${width}x${height} ${theme}: ${JSON.stringify(layout)}`)
await screenshot(`workspace-${width}x${height}-${theme}`)
await js('document.querySelector(".workspace-menu__conversation").focus()')
await key('End')
await wait('!!document.activeElement.closest(".workspace-menu__conversations [aria-posinset=\\"11\\"]")')
const wheelPoint = await js<{ x: number; y: number }>(`(() => {
const r = document.querySelector('.workspace-menu__conversations').getBoundingClientRect();
return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) };
})()`)
win.webContents.sendInputEvent({ type: 'mouseWheel', ...wheelPoint, deltaX: 0, deltaY: -500, canScroll: true })
await wait('document.querySelector(".workspace-menu__conversations").scrollTop > 0')
const scrolling = await js<{ visible: boolean; scrolled: boolean; noOverflow: boolean; geometry: number[] }>(`(() => {
const list = document.querySelector('.workspace-menu__conversations'), row = document.activeElement;
const r = row.getBoundingClientRect(), bounds = list.getBoundingClientRect();
return { visible: r.top >= bounds.top - 1 && r.bottom <= bounds.bottom + 1,
scrolled: list.scrollTop > 0, noOverflow: list.scrollWidth === list.clientWidth,
geometry: [r.top, r.bottom, bounds.top, bounds.bottom, list.scrollTop, list.scrollHeight, list.clientHeight, list.scrollWidth, list.clientWidth] };
})()`)
assert(scrolling.visible && scrolling.scrolled && scrolling.noOverflow, JSON.stringify(scrolling))
const stable = await js(`new Promise(resolve => {
const samples = [];
const sample = () => { const r = document.querySelector('.workspace-menu').getBoundingClientRect(); samples.push([r.top, r.height]);
if (samples.length < 12) requestAnimationFrame(sample); else resolve(samples.every(r => r[0] === samples[0][0] && r[1] === samples[0][1])); };
requestAnimationFrame(sample);
})`)
assert(stable, 'Menu geometry must remain stable during internal scroll')
if (width === 1280 && height === 800 && theme === 'light') {
win.setContentSize(1280, 480)
await wait(`(() => {
const menu = document.querySelector('.workspace-menu').getBoundingClientRect();
const trigger = document.querySelector('.project-switcher__control').getBoundingClientRect();
return innerHeight === 480 && menu.top === trigger.bottom + 8 && menu.bottom <= innerHeight - 16;
})()`)
win.setContentSize(width, height)
await wait('innerHeight === 800 && document.querySelector(".workspace-menu").getBoundingClientRect().height > 600')
}
await key('Escape')
await wait('!document.querySelector(".workspace-menu") && document.activeElement.matches(".project-switcher__trigger")')
workspaceLayouts.push({ width, height, theme, ...layout })
workspaceLayouts.push({ width, height, theme, ...layout, internalScroll: true, stable })
}
}
evidence.workspaceLayouts = workspaceLayouts
evidence.workspaceTriggerHeight = idleTrigger
await key('Escape')
win.setContentSize(1280, 800)
await wait('innerWidth === 1280')
await click('.sidebar-toggle')
await wait('!document.querySelector(".sidebar").inert')
await click('.project-switcher__trigger')
await click('.workspace-menu__archived')
await click('.workspace-menu__archived-project button')
await wait('!document.querySelector(".workspace-menu__archived-project")')
assert.equal(database.getProject(archivedProject.id).status, 'active')
assert(await js(`!document.querySelector('.project-switcher__trigger').textContent.includes('Archived workspace')`), 'Restore must not switch project')
await click('.workspace-menu__archive-heading button')
const archivedRow = `[data-list-window-row="${archivedProject.id}"] [role="menuitemradio"]`
await js(`document.querySelector(${JSON.stringify(archivedRow)}).focus()`)
await wait('document.querySelector(".workspace-menu__conversations").textContent.includes("Archived history")')
await click('.workspace-menu__enter button')
await wait('document.querySelector(".project-switcher__trigger").textContent.includes("Archived workspace") && document.body.innerText.includes("Preserved archived message")')
assert.equal(database.listConversationSummaries().filter(item => item.projectId === archivedProject.id).length, 1, 'Entry must not create a replacement conversation')
await click('.project-switcher__trigger')
await js(`document.querySelector('[data-list-window-row="${project.id}"] [role="menuitemradio"]').focus()`)
await click('.workspace-menu__enter button')
await wait('!!document.querySelector(".message-html-preview")')
evidence.archiveRestored = true
win.setContentSize(760, 700)
await wait('document.querySelector(".sidebar").inert')
await click('.sidebar-toggle')
@@ -221,7 +296,7 @@ app.whenReady().then(async () => {
const tabs = [...document.querySelectorAll('.workspace-menu__categories [role="tab"]')];
return tabs.length === 3 && tabs[0].getAttribute('aria-selected') === 'true' &&
tabs.every(tab => tab.getBoundingClientRect().top === tabs[0].getBoundingClientRect().top) &&
document.querySelectorAll('.workspace-menu [role="menuitemradio"]').length === 1 &&
document.querySelectorAll('.workspace-menu [role="menuitemradio"]').length === 2 &&
!document.querySelector('.workspace-menu__scope');
})()`))
evidence.workspaceCategories = true
@@ -312,7 +387,23 @@ app.whenReady().then(async () => {
await waitNative(true)
assert.equal(native()!.webContents.id, nativeId)
assert.equal(await native()!.webContents.executeJavaScript('window.overlayToken'), 'same-document')
if (await js('document.querySelector(".sidebar").inert')) await click('.sidebar-toggle')
await wait('!document.querySelector(".sidebar").inert')
await js('Promise.all(document.getAnimations().filter(a => a instanceof CSSTransition).map(a => a.finished.catch(() => {})))')
await click('.project-switcher__trigger')
await wait('!!document.querySelector(".workspace-menu")')
const menuOverlaps = await js<boolean>(`(() => {
const a = document.querySelector('.workspace-menu').getBoundingClientRect(), b = document.querySelector('.assistant-sidebar__browser-viewport').getBoundingClientRect();
return a.right > b.left && a.left < b.right && a.top < b.bottom && a.bottom > b.top;
})()`)
assert.equal(menuOverlaps, width === 1000)
await waitNative(!menuOverlaps)
await key('Escape')
await waitNative(true)
assert.equal(native()!.webContents.id, nativeId)
assert.equal(await native()!.webContents.executeJavaScript('window.overlayToken'), 'same-document')
}
evidence.workspaceBrowserOcclusion = true
evidence.tooltip = true
evidence.modelAttempts = (globalThis as typeof globalThis & { overlayModelAttempts?: number }).overlayModelAttempts ?? 0
assert.equal(evidence.modelAttempts, 0)
+1 -1
View File
@@ -43,7 +43,7 @@ app.whenReady().then(async () => {
{ capability: 'text', getStatus: async () => ({ available: true, name: 'Switch test', capability: 'text' }) } as never,
'CommandOrControl+Shift+Space', settings, capabilities,
{ clear() {}, cancelImport() {}, getDraft: () => [] } as never, knowledge,
database, { clear() {} } as never, {} as never, async () => {}, undefined, undefined,
database, {} as never, async () => {}, undefined, undefined,
undefined, undefined, application, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, { getPending: async () => undefined } as never)
const js = <T = unknown>(code: string): Promise<T> => win.webContents.executeJavaScript(code)
+6 -8
View File
@@ -150,7 +150,7 @@ app
await wait('document.querySelectorAll(".supervisor-activity__item").length === 3')
} else if (view === 'pending' || view === 'failure') {
await js('document.querySelector(".supervisor-workspace__toolbar .primary-button").click()')
await wait(view === 'pending' ? '!!document.querySelector(".supervisor-workspace [role=status]")' : '!!document.querySelector(".supervisor-workspace [role=alert]")')
await wait(`document.documentElement.dataset.reviewNoticeTone === '${view === 'pending' ? 'info' : 'error'}'`)
}
await js('document.fonts.ready')
for (const theme of ['light', 'dark']) {
@@ -171,7 +171,7 @@ app
tail:document.querySelector('.supervisor-workspace__prose')?.textContent.includes('长摘要末尾'),
controls:[...document.querySelectorAll('.supervisor-activity > .supervisor-workspace__action-bar > *')].map(e=>{const r=e.getBoundingClientRect();return {left:r.left,right:r.right,top:r.top};}),
toolbarControls:[...document.querySelectorAll('.supervisor-workspace__toolbar select, .supervisor-workspace__toolbar button')].map(e=>({disabled:e.disabled,text:e.textContent,top:e.getBoundingClientRect().top,bottom:e.getBoundingClientRect().bottom})),
notice:box('.supervisor-workspace__run-status'), dismiss:box('.supervisor-workspace__run-status .icon-button'),
notice:box('.supervisor-workspace__run-status'),
cancel:box('.supervisor-activity__item .danger-ghost') };
})()`)
assert(report.pageWidth <= width, 'No page overflow')
@@ -188,10 +188,8 @@ app
assert(report.prose.left - report.recap.left <= 25, 'No separately centered inner body')
if (view === 'pending') {
assert(report.toolbarControls.every(control => !control.disabled), 'Long review keeps toolbar enabled')
assert.equal(report.toolbarControls[2].text, '回顾当前进展', 'Start name remains stable')
assert(report.dismiss.width >= 28 && report.dismiss.height >= 28, 'Notice close target remains usable')
assert(report.dismiss.right <= report.notice.right && report.dismiss.bottom <= report.notice.bottom, 'Close remains inside notice')
assert(report.notice.right - report.dismiss.right <= 20, 'Notice close stays at the right edge on narrow screens')
assert.equal(report.toolbarControls[2].text, '回顾', 'Start name remains stable')
assert.equal(report.notice, undefined, 'Operation feedback does not occupy workspace layout')
if (width >= 1024) assert(Math.max(...report.toolbarControls.map(control => control.top)) - Math.min(...report.toolbarControls.map(control => control.top)) <= 4, 'Toolbar stays on one aligned row')
}
} else {
@@ -399,9 +397,9 @@ app
await js('document.querySelector(".supervisor-workspace__toolbar .primary-button").click(); document.querySelector(".page-shell").scrollTop = 0')
await settle()
assert(await js('document.querySelector(".supervisor-workspace__recap").textContent.includes("上期结果")'))
assert(await js('document.querySelector(".supervisor-workspace [role=status]").textContent.includes("新回顾正在整理")'))
assert(await js('!document.querySelector(".supervisor-workspace__run-status")'))
await writeFile(join(artifacts, 'recap-running-history-390.png'), (await win.webContents.capturePage()).toPNG())
await js('document.querySelector(".supervisor-workspace [role=status] button").click()')
await js('document.querySelector("#supervisor-tab-activity").click()')
await wait('document.querySelector("#supervisor-tab-activity").getAttribute("aria-selected") === "true"')
}
}
@@ -265,6 +265,7 @@ createRoot(document.getElementById('root')!).render(
/>
</div> : <PageShell variant="supervisor">
<HeartbeatCenter
onNotify={(notice) => { document.documentElement.dataset.reviewNoticeTone = notice.tone }}
applicationSettings={applicationSettingsSchema.parse({ checkUpdatesOnStartup: true, updateSource: 'github', modelDownloadSource: 'modelscope', localToolEnvironment: defaultLocalToolEnvironmentSettings, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false })}
onUpdateApplicationSettings={async () => true}
configs={menu || params.has('plan-only') ? [{ id: 'plan', name: '模拟每日回顾', scope: { kind: 'global' }, timezone: 'Asia/Shanghai', recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 48, retentionDays: 90, nextRunAt: '2026-09-24T01:00:00.000Z', createdAt, updatedAt: createdAt }] : []}
+1 -1
View File
@@ -37,7 +37,7 @@ app.whenReady().then(async () => {
{ getEnabledBuiltinMcpServerIds: async () => [] } as never, new ContextManager(),
{ snapshot: () => ({ libraries: [], sources: [], documents: [], entities: [], relations: [], evidence: [], tasks: [] }),
database: { externalStore: { listBindings: () => [] } }, external: { listInstances: () => [] } } as never,
database, { clear() {} } as never, {} as never, async () => {}, undefined, undefined, undefined, undefined, appSettings,
database, {} as never, async () => {}, undefined, undefined, undefined, undefined, appSettings,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
{ getPending: async () => undefined } as never)
const run = <T = unknown>(script: string): Promise<T> => win.webContents.executeJavaScript(script, true)
+123
View File
@@ -0,0 +1,123 @@
import { app, BrowserWindow, safeStorage } from 'electron'
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { registerIpcHandlers } from '../../src/main/ipc'
import { AssistantDatabase } from '../../src/main/assistant/assistant-database'
import { ApplicationSettingsStore } from '../../src/main/application-settings-store'
import { CapabilityService } from '../../src/main/capabilities/capability-service'
import { RuntimeSettingsStore } from '../../src/main/runtime-settings-store'
const directory = process.env.GB_MCP_DIRECTORY!
app.setPath('userData', join(directory, 'profile'))
app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion')
app.whenReady().then(async () => {
const win = new BrowserWindow({ show: true, width: 1280, height: 900, useContentSize: true,
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false,
backgroundThrottling: false, preload: join(directory, 'preload.cjs') } })
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
database.initialize(directory)
const conversationId = randomUUID()
database.saveLocalConversations([{ header: { id: conversationId, projectId: database.listProjects()[0]!.id,
title: 'Confirmation sentinel', updatedAt: Date.now() }, messages: [
{ id: randomUUID(), role: 'assistant', state: 'complete', createdAt: Date.now(), content: 'Keep until confirmed' }
] }])
const applicationSettings = new ApplicationSettingsStore(join(directory, 'application.json'))
await applicationSettings.update({ checkUpdatesOnStartup: false })
const cipher = { isAvailable: () => safeStorage.isEncryptionAvailable(),
encrypt: (value: string) => safeStorage.encryptString(value), decrypt: (value: Buffer) => safeStorage.decryptString(value) }
const settings = new RuntimeSettingsStore(join(directory, 'runtime.json'), cipher, {})
const capabilities = new CapabilityService(join(directory, 'capabilities.json'),
join(directory, 'builtin-skills'), join(directory, 'imported-skills'), cipher)
// App, preload, IPC, settings and SQLite are production; unrelated startup services are isolated.
const dispose = registerIpcHandlers(win,
{ capability: 'text', getStatus: async () => ({ available: true, name: 'UI regression', capability: 'text' }) } as never,
'CommandOrControl+Shift+Space', settings, capabilities,
{ clear() {}, cancelImport() {}, getDraft: () => [] } as never,
{ snapshot: () => ({ libraries: [], sources: [], documents: [], entities: [], relations: [], evidence: [], tasks: [] }),
database: { externalStore: { listBindings: () => [] } }, external: { listInstances: () => [] } } as never,
database, {} as never, async () => {}, undefined, undefined,
undefined, undefined, applicationSettings, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, { getPending: async () => undefined } as never)
const js = <T = unknown>(code: string): Promise<T> => win.webContents.executeJavaScript(code)
const wait = async (code: string): Promise<void> => {
for (let i = 0; i < 400; i++) {
if (await js(code)) return
await new Promise(resolve => setTimeout(resolve, 25))
}
throw new Error(`Timeout: ${code}\n${await js('document.body.innerText')}`)
}
const click = async (selector: string): Promise<void> => {
await wait(`!!document.querySelector(${JSON.stringify(selector)})`)
await js(`document.querySelector(${JSON.stringify(selector)}).scrollIntoView({block:'center'})`)
await js('new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))')
const point = await js<{ x: number; y: number; hit: boolean }>(`(() => {
const e=document.querySelector(${JSON.stringify(selector)}),r=e.getBoundingClientRect(),x=Math.round(r.x+r.width/2),y=Math.round(r.y+r.height/2);
return {x,y,hit:e.contains(document.elementFromPoint(x,y))}; })()`)
win.focus(); win.webContents.focus()
win.webContents.sendInputEvent({ type: 'mouseMove', x: point.x, y: point.y })
await wait(`(() => { const e=document.querySelector(${JSON.stringify(selector)}); return e.contains(document.elementFromPoint(${point.x},${point.y})); })()`)
win.webContents.sendInputEvent({ type: 'mouseDown', x: point.x, y: point.y, button: 'left', clickCount: 1 })
win.webContents.sendInputEvent({ type: 'mouseUp', x: point.x, y: point.y, button: 'left', clickCount: 1 })
}
const escape = async (): Promise<void> => {
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' })
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' })
}
const preserved = (): boolean => database.listConversationSummaries().some(item => item.id === conversationId)
const assertNoMode = async (selector: string): Promise<void> => {
assert(await js(`(() => {
const root=document.querySelector(${JSON.stringify(selector)});
return !!root && !root.querySelector('.composer-picker--mode, [name="workMode"], [name="defaultWorkMode"]') &&
![...root.querySelectorAll('button, label, [role="group"]')].some(e => /^(Ask|Execute|Plan|Default mode|Work mode)$/.test((e.getAttribute('aria-label') || e.textContent).trim()));
})()`), `Product work mode must be absent in ${selector}`)
}
try {
await win.loadURL(process.env.GB_MCP_URL!)
await wait('!!document.querySelector(".composer textarea") && document.body.innerText.includes("Keep until confirmed")')
await assertNoMode('.composer')
await click('.composer__options-trigger')
await wait('!!document.querySelector(".composer__options")')
await assertNoMode('.composer__options')
await escape()
await click('.project-switcher__trigger')
await click('.project-switcher__menu-settings')
await wait('!!document.querySelector(".project-create-card")')
await assertNoMode('.project-create-card')
await escape()
await wait('!document.querySelector(".project-create-card")')
await click('button[aria-label="Settings"]')
await wait('!!document.querySelector(".settings-tabs")')
assert(await js('!document.querySelector("#settings-tab-security")'))
await click('#settings-tab-platform-features')
const danger = '.settings-section--danger'
await wait(`!!document.querySelector('${danger} > .danger-button')`)
assert.equal(await js(`(() => {
let e=document.querySelector('#settings-tab-platform-features').previousElementSibling;
while (e && !e.matches('.settings-tabs__group-label')) e=e.previousElementSibling;
return e?.textContent.trim();
})()`), 'General')
assert(preserved())
await click(`${danger} > .danger-button`)
await wait(`!!document.querySelector('${danger} .danger-actions')`)
assert(preserved(), 'First click must not delete data')
await click(`${danger} .danger-actions .secondary-button`)
await wait(`!document.querySelector('${danger} .danger-actions')`)
assert(preserved(), 'Cancel must preserve data')
await click(`${danger} > .danger-button`)
await click(`${danger} .danger-actions .danger-button`)
await wait(`!document.querySelector('${danger} .danger-actions')`)
assert.equal(preserved(), false, 'Confirmed clear must reach production IPC and SQLite')
await win.loadURL(process.env.GB_MCP_URL!)
await wait('!!document.querySelector(".composer textarea")')
assert.equal(preserved(), false)
assert(await js('!document.body.innerText.includes("Keep until confirmed")'))
const modelAttempts = (globalThis as typeof globalThis & { overlayModelAttempts?: number }).overlayModelAttempts ?? 0
assert.equal(modelAttempts, 0)
await writeFile(join(directory, 'result.json'), JSON.stringify({ composer: true, project: true,
securityAbsent: true, generalClearData: true, cancelPreservedData: true, confirmedClear: true,
persistedAfterReload: true, modelAttempts }))
} finally { await dispose(); win.destroy(); database.close() }
app.exit(0)
}).catch(error => { console.error(error); app.exit(1) })
@@ -128,7 +128,7 @@ app.whenReady().then(async () => {
await settle()
const scroll = "document.querySelector('.workbar-shell__tab-scroll')"
for (const many of [false, true]) {
if (many) { await click('#many-tabs'); await wait('document.querySelectorAll("[role=tab]").length===16') }
if (many) { await click('#many-tabs'); await wait('document.querySelectorAll("[role=tab]").length===15') }
await addVisible()
assert(await js(`(() => {
const s=${scroll}.getBoundingClientRect(),buttons=[...document.querySelectorAll('.workbar-shell__scroll-button')];
@@ -162,7 +162,7 @@ app.whenReady().then(async () => {
await click('.workbar-shell__add')
await wait('!!document.querySelector(".workbar-shell__catalog")')
await click('.workbar-shell__catalog-choice:has(.workbar-shell__app-icon--browser)')
await wait('document.querySelectorAll("[role=tab]").length===17 && !document.querySelector(".workbar-shell__catalog")')
await wait('document.querySelectorAll("[role=tab]").length===16 && !document.querySelector(".workbar-shell__catalog")')
await addVisible()
await key('Left')
await wait('document.querySelector(".workbar-shell__tab-item:nth-last-child(2) [role=tab]").getAttribute("aria-selected")==="true"')
@@ -175,8 +175,8 @@ app.whenReady().then(async () => {
await wait('!document.querySelector(".workbar-shell__scroll-button:last-child").disabled')
const closingId = await js('document.querySelector(".workbar-shell__tab-item--active [role=tab]").id')
await click('.workbar-shell__tab-item--active .workbar-shell__tab-close')
await wait(`document.querySelectorAll('[role=tab]').length===16 && !document.getElementById(${JSON.stringify(closingId)})`)
observations.push({ locale, theme, workbar: 'default and 16 long tabs; native add/catalog/create, arrows, Home/End, close at right viewport edge with right arrow enabled' })
await wait(`document.querySelectorAll('[role=tab]').length===15 && !document.getElementById(${JSON.stringify(closingId)})`)
observations.push({ locale, theme, workbar: 'default and 15 long tabs; native add/catalog/create, arrows, Home/End, close at right viewport edge with right arrow enabled' })
await win.loadURL(`${process.env.GB_LAYOUT_URL}?surface=settings&locale=${locale}&theme=${theme}`)
await wait('!!document.querySelector(".model-connection-detail select")')
@@ -189,7 +189,7 @@ app.whenReady().then(async () => {
font: getComputedStyle(nav.querySelector('strong')).fontSize };
})()`)
assert.deepEqual(navigation.ids, ['appearance', 'platform-features', 'model', 'context-control', 'runtime',
'document-parsing', 'channels', 'roles', 'capabilities', 'security', 'about'].map(id => `settings-tab-${id}`))
'document-parsing', 'channels', 'roles', 'capabilities', 'about'].map(id => `settings-tab-${id}`))
assert.equal(navigation.groups, 4)
assert.equal(navigation.descriptions, 0)
assert.equal(navigation.font, '13px')
@@ -41,6 +41,8 @@ app.whenReady().then(async () => {
const click = async selector => {
win.focus(); win.webContents.focus()
await wait('document.hasFocus()')
await js(`document.querySelector(${JSON.stringify(selector)}).scrollIntoView({block:'nearest',inline:'nearest'})`)
await settle()
const p = await point(selector)
win.webContents.sendInputEvent({ type: 'mouseMove', x: p.x, y: p.y })
await settle()
@@ -70,7 +72,7 @@ app.whenReady().then(async () => {
assert(after.controls.every(c => c.hit && c.outside && c.height > 0), 'Docked controls must remain visible and hit-testable')
assert.equal(after.bodyScroll, 0, 'Sidebar body must not scroll with files')
assert(after.bodyOverflow <= 1, 'Files must not overflow the sidebar body')
assert(after.horizontalOverflow.every(value => value <= 1), 'Unexpected horizontal overflow')
assert(after.horizontalOverflow.slice(0, 3).every(value => value <= 1), 'Files must scroll inside their viewport without widening the sidebar')
assert(after.viewport.bottom <= after.nav.bottom + 1, 'Files viewport exceeds navigation bounds')
assert(after.viewport.y >= Math.max(...after.fixed.filter(Boolean).map(r => r.bottom)) - 1, 'Controls overlap files viewport')
}
@@ -138,8 +140,9 @@ app.whenReady().then(async () => {
assert.equal(await js(`${viewport}.scrollTop`), after.scrollTop, 'Git view return must retain files scroll')
docked(before, await geometry())
}
for (const index of [1, 2]) {
await click(`.workspace-files__actions button:nth-child(${index})`)
await js(`${viewport}.scrollLeft=0`)
for (const label of ['New file', 'New directory']) {
await click(`.workspace-files__actions button[aria-label="${label}"]`)
await wait('!!document.querySelector("[role=dialog]")')
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' })
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Escape' })
+1 -1
View File
@@ -15,7 +15,7 @@ const listDirectory = async (path: string): Promise<WorkspaceDirectoryListing> =
...(!path ? [{ name: 'documents', path: 'documents', type: 'directory' as const }] : []),
...Array.from({ length: 120 }, (_, index) => {
const name = `file-${String(index + 1).padStart(3, '0')}-workspace-notes.md`
return { name, path: path ? `${path}/${name}` : name, type: 'file' as const }
return { name, path: path ? `${path}/${name}` : name, type: 'file' as const, modifiedAt: '2026-10-04T10:00:00.000Z', createdAt: '2026-10-01T10:00:00.000Z' }
})
]
})
+76
View File
@@ -0,0 +1,76 @@
import { app, BrowserWindow, dialog } from 'electron'
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { registerIpcHandlers } from '../../src/main/ipc'
import { AssistantDatabase } from '../../src/main/assistant/assistant-database'
import { ApplicationSettingsStore } from '../../src/main/application-settings-store'
import { defaultRuntimeSettings } from '../../src/shared/contracts'
const directory = process.env.GB_IMPORT_DIRECTORY!
app.setPath('userData', join(directory, 'profile'))
app.whenReady().then(async () => {
const win = new BrowserWindow({ show: true, width: 1400, height: 900, webPreferences: {
sandbox: true, contextIsolation: true, nodeIntegration: false, backgroundThrottling: false, preload: join(directory, 'preload.cjs')
} })
const database = new AssistantDatabase(join(directory, 'assistant.sqlite'))
const workspace = join(directory, 'workspace'); await mkdir(workspace)
database.initialize(workspace)
const project = database.listProjects()[0]!
await mkdir(join(project.rootPath, 'selected'))
const source = join(directory, 'source'); await mkdir(source)
const bytes = Buffer.alloc(700001).map((_, index) => index % 256)
await writeFile(join(source, 'import.bin'), bytes)
const image = database.createImageArtifact({ projectId: project.id, title: 'Existing conversation image', mimeType: 'image/png', base64: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=' })
database.saveLocalConversations([{ header: { id: randomUUID(), projectId: project.id, title: 'Workspace import', updatedAt: Date.now() },
messages: [{ id: randomUUID(), role: 'assistant', state: 'complete', content: 'Existing conversation image', createdAt: Date.now(), artifactIds: [image.id] }] }])
const applicationSettings = new ApplicationSettingsStore(join(directory, 'application.json'))
await applicationSettings.update({ checkUpdatesOnStartup: false })
const settings = { ...defaultRuntimeSettings, workspacePath: project.rootPath, modelProfiles: [], embeddingConnections: [] }
const dispose = registerIpcHandlers(win, { capability: 'text', getStatus: async () => ({ available: true, name: 'Import test', capability: 'text' }) } as never,
'CommandOrControl+Shift+Space', { getResolvedSettings: async () => settings, getPublicSettings: async () => settings } as never,
{} as never, { clear() {}, cancelImport() {}, getDraft: () => [] } as never,
{ snapshot: () => ({ libraries: [], sources: [], documents: [], entities: [], relations: [], evidence: [], tasks: [] }), database: { externalStore: { listBindings: () => [] } }, external: { listInstances: () => [] } } as never,
database, {} as never, async () => {}, undefined, undefined, undefined, undefined, applicationSettings,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, { getPending: async () => undefined } as never)
let picks = 0
// Only the native chooser is deterministic; App, preload, IPC and filesystem are production.
dialog.showOpenDialog = (async () => { picks++; return { canceled: picks === 3, filePaths: picks === 3 ? [] : [join(source, 'import.bin')] } }) as typeof dialog.showOpenDialog
const run = <T = unknown>(code: string): Promise<T> => win.webContents.executeJavaScript(code, true)
const wait = async (code: string): Promise<void> => {
for (let i = 0; i < 300; i++) { if (await run(code)) return; await new Promise(resolve => setTimeout(resolve, 50)) }
throw new Error(`Timed out: ${code}`)
}
const button = (label: string): string => `[...document.querySelectorAll('button')].find(e=>(e.getAttribute('aria-label')||e.textContent.trim())===${JSON.stringify(label)}&&e.getClientRects().length)`
const click = async (label: string): Promise<void> => { await wait(`Boolean(${button(label)})`); await run(`${button(label)}.click()`) }
try {
await win.loadURL(process.env.GB_IMPORT_URL!)
await wait('Boolean(document.querySelector(".message-generated-image img"))')
await wait('document.querySelector(".message-generated-image img").naturalWidth===1')
assert.equal(await run('[...document.querySelectorAll("[role=tab]")].some(e=>e.textContent==="Results")'), false)
if (await run('document.querySelector(".assistant-sidebar-toggle").getAttribute("aria-expanded")==="false"')) await click('Toggle assistant workspace')
await click('Files')
await click('selected')
await wait('document.body.innerText.includes("Destination: selected")')
await click('Import files')
await wait('document.body.innerText.includes("Imported 1 files into selected.")')
await wait('Boolean([...document.querySelectorAll(".workspace-files__row")].find(e=>e.title==="selected/import.bin"))')
assert.deepEqual(await readFile(join(project.rootPath, 'selected/import.bin')), bytes)
assert.deepEqual(await readdir(project.rootPath), ['selected'])
assert.equal(database.listArtifacts().length, 1)
assert.equal(database.getArtifact(image.id).content, image.content)
await click('Import files')
await wait('document.body.innerText.includes("EEXIST")')
assert.deepEqual(await readFile(join(project.rootPath, 'selected/import.bin')), bytes)
await click('Import files')
await wait(`!${button('Import files')}.disabled`)
await win.reload()
await wait('Boolean(document.querySelector(".message-generated-image img")) && document.querySelector(".message-generated-image img").naturalWidth===1')
await writeFile(join(directory, 'result.json'), JSON.stringify({ passed: true, picks, bytes: bytes.length, imageAfterReload: true, artifactCount: database.listArtifacts().length, modelCalls: 0 }))
await dispose(); database.close(); win.destroy(); app.exit(0)
} catch (error) {
await writeFile(join(directory, 'result.json'), JSON.stringify({ error: String(error) }))
console.error(error); app.exit(1)
}
}).catch(error => { console.error(error); app.exit(1) })