@steerable/agent-shell-web 0.6.49 → 0.6.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,26 @@
1
- import { useRef, useState } from 'react';
2
- import { LuActivity, LuBlocks, LuBot, LuChartBar, LuDownload, LuMonitor, LuNetwork, LuPlug, LuSearch, LuSettings, LuShieldCheck } from 'react-icons/lu';
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ LuActivity,
4
+ LuBlocks,
5
+ LuBot,
6
+ LuChartBar,
7
+ LuCodeXml,
8
+ LuDownload,
9
+ LuGitFork,
10
+ LuInfo,
11
+ LuMonitor,
12
+ LuNetwork,
13
+ LuPlug,
14
+ LuSearch,
15
+ LuSettings,
16
+ LuShieldCheck,
17
+ } from 'react-icons/lu';
3
18
  import { useOutletContext, useSearchParams } from 'react-router-dom';
4
19
  import { isElectron } from '@/lib/electron-bridge';
20
+ import { useAppRelease } from '@/components/SidebarRelease';
5
21
  import { AgentsSettingsPanel } from '@/components/settings/AgentsSettingsPanel';
6
22
  import { AppearanceSettingsPanel } from '@/components/settings/AppearanceSettingsPanel';
23
+ import { OrchestrationSettingsPanel } from '@/components/settings/OrchestrationSettingsPanel';
7
24
  import { AppUpdateSettingsPanel } from '@/components/settings/AppUpdateSettingsPanel';
8
25
  import { PortableSettingsPanel } from '@/components/settings/PortableSettingsPanel';
9
26
  import { PythonRunnerSettingsPanel } from '@/components/settings/PythonRunnerSettingsPanel';
@@ -21,6 +38,7 @@ import { SettingsSaveButton } from '@/components/settings/SettingsSaveButton';
21
38
  import { TelemetrySettingsPanel } from '@/components/settings/TelemetrySettingsPanel';
22
39
  import { UsagePanel } from '@/components/settings/UsagePanel';
23
40
  import { WebSearchSettingsPanel } from '@/components/settings/WebSearchSettingsPanel';
41
+ import { SettingsNavMenu, type SettingsNavItem } from '@/components/settings/SettingsNavMenu';
24
42
  import { getPackSettingsPanels } from '@/packs/registry';
25
43
  import { settingsChrome } from '@/lib/host-tools';
26
44
  import { isPortableEnabled } from '@/lib/portable';
@@ -48,13 +66,167 @@ export function SettingsPage() {
48
66
  const [searchParams] = useSearchParams();
49
67
  const section = resolveSection(searchParams.get('section'));
50
68
  const catalog = useOutletContext<AgentOutletContext | null>();
69
+ const release = useAppRelease();
51
70
  const llmPanelRef = useRef<LlmSettingsPanelHandle>(null);
71
+ const pageContainerRef = useRef<HTMLDivElement>(null);
72
+ const scrollRef = useRef<HTMLDivElement>(null);
73
+ const contentRef = useRef<HTMLDivElement>(null);
74
+ const [containerWidth, setContainerWidth] = useState<number>(0);
75
+ const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
76
+ const [renderedSectionIds, setRenderedSectionIds] = useState<Set<string> | null>(null);
52
77
  const [llmSaveUi, setLlmSaveUi] = useState<LlmSaveUi>({
53
78
  saving: false,
54
79
  savedOk: false,
55
80
  loading: true,
56
81
  });
57
82
 
83
+ useEffect(() => {
84
+ const el = pageContainerRef.current;
85
+ if (!el || typeof ResizeObserver === 'undefined') return;
86
+ const ro = new ResizeObserver((entries) => {
87
+ for (const entry of entries) {
88
+ setContainerWidth(entry.contentRect.width);
89
+ }
90
+ });
91
+ ro.observe(el);
92
+ return () => ro.disconnect();
93
+ }, []);
94
+
95
+ // 扫描右侧实际挂载且未被隐藏 (display !== 'none') 的设置分段
96
+ const updateRenderedSections = useCallback(() => {
97
+ const container = contentRef.current;
98
+ if (!container) return;
99
+ const sectionEls = container.querySelectorAll<HTMLElement>('[data-testid^="settings-section-"]');
100
+ const ids = new Set<string>();
101
+ sectionEls.forEach((el) => {
102
+ const tid = el.getAttribute('data-testid');
103
+ if (!tid) return;
104
+ const id = tid.replace('settings-section-', '');
105
+ if (typeof window !== 'undefined' && window.getComputedStyle) {
106
+ const style = window.getComputedStyle(el);
107
+ if (style.display === 'none' || style.visibility === 'hidden') return;
108
+ }
109
+ ids.add(id);
110
+ });
111
+
112
+ setRenderedSectionIds((prev) => {
113
+ if (prev && prev.size === ids.size && [...ids].every((id) => prev.has(id))) {
114
+ return prev;
115
+ }
116
+ return ids;
117
+ });
118
+ }, []);
119
+
120
+ useLayoutEffect(() => {
121
+ updateRenderedSections();
122
+ const container = contentRef.current;
123
+ if (!container || typeof MutationObserver === 'undefined') return;
124
+ const observer = new MutationObserver(() => {
125
+ updateRenderedSections();
126
+ });
127
+ observer.observe(container, {
128
+ childList: true,
129
+ subtree: true,
130
+ attributes: true,
131
+ attributeFilter: ['style', 'class', 'hidden'],
132
+ });
133
+ return () => observer.disconnect();
134
+ }, [updateRenderedSections]);
135
+
136
+ // 页面宽度 >= 920px 视为宽屏(有足够空间并排容纳左侧边栏目录与主设置表单)
137
+ const isWide = containerWidth >= 920;
138
+
139
+ const allCandidates = useMemo<SettingsNavItem[]>(() => {
140
+ if (section !== 'general') return [];
141
+ const list: SettingsNavItem[] = [];
142
+
143
+ if (settingsChrome('appearance')) {
144
+ list.push({ id: 'appearance', label: '界面', Icon: LuMonitor });
145
+ }
146
+ list.push({ id: 'orchestration', label: '协同编排', Icon: LuGitFork });
147
+
148
+ if (settingsChrome('llm')) {
149
+ list.push({ id: 'llm', label: '本地模型设置', Icon: LuSettings });
150
+ }
151
+ if (settingsChrome('web-search')) {
152
+ list.push({ id: 'web-search', label: '网络搜索', Icon: LuSearch });
153
+ }
154
+ if (settingsChrome('usage')) {
155
+ list.push({ id: 'usage', label: '用量与成本', Icon: LuChartBar });
156
+ }
157
+ if (settingsChrome('diagnose')) {
158
+ list.push({ id: 'diagnose', label: '链路诊断', Icon: LuNetwork });
159
+ }
160
+ if (settingsChrome('security')) {
161
+ list.push({ id: 'security', label: '安全', Icon: LuShieldCheck });
162
+ }
163
+ if (settingsChrome('insights')) {
164
+ list.push({ id: 'insights', label: '帮助改进产品', Icon: LuChartBar });
165
+ }
166
+ if (settingsChrome('telemetry')) {
167
+ list.push({ id: 'telemetry', label: '遥测(OTLP)', Icon: LuActivity });
168
+ }
169
+
170
+ const packPanels = getPackSettingsPanels();
171
+ for (const panel of packPanels) {
172
+ list.push({
173
+ id: panel.panelId,
174
+ label: panel.title,
175
+ Icon: panel.Icon ?? LuSettings,
176
+ });
177
+ }
178
+
179
+ if (isPortableEnabled()) {
180
+ list.push({ id: 'portable', label: '备份与迁移', Icon: LuDownload });
181
+ }
182
+ if (isElectron()) {
183
+ list.push({ id: 'python-runner', label: 'Python 运行环境', Icon: LuCodeXml });
184
+ }
185
+ if (release.version) {
186
+ list.push({ id: 'update', label: '软件更新', Icon: LuInfo });
187
+ }
188
+
189
+ return list;
190
+ }, [section, release.version]);
191
+
192
+ // 严格过滤:右侧实际未挂载或隐藏的分段,左侧菜单一律不显示
193
+ const menuItems = useMemo<SettingsNavItem[]>(() => {
194
+ if (section !== 'general') return [];
195
+ if (renderedSectionIds !== null) {
196
+ return allCandidates.filter((item) => renderedSectionIds.has(item.id));
197
+ }
198
+ return allCandidates;
199
+ }, [section, allCandidates, renderedSectionIds]);
200
+
201
+ const handleSelectSection = useCallback((id: string) => {
202
+ setActiveSectionId(id);
203
+ const target = document.querySelector(`[data-testid="settings-section-${id}"]`);
204
+ if (target) {
205
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
206
+ }
207
+ }, []);
208
+
209
+ const handleScroll = useCallback(() => {
210
+ if (!scrollRef.current || menuItems.length === 0) return;
211
+ const containerTop = scrollRef.current.getBoundingClientRect().top;
212
+ let closestId: string | null = null;
213
+ let minDistance = Infinity;
214
+
215
+ for (const item of menuItems) {
216
+ const el = document.querySelector(`[data-testid="settings-section-${item.id}"]`);
217
+ if (el) {
218
+ const top = el.getBoundingClientRect().top - containerTop;
219
+ if (top <= 120 && Math.abs(top) < minDistance) {
220
+ minDistance = Math.abs(top);
221
+ closestId = item.id;
222
+ }
223
+ }
224
+ }
225
+ if (closestId) {
226
+ setActiveSectionId(closestId);
227
+ }
228
+ }, [menuItems]);
229
+
58
230
  const title =
59
231
  section === 'mcp'
60
232
  ? 'MCP 设置'
@@ -65,7 +237,7 @@ export function SettingsPage() {
65
237
  : '设置';
66
238
 
67
239
  return (
68
- <div className="flex h-full w-full flex-col overflow-hidden">
240
+ <div ref={pageContainerRef} className="flex h-full w-full flex-col overflow-hidden">
69
241
  <header className="flex h-9 flex-shrink-0 items-center justify-between gap-2 border-b border-agent-border px-2.5">
70
242
  <h1 className="text-xs font-semibold text-agent-foreground">{title}</h1>
71
243
  {section === 'general' && settingsChrome('llm') && (
@@ -79,13 +251,30 @@ export function SettingsPage() {
79
251
  )}
80
252
  </header>
81
253
 
82
- <div className="min-h-0 flex-1 overflow-y-auto">
83
- <div className="mx-auto max-w-3xl space-y-4 px-3 py-3">
84
- {!isElectron() && (
85
- <p className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive">
86
- 浏览器预览模式 — 没有 Electron IPC 桥接,部分设置不可用。
87
- </p>
88
- )}
254
+ <div
255
+ ref={scrollRef}
256
+ onScroll={handleScroll}
257
+ className="min-h-0 flex-1 overflow-y-auto"
258
+ >
259
+ <div
260
+ className={`mx-auto px-4 py-3 ${
261
+ isWide && menuItems.length > 0 ? 'flex max-w-5xl justify-center gap-6' : 'max-w-3xl'
262
+ }`}
263
+ >
264
+ {isWide && menuItems.length > 0 ? (
265
+ <SettingsNavMenu
266
+ items={menuItems}
267
+ activeId={activeSectionId ?? menuItems[0]?.id ?? null}
268
+ onSelect={handleSelectSection}
269
+ />
270
+ ) : null}
271
+
272
+ <div ref={contentRef} className="min-w-0 flex-1 max-w-3xl space-y-4">
273
+ {!isElectron() && (
274
+ <p className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive">
275
+ 浏览器预览模式 — 没有 Electron IPC 桥接,部分设置不可用。
276
+ </p>
277
+ )}
89
278
 
90
279
  {section === 'skills' && (
91
280
  <section className="space-y-2" data-testid="settings-section-skills">
@@ -129,6 +318,14 @@ export function SettingsPage() {
129
318
  </section>
130
319
  )}
131
320
 
321
+ <section className="space-y-2" data-testid="settings-section-orchestration">
322
+ <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
323
+ <LuSettings className="h-3.5 w-3.5 text-agent-muted-foreground" />
324
+ 协同编排
325
+ </h2>
326
+ <OrchestrationSettingsPanel />
327
+ </section>
328
+
132
329
  {settingsChrome('llm') && (
133
330
  <section className="space-y-2" data-testid="settings-section-llm">
134
331
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
@@ -235,6 +432,7 @@ export function SettingsPage() {
235
432
  <AppUpdateSettingsPanel />
236
433
  </>
237
434
  )}
435
+ </div>
238
436
  </div>
239
437
  </div>
240
438
  </div>