From 781a6c8beb51b2863786a7ee7b7afbfcc1ecbd94 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Fri, 11 Sep 2026 16:00:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(plugins):=20=E6=82=AC=E6=B5=AE=E8=83=B6?= =?UTF-8?q?=E5=9B=8A=E4=B8=89=E7=82=B9=E4=BF=AE=E6=AD=A3=E2=80=94=E2=80=94?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE/=E9=9D=A2=E6=9D=BF=E5=A4=B4/=E5=88=86?= =?UTF-8?q?=E7=BB=84=E6=89=8B=E9=A3=8E=E7=90=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/renderer/src/App.tsx | 5 +- .../src/renderer/src/components/TitleBar.tsx | 13 +- .../src/plugins/PluginFloatingCapsule.tsx | 312 ++++++++++-------- docs/plugins.md | 2 +- docs/plugins/04-ui-slots.md | 6 +- packages/plugin-sdk/src/status.ts | 8 +- 6 files changed, 201 insertions(+), 145 deletions(-) diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 2e0102d..89f316c 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -424,8 +424,11 @@ export default function App(): JSX.Element { title={activeView.view === 'chat' ? sessionTitle : activePlugin?.manifest.name} panelOpen={sideOpen} onTogglePanel={toggleSide} - extra={} /> + {/* 悬浮状态胶囊:main 区右上角(侧面板打开时左移让位) */} +
+ +
{/* 聊天列:切到插件视图时 display:none 保活(草稿/流式/滚动全保留) */}
void - /** 标题右侧插槽(插件状态胶囊等;聊天/插件视图下均可见) */ - extra?: ReactNode }): JSX.Element { const name = title?.trim() const [maximized, setMaximized] = useState(false) @@ -27,7 +25,6 @@ export function TitleBar({ return (
{name || '新会话'}
- {extra &&
{extra}
}
- ) : ( -
- {content} -
- ) - })} -
- ))} + {rows.map((row, ri) => { + const clickable = Boolean(row.openView) + const content = ( + <> + {row.icon && } + + {row.label} + {row.detail && {row.detail}} + + {row.trailing && ( + + {row.trailing} + + )} + + ) + return clickable ? ( + + ) : ( +
+ {content} +
+ ) + })} ) } @@ -109,12 +103,14 @@ export function PluginFloatingCapsule({ onOpenView: (view: string, params?: unknown) => void }): ReactNode { const [open, setOpen] = useState(false) - const [collapsedSections, setCollapsedSections] = useState(loadCollapsedSections) + const [collapsedAreas, setCollapsedAreas] = useState(loadCollapsedSections) + /** 分组手风琴(展开的 section 键 pluginId:itemId:idx;默认收起,会话内记忆) */ + const [openSections, setOpenSections] = useState>(() => new Set()) const rootRef = useRef(null) useEffect(() => { - localStorage.setItem(SECTION_STORAGE_KEY, JSON.stringify(collapsedSections)) - }, [collapsedSections]) + localStorage.setItem(SECTION_STORAGE_KEY, JSON.stringify(collapsedAreas)) + }, [collapsedAreas]) // 面板开着时:点击外部 / Esc 收回最小化 useEffect(() => { @@ -149,11 +145,19 @@ export function PluginFloatingCapsule({ byPlugin.set(entry.pluginId, arr) } - const toggleSection = (pluginId: string): void => { - setCollapsedSections((prev) => + const toggleArea = (pluginId: string): void => { + setCollapsedAreas((prev) => prev.includes(pluginId) ? prev.filter((id) => id !== pluginId) : [...prev, pluginId] ) } + const toggleSection = (key: string): void => { + setOpenSections((prev) => { + const next = new Set(prev) + if (next.has(key)) next.delete(key) + else next.add(key) + return next + }) + } if (!open) { return ( @@ -162,7 +166,7 @@ export function PluginFloatingCapsule({ type="button" title="插件状态(点击展开)" onClick={() => setOpen(true)} - className="flex max-w-48 shrink-0 items-center gap-1.5 rounded-full border border-line bg-surface px-2 py-0.5 text-[11px] leading-[18px] text-ink-2 transition-colors hover:border-line-strong hover:text-ink" + className="flex max-w-48 shrink-0 items-center gap-1.5 rounded-full border border-line bg-surface px-2 py-0.5 text-[11px] leading-[18px] text-ink-2 shadow-card transition-colors hover:border-line-strong hover:text-ink" > -
- {/* 面板头:总量 + 收回最小化 */} -
-
- 插件状态 - {byPlugin.size} 个插件 -
- -
- {/* 固定高度内容区:插件太多/内容过长向下滚动 */} +
+ {/* 固定高度内容区:分组手风琴 + 溢出滚动 */}
- {[...byPlugin.entries()].map(([pluginId, pluginEntries]) => { + {[...byPlugin.entries()].map(([pluginId, pluginEntries], areaIdx) => { const plugin = plugins.find((p) => p.manifest.id === pluginId) - const collapsed = collapsedSections.includes(pluginId) + const areaCollapsed = collapsedAreas.includes(pluginId) return ( -
- {/* 区域头:插件专属,可收起/展开 */} - - {!collapsed && ( -
+
0 ? 'mt-1 border-t border-line pt-1' : ''}> + {/* 区域头 = 插件名;第一个区域右侧是收回药丸的 ⤡ */} +
+ + {areaIdx === 0 && ( + + )} +
+ {!areaCollapsed && ( +
{pluginEntries.map((entry) => { const clickable = Boolean(entry.item.openView) - const body = ( - <> - - {entry.item.icon ? ( - - ) : null} - {entry.item.text} - - ) return (
- {clickable ? ( - + {entry.item.detail ? ( + // 有详情:分组手风琴(默认收起,标题行 + 行尾徽标;无标题分组平铺) + entry.item.detail.sections.map((section, si) => { + if (!section.title) { + return + } + const key = `${pluginId}:${entry.itemId}:${si}` + const expanded = openSections.has(key) + return ( +
+ + {expanded && ( +
+ +
+ )} +
+ ) + }) ) : ( -
{body}
- )} - {entry.item.detail && ( -
- -
+ // 纯文本条目行 + clickable ? ( + + ) : ( +
+ + {entry.item.icon ? ( + + ) : null} + + {entry.item.text} + +
+ ) )}
) diff --git a/docs/plugins.md b/docs/plugins.md index 71df7e6..65a357a 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -181,7 +181,7 @@ api.ui.onSettingsAction((key) => {}) // button 条目点击回调(key = 按钮 | 侧栏入口(B1) | manifest icon/name/order;点进插件整页视图 | ✅ | | 插件整页 Main | 内置组件级 / 外置 renderer 组件 / 外置 iframe;聊天列 display:none 保活 | ✅ | | 面板 tab(B6) | `panel: true`;SidePanel 零改动承载(内置组件 / 外置 renderer 组件 / iframe) | ✅ | -| 状态胶囊(B2+B3) | `setStatusItem` 数据驱动,标题栏**悬浮胶囊**统一承载(ZCode 复刻):最小化药丸展示最新一条消息(宿主记录 updatedAt),点击展开面板——每插件一个专属区域、区域可收起/展开(记忆跨启动)、固定高度溢出滚动、行尾彩色数值与行内深链 | ✅ | +| 状态胶囊(B2+B3) | `setStatusItem` 数据驱动,**悬浮胶囊**挂 main 区右上角(ZCode 复刻):最小化药丸展示最新一条消息(宿主记录 updatedAt),展开面板头即插件名、`detail` 分组为手风琴(默认收起显示 标题+trailing 徽标,点击展开行)、固定高度滚动、行尾彩色数值与行内深链 | ✅ | | 深链 | `openView(view, params)`(main 侧 IPC 广播 / 页侧桥直通) | ✅ | | 工具栏按钮(B5) | `registerToolbarButton` 数据已通,Composer 工具行渲染 | ⏳ 下版本 | | 聊天流富组件(B4) | ```类型``` 围栏块 → 宿主元组件(data-table/stat-grid/step-list/kv-card) | ⏳ 下版本 | diff --git a/docs/plugins/04-ui-slots.md b/docs/plugins/04-ui-slots.md index 2d3d684..0e26c1f 100644 --- a/docs/plugins/04-ui-slots.md +++ b/docs/plugins/04-ui-slots.md @@ -4,10 +4,10 @@ ## 1. 状态胶囊(悬浮胶囊:最小化药丸 ⇄ 展开面板) -所有插件**共用一个悬浮胶囊**(标题栏右侧,ZCode 复刻)。插件照常 `setStatusItem`,宿主记录每次写入时间,两种形态: +所有插件**共用一个悬浮胶囊**,悬浮于 main 区域右上角(侧面板打开时自动左移让位;ZCode 复刻)。插件照常 `setStatusItem`,宿主记录每次写入时间,两种形态: - **最小化药丸**:展示「最新的消息」(全部插件中 `updatedAt` 最新的一条:tone 点 + 插件图标 + 文本;多于一条时带总数角标),点击展开面板; -- **展开面板**:每个插件一个**专属区域**——区域头(插件图标 + 名称 + 条目数)可收起/展开(记忆跨启动),区域内是该插件的条目与详情行;面板高度固定,内容太多向下滚动;点击面板外部 / Esc 收回药丸。 +- **展开面板**:面板头就是插件名(第一个区域的右侧是收回药丸的 ⤡);多个插件时各占一个区域、区域头可收起/展开(记忆跨启动)。区域内 `detail` 的**分组是手风琴**——默认收起只显示「分组标题 + 行尾徽标」,点击展开 rows(ZCode 式「计划 ›」);无标题分组直接平铺。面板高度固定,内容太多向下滚动;点击面板外部 / Esc 收回药丸。 ```js // ① 纯展示(只出现在药丸/区域行) @@ -21,7 +21,7 @@ api.ui.setStatusItem('conn', { openView: { view: 'my-plugin' } // 点击跳到插件整页视图 }) -// ③ 携详情(声明 detail 时区域行下展开分组明细) +// ③ 携详情(分组 = 手风琴:收起显示 标题+trailing 徽标,点击展开 rows) api.ui.setStatusItem('tasks', { text: '任务 2/5', tone: 'accent', diff --git a/packages/plugin-sdk/src/status.ts b/packages/plugin-sdk/src/status.ts index 3523640..e090452 100644 --- a/packages/plugin-sdk/src/status.ts +++ b/packages/plugin-sdk/src/status.ts @@ -28,10 +28,14 @@ export interface StatusDetailRow { openView?: OpenViewAction } -/** 详情弹层分组 */ +/** 详情弹层分组(悬浮面板中为手风琴:默认收起只显示标题行,点击展开 rows) */ export interface StatusDetailSection { - /** 分组标题(如「Git 工具」「计划」);缺省无标题 */ + /** 分组标题(如「计划」「进程」);缺省无标题(rows 直接平铺、不可收起) */ title?: string + /** 标题行尾徽标(如「4/4」「35分7秒」;收起态也可见——ZCode 式摘要) */ + trailing?: string + /** 行尾徽标色调,缺省 default */ + trailingTone?: StatusTone rows: StatusDetailRow[] }