From eb8177bc22be5a3f6b0cd876a3b6359012cadea1 Mon Sep 17 00:00:00 2001 From: Aisland Date: Thu, 20 Aug 2026 00:22:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20busy=20state=20on=20bind=20dots=20?= =?UTF-8?q?=E2=80=94=20bound=20conversation=20running=20shows=20alternatin?= =?UTF-8?q?g=20blinking=20blue=20glow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01_content/src/client/index.tsx | 14 ++- 01_content/src/client/locales.ts | 2 + 01_content/src/client/styles.ts | 12 ++- .../worklogs/2026-08-16_worktable-v2.md | 13 +++ 04_test/probe-busy.cjs | 91 +++++++++++++++++++ AGENTS.md | 3 + 6 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 04_test/probe-busy.cjs diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index 5f82f4e..02890a8 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -1043,12 +1043,16 @@ function buildCustomLayoutPrompt(req: string): string { }, []) /** 项目 → 提醒态:绑定会话 completed=done / pendingInteraction=need(ack 过则不亮) */ - const bindNotifyMap: Record = useMemo(() => { - const map: Record = {} + const bindNotifyMap: Record = useMemo(() => { + const map: Record = {} const byId = sessionsSnapshotStore.snapshot?.byId ?? {} const ack = loadNotifyAck() for (const [pid, sid] of Object.entries(projects.bindings)) { - const state = sessionNotifyState(byId[sid]) + const e = byId[sid] + if (!e) continue + // 优先级:工作中(蓝色交替闪烁)> 完成(绿)> 待决(黄) + if (e.running === true) { map[pid] = 'busy'; continue } + const state = sessionNotifyState(e) if (state && ack[sid] !== state) map[pid] = state } return map @@ -1361,7 +1365,7 @@ function buildCustomLayoutPrompt(req: string): string { const arrow = kids.find((k: any) => k.tagName === 'SPAN' && String((k as HTMLElement).textContent ?? '').trim() === '›') if (arrow) (arrow as HTMLElement).classList.add('dsh-wt_resArrow') const tip = bound - ? t('bind.tipBound', { name: boundSessionTitle(bound) }) + (bindNotifyMap[id] === 'done' ? t('bind.tipDone') : bindNotifyMap[id] === 'need' ? t('bind.tipNeed') : '') + ? t('bind.tipBound', { name: boundSessionTitle(bound) }) + (bindNotifyMap[id] === 'busy' ? t('bind.tipBusy') : bindNotifyMap[id] === 'done' ? t('bind.tipDone') : bindNotifyMap[id] === 'need' ? t('bind.tipNeed') : '') : t('bind.tipUnbound') bindBtn.setAttribute('data-tip', tip) bindBtn.setAttribute('aria-label', tip) @@ -1939,7 +1943,7 @@ function buildCustomLayoutPrompt(req: string): string { tabIndex={0} data-bound={bindNotifyMap[l.id] ?? (projects.bindings[l.id] ? 'true' : 'false')} data-tip={projects.bindings[l.id] - ? t('bind.tipBound', { name: boundSessionTitle(projects.bindings[l.id]) }) + (bindNotifyMap[l.id] === 'done' ? t('bind.tipDone') : bindNotifyMap[l.id] === 'need' ? t('bind.tipNeed') : '') + ? t('bind.tipBound', { name: boundSessionTitle(projects.bindings[l.id]) }) + (bindNotifyMap[l.id] === 'busy' ? t('bind.tipBusy') : bindNotifyMap[l.id] === 'done' ? t('bind.tipDone') : bindNotifyMap[l.id] === 'need' ? t('bind.tipNeed') : '') : t('bind.tipUnbound')} aria-label={projects.bindings[l.id] ? t('bind.tipBound', { name: boundSessionTitle(projects.bindings[l.id]) }) : t('bind.tipUnbound')} onClick={(e) => { e.stopPropagation(); openBindPick(l.id, e.currentTarget as HTMLElement) }} diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts index ef9ceaf..fee1d52 100644 --- a/01_content/src/client/locales.ts +++ b/01_content/src/client/locales.ts @@ -94,6 +94,7 @@ export const zh = { 'bind.titleBound': '已绑定对话(●●):点击更换或解绑', 'bind.tipUnbound': '未绑定对话:点击选择要跟随的对话', 'bind.tipDone': '(任务已完成,点击项目查看)', + 'bind.tipBusy': '(对话正在处理任务…)', 'bind.tipNeed': '(需要你的决定,点击项目处理)', 'bind.folder': '项目文件夹', 'bind.folderChange': '更改', @@ -226,6 +227,7 @@ export const en = { 'bind.titleBound': 'Conversation bound (●●): click to change or unbind', 'bind.tipUnbound': 'No conversation bound — click to pick one', 'bind.tipDone': ' (task completed — click the project to view)', + 'bind.tipBusy': ' (conversation is working on the task…)', 'bind.tipNeed': ' (needs your decision — click the project to handle)', 'bind.folder': 'Project folder', 'bind.folderChange': 'Change', diff --git a/01_content/src/client/styles.ts b/01_content/src/client/styles.ts index 4af3bd7..c0b7705 100644 --- a/01_content/src/client/styles.ts +++ b/01_content/src/client/styles.ts @@ -159,17 +159,23 @@ export const css = [ '.dsh-wt_bindBtn{position:absolute;right:26px;top:50%;transform:translateY(-50%);width:22px;height:22px;margin:0;display:block;border-radius:6px;color:var(--dsw-alias-label-secondary,#9aa4b2);cursor:pointer;user-select:none}', '.dsh-wt_bindBtn:hover{background:var(--dsw-alias-fill-l2,rgba(255,255,255,.09));color:var(--dsw-alias-label-primary,#e6e8eb)}', '.dsh-wt_bindBtn[data-bound=true]{color:var(--dsw-alias-label-primary,#e6e8eb)}', - // 任务完成提醒镜像:绿色发光 / 待决提醒:黄色发光(点击项目即 ack 恢复常态实心) + // 任务完成提醒镜像:绿色发光 / 待决提醒:黄色发光(点击项目即 ack 恢复常态实心); + // 工作中:蓝色发光 + 两圆交替亮灭(对应 DSH 对话的转圈标记,无需 ack,running 变 false 自动停) '.dsh-wt_bindBtn[data-bound=done]{color:#3fb950}', '.dsh-wt_bindBtn[data-bound=need]{color:#d29922}', - '.dsh-wt_bindBtn[data-bound=done] .dsh-wt_bindCircles,.dsh-wt_bindBtn[data-bound=need] .dsh-wt_bindCircles{filter:drop-shadow(0 0 3px currentColor)}', + '.dsh-wt_bindBtn[data-bound=busy]{color:#4f8ef7}', + '@keyframes dsh-wt-busyA{0%,44%{opacity:1}50%,94%{opacity:.12}100%{opacity:1}}', + '@keyframes dsh-wt-busyB{0%,44%{opacity:.12}50%,94%{opacity:1}100%{opacity:.12}}', + '.dsh-wt_bindBtn[data-bound=busy] .dsh-wt_bindCircles::before{animation:dsh-wt-busyA 1s ease-in-out infinite}', + '.dsh-wt_bindBtn[data-bound=busy] .dsh-wt_bindCircles::after{animation:dsh-wt-busyB 1s ease-in-out infinite}', + '.dsh-wt_bindBtn[data-bound=done] .dsh-wt_bindCircles,.dsh-wt_bindBtn[data-bound=need] .dsh-wt_bindCircles,.dsh-wt_bindBtn[data-bound=busy] .dsh-wt_bindCircles{filter:drop-shadow(0 0 3px currentColor)}', // 气泡为 body 级独立元素(.dsh-wt_bindTip),此处只保留按钮本体样式 '.dsh-wt_bindTip{position:fixed;transform:translateY(-50%);padding:5px 9px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:6px;background:var(--dsw-alias-fill-l2,#171b22);color:var(--dsw-alias-label-primary,#e6e8eb);font-size:11px;line-height:15px;white-space:nowrap;pointer-events:none;z-index:1200;box-shadow:0 4px 12px rgba(0,0,0,.35);display:none}', '.dsh-wt_bindCircles{position:absolute;left:50%;top:50%;width:17px;height:7px;transform:translate(-50%,-50%)}', '.dsh-wt_bindCircles::before,.dsh-wt_bindCircles::after{content:\'\';position:absolute;top:50%;width:6px;height:6px;margin-top:-3px;border-radius:50%;box-sizing:border-box;border:1.2px solid currentColor;background:transparent}', '.dsh-wt_bindCircles::before{left:0}', '.dsh-wt_bindCircles::after{left:10px}', - '.dsh-wt_bindBtn[data-bound=true] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=true] .dsh-wt_bindCircles::after,.dsh-wt_bindBtn[data-bound=done] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=done] .dsh-wt_bindCircles::after,.dsh-wt_bindBtn[data-bound=need] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=need] .dsh-wt_bindCircles::after{background:currentColor}', + '.dsh-wt_bindBtn[data-bound=true] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=true] .dsh-wt_bindCircles::after,.dsh-wt_bindBtn[data-bound=done] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=done] .dsh-wt_bindCircles::after,.dsh-wt_bindBtn[data-bound=need] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=need] .dsh-wt_bindCircles::after,.dsh-wt_bindBtn[data-bound=busy] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=busy] .dsh-wt_bindCircles::after{background:currentColor}', // 布局条目卡片 '.dsh-wt_layout{display:flex;align-items:center;gap:7px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02));color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-align:left}', diff --git a/02_process/worklogs/2026-08-16_worktable-v2.md b/02_process/worklogs/2026-08-16_worktable-v2.md index e0a393b..c8f8240 100644 --- a/02_process/worklogs/2026-08-16_worktable-v2.md +++ b/02_process/worklogs/2026-08-16_worktable-v2.md @@ -977,6 +977,19 @@ - 备注:模板路由为服务端改动,需重启 dsh web 后生效(重启后浏览器可直开 /api/worktable/template/dshell.html 预览模板)。 +## 补记(绑定圆点新增「工作中」状态:蓝色交替闪烁) + +- 用户要求:绑定对话正在处理任务时(DSH 原生转圈标记同款场景),双圆点蓝色发光、两个点 + 交替亮灭。 +- 实现:bindNotifyMap 增 'busy'(byId[sid].running === true,优先级 busy>done>need,无 ack); + 卡片 data-bound=busy;CSS 蓝色 #4f8ef7 + drop-shadow 发光 + dsh-wt-busyA/B 关键帧(1s + ease-in-out,两圆相位差半周期交替亮灭);hover 气泡追加「对话正在处理任务…」。 +- 验证(probe-busy.cjs,桩注入 running):busyAttr=busy、色 rgb(79,142,247)、beforeAnim= + dsh-wt-busyA/afterAnim=dsh-wt-busyB、glow/fill 齐备、tip 含「处理任务」✓;running=false+ + completed=true → 切回 done 绿 ✓;functional-diag 全 20 STEP ERROR_COUNT: 0。 +- 备注:纯客户端改动,F5 即生效。 + + diff --git a/04_test/probe-busy.cjs b/04_test/probe-busy.cjs new file mode 100644 index 0000000..eac7192 --- /dev/null +++ b/04_test/probe-busy.cjs @@ -0,0 +1,91 @@ +// probe-busy.cjs — 工作中状态:running=true → 双圆蓝色发光交替闪烁(动画名 + 颜色验证) +const http = require('http'); +const { spawn } = require('child_process'); +const WebSocket = require('ws'); +const PORT = 9357; +const proc = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [ + '--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', + '--window-size=1440,900', '--force-device-scale-factor=1', + '--remote-debugging-port=' + PORT, + '--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-busy', + 'about:blank', +], { stdio: 'ignore' }); +const getJSON = (p) => new Promise((res, rej) => { + http.get({ host: '127.0.0.1', port: PORT, path: p }, (r) => { let d=''; r.on('data',c=>d+=c); r.on('end',()=>{try{res(JSON.parse(d))}catch(e){rej(e)}}) }).on('error', rej); +}); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +(async () => { + let list = null; + for (let i = 0; i < 40; i++) { try { list = await getJSON('/json/list'); if (list && list.length) break; } catch {} await sleep(500); } + const target = list.find((t) => t.type === 'page') || list[0]; + const ws = new WebSocket(target.webSocketDebuggerUrl); + let id = 0; const pending = new Map(); + const send = (method, params) => new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); }); + ws.on('message', (raw) => { const m = JSON.parse(String(raw)); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } }); + await new Promise((r) => ws.on('open', r)); + await send('Runtime.enable'); + await send('Page.enable'); + await send('Page.addScriptToEvaluateOnNewDocument', { + source: "try{ if(window===window.top && location.origin==='http://127.0.0.1:3080'){ localStorage.setItem('dsh.worktable.projects.v1', JSON.stringify({order:[],lastUsed:{},hidden:[],nameOverrides:{},shortcuts:[],layouts:[{id:'t-layout',title:'L',top:null,left:null,main:[{id:'p1',title:'W',min:200,content:null}],chatWidth:{default:320,min:240,max:600},chatSide:'right',chatFullHeight:false}],bindings:{},folders:{}})); localStorage.removeItem('dsh.worktable.notifyAck.v1'); } }catch(e){}", + }); + await send('Page.navigate', { url: 'http://127.0.0.1:3080/' }); + const evaluate = async (expr) => { + const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); + if (r.result && r.result.exceptionDetails) return { __error: (r.result.exceptionDetails.exception || {}).description || r.result.exceptionDetails.text }; + return r.result && r.result.result ? r.result.result.value : undefined; + }; + let ready = null; + for (let i = 0; i < 30; i++) { await sleep(1000); ready = await evaluate("!!window.__dshWorktable && !!window.__dshSyncSessionScope"); if (ready === true) break; } + await sleep(1500); + const step = await evaluate(`(async function(){ + var out = {}; + var sleep = function(ms){ return new Promise(function(r){ setTimeout(r, ms); }); }; + var vis = function(sel){ return [].slice.call(document.querySelectorAll(sel)).find(function(el){ return (getComputedStyle(el).visibility!=='hidden'&&el.getBoundingClientRect().height>0); }); }; + try { + var S = window.__dshSessions; + var card = document.querySelector('.dsh-wt_layout'); + var bbtn = card.querySelector('.dsh-wt_bindBtn'); + bbtn.click(); + await sleep(900); + var items = [].slice.call(document.querySelectorAll('.dsh-wt_bindList .dsh-wt_selectItem')); + if (items[0]) { items[0].click(); } + await sleep(800); + var boundId = JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).bindings['t-layout']; + var listStore = S.list; + var orig = listStore.getSnapshot.bind(listStore); + var fake = function(patch){ return function(){ var s = orig(); var e = s.byId && s.byId[boundId]; if (e) { s.byId = Object.assign({}, s.byId); s.byId[boundId] = Object.assign({}, e, patch); } return s; }; }; + // running=true → busy + listStore.getSnapshot = fake({ running: true }); + window.__dshSyncSessionScope(); + await sleep(600); + var btn = card.querySelector('.dsh-wt_bindBtn'); + out.busyAttr = btn.getAttribute('data-bound'); + out.busyColor = getComputedStyle(btn).color; + var b4 = getComputedStyle(btn.querySelector('.dsh-wt_bindCircles'), '::before'); + var b5 = getComputedStyle(btn.querySelector('.dsh-wt_bindCircles'), '::after'); + out.beforeAnim = b4.animationName; out.afterAnim = b5.animationName; + out.glow = getComputedStyle(btn.querySelector('.dsh-wt_bindCircles')).filter; + out.fill = b4.backgroundColor; + out.tipBusy = (btn.getAttribute('data-tip')||'').indexOf('\u5904\u7406\u4efb\u52a1') >= 0; + // running=false + completed=true → done(优先级切换) + listStore.getSnapshot = fake({ running: false, completed: true }); + window.__dshSyncSessionScope(); + await sleep(600); + var btn2 = card.querySelector('.dsh-wt_bindBtn'); + out.doneAttrAfter = btn2.getAttribute('data-bound'); + out.doneColorAfter = getComputedStyle(btn2).color; + listStore.getSnapshot = orig; + // 清理:解绑 + bbtn = card.querySelector('.dsh-wt_bindBtn'); + bbtn.click(); + await sleep(900); + var unbind = vis('.dsh-wt_bindUnbind'); + if (unbind) { unbind.click(); } + await sleep(500); + localStorage.removeItem('dsh.worktable.notifyAck.v1'); + } catch(e) { out.err = String(e); } + return JSON.stringify(out); + })()`); + console.log('BUSY:', JSON.stringify(step)); + ws.close(); proc.kill(); process.exit(0); +})().catch((e) => { console.error('FATAL', e && e.stack || e); try { proc.kill(); } catch {} process.exit(1); }); diff --git a/AGENTS.md b/AGENTS.md index a1faca7..233fd51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,9 @@ node --check lib/index.js 绿色发光(data-bound=done);pendingInteraction != null → 黄色发光(data-bound=need); 点开项目 = ack(notifyAck.v1 按会话存状态)恢复常态实心。数据源 = sessionsSnapshotStore (syncSessionScope 写入完整快照并通知监听者);跨状态(done↔need)会重新点亮。 + **工作中(busy)**:byId[sid].running === true → data-bound=busy,蓝色 #4f8ef7 发光 + + dsh-wt-busyA/B 关键帧两圆交替亮灭(对应 DSH 转圈标记);优先级 busy > done > need, + 无需 ack,running 变 false 自动切换。 ## 安装 / 重启