feat: console glow cards, icon theme toggle, cold preview warmup, unified naming

This commit is contained in:
Aisland
2026-08-21 01:04:29 +08:00
parent 249f09d8d6
commit 142860b15a
10 changed files with 592 additions and 47 deletions
+91 -8
View File
@@ -312,10 +312,10 @@ const CONSOLE_ID = 'wt-console'
const CONSOLE_ICON = '🖥️'
/** 控制室默认布局:单一锁死大窗格(项目卡片网格)+ 右侧对话 */
function buildConsoleSpec(): LayoutSpec {
function buildConsoleSpec(t: (key: string) => string): LayoutSpec {
return {
id: CONSOLE_ID,
title: '工作台',
title: t('console.name'),
icon: CONSOLE_ICON,
top: null,
main: [{
@@ -348,6 +348,70 @@ function listWorkspaces(): { id: string; title: string; path: string }[] {
} catch { return [] }
}
/** 冷会话最近消息缓存与预热:宿主 history 只读通道(face.history 为运行期内建方法,非公开接口,
* 只读侦察确认可用;拉取是带宽成本不是 Token 成本,模型不参与)。 */
const previewCache = new Map<string, string>()
const previewFetching = new Set<string>()
let previewSweepBusy = false
let previewTimer: number | null = null
/** 从 history 事件流尾部提取最近一条成品消息文本(优先 text 块,回退任意文本) */
async function coldPreviewOf(face: any): Promise<string> {
if (!face || typeof face.history !== 'function') return ''
const r1 = await face.history({ maxMessages: 2 })
const evs = r1?.result?.value?.events
if (!Array.isArray(evs)) return ''
const textOf = (blocks: any): string => {
let fallback = ''
if (!Array.isArray(blocks)) return ''
for (const b of blocks) {
const s = typeof b?.text === 'string' ? b.text.trim().replace(/\s+/g, ' ') : ''
if (!s) continue
if (b?.type === 'text') return s.slice(0, 140)
if (!fallback) fallback = s.slice(0, 140)
}
return fallback
}
for (let i = evs.length - 1; i >= 0; i--) {
const ev = evs[i]?.event
if (!ev) continue
const d = ev.data ?? {}
if (ev.type === 'user/message') {
const s = textOf(d.content ?? d.blocks)
if (s) return s
} else if (ev.type === 'assistant/message') {
const m = d.message ?? d
const s = textOf(m.content ?? m.blocks)
if (s) return s
}
}
return ''
}
/** 预热所有已绑定会话的最近消息(逐个只读拉取;已在拉/已拉过未完成的跳过,失败静默回退内存路径) */
async function sweepPreviews() {
if (previewSweepBusy) return
previewSweepBusy = true
try {
const sids = Array.from(new Set(Object.values(projectBindingsRef.current).filter((x): x is string => !!x)))
for (const sid of sids) {
if (previewFetching.has(sid)) continue
previewFetching.add(sid)
try {
const face = sessionBridge?.sessions?.binding?.(sid)?.session
const txt = await coldPreviewOf(face)
if (txt) { previewCache.set(sid, txt); notifyConsole() }
} catch {} finally { previewFetching.delete(sid) }
}
} finally { previewSweepBusy = false }
}
/** 防抖调度:控制室开着且会话快照变化时刷新预览(合并 6s 内的连续变化) */
function schedulePreviewSweep() {
if (previewTimer != null) return
previewTimer = window.setTimeout(() => { previewTimer = null; sweepPreviews() }, 6000)
}
/** 从会话快照节点里提取最近一条文本(纯读内存镜像,零 Token;无则 '') */
function lastTextOf(sid: string): string {
try {
@@ -815,6 +879,8 @@ function WorktableSection(props: any) {
)
/** 控制室动作引用(每渲染同步,供 env 闭包读取) */
const actionsRef = useRef<{ openSplit: (spec: LayoutSpec) => void; openConsole: (explicitBound?: string | null) => void } | null>(null)
/** 提醒确认动作引用(每渲染同步;env 闭包用) */
const ackRef = useRef<((projectId: string) => void) | null>(null)
/** 视图状态最新快照(供 env 闭包读取控制室主题) */
const viewRef = useRef(view)
viewRef.current = view
@@ -881,16 +947,21 @@ function WorktableSection(props: any) {
} catch {}
return null
}
const ackMap = loadNotifyAck()
const make = (id: string, name: string, icon: string, self: boolean): ConsoleCardData => {
const sid = pr.projects.bindings[id]
const status = statusOf(sid)
// 发光 = 完成/待决且本轮未被确认;点卡片确认后熄灭
const glow = !!sid && ((status === 'done' && ackMap[sid] !== 'done') || (status === 'need' && ackMap[sid] !== 'need'))
return {
id, name, icon,
status: statusOf(sid),
status,
runtimeMs: runtimeOf(sid),
kids: sid ? kidsSetOf(sid).size : 0,
preview: sid ? lastTextOf(sid) : '',
preview: sid ? (previewCache.get(sid) ?? lastTextOf(sid)) : '',
bound: !!sid,
self,
glow,
}
}
const cards: ConsoleCardData[] = []
@@ -965,6 +1036,11 @@ function WorktableSection(props: any) {
getCards: () => getConsoleCards(),
getTheme: () => viewRef.current.consoleTheme ?? 'system',
setTheme: (th: 'dark' | 'light' | 'system') => persistView({ consoleTheme: th }),
onAck: (id) => { ackRef.current?.(id); setNotifyTick((t) => t + 1); notifyConsole() },
refreshPreviews: () => {
if (previewTimer != null) { window.clearTimeout(previewTimer); previewTimer = null }
sweepPreviews()
},
onOpen: (id) => {
if (id === CONSOLE_ID) return
const pr = projectsRef.current.projects
@@ -1226,7 +1302,7 @@ function buildCustomLayoutPrompt(req: string): string {
/** 打开「工作台」控制室:已绑定 → 打开并切换绑定对话;explicitBound 供强制绑定流程传入刚绑定的会话 */
const openConsole = useCallback((explicitBound?: string | null) => {
const spec = projects.views[CONSOLE_ID] ?? buildConsoleSpec()
const spec = projects.views[CONSOLE_ID] ?? buildConsoleSpec((k) => t(k as WorktableKey))
engineIdsRef.current.add(CONSOLE_ID)
splitStore.open(spec)
if (splitStore.active && splitStore.spec?.id === CONSOLE_ID) {
@@ -1238,8 +1314,10 @@ function buildCustomLayoutPrompt(req: string): string {
if (bound) { try { sessionBridge?.sessions?.open?.(bound) } catch {} }
ackProjectNotify(CONSOLE_ID)
try { notifyConsole() } catch {}
// 打开即预热所有绑定会话的最近消息(冷会话走 history 只读通道)
try { sweepPreviews() } catch {}
}
}, [projects.views])
}, [projects.views, t])
/** 控制室卡片点击:已绑定 → 打开控制室;未绑定 → 强制绑定弹窗 */
const clickConsoleCard = (anchor: HTMLElement) => {
@@ -1366,9 +1444,13 @@ function buildCustomLayoutPrompt(req: string): string {
copyToastTimerRef.current = window.setTimeout(() => setCopiedToast(null), 6000)
}
// 会话快照变化 → 重渲染(驱动项目卡片的完成/待决提醒镜像 + 控制室面板)
// 会话快照变化 → 重渲染(驱动项目卡片的完成/待决提醒镜像 + 控制室面板 + 预览防抖刷新)
useEffect(() => {
const l = () => { setNotifyTick((t) => t + 1); notifyConsole() }
const l = () => {
setNotifyTick((t) => t + 1)
notifyConsole()
if (splitStore.active && splitStore.spec?.id === CONSOLE_ID) schedulePreviewSweep()
}
sessionsSnapshotStore.listeners.add(l)
return () => { sessionsSnapshotStore.listeners.delete(l) }
}, [])
@@ -1493,6 +1575,7 @@ function buildCustomLayoutPrompt(req: string): string {
if (st === 'done') saveNotifyAck(sid, 'done')
}
}
ackRef.current = ackProjectNotify
// ── 有效排序 ──
// 手动:持久化 order(过滤已卸载 id)→ 新注册 id 与布局 id 追加尾部;
+4 -4
View File
@@ -6,7 +6,7 @@
/** Simplified Chinese dictionary (the key-set source of truth). */
export const zh = {
'title': '工作台',
'title': '控制室',
'rail.title': '工作台(展开侧边栏查看)',
'handle.title': '按住拖动位置 · 双击复位',
'handle.aria': '拖动工作台位置',
@@ -113,7 +113,7 @@ export const zh = {
'customLayout.copyFail': '复制失败,请手动全选输入框内容复制。',
'custom.autoBound': '已自动把这个项目绑定到这个对话(卡片上的 ●● 按钮可更换或解绑)。',
'custom.keptBinding': '该项目已有绑定的对话,按规则未自动更改。',
'console.name': '工作台',
'console.name': '控制室',
'console.title': '控制室',
'console.themeLabel': '面板主题',
'console.themeDark': '深色',
@@ -167,7 +167,7 @@ export type WorktableKey = keyof typeof zh
/** English dictionary, checked complete against the zh key set. */
export const en = {
'title': 'Worktable',
'title': 'Console',
'rail.title': 'Worktable (expand sidebar to view)',
'handle.title': 'Drag to move · double-click to reset',
'handle.aria': 'Drag worktable position',
@@ -274,7 +274,7 @@ export const en = {
'customLayout.copyFail': 'Copy failed — select the text above and copy it manually.',
'custom.autoBound': 'This project was auto-bound to this conversation (use the ●● button on its card to change or unbind).',
'custom.keptBinding': 'This project already has a bound conversation, so it was not auto-changed.',
'console.name': 'Worktable',
'console.name': 'Console',
'console.title': 'Console',
'console.themeLabel': 'Panel theme',
'console.themeDark': 'Dark',
+30 -9
View File
@@ -206,6 +206,8 @@ export type ConsoleCardData = {
bound: boolean
/** 是否为「工作台」自己(卡片点击无操作) */
self: boolean
/** 完成/待决且未被确认:卡片发对应色光(点击确认后熄灭) */
glow: boolean
}
type SplitEnv = {
@@ -226,6 +228,10 @@ type SplitEnv = {
getCards: () => ConsoleCardData[]
onOpen: (id: string) => void
onJump: (id: string) => void
/** 点发光卡片:确认(熄灭光)再打开 */
onAck: (id: string) => void
/** 冷会话消息预热(打开控制室时拉最近消息) */
refreshPreviews: () => void
getTheme: () => 'dark' | 'light' | 'system'
setTheme: (th: 'dark' | 'light' | 'system') => void
}
@@ -975,6 +981,10 @@ function ConsolePane() {
try { mq.addEventListener('change', f) } catch { try { (mq as any).addListener(f) } catch {} }
return () => { try { mq.removeEventListener('change', f) } catch { try { (mq as any).removeListener(f) } catch {} } }
}, [])
// 打开控制室 = 预热所有绑定会话的最近消息(冷会话走宿主 history 只读通道)
useEffect(() => {
splitEnv?.console?.refreshPreviews?.()
}, [])
// 运行时长的分钟级刷新:每秒重渲染一次(仅控制室开着时存在)
useEffect(() => {
const iv = window.setInterval(() => setNow(Date.now()), 1000)
@@ -1001,10 +1011,10 @@ function ConsolePane() {
return h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' + String(ss).padStart(2, '0') : m + ':' + String(ss).padStart(2, '0')
}
const statusLabel: Record<string, string> = { idle: T('console.idle'), busy: T('console.busy'), need: T('console.need'), done: T('console.done') }
const themeOpts: { mode: 'dark' | 'light' | 'system'; key: string }[] = [
{ mode: 'dark', key: 'console.themeDark' },
{ mode: 'light', key: 'console.themeLight' },
{ mode: 'system', key: 'console.themeSystem' },
const themeOpts: { mode: 'dark' | 'light' | 'system'; icon: string; key: string }[] = [
{ mode: 'dark', icon: '🌙', key: 'console.themeDark' },
{ mode: 'light', icon: '☀️', key: 'console.themeLight' },
{ mode: 'system', icon: '🖥️', key: 'console.themeSystem' },
]
return (
<div className="dsh-wt_console" data-wt-theme={resolvedTheme}>
@@ -1016,8 +1026,10 @@ function ConsolePane() {
key={o.mode}
type="button"
className={'dsh-wt_consoleThemeBtn' + (themeMode === o.mode ? ' dsh-wt_consoleThemeBtnOn' : '')}
title={T(o.key)}
aria-label={T(o.key)}
onClick={() => setTheme(o.mode)}
>{T(o.key)}</button>
><span aria-hidden>{o.icon}</span></button>
))}
</div>
</div>
@@ -1027,18 +1039,27 @@ function ConsolePane() {
key={c.id}
role={c.self ? undefined : 'button'}
tabIndex={c.self ? -1 : 0}
className={'dsh-wt_consoleCard' + (c.self ? ' dsh-wt_consoleCardSelf' : '')}
className={'dsh-wt_consoleCard' + (c.self ? ' dsh-wt_consoleCardSelf' : '')
+ (c.status === 'busy' ? ' dsh-wt_consoleCard-busy' : '')
+ (c.glow && c.status === 'done' ? ' dsh-wt_consoleCard-glowDone' : '')
+ (c.glow && c.status === 'need' ? ' dsh-wt_consoleCard-glowNeed' : '')}
title={c.name}
onClick={() => { if (!c.self && env) env.onOpen(c.id) }}
onClick={() => {
if (c.self || !env) return
if (c.glow) env.onAck?.(c.id) // 点发光卡片:先确认熄光,再进入
env.onOpen(c.id)
}}
>
<div className="dsh-wt_consoleCardHead">
<span className="dsh-wt_consoleIcon" aria-hidden>{c.icon}</span>
<span className="dsh-wt_consoleName">{c.name}</span>
<span className={'dsh-wt_consoleDot dsh-wt_consoleDot-' + c.status} aria-hidden />
</div>
<div className="dsh-wt_consoleStatusRow">
<span className={'dsh-wt_consoleStatus dsh-wt_consoleStatus-' + c.status}>{statusLabel[c.status]}</span>
{c.runtimeMs != null && <span className="dsh-wt_consoleRuntime">⏱ {fmtDur(c.runtimeMs)}</span>}
</div>
<div className="dsh-wt_consoleBadges">
<span className={'dsh-wt_consoleBadge dsh-wt_consoleBadge-' + c.status}>{statusLabel[c.status]}</span>
{c.runtimeMs != null && <span className="dsh-wt_consoleBadge">⏱ {fmtDur(c.runtimeMs)}</span>}
<span className="dsh-wt_consoleBadge">{T('console.kids')} {c.kids}</span>
</div>
<div className={'dsh-wt_consolePreview' + (c.preview ? '' : ' dsh-wt_consolePreviewNone')} title={c.preview}>
+38 -20
View File
@@ -387,30 +387,48 @@ export const css = xtermCss + '\n' + [
'.dsh-wt_consoleHead{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}',
'.dsh-wt_consoleTitle{font-size:14px;font-weight:700;line-height:20px;color:var(--wt-text)}',
'.dsh-wt_consoleTheme{display:inline-flex;align-items:center;padding:2px;border:1px solid var(--wt-border);border-radius:999px;background:var(--wt-card)}',
'.dsh-wt_consoleThemeBtn{flex:none;padding:2px 10px;border:none;border-radius:999px;background:transparent;color:var(--wt-text2);font:inherit;font-size:11px;line-height:18px;cursor:pointer}',
'.dsh-wt_consoleThemeBtnOn{background:var(--wt-chip);color:var(--wt-text);font-weight:600}',
'.dsh-wt_consoleGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;align-content:start}',
'.dsh-wt_consoleCard{position:relative;display:flex;flex-direction:column;gap:8px;min-height:96px;padding:12px 14px;background:var(--wt-card);border:1px solid var(--wt-border);border-radius:12px;cursor:pointer;transition:transform .12s ease,border-color .12s ease,box-shadow .12s ease}',
'.dsh-wt_consoleCard:hover{transform:translateY(-1px);border-color:var(--wt-borderHover);box-shadow:var(--wt-shadow)}',
'.dsh-wt_consoleCardSelf{cursor:default;opacity:.96}',
'.dsh-wt_consoleCardHead{display:flex;align-items:center;gap:8px;min-width:0}',
'.dsh-wt_consoleIcon{flex:none;font-size:20px;line-height:1}',
'.dsh-wt_consoleName{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:600;line-height:20px;color:var(--wt-text)}',
'.dsh-wt_consoleDot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--wt-text3)}',
'.dsh-wt_consoleDot-busy{background:var(--dsw-alias-state-accent-primary,#4f8ef7);box-shadow:0 0 0 3px rgba(79,142,247,.15)}',
'.dsh-wt_consoleDot-need{background:var(--dsw-alias-state-warning,#d29922);box-shadow:0 0 0 3px rgba(210,153,34,.15)}',
'.dsh-wt_consoleDot-done{background:var(--dsw-alias-state-success,#3fb950);box-shadow:0 0 0 3px rgba(63,185,80,.15)}',
'.dsh-wt_consoleThemeBtn{flex:none;padding:3px 9px;border:none;border-radius:999px;background:transparent;color:var(--wt-text3);font:inherit;font-size:12px;line-height:18px;cursor:pointer;opacity:.85}',
'.dsh-wt_consoleThemeBtn:hover{opacity:1;background:var(--wt-chip)}',
'.dsh-wt_consoleThemeBtnOn{background:var(--wt-chip);color:var(--wt-text);opacity:1}',
'.dsh-wt_consoleGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;align-content:start}',
'.dsh-wt_consoleCard{position:relative;display:flex;flex-direction:column;gap:10px;aspect-ratio:1/1;min-height:150px;padding:16px;background:var(--wt-card);border:1px solid var(--wt-border);border-radius:14px;cursor:pointer;transition:transform .12s ease,border-color .12s ease,box-shadow .12s ease;overflow:hidden}',
'.dsh-wt_consoleCard:hover{transform:translateY(-2px);border-color:var(--wt-borderHover);box-shadow:var(--wt-shadow)}',
'.dsh-wt_consoleCardSelf{cursor:default;opacity:.97}',
'.dsh-wt_consoleCardHead{display:flex;align-items:center;gap:9px;min-width:0}',
'.dsh-wt_consoleIcon{flex:none;font-size:24px;line-height:1}',
'.dsh-wt_consoleName{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14.5px;font-weight:700;line-height:22px;color:var(--wt-text)}',
'.dsh-wt_consoleDot{flex:none;width:9px;height:9px;border-radius:50%;background:var(--wt-text3)}',
'.dsh-wt_consoleDot-busy{background:var(--dsw-alias-state-accent-primary,#4f8ef7);box-shadow:0 0 0 4px rgba(79,142,247,.16)}',
'.dsh-wt_consoleDot-need{background:var(--dsw-alias-state-warning,#d29922);box-shadow:0 0 0 4px rgba(210,153,34,.16)}',
'.dsh-wt_consoleDot-done{background:var(--dsw-alias-state-success,#3fb950);box-shadow:0 0 0 4px rgba(63,185,80,.16)}',
'.dsh-wt_consoleStatusRow{display:flex;align-items:baseline;gap:8px;min-width:0}',
'.dsh-wt_consoleStatus{flex:none;font-size:17px;font-weight:700;line-height:24px;color:var(--wt-text2)}',
'.dsh-wt_consoleStatus-busy{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
'.dsh-wt_consoleStatus-need{color:var(--dsw-alias-state-warning,#d29922)}',
'.dsh-wt_consoleStatus-done{color:var(--dsw-alias-state-success,#3fb950)}',
'.dsh-wt_consoleRuntime{flex:none;font-size:13px;font-weight:600;line-height:20px;color:var(--wt-text2);font-variant-numeric:tabular-nums}',
'.dsh-wt_consoleBadges{display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
'.dsh-wt_consoleBadge{flex:none;padding:1px 7px;border-radius:999px;font-size:10.5px;line-height:16px;color:var(--wt-text2);background:var(--wt-chip)}',
'.dsh-wt_consoleBadge-busy{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
'.dsh-wt_consoleBadge-need{color:var(--dsw-alias-state-warning,#d29922)}',
'.dsh-wt_consoleBadge-done{color:var(--dsw-alias-state-success,#3fb950)}',
'.dsh-wt_consolePreview{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;color:var(--wt-text2)}',
'.dsh-wt_consolePreviewNone{color:var(--wt-text3);font-style:italic}',
'.dsh-wt_consoleJump{position:absolute;top:10px;right:10px;width:22px;height:22px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);font-size:12px;line-height:1;cursor:pointer;opacity:0;transition:opacity .12s ease}',
'.dsh-wt_consoleBadge{flex:none;padding:2px 9px;border-radius:999px;font-size:12px;font-weight:600;line-height:18px;color:var(--wt-text2);background:var(--wt-chip)}',
'.dsh-wt_consolePreview{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:12.5px;line-height:18px;color:var(--wt-text2);word-break:break-word}',
'.dsh-wt_consolePreviewNone{color:var(--wt-text3);font-style:italic;-webkit-line-clamp:1}',
// 三色光效:完成=绿光、待决=黄光(背景泛光,点击确认后熄灭);工作=蓝色描边光晕顺时针旋转
'.dsh-wt_consoleCard-glowDone{border-color:rgba(63,185,80,.45);box-shadow:0 0 22px rgba(63,185,80,.22)}',
'.dsh-wt_consoleCard-glowDone::after{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(130% 130% at 50% 0%,rgba(63,185,80,.16),transparent 62%);pointer-events:none}',
'.dsh-wt_consoleCard-glowNeed{border-color:rgba(210,153,34,.5);box-shadow:0 0 22px rgba(210,153,34,.22)}',
'.dsh-wt_consoleCard-glowNeed::after{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(130% 130% at 50% 0%,rgba(210,153,34,.17),transparent 62%);pointer-events:none}',
'@property --consoleAngle{syntax:"<angle>";initial-value:0deg;inherits:false}',
'.dsh-wt_consoleCard-busy::before{content:"";position:absolute;inset:-2px;border-radius:16px;padding:2px;background:conic-gradient(from var(--consoleAngle),transparent 0deg,rgba(79,142,247,0) 30deg,rgba(79,142,247,.95) 80deg,rgba(79,142,247,0) 130deg,transparent 200deg);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;animation:consoleAngleSpin 1.8s linear infinite;pointer-events:none}',
'@keyframes consoleAngleSpin{to{--consoleAngle:360deg}}',
'.dsh-wt_consoleJump{position:absolute;top:12px;right:12px;width:24px;height:24px;padding:0;border:none;border-radius:7px;background:transparent;color:var(--wt-text3);font-size:13px;line-height:1;cursor:pointer;opacity:0;transition:opacity .12s ease}',
'.dsh-wt_consoleCard:hover .dsh-wt_consoleJump{opacity:1}',
'.dsh-wt_consoleJump:hover{color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--wt-chip)}',
'.dsh-wt_consoleEmpty{padding:40px 0;text-align:center;font-size:12px;color:var(--wt-text3)}',
// 控制室入口卡:统筹主程序的差异化(渐变底 + 青色描边 + 微光,克制不喧宾夺主)
'.dsh-wt_projects .dsh-wt_consoleEntry{background:linear-gradient(135deg,rgba(79,142,247,.14),rgba(139,92,246,.10));border-color:rgba(79,142,247,.4);box-shadow:0 0 12px rgba(79,142,247,.14)}',
'.dsh-wt_projects .dsh-wt_consoleEntry:hover{background:linear-gradient(135deg,rgba(79,142,247,.2),rgba(139,92,246,.14));border-color:rgba(79,142,247,.6);box-shadow:0 0 16px rgba(79,142,247,.22)}',
'.dsh-wt_projects .dsh-wt_consoleEntry[data-on=true]{border-color:rgba(79,142,247,.7);box-shadow:0 0 16px rgba(79,142,247,.28)}',
'.dsh-wt_projects .dsh-wt_consoleEntry .dsh-wt_layoutIcon{font-size:15px}',
'.dsh-wt_projects .dsh-wt_consoleEntry .dsh-wt_layoutName{font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb)}',
// 控制室强制绑定弹窗:左加入现有 / 右新建对话
'.dsh-wt_consoleBindCols{display:flex;gap:12px}',
'.dsh-wt_consoleBindCol{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}',