@wonanut/dsh-agent-lab 0.0.0-stage → 0.2.0

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.
package/src/90-page.js ADDED
@@ -0,0 +1,476 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Agent 实验室 · 页面外壳
3
+ * Owns the runtime lifecycle (createLab / dispose), the three top-level tabs,
4
+ * the session column, command shortcuts and the toast surface.
5
+ * ───────────────────────────────────────────────────────────── */
6
+
7
+ const AL_TABS = [
8
+ { id: 'lab', label: '体验 TUI', icon: 'terminal' },
9
+ { id: 'dataflow', label: '数据流', icon: 'route' },
10
+ { id: 'source', label: '源码', icon: 'code' },
11
+ { id: 'docs', label: '原理', icon: 'book' },
12
+ ];
13
+
14
+ function SessionsPanel(props) {
15
+ const view = props.view;
16
+ const busy = Boolean(view.busy);
17
+ const sessions = view.sessions || [];
18
+ const [confirmId, setConfirmId] = useState(null);
19
+ const [confirmReset, setConfirmReset] = useState(false);
20
+
21
+ useEffect(() => {
22
+ if (confirmId && !sessions.some((session) => session.id === confirmId)) setConfirmId(null);
23
+ }, [sessions, confirmId]);
24
+
25
+ return e(
26
+ 'aside',
27
+ { className: 'al-panel al-sessions-panel', 'aria-label': '会话列表' },
28
+ e(
29
+ 'header',
30
+ { className: 'al-panel-head' },
31
+ e(
32
+ 'div',
33
+ { className: 'al-panel-title' },
34
+ e(Icon, { name: 'layers', size: 14 }),
35
+ '会话',
36
+ e('span', { className: 'al-panel-sub' }, String(sessions.length)),
37
+ ),
38
+ e(
39
+ 'div',
40
+ { style: { display: 'flex', alignItems: 'center', gap: 2 } },
41
+ e(IconBtn, { icon: 'chevronLeft', size: 14, title: '折叠会话目录', onClick: props.onCollapse }),
42
+ e(IconBtn, { icon: 'plus', title: '新建会话', onClick: props.onNew, disabled: busy }),
43
+ ),
44
+ ),
45
+ e(
46
+ 'div',
47
+ { className: 'al-panel-body al-session-list' },
48
+ sessions.map((session) => {
49
+ const active = session.id === view.activeId;
50
+ const confirming = confirmId === session.id;
51
+ return e(
52
+ 'div',
53
+ {
54
+ key: session.id,
55
+ className: 'al-session' + (active ? ' on' : ''),
56
+ role: 'button',
57
+ tabIndex: 0,
58
+ 'aria-current': active ? 'true' : undefined,
59
+ onClick: () => { if (!busy && !active) props.onSwitch(session.id); },
60
+ onKeyDown: (event) => { if (event.key === 'Enter' && !busy && !active) props.onSwitch(session.id); },
61
+ title: busy ? '运行中不能切换会话' : '切换到该会话',
62
+ },
63
+ e(
64
+ 'div',
65
+ { className: 'al-session-top' },
66
+ e('span', { className: 'al-session-title' }, session.title || '新会话'),
67
+ e(IconBtn, {
68
+ icon: confirming ? 'alert' : 'trash',
69
+ size: 12,
70
+ danger: confirming,
71
+ className: 'al-session-del' + (confirming ? ' confirm' : ''),
72
+ title: confirming ? '再点一次删除' : '删除会话',
73
+ onClick: (event) => {
74
+ event.stopPropagation();
75
+ if (confirming) {
76
+ props.onDelete(session.id);
77
+ setConfirmId(null);
78
+ } else {
79
+ setConfirmId(session.id);
80
+ }
81
+ },
82
+ }),
83
+ ),
84
+ e(
85
+ 'div',
86
+ { className: 'al-session-meta' },
87
+ e('span', null, session.messageCount + ' 条'),
88
+ e('span', null, '·'),
89
+ e('span', null, session.tokens + ' tok'),
90
+ e('span', null, '·'),
91
+ e('span', null, fmtClock(session.updatedAt)),
92
+ ),
93
+ );
94
+ }),
95
+ sessions.length === 0
96
+ ? e(Empty, { icon: 'layers', title: '还没有会话', body: '新建一个会话,或者直接回到终端开始输入。' })
97
+ : null,
98
+ ),
99
+ e(
100
+ 'footer',
101
+ { className: 'al-panel-foot' },
102
+ e('span', { title: '长期记忆跨会话共享' }, '记忆 ', String((view.memory || []).length), ' 条'),
103
+ confirmReset
104
+ ? e(
105
+ 'span',
106
+ { style: { display: 'flex', gap: 5 } },
107
+ e(Btn, { size: 'sm', variant: 'danger', onClick: () => { props.onReset(); setConfirmReset(false); } }, '确认重置'),
108
+ e(Btn, { size: 'sm', variant: 'ghost', onClick: () => setConfirmReset(false) }, '取消'),
109
+ )
110
+ : e(IconBtn, { icon: 'refresh', size: 12, title: '重置全部本地数据', onClick: () => setConfirmReset(true), disabled: busy }),
111
+ ),
112
+ );
113
+ }
114
+
115
+ function SessionsRail(props) {
116
+ return e(
117
+ 'button',
118
+ { type: 'button', className: 'al-sessions-rail', onClick: props.onExpand, title: '展开会话目录' },
119
+ e('span', { className: 'al-rail-icon' }, e(Icon, { name: 'layers', size: 15 })),
120
+ e(Icon, { name: 'chevronRight', size: 12 }),
121
+ e('span', { className: 'al-rail-text' }, '会话 ' + String(props.count || 0)),
122
+ );
123
+ }
124
+
125
+ function PageLoader(props) {
126
+ return e(
127
+ 'div',
128
+ { className: 'al-empty', style: { flex: 1 } },
129
+ e('div', { className: 'al-empty-icon' }, e(Icon, { name: props.error ? 'alert' : 'terminal', size: 20 })),
130
+ e('div', { className: 'al-empty-title' }, props.error ? '初始化失败' : '正在初始化迷你 Agent…'),
131
+ e('div', { className: 'al-empty-body' }, props.error || '加载会话、记忆与本地示例数据。'),
132
+ );
133
+ }
134
+
135
+ function Page(props) {
136
+ const [view, setView] = useState(null);
137
+ const [tab, setTab] = useState('lab');
138
+ const [inspectorTab, setInspectorTab] = useState('flow');
139
+ const [loadError, setLoadError] = useState(null);
140
+ const [toast, setToast] = useState(null);
141
+ const [realStatus, setRealStatus] = useState(null);
142
+ const [sessionsCollapsed, setSessionsCollapsed] = useState(() => {
143
+ try {
144
+ const raw = localStorage.getItem('dsh.agent-lab.ui.v1');
145
+ return Boolean(raw && JSON.parse(raw).sessionsCollapsed === true);
146
+ } catch (error) {
147
+ return false;
148
+ }
149
+ });
150
+ const labRef = useRef(null);
151
+ const toastTimer = useRef(null);
152
+
153
+ const showToast = useCallback((text, icon) => {
154
+ setToast({ text, icon: icon || 'check' });
155
+ if (toastTimer.current) clearTimeout(toastTimer.current);
156
+ toastTimer.current = setTimeout(() => setToast(null), 2200);
157
+ }, []);
158
+
159
+ useEffect(() => () => { if (toastTimer.current) clearTimeout(toastTimer.current); }, []);
160
+
161
+ useEffect(() => {
162
+ let lab;
163
+ try {
164
+ lab = createLab();
165
+ } catch (error) {
166
+ setLoadError((error && error.message) || 'createLab 失败');
167
+ return undefined;
168
+ }
169
+ labRef.current = lab;
170
+ const unsubscribe = lab.onEvent(() => setView(lab.snapshot()));
171
+ lab.setModelAdapter(alCreateRealModelAdapter());
172
+ setView(lab.snapshot());
173
+ let alive = true;
174
+ alRealModelStatus()
175
+ .then((status) => {
176
+ if (!alive) return;
177
+ setRealStatus(status);
178
+ lab.setModelInfo(status);
179
+ if (lab.snapshot().modelMode === 'real' && !status.available) {
180
+ lab.setModelMode('sim');
181
+ showToast('真实模型不可用,已切回模拟模式', 'alert');
182
+ }
183
+ })
184
+ .catch((error) => {
185
+ if (!alive) return;
186
+ const status = { available: false, error: (error && error.message) || '无法连接 Host 路由' };
187
+ setRealStatus(status);
188
+ lab.setModelInfo({ available: false });
189
+ if (lab.snapshot().modelMode === 'real') {
190
+ lab.setModelMode('sim');
191
+ showToast('真实模型不可用,已切回模拟模式', 'alert');
192
+ }
193
+ });
194
+ return () => {
195
+ alive = false;
196
+ unsubscribe();
197
+ lab.dispose();
198
+ labRef.current = null;
199
+ };
200
+ }, [showToast]);
201
+
202
+ const newSession = useCallback(() => {
203
+ const lab = labRef.current;
204
+ if (!lab) return;
205
+ lab.newSession();
206
+ setInspectorTab('flow');
207
+ }, []);
208
+
209
+ const onBudget = useCallback((value) => {
210
+ const lab = labRef.current;
211
+ if (!lab) return;
212
+ if (lab.setBudget(value)) showToast('教学预算已调整为 ' + alFormatTokens(value) + ' tok', 'layers');
213
+ else showToast('正在运行中,结束后再调整预算', 'alert');
214
+ }, [showToast]);
215
+
216
+ const toggleSessions = useCallback(() => {
217
+ setSessionsCollapsed((prev) => {
218
+ const next = !prev;
219
+ try {
220
+ localStorage.setItem('dsh.agent-lab.ui.v1', JSON.stringify({ sessionsCollapsed: next }));
221
+ } catch (error) {
222
+ /* 本地存储不可用时只影响记忆 */
223
+ }
224
+ return next;
225
+ });
226
+ }, []);
227
+
228
+ const onSend = useCallback((raw) => {
229
+ const lab = labRef.current;
230
+ if (!lab) return;
231
+ const text = String(raw == null ? '' : raw).trim();
232
+ if (!text) return;
233
+ if (text === '/compact') {
234
+ lab.compact();
235
+ showToast('正在压缩上下文', 'compress');
236
+ return;
237
+ }
238
+ if (text === '/new' || text === '/clear') {
239
+ lab.newSession();
240
+ setInspectorTab('flow');
241
+ showToast(text === '/clear' ? '已开始新会话' : '已新建会话', 'plus');
242
+ return;
243
+ }
244
+ if (text === '/memory') {
245
+ setInspectorTab('memory');
246
+ showToast('已切到记忆面板', 'memory');
247
+ return;
248
+ }
249
+ if (text === '/flow') {
250
+ setInspectorTab('flow');
251
+ showToast('已切到流转面板', 'route');
252
+ return;
253
+ }
254
+ lab.submit(text);
255
+ }, [showToast]);
256
+
257
+ const onCommand = useCallback((name) => {
258
+ const lab = labRef.current;
259
+ if (!lab) return;
260
+ if (name === 'compact') {
261
+ lab.compact();
262
+ showToast('正在压缩上下文', 'compress');
263
+ } else if (name === 'new') {
264
+ lab.newSession();
265
+ setInspectorTab('flow');
266
+ showToast('已新建会话', 'plus');
267
+ } else if (name === 'clear') {
268
+ lab.newSession();
269
+ setInspectorTab('flow');
270
+ showToast('已开始新会话', 'refresh');
271
+ } else if (name === 'memory') {
272
+ setInspectorTab('memory');
273
+ } else if (name === 'context') {
274
+ setInspectorTab('context');
275
+ }
276
+ }, [showToast]);
277
+
278
+ const onModelMode = useCallback((mode) => {
279
+ const lab = labRef.current;
280
+ if (!lab) return;
281
+ if (mode === 'real') {
282
+ if (!realStatus || !realStatus.available) {
283
+ showToast(realStatus && realStatus.error ? '真实模型不可用:' + realStatus.error : '真实模型不可用:请检查 DeepSeek 账号登录', 'alert');
284
+ return;
285
+ }
286
+ const changed = lab.setModelMode('real');
287
+ if (changed) showToast('真实模式 · ' + (realStatus.modelName || realStatus.model) + '(消耗账号额度)', 'bolt');
288
+ return;
289
+ }
290
+ if (lab.setModelMode('sim')) showToast('已切回模拟模式', 'cpu');
291
+ }, [realStatus, showToast]);
292
+
293
+ const onLaunchReal = useCallback(() => {
294
+ if (!props.onLaunchReal) return;
295
+ const prompt = (view && view.lastInput) || '用一个最小 agent 的循环解释工具调用是什么。';
296
+ let ok = false;
297
+ try {
298
+ ok = props.onLaunchReal(prompt);
299
+ } catch (error) {
300
+ ok = false;
301
+ }
302
+ showToast(ok ? '已在新会话中用真实 Agent 运行' : '当前环境无法创建会话', ok ? 'external' : 'alert');
303
+ }, [props.onLaunchReal, view, showToast]);
304
+
305
+ if (!view) {
306
+ return e(
307
+ 'div',
308
+ { className: 'al-root' },
309
+ e('style', { dangerouslySetInnerHTML: { __html: AL_CSS + TUI_CSS + INSPECTOR_CSS } }),
310
+ e('div', { className: 'al-inner' }, e(PageLoader, { error: loadError })),
311
+ );
312
+ }
313
+
314
+ const activeSession = (view.sessions || []).find((session) => session.id === view.activeId);
315
+ const activeTitle = activeSession ? activeSession.title : '新会话';
316
+
317
+ return e(
318
+ 'div',
319
+ { className: 'al-root' },
320
+ e('style', { dangerouslySetInnerHTML: { __html: AL_CSS + TUI_CSS + INSPECTOR_CSS } }),
321
+ e(
322
+ 'div',
323
+ { className: 'al-inner' },
324
+ e(
325
+ 'header',
326
+ { className: 'al-head' },
327
+ e(
328
+ 'div',
329
+ { className: 'al-brand' },
330
+ e('div', { className: 'al-logo' }, e(Icon, { name: 'terminal', size: 23, strokeWidth: 1.8 })),
331
+ e(
332
+ 'div',
333
+ { style: { minWidth: 0 } },
334
+ e(
335
+ 'div',
336
+ { className: 'al-title-row' },
337
+ e('h1', { className: 'al-title' }, 'Agent 实验室'),
338
+ e(Chip, { tone: 'brand', icon: 'spark' }, '迷你 Agent'),
339
+ view.modelMode === 'real'
340
+ ? e(Chip, { tone: 'violet', icon: 'bolt' }, '真实 · ' + ((realStatus && (realStatus.modelName || realStatus.model)) || 'DeepSeek'))
341
+ : e(Chip, { icon: 'cpu' }, '模拟 · 离线'),
342
+ ),
343
+ e('p', { className: 'al-sub' }, '输入一句话,实时看它经过上下文、模型、工具与记忆;再翻源码和原理,把 agent 拆开看。'),
344
+ ),
345
+ ),
346
+ e(
347
+ 'div',
348
+ { className: 'al-head-actions' },
349
+ e(
350
+ 'div',
351
+ { className: 'al-model-switch', role: 'group', 'aria-label': '模型模式' },
352
+ e('span', { className: 'al-ms-label' }, '模型模式'),
353
+ e(
354
+ 'button',
355
+ {
356
+ type: 'button',
357
+ className: view.modelMode === 'real' ? '' : 'on',
358
+ disabled: view.busy,
359
+ onClick: () => onModelMode('sim'),
360
+ title: '本地规则模拟:离线、可复现、不消耗额度',
361
+ },
362
+ '模拟',
363
+ ),
364
+ e(
365
+ 'button',
366
+ {
367
+ type: 'button',
368
+ className: 'real' + (view.modelMode === 'real' ? ' on' : ''),
369
+ disabled: view.busy || !(realStatus && realStatus.available),
370
+ onClick: () => onModelMode('real'),
371
+ title: realStatus && realStatus.available
372
+ ? '调用 DSH 当前模型:' + (realStatus.modelName || realStatus.model) + '(消耗账号额度)'
373
+ : '真实模型不可用:' + ((realStatus && realStatus.error) || '请检查 DeepSeek 账号登录'),
374
+ },
375
+ '真实 DeepSeek' + (realStatus && realStatus.available ? ' · ' + (realStatus.modelName || realStatus.model) : ''),
376
+ ),
377
+ ),
378
+ props.onLaunchReal
379
+ ? e(Btn, { variant: 'ghost', icon: 'external', onClick: onLaunchReal, title: '把当前这轮输入交给 DSH 主 Agent(含工具与工作区)再跑一遍' }, '交给 DSH Agent')
380
+ : null,
381
+ e(Btn, { variant: 'primary', icon: 'plus', onClick: newSession, disabled: view.busy }, '新建会话'),
382
+ ),
383
+ ),
384
+ e(
385
+ 'div',
386
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', flex: 'none' } },
387
+ e(
388
+ 'div',
389
+ { className: 'al-tabs', role: 'tablist', 'aria-label': 'Agent 实验室导航' },
390
+ AL_TABS.map((item) =>
391
+ e(
392
+ 'button',
393
+ {
394
+ key: item.id,
395
+ type: 'button',
396
+ role: 'tab',
397
+ 'aria-selected': tab === item.id,
398
+ className: 'al-tab' + (tab === item.id ? ' on' : ''),
399
+ onClick: () => setTab(item.id),
400
+ },
401
+ e(Icon, { name: item.icon, size: 13 }),
402
+ item.label,
403
+ ),
404
+ ),
405
+ ),
406
+ e(
407
+ 'div',
408
+ { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 } },
409
+ e(Chip, { tone: view.busy ? 'brand' : 'neutral', icon: view.busy ? 'bolt' : 'check' }, view.busy ? 'Agent 运行中' : '就绪'),
410
+ e('span', { className: 'al-panel-sub', style: { whiteSpace: 'nowrap' } }, '所有状态实时同步到检查器'),
411
+ ),
412
+ ),
413
+ tab === 'lab'
414
+ ? e(
415
+ 'div',
416
+ { className: 'al-tabpane' },
417
+ e(
418
+ 'div',
419
+ { className: 'al-lab' + (sessionsCollapsed ? ' collapsed' : '') },
420
+ sessionsCollapsed
421
+ ? e(SessionsRail, { count: view.sessions.length, onExpand: toggleSessions })
422
+ : e(SessionsPanel, {
423
+ view,
424
+ onNew: newSession,
425
+ onCollapse: toggleSessions,
426
+ onSwitch: (id) => { const lab = labRef.current; if (lab) lab.switchSession(id); },
427
+ onDelete: (id) => {
428
+ const lab = labRef.current;
429
+ if (!lab) return;
430
+ lab.deleteSession(id);
431
+ showToast('已删除会话', 'trash');
432
+ },
433
+ onReset: () => {
434
+ const lab = labRef.current;
435
+ if (!lab) return;
436
+ lab.resetAll();
437
+ setInspectorTab('flow');
438
+ showToast('已重置本地数据', 'refresh');
439
+ },
440
+ }),
441
+ e(TuiPanel, {
442
+ view,
443
+ activeTitle,
444
+ realStatus,
445
+ onMode: onModelMode,
446
+ onSend,
447
+ onCommand,
448
+ onPace: (pace) => { const lab = labRef.current; if (lab) lab.setPace(pace); },
449
+ }),
450
+ e(Inspector, {
451
+ view,
452
+ tab: inspectorTab,
453
+ onTab: setInspectorTab,
454
+ onDeleteMemory: (id) => { const lab = labRef.current; if (lab) lab.deleteMemory(id); },
455
+ onClearMemory: () => { const lab = labRef.current; if (lab) lab.clearMemory(); },
456
+ onBudget,
457
+ }),
458
+ ),
459
+ )
460
+ : tab === 'dataflow'
461
+ ? e('div', { className: 'al-tabpane scroll' }, e(DataFlowPage, { view, onBudget }))
462
+ : tab === 'source'
463
+ ? e('div', { className: 'al-tabpane scroll' }, e(SourcePage))
464
+ : e(
465
+ 'div',
466
+ { className: 'al-tabpane scroll' },
467
+ e(DocsPage, {
468
+ onNavigate: (pageId) => {
469
+ if (pageId === 'source' || pageId === 'lab' || pageId === 'docs') setTab(pageId);
470
+ },
471
+ }),
472
+ ),
473
+ ),
474
+ toast ? e('div', { className: 'al-toast', role: 'status' }, e(Icon, { name: toast.icon, size: 14 }), toast.text) : null,
475
+ );
476
+ }
@@ -0,0 +1,58 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Agent 实验室 · Client entry
3
+ * Registers the standalone sidebar entry (left rail) and its main panel.
4
+ * The slot registration pattern is the same one used by shipped pages:
5
+ * sidebar.panellist → the icon + label in the left navigation
6
+ * main → the page body, dispatched by the same id
7
+ * ───────────────────────────────────────────────────────────── */
8
+
9
+ function AgentLabPanelIcon(props) {
10
+ const size = (props && props.size) || 16;
11
+ return e(
12
+ 'svg',
13
+ {
14
+ width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
15
+ stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round',
16
+ 'aria-hidden': 'true',
17
+ },
18
+ e('rect', { x: 3.2, y: 4.4, width: 17.6, height: 15.2, rx: 3 }),
19
+ e('path', { d: 'M7.4 9.4l2.6 2.6-2.6 2.6' }),
20
+ e('path', { d: 'M12.8 14.6h4' }),
21
+ );
22
+ }
23
+
24
+ function apply(ctx) {
25
+ const launchReal = (prompt) => {
26
+ const workspace = ctx.uiWorkspace || ctx.get('uiWorkspace');
27
+ if (!workspace || typeof workspace.startSession !== 'function') return false;
28
+ try {
29
+ workspace.startSession(void 0, { prompt: prompt, whenDraftExists: 'append' });
30
+ return true;
31
+ } catch (error) {
32
+ return false;
33
+ }
34
+ };
35
+
36
+ ctx.slots.inject('main', () =>
37
+ ctx.slots.register(
38
+ {
39
+ name: 'main',
40
+ key: 'agent-lab',
41
+ inject: () => ({ onLaunchReal: launchReal }),
42
+ },
43
+ Page,
44
+ ),
45
+ );
46
+
47
+ ctx.slots.inject('sidebar.panellist', () =>
48
+ ctx.slots.register(
49
+ {
50
+ name: 'sidebar.panellist',
51
+ id: 'agent-lab',
52
+ order: 6,
53
+ label: () => 'Agent 实验室',
54
+ },
55
+ AgentLabPanelIcon,
56
+ ),
57
+ );
58
+ }