feat(console): card shape selector (square/circle) + circle-mode layout/fonts + done-glow re-light fix

This commit is contained in:
Aisland
2026-09-01 02:03:46 +08:00
parent 6d74dad5d5
commit 8f9d72c05f
4 changed files with 51 additions and 1 deletions
+16
View File
@@ -69,6 +69,8 @@ type ViewState = {
consoleTheme?: 'dark' | 'light' | 'system'
/** 控制室每行显示项目数(1-5,默认 3)*/
consoleCols?: number
/** 控制室卡片形状:square/circle(默认 square)*/
consoleShape?: 'square' | 'circle'
}
/** 卡片上报的项目元信息(协议 v2)。 */
@@ -295,6 +297,7 @@ function loadView(): ViewState {
floatTop: typeof p.floatTop === 'number' ? p.floatTop : null,
consoleTheme: p.consoleTheme === 'dark' || p.consoleTheme === 'light' || p.consoleTheme === 'system' ? p.consoleTheme : 'system',
consoleCols: typeof p.consoleCols === 'number' && p.consoleCols >= 1 && p.consoleCols <= 5 ? Math.round(p.consoleCols) : 3,
consoleShape: p.consoleShape === 'circle' ? 'circle' : 'square',
}
} catch {
return { ...DEFAULT_VIEW }
@@ -921,6 +924,8 @@ function buildMountContent(folder: string, d: any): any {
/** 每会话最近一次观察到的「需要判断」布尔值:状态转移时清除旧 ack(新一轮待决重新点亮) */
const notifyStateSeenRef: { current: Record<string, boolean> } = { current: {} }
/** 每会话最近观察到的「已完成」布尔值:新一次完成时清除旧 done ack(控制室绿光重新点亮) */
const doneSeenRef: { current: Record<string, boolean> } = { current: {} }
/** 清除某会话的 ack 记录(状态转移时调用,保证新问题不会被旧确认压住) */
function clearNotifyAck(sid: string) {
try {
@@ -1196,6 +1201,11 @@ function WorktableSection(props: any) {
if (Array.isArray(face?.pending) && face.pending.length > 0) return 'need'
} catch {}
if (e.completed === true) return 'done'
// 会话面兜底:宿主快照可能滞后或缺少完成字段
try {
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.()
if (face && face.completed === true) return 'done'
} catch {}
if (e.running === true) return 'busy'
return 'idle'
}
@@ -1328,6 +1338,8 @@ function WorktableSection(props: any) {
setTheme: (th: 'dark' | 'light' | 'system') => persistView({ consoleTheme: th }),
getCols: () => viewRef.current.consoleCols ?? 3,
setCols: (n: number) => persistView({ consoleCols: n }),
getShape: () => viewRef.current.consoleShape ?? 'square',
setShape: (s: 'square' | 'circle') => persistView({ consoleShape: s }),
onAck: (id) => { ackRef.current?.(id); setNotifyTick((t) => t + 1); notifyConsole() },
refreshPreviews: () => {
if (previewTimer != null) { window.clearTimeout(previewTimer); previewTimer = null }
@@ -1837,11 +1849,15 @@ function buildCustomLayoutPrompt(req: string): string {
const e = byId[sid]
if (!e) continue
if (e.completed === true) {
// 新一次完成 → 清除旧 done ack,控制室绿光重新点亮(即使旧完成已被确认过)
if (doneSeenRef.current[sid] !== true) clearNotifyAck(sid)
doneSeenRef.current[sid] = true
if (!mountConsumedRef.current.has(sid)) {
mountConsumedRef.current.add(sid)
tryAutoMount(pid, sid)
}
} else {
doneSeenRef.current[sid] = false
mountConsumedRef.current.delete(sid)
}
}
+6
View File
@@ -118,6 +118,9 @@ export const zh = {
'console.themeLabel': '面板主题',
'console.cols': '每行',
'console.colsLabel': '每行显示项目数',
'console.shapeLabel': '卡片形状',
'console.shapeSquare': '方形',
'console.shapeCircle': '圆形',
'console.themeDark': '深色',
'console.themeLight': '白色',
'console.themeSystem': '跟随系统',
@@ -298,6 +301,9 @@ export const en = {
'console.themeLabel': 'Panel theme',
'console.cols': 'Per row',
'console.colsLabel': 'Projects per row',
'console.shapeLabel': 'Card shape',
'console.shapeSquare': 'Square',
'console.shapeCircle': 'Circle',
'console.themeDark': 'Dark',
'console.themeLight': 'Light',
'console.themeSystem': 'System',
+14 -1
View File
@@ -1017,6 +1017,7 @@ function ConsolePane() {
const [, setTick] = useState(0)
const [now, setNow] = useState(() => Date.now())
const [cols, setColsState] = useState<number>(() => splitEnv?.console?.getCols?.() ?? 3)
const [shape, setShapeState] = useState<'square' | 'circle'>(() => splitEnv?.console?.getShape?.() ?? 'square')
const gridRef = useRef<HTMLDivElement | null>(null)
const firstRectsRef = useRef<Map<string, { x: number; y: number }> | null>(null)
const onCols = (n: number) => {
@@ -1033,6 +1034,10 @@ function ConsolePane() {
setColsState(n)
splitEnv?.console?.setCols?.(n)
}
const onShape = (s: 'square' | 'circle') => {
setShapeState(s)
splitEnv?.console?.setShape?.(s)
}
useLayoutEffect(() => {
const first = firstRectsRef.current
if (!first) return
@@ -1108,8 +1113,16 @@ function ConsolePane() {
{ mode: 'system', icon: '🖥️', key: 'console.themeSystem' },
]
return (
<div className="dsh-wt_console" data-wt-theme={resolvedTheme}>
<div className="dsh-wt_console" data-wt-theme={resolvedTheme} data-wt-shape={shape}>
<div className="dsh-wt_consoleHead">
<div className="dsh-wt_consoleShape" role="group" aria-label={T('console.shapeLabel')}>
<button type="button" className={'dsh-wt_consoleShapeBtn' + (shape === 'square' ? ' dsh-wt_consoleShapeBtnOn' : '')} title={T('console.shapeSquare')} aria-label={T('console.shapeSquare')} onClick={() => onShape('square')}>
<svg width="12" height="12" viewBox="0 0 16 16" aria-hidden><rect x="4" y="4" width="8" height="8" rx="1.5" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
</button>
<button type="button" className={'dsh-wt_consoleShapeBtn' + (shape === 'circle' ? ' dsh-wt_consoleShapeBtnOn' : '')} title={T('console.shapeCircle')} aria-label={T('console.shapeCircle')} onClick={() => onShape('circle')}>
<svg width="12" height="12" viewBox="0 0 16 16" aria-hidden><circle cx="8" cy="8" r="4.5" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
</button>
</div>
<div className="dsh-wt_consoleTheme" role="group" aria-label={T('console.themeLabel')}>
{themeOpts.map((o) => (
<button
+15
View File
@@ -408,6 +408,21 @@ export const css = xtermCss + '\n' + [
'.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_consoleShape{display:inline-flex;align-items:center;padding:2px;border:1px solid var(--wt-border);border-radius:999px;background:var(--wt-card)}',
'.dsh-wt_consoleShapeBtn{flex:none;width:26px;height:26px;padding:0;border:none;border-radius:999px;background:transparent;color:var(--wt-text3);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;opacity:.85}',
'.dsh-wt_consoleShapeBtn:hover{opacity:1;background:var(--wt-chip)}',
'.dsh-wt_consoleShapeBtnOn{background:var(--wt-chip);color:var(--wt-text);opacity:1}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleGrid{max-width:calc(var(--wt-cols,3)*268px + (var(--wt-cols,3) - 1)*64px)}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleCard{border-radius:50%;justify-content:center;align-items:center;padding:24px 24px 38px;gap:9px}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleCardHead{width:100%;flex-direction:column;justify-content:center;gap:5px;text-align:center}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleIcon{font-size:27px;line-height:1}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleName{flex:none;max-width:88%;font-size:19px;font-weight:700;line-height:26px;text-align:center}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleDivider{width:56%;margin:2px auto}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleStatusRow{justify-content:center;gap:8px}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleStatus{font-size:17px;line-height:24px}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleRuntime{font-size:15px;line-height:21px}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consolePreview{text-align:center;max-width:82%;margin:0 auto;-webkit-line-clamp:2;font-size:9px;line-height:13px}',
'.dsh-wt_console[data-wt-shape=circle] .dsh-wt_consoleCard-busy::before{border-radius:50%}',
'.dsh-wt_consoleGrid{display:flex;flex-wrap:wrap;justify-content:center;gap:64px;align-content:start;margin-top:clamp(32px,9vh,110px);width:100%;max-width:calc(var(--wt-cols,3)*242px + (var(--wt-cols,3) - 1)*64px);margin-left:auto;margin-right:auto}',
'.dsh-wt_consoleCols{display:inline-flex;align-items:center;gap:8px;padding:2px 8px;border:1px solid var(--wt-border);border-radius:999px;background:var(--wt-card);color:var(--wt-text3);font-size:11px}',
'.dsh-wt_consoleBar{position:relative;width:128px;height:20px;border-radius:999px;background:rgba(255,255,255,.08);cursor:pointer;flex:none}',