v0.6.0: beautifului 整合 + 会话/工作区/队列/权限/命令/附件全链路

- beautifului.dev 组件直连:Sidebar Nav、Tool Chips、Approval Card(抽屉化)、Streaming/Thinking、Code Block、Selection Actions、Prompt Bar、Search
This commit is contained in:
Rick Ye
2026-09-06 09:57:14 +08:00
parent 3c2e9e96c5
commit a5951849c8
96 changed files with 11828 additions and 4649 deletions
+263
View File
@@ -0,0 +1,263 @@
// 真实 App 渲染层截图(离屏,注入假 IPC 数据):
// npx electron scripts/capture-app.cjs
// 产出 preview-shots/app-*.png:展开态 / 收起态 / markdown 渲染
const { app, BrowserWindow, ipcMain } = require('electron')
const { mkdirSync, writeFileSync, existsSync } = require('node:fs')
const { join } = require('node:path')
app.commandLine.appendSwitch('disable-gpu')
const ROOT = join(__dirname, '..')
const OUT = join(ROOT, 'preview-shots')
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const MARKDOWN_SAMPLE = [
'## 1. "这是演示数据,仅用于验证链路"',
'',
'我配置的 `hotword_lookup` 工具目前连的是**演示实现(demo)**,[项目主页](https://example.com)。',
'',
'| 说明 | 结论 |',
'| :--- | ---: |',
'| **工具可用** | 工具这个"壳"是好的 —— 链路没有问题 |',
'| **数据源是空的** | 它背后的"仓库"是空的,查询返回"无匹配项" |',
'',
'- 列表项一,包含 `inline code`',
'- 列表项二,**加粗** 与 ~~删除线~~',
'',
'```python',
"print('Hello llm-ui!')",
'for i in range(3):',
' print(i)',
'```',
'',
'```rust',
'fn main() {',
' println!("Hello, World!");',
'}',
'```'
].join('\n')
const FAKE_STATE = {
sessionId: 's1',
sessionName: 'hi',
streaming: false,
activeTools: [],
messageCount: 2,
workspace: 'C:\\fake\\workspace'
}
const FAKE_SESSIONS = [
{ id: 's1', path: 'C:\\fake\\s1.jsonl', name: 'hi', firstMessage: '帮我写一个hello world', created: '2026-01-01', modified: '2026-01-02', messageCount: 2 },
{ id: 's2', path: 'C:\\fake\\s2.jsonl', name: 'hello world', firstMessage: '帮我写一个hello world', created: '2026-01-01', modified: '2026-01-02', messageCount: 1 }
]
function registerFakeIpc() {
ipcMain.handle('agent:state', () => FAKE_STATE)
ipcMain.handle('sessions:list', () => FAKE_SESSIONS)
ipcMain.handle('workspaces:list', () => [{ name: 'Default', path: 'C:\\fake\\workspace' }])
ipcMain.handle('models:catalog', () => ({ providers: [] }))
ipcMain.handle('settings:get', () => ({ systemPrompt: '' }))
ipcMain.handle('app:versions', () => ({ app: 'dev', electron: 'x', node: 'x', chrome: 'x', user: 'x' }))
ipcMain.handle('agent:send', () => undefined)
ipcMain.handle('chat:resources', () => ({
commands: [{ name: 'daily-report', description: '生成每日经营日报', kind: 'prompt' }],
skills: [{ name: 'hotword-analysis', description: '分析近 7 天类目热点词' }]
}))
ipcMain.handle('files:list', () => [
{ path: 'data/products.csv', name: 'products.csv', isDir: false },
{ path: 'docs/sop.md', name: 'sop.md', isDir: false },
{ path: 'hello_world.py', name: 'hello_world.py', isDir: false },
{ path: 'assets', name: 'assets', isDir: true }
])
}
async function capture(win, name) {
// 离屏窗口 capturePage 常给旧帧:连拍多张,取最后一张(大概率已提交新帧)
let image = null
for (let k = 0; k < 4; k += 1) {
win.webContents.invalidate()
await sleep(320)
image = await win.webContents.capturePage()
}
writeFileSync(join(OUT, name), image.toPNG())
console.log('saved', name, `${image.getSize().width}x${image.getSize().height}`)
}
async function run() {
mkdirSync(OUT, { recursive: true })
registerFakeIpc()
const preload = join(ROOT, 'out', 'preload', 'index.cjs')
const win = new BrowserWindow({
width: 1280,
height: 900,
show: false,
webPreferences: {
paintWhenInitiallyHidden: true,
backgroundThrottling: false,
preload: existsSync(preload) ? preload : undefined,
contextIsolation: true
}
})
await win.loadFile(join(ROOT, 'out', 'renderer', 'index.html'))
// 清掉历史 localStorage(收起态持久化),再重载让侧栏以展开态启动
await win.webContents.executeJavaScript('try { localStorage.clear() } catch {}')
await win.reload()
await sleep(1200)
// 注入一条含 markdown 的会话(user + assistant)
win.webContents.send('agent:event', {
type: 'session_switched',
state: FAKE_STATE,
messages: [
{ role: 'user', content: '解释一下那两句话的意思' },
{ role: 'assistant', content: [{ type: 'text', text: MARKDOWN_SAMPLE }] }
],
activeTools: {}
})
await sleep(1200)
console.log(
'hl-diag:',
await win.webContents.executeJavaScript(
`(() => { const codes = [...document.querySelectorAll('code')]; return JSON.stringify({ ready: window.__shikiReady, codes: codes.map((c) => c.querySelectorAll('span[style]').length) }) })()`
)
)
await capture(win, 'app-markdown.png')
// 注入排队消息场景(streaming + 队列两条)
win.webContents.send('agent:event', { type: 'agent_start' })
win.webContents.send('agent:event', {
type: 'queue_update',
steering: ['设置里面做成这样,分成基础设置、Agent 能力'],
followUp: ['随后帮我补上skills、命令和附件的支持,chatbox里面/调起命令或skill,@则是引用文件,加号是附件']
})
await sleep(900)
win.webContents.invalidate()
await sleep(300)
FAKE_STATE.permissionMode = 'confirm'
await capture(win, 'app-queue.png')
// 权限模式菜单
await win.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`)
win.webContents.send('agent:event', { type: 'agent_end' })
win.webContents.send('agent:event', { type: 'queue_update', steering: [], followUp: [] })
await sleep(500)
await win.webContents.executeJavaScript(
`[...document.querySelectorAll('button')].find((b) => b.title.startsWith('权限模式'))?.click()`
)
await sleep(500)
win.webContents.invalidate()
await sleep(300)
console.log(
'perm-diag:',
await win.webContents.executeJavaScript(
`(() => { const panels = [...document.querySelectorAll('div.fixed.z-40')]; const m = panels.find((d) => d.textContent.includes('减少确认次数')); if (!m) return 'closed'; const r = m.getBoundingClientRect(); const cs = getComputedStyle(m); return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, opacity: cs.opacity, display: cs.display, zIndex: cs.zIndex, buttons: m.querySelectorAll('button').length }) })()`
)
)
await capture(win, 'app-permissions.png')
// 结束流式,恢复常规状态
win.webContents.send('agent:event', { type: 'agent_end' })
win.webContents.send('agent:event', { type: 'queue_update', steering: [], followUp: [] })
await sleep(400)
// "/" 命令/技能面板
const typeInPrompt = (text) =>
win.webContents.executeJavaScript(
`(() => { const ta = document.querySelector('textarea[aria-label="Prompt"]'); if (!ta) return 'no-ta'; const set = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; set.call(ta, ${JSON.stringify(text)}); ta.dispatchEvent(new Event('input', { bubbles: true })); return 'ok' })()`
)
await typeInPrompt('/')
await sleep(600)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-slash-menu.png')
// "@ " 文件引用
await typeInPrompt('帮我看看 @')
await sleep(600)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-at-menu.png')
await typeInPrompt('')
// 注入审批卡
win.webContents.send('agent:event', {
type: 'approval_request',
request: {
id: 'approval-1',
toolName: 'write',
args: { path: 'hello_world.py', content: 'def main():\n print("Hello world")\n\nmain()' }
}
})
await sleep(800)
win.webContents.invalidate()
await sleep(400)
await capture(win, 'app-approval.png')
// 行内操作浮层:悬停操作按钮点击后弹出(重命名 / 删除)
await win.webContents.executeJavaScript(
`[...document.querySelectorAll('button')].find((b) => b.title === '重命名会话')?.click()`
)
await sleep(500)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-row-rename.png')
await win.webContents.executeJavaScript(
`[...document.querySelectorAll('button')].find((b) => b.title === '删除会话')?.click()`
)
await sleep(500)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-row-delete.png')
await win.webContents.executeJavaScript(`document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))`)
// 设置页:Ctrl+, 打开,截「模型设置」与「常规」
await win.webContents.executeJavaScript(
`window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', ctrlKey: true, bubbles: true }))`
)
await sleep(600)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-settings-model.png')
await win.webContents.executeJavaScript(
`[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '常规')?.click()`
)
await sleep(400)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-settings-general.png')
await win.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`)
console.log(
'diag:',
await win.webContents.executeJavaScript(
`(() => { const btn = document.querySelector('button[aria-label="拒绝并关闭"]'); if (!btn) return 'missing'; const card = btn.closest('div[class*="rounded-card"]') ?? btn.parentElement; const r = card.getBoundingClientRect(); return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, winH: innerHeight, opacity: getComputedStyle(card).opacity }) })()`
)
)
// 侧栏收起回归检查
const collapsed = await win.webContents.executeJavaScript(
`(() => { const b = document.querySelector('[aria-label="折叠侧栏"]'); if (!b) return 'no-button'; b.click(); return 'clicked' })()`
)
console.log('collapse:', collapsed)
await sleep(800)
const geo = await win.webContents.executeJavaScript(
`(() => {
const aside = document.querySelector('aside[aria-label="工作区导航"]')
const row = document.querySelector('.sidebar-row')
const icon = row ? row.querySelector('span') : null
if (!aside || !row || !icon) return 'missing'
const a = aside.getBoundingClientRect()
const i = icon.getBoundingClientRect()
return JSON.stringify({ rail: Math.round(a.width), iconCenter: Math.round(i.left - a.left + i.width / 2) })
})()`
)
console.log('collapsed-geo:', geo)
await capture(win, 'app-collapsed.png')
app.exit(0)
}
app.whenReady().then(run).catch((err) => {
console.error(err)
app.exit(1)
})