@steerable/agent-shell-web 0.6.15

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.
Files changed (121) hide show
  1. package/LICENSE +91 -0
  2. package/README.md +20 -0
  3. package/package.json +69 -0
  4. package/public/favicon.ico +0 -0
  5. package/public/favicon.png +0 -0
  6. package/src/AppShell.tsx +24 -0
  7. package/src/assets/logo-generic.png +0 -0
  8. package/src/brand.test.ts +22 -0
  9. package/src/brand.ts +48 -0
  10. package/src/components/AgentSidebar.test.tsx +127 -0
  11. package/src/components/AgentSidebar.tsx +1010 -0
  12. package/src/components/ChatHeader.test.tsx +230 -0
  13. package/src/components/ChatHeader.tsx +436 -0
  14. package/src/components/DockPanelHeader.tsx +69 -0
  15. package/src/components/LocalLlmSettingsModal.test.tsx +372 -0
  16. package/src/components/LocalLlmSettingsModal.tsx +72 -0
  17. package/src/components/TerminalPanel.tsx +68 -0
  18. package/src/components/TerminalView.tsx +228 -0
  19. package/src/components/chat/ApprovalModal.test.tsx +83 -0
  20. package/src/components/chat/ApprovalModal.tsx +229 -0
  21. package/src/components/chat/AskUserModalHost.test.tsx +176 -0
  22. package/src/components/chat/AskUserModalHost.tsx +61 -0
  23. package/src/components/chat/AskUserQuestionMenu.tsx +621 -0
  24. package/src/components/chat/AssistantMessage.test.tsx +68 -0
  25. package/src/components/chat/AssistantMessage.tsx +469 -0
  26. package/src/components/chat/ChatInput.test.tsx +609 -0
  27. package/src/components/chat/ChatInput.tsx +2068 -0
  28. package/src/components/chat/ChatProjectBadge.tsx +362 -0
  29. package/src/components/chat/EmptyChat.tsx +18 -0
  30. package/src/components/chat/ExecPolicyPicker.test.tsx +26 -0
  31. package/src/components/chat/ExecPolicyPicker.tsx +120 -0
  32. package/src/components/chat/ExecutedActionsCard.tsx +216 -0
  33. package/src/components/chat/InterruptedTurnCard.tsx +53 -0
  34. package/src/components/chat/LocalChatPanel.tsx +408 -0
  35. package/src/components/chat/Markdown.test.tsx +170 -0
  36. package/src/components/chat/Markdown.tsx +432 -0
  37. package/src/components/chat/MessageList.tsx +332 -0
  38. package/src/components/chat/ModelPicker.test.tsx +149 -0
  39. package/src/components/chat/ModelPicker.tsx +289 -0
  40. package/src/components/chat/OrchestrationChildrenCard.tsx +40 -0
  41. package/src/components/chat/PlanTodoList.tsx +121 -0
  42. package/src/components/chat/ProjectTrustBanner.tsx +91 -0
  43. package/src/components/chat/SessionTreeModal.test.tsx +161 -0
  44. package/src/components/chat/SessionTreeModal.tsx +221 -0
  45. package/src/components/chat/StreamingStatus.tsx +95 -0
  46. package/src/components/chat/TaskOutcomeCards.test.tsx +97 -0
  47. package/src/components/chat/TaskOutcomeCards.tsx +128 -0
  48. package/src/components/chat/TaskPanelModal.test.tsx +198 -0
  49. package/src/components/chat/TaskPanelModal.tsx +310 -0
  50. package/src/components/chat/TaskProcessPanel.test.tsx +111 -0
  51. package/src/components/chat/TaskProcessPanel.tsx +208 -0
  52. package/src/components/chat/TurnProcessGroup.test.tsx +161 -0
  53. package/src/components/chat/TurnProcessGroup.tsx +193 -0
  54. package/src/components/chat/UnifiedDiff.tsx +31 -0
  55. package/src/components/chat/UserMessage.tsx +155 -0
  56. package/src/components/chat/elapsed.test.ts +42 -0
  57. package/src/components/chat/elapsed.ts +45 -0
  58. package/src/components/chat/executed-actions-model.test.ts +134 -0
  59. package/src/components/chat/executed-actions-model.ts +143 -0
  60. package/src/components/chat/orchestration-children-model.test.ts +115 -0
  61. package/src/components/chat/orchestration-children-model.ts +104 -0
  62. package/src/components/chat/timestamp.ts +24 -0
  63. package/src/components/chat/turn-timeline.test.ts +97 -0
  64. package/src/components/chat/turn-timeline.ts +132 -0
  65. package/src/components/chat/useChatTasks.test.ts +136 -0
  66. package/src/components/chat/useChatTasks.ts +134 -0
  67. package/src/components/chat/useCopy.ts +62 -0
  68. package/src/components/settings/AgentCapabilityFields.tsx +343 -0
  69. package/src/components/settings/AgentsSettingsPanel.test.tsx +229 -0
  70. package/src/components/settings/AgentsSettingsPanel.tsx +412 -0
  71. package/src/components/settings/DiagnoseSettingsPanel.tsx +119 -0
  72. package/src/components/settings/InsightsSettingsPanel.test.tsx +132 -0
  73. package/src/components/settings/InsightsSettingsPanel.tsx +309 -0
  74. package/src/components/settings/LlmSettingsPanel.tsx +875 -0
  75. package/src/components/settings/McpSettingsPanel.tsx +466 -0
  76. package/src/components/settings/ModelCapabilityChips.tsx +46 -0
  77. package/src/components/settings/ModelIdCombobox.test.tsx +88 -0
  78. package/src/components/settings/ModelIdCombobox.tsx +185 -0
  79. package/src/components/settings/SecuritySettingsPanel.test.tsx +122 -0
  80. package/src/components/settings/SecuritySettingsPanel.tsx +169 -0
  81. package/src/components/settings/SettingsSaveButton.tsx +35 -0
  82. package/src/components/settings/SkillsSettingsPanel.test.tsx +62 -0
  83. package/src/components/settings/SkillsSettingsPanel.tsx +334 -0
  84. package/src/components/settings/TelemetrySettingsPanel.tsx +173 -0
  85. package/src/components/settings/UsagePanel.tsx +196 -0
  86. package/src/components/settings/WebSearchSettingsPanel.test.tsx +90 -0
  87. package/src/components/settings/WebSearchSettingsPanel.tsx +183 -0
  88. package/src/components/settings/compat-flags-model.test.ts +71 -0
  89. package/src/components/settings/compat-flags-model.ts +57 -0
  90. package/src/components/settings/llm-vendors.test.ts +171 -0
  91. package/src/components/settings/llm-vendors.ts +275 -0
  92. package/src/components/settings/model-capabilities.test.ts +88 -0
  93. package/src/components/settings/model-capabilities.ts +123 -0
  94. package/src/components/settings/preset-choice-model.test.ts +96 -0
  95. package/src/components/settings/preset-choice-model.ts +75 -0
  96. package/src/fixtures/browser-dev-data.json +315 -0
  97. package/src/hooks/useChatsAndAgents.ts +182 -0
  98. package/src/layouts/AgentLayout.tsx +574 -0
  99. package/src/lib/attachments.ts +78 -0
  100. package/src/lib/browser-dev-electron-mock.ts +640 -0
  101. package/src/lib/chat-title.ts +33 -0
  102. package/src/lib/chat-transport.ts +346 -0
  103. package/src/lib/date-groups.ts +34 -0
  104. package/src/lib/demo-flag.ts +15 -0
  105. package/src/lib/electron-bridge.ts +270 -0
  106. package/src/lib/exec-policy.ts +25 -0
  107. package/src/lib/http-bridge.ts +293 -0
  108. package/src/lib/insights.ts +17 -0
  109. package/src/lib/local-api.ts +714 -0
  110. package/src/lib/pending-first-message.test.ts +17 -0
  111. package/src/lib/pending-first-message.ts +36 -0
  112. package/src/lib/slash-sources.ts +191 -0
  113. package/src/main.tsx +103 -0
  114. package/src/packs/registry.ts +64 -0
  115. package/src/pages/AgentPage.tsx +1226 -0
  116. package/src/pages/ChatPanelPreviewPage.tsx +264 -0
  117. package/src/pages/SettingsPage.test.tsx +102 -0
  118. package/src/pages/SettingsPage.tsx +197 -0
  119. package/src/styles/index.css +156 -0
  120. package/src/vite-env.d.ts +1 -0
  121. package/vite.base.ts +94 -0
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ setPendingFirstMessage,
4
+ takePendingFirstMessage,
5
+ } from './pending-first-message';
6
+
7
+ describe('pending-first-message(首页首条消息接力)', () => {
8
+ it('按 chatId 取出一次后清空,错配的 chatId 不消费', () => {
9
+ setPendingFirstMessage({ chatId: 'chat-a', content: '秋天到了,随便聊一句吧。' });
10
+ expect(takePendingFirstMessage('chat-b')).toBeNull();
11
+ expect(takePendingFirstMessage('chat-a')).toEqual({
12
+ chatId: 'chat-a',
13
+ content: '秋天到了,随便聊一句吧。',
14
+ });
15
+ expect(takePendingFirstMessage('chat-a')).toBeNull();
16
+ });
17
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 首页输入框 → 新对话首条消息的接力棒。
3
+ *
4
+ * EmptyChatGate(无 chatId 的落地页)的输入框提交时,流程是
5
+ * "先 createChat → 再 navigate → 新 chat 视图挂载后才发得出消息"。
6
+ * 但 submit 时拿到的文本在 navigate 之后就离开了作用域——react-router 的
7
+ * location.state 能跨跳转,可 hash 路由 + HMR 下它不够稳(刷新即丢,
8
+ * 且 AgentChatLoader 的 key 重挂载语义会让读取时机变得微妙)。
9
+ *
10
+ * 这里用模块级单例暂存:写入方(EmptyChatGate)在拿到 chatId 后、跳转前
11
+ * set;消费方(AgentChatView)挂载后 take 一次并自动发送。take 按
12
+ * chatId 匹配且取出即清,StrictMode 双跑 effect / 重复挂载都不会重发。
13
+ * 页面刷新后暂存丢失——可接受:刷新发生在流式开始前的话,chat 已经建好,
14
+ * 用户重发一次即可。
15
+ */
16
+
17
+ export interface PendingFirstMessage {
18
+ chatId: string;
19
+ content: string;
20
+ metadata?: Record<string, unknown>;
21
+ }
22
+
23
+ let pending: PendingFirstMessage | null = null;
24
+
25
+ export function setPendingFirstMessage(msg: PendingFirstMessage): void {
26
+ pending = msg;
27
+ }
28
+
29
+ export function takePendingFirstMessage(
30
+ chatId: string,
31
+ ): PendingFirstMessage | null {
32
+ if (!pending || pending.chatId !== chatId) return null;
33
+ const msg = pending;
34
+ pending = null;
35
+ return msg;
36
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Slash sources — the local skills + MCP tools behind every `/` token.
3
+ *
4
+ * One source of truth for two surfaces that must agree on "is `/foo` a real
5
+ * tool?":
6
+ * - `ChatInput` — the `/` suggestion menu and the composer's tool chips.
7
+ * - `Markdown` — the same chips replayed inside sent message bubbles.
8
+ *
9
+ * The catalog is cached at module scope and shared by every subscriber, so a
10
+ * chat with 50 rendered messages still costs one fetch. `refreshSlashSources`
11
+ * re-reads it (MCP servers and skills can be added/removed in settings while
12
+ * a chat is open) and pushes the result to all live subscribers.
13
+ */
14
+
15
+ import { useCallback, useEffect, useState } from 'react';
16
+ import { getPackHiddenSlashSkills } from '../packs/registry';
17
+ import { getElectronBridge, isElectron } from './electron-bridge';
18
+
19
+ export type SkillItem = {
20
+ /** Skill directory name (e.g. `70-plan-mode`); absent on hand-built items. */
21
+ id?: string;
22
+ name: string;
23
+ displayName?: string;
24
+ description: string;
25
+ isBuiltin?: boolean;
26
+ };
27
+
28
+ export type McpToolItem = {
29
+ /** First-class tool name `mcp__<serverKey>__<toolName>`, inserted verbatim. */
30
+ token: string;
31
+ toolName: string;
32
+ serverKey: string;
33
+ serverName: string;
34
+ description: string;
35
+ };
36
+
37
+ export interface SlashSources {
38
+ skills: SkillItem[];
39
+ mcpTools: McpToolItem[];
40
+ }
41
+
42
+ /**
43
+ * 系统内置的身份/约束/指导类技能:由引擎按条件自动注入系统提示词,不是用户
44
+ * 手动指定执行的业务技能,因此不进 "/" 菜单,也不渲染成工具卡片。技能名与
45
+ * 目录名都登记,两种写法都能命中。
46
+ */
47
+ const HIDDEN_SLASH_SKILLS = new Set([
48
+ // 助手身份人设底座
49
+ '00-identity',
50
+ 'identity',
51
+ // 计划模式约束(由输入框底部的 Agent/Plan 开关专控)
52
+ '70-plan-mode',
53
+ 'plan-mode',
54
+ // 工具调用规范
55
+ '80-tool-usage',
56
+ 'tool-usage',
57
+ // 零容忍光说不做
58
+ '81-anti-deferred',
59
+ 'anti-deferred',
60
+ 'anti-deferred-execution',
61
+ // 零容忍捏造与过时缓存
62
+ '82-data-grounding',
63
+ 'data-grounding',
64
+ // 本地命令/文件读写规范
65
+ '85-local-exec',
66
+ 'local-exec',
67
+ // 主动编程补位规范
68
+ '86-proactive-coding',
69
+ 'proactive-coding',
70
+ // 场景包的隐藏技能由包渲染层声明,
71
+ // 见 getPackHiddenSlashSkills(0.4 起不再硬编码在这里)。
72
+ ]);
73
+
74
+ /** True for skills the engine injects on its own; they never reach the user. */
75
+ export function isHiddenSlashSkill(skill: SkillItem): boolean {
76
+ const packHidden = getPackHiddenSlashSkills();
77
+ return (
78
+ HIDDEN_SLASH_SKILLS.has(skill.name) ||
79
+ (skill.id !== undefined && HIDDEN_SLASH_SKILLS.has(skill.id)) ||
80
+ packHidden.has(skill.name) ||
81
+ (skill.id !== undefined && packHidden.has(skill.id))
82
+ );
83
+ }
84
+
85
+ /**
86
+ * `/token` occurrences: line start or after whitespace, no inner slash. Keeps
87
+ * paths (`/mnt/c`), URLs and fractions out — same rule the backend's
88
+ * `parseUserMessageTriggers` uses to decide what counts as a trigger.
89
+ */
90
+ export const SLASH_TOKEN_PATTERN = /(?:^|\s)(\/[^\s/]+)/g;
91
+
92
+ export type SlashToolRef = { type: 'skill' | 'mcp'; id: string; label: string };
93
+
94
+ /**
95
+ * Resolve a bare `/` token name to the tool it names, or null when nothing
96
+ * matches. `mcp__*` resolves even without a catalog entry: the namespace is
97
+ * unambiguous and the server may be temporarily disconnected.
98
+ */
99
+ export function resolveSlashTool(
100
+ name: string,
101
+ skills: SkillItem[] = [],
102
+ mcpTools: McpToolItem[] = [],
103
+ ): SlashToolRef | null {
104
+ const key = name.toLowerCase();
105
+
106
+ const mcp = mcpTools.find((t) => t.token.toLowerCase() === key);
107
+ if (mcp) return { type: 'mcp', id: mcp.token, label: name };
108
+ if (key.startsWith('mcp__')) return { type: 'mcp', id: name, label: name };
109
+
110
+ const skill = skills.find(
111
+ (s) =>
112
+ s.name.toLowerCase() === key ||
113
+ (s.displayName !== undefined && s.displayName.toLowerCase() === key),
114
+ );
115
+ if (skill && !isHiddenSlashSkill(skill)) {
116
+ return { type: 'skill', id: skill.name, label: name };
117
+ }
118
+ return null;
119
+ }
120
+
121
+ const EMPTY: SlashSources = { skills: [], mcpTools: [] };
122
+
123
+ let cached: SlashSources = EMPTY;
124
+ let inflight: Promise<SlashSources> | null = null;
125
+ const listeners = new Set<(sources: SlashSources) => void>();
126
+
127
+ async function load(): Promise<SlashSources> {
128
+ const bridge = getElectronBridge();
129
+ if (!bridge) return cached;
130
+
131
+ let skills = cached.skills;
132
+ let mcpTools = cached.mcpTools;
133
+
134
+ try {
135
+ const res = await bridge.localBackend.request<{ skills: SkillItem[] }>({
136
+ method: 'GET',
137
+ path: '/api/v2/chat-agents/skills',
138
+ });
139
+ skills = (res.skills || []).filter((s) => !isHiddenSlashSkill(s));
140
+ } catch (err) {
141
+ console.error('slash-sources failed to fetch skills:', err);
142
+ }
143
+
144
+ try {
145
+ const res = await bridge.localBackend.request<{ mcpTools: McpToolItem[] }>({
146
+ method: 'GET',
147
+ path: '/api/v2/chat-agents/mcp-tools',
148
+ });
149
+ mcpTools = res.mcpTools || [];
150
+ } catch (err) {
151
+ // MCP 服务不可用时静默降级——技能候选不受影响
152
+ console.error('slash-sources failed to fetch MCP tools:', err);
153
+ }
154
+
155
+ cached = { skills, mcpTools };
156
+ for (const listener of listeners) listener(cached);
157
+ return cached;
158
+ }
159
+
160
+ /** Re-read the catalog. Concurrent callers share one in-flight request. */
161
+ export function refreshSlashSources(): Promise<SlashSources> {
162
+ if (!isElectron()) return Promise.resolve(cached);
163
+ if (!inflight) {
164
+ inflight = load().finally(() => {
165
+ inflight = null;
166
+ });
167
+ }
168
+ return inflight;
169
+ }
170
+
171
+ /**
172
+ * Subscribe to the shared catalog. Fetches once on first mount; call
173
+ * `refresh` when the `/` menu opens so a just-connected MCP server shows up.
174
+ */
175
+ export function useSlashSources(): SlashSources & {
176
+ refresh: () => Promise<SlashSources>;
177
+ } {
178
+ const [sources, setSources] = useState<SlashSources>(cached);
179
+
180
+ useEffect(() => {
181
+ listeners.add(setSources);
182
+ void refreshSlashSources();
183
+ return () => {
184
+ listeners.delete(setSources);
185
+ };
186
+ }, []);
187
+
188
+ const refresh = useCallback(() => refreshSlashSources(), []);
189
+
190
+ return { skills: sources.skills, mcpTools: sources.mcpTools, refresh };
191
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,103 @@
1
+ import { StrictMode, lazy, Suspense, type ReactNode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { createHashRouter, RouterProvider } from 'react-router-dom';
4
+ import { AppShell } from './AppShell';
5
+ import { AgentLayout } from './layouts/AgentLayout';
6
+ import { AgentPage } from './pages/AgentPage';
7
+ import { SettingsPage } from './pages/SettingsPage';
8
+ import { getPackRoutes } from './packs/registry';
9
+ import { getBrandLogoUrl, BRAND_NAME } from './brand';
10
+ import './styles/index.css';
11
+
12
+ // 浏览器 dev mock 的安装在 bootstrap() 开头(动态 import + DEV 门)。
13
+
14
+ // Dev-only preview harness. Lazy-loaded behind `import.meta.env.DEV` so the
15
+ // chunk vanishes from production builds entirely — Rollup tree-shakes the
16
+ // dynamic import when the condition is statically `false`. The route itself
17
+ // is also conditionally registered; in prod a stray `#/preview/chat` URL
18
+ // falls through to react-router's default "no match" handling (the layout
19
+ // outlet just renders nothing).
20
+ const ChatPanelPreviewPage = import.meta.env.DEV
21
+ ? lazy(() => import('./pages/ChatPanelPreviewPage'))
22
+ : null;
23
+
24
+ // 用 HashRouter 而不是 BrowserRouter:Electron loadFile() 加载本地 index.html
25
+ // 时 URL 是 file://... 没有真正的 server 解析路径段,BrowserRouter 在刷新或
26
+ // 跳转时会 404。HashRouter 把路由放进 # 后面,纯客户端解析,跟 file:// 兼容。
27
+ //
28
+ // 路由分层:
29
+ // AppShell 全局顶层 chrome(浏览器预览提示等)
30
+ // ├─ AgentLayout 带 sidebar 的主界面(终端是它的内嵌面板,不是路由)
31
+ // │ ├─ / 默认进 AgentPage(无 chatId)
32
+ // │ ├─ /agent
33
+ // │ ├─ /agent/:chatId
34
+ // │ └─ /settings 设置页(智能体 / Skill / MCP / 综合,按 ?section= 分页面)
35
+ // └─ <包路由> 场景包注册的独立页(如包的调试日志窗)
36
+ /**
37
+ * web 应用 bootstrap(2.3 起由产品入口调用)。
38
+ *
39
+ * shell 本体不装载任何包的渲染层贡献:产品入口
40
+ * (products/<id>/web/main.tsx)先静态调用包的 register*Renderer(),
41
+ * 再调本函数——路由注册发生在路由表创建之前。
42
+ */
43
+ export async function bootstrap(): Promise<void> {
44
+ if (import.meta.env.DEV) {
45
+ // 动态 import:prod 构建里 Rollup 把 DEV 分支连同 mock(含其 fixtures)
46
+ // 整体树摇掉,浏览器 dev mock 数据不进产物。
47
+ const { installBrowserDevElectronMock } = await import('./lib/browser-dev-electron-mock');
48
+ installBrowserDevElectronMock();
49
+ }
50
+
51
+ // 标题与 favicon 在 bootstrap 时设置——此刻产品入口已完成包注册
52
+ // (品牌 logo 由包经 setBrandLogoUrl 注入,见 brand.ts)。
53
+ document.title = BRAND_NAME;
54
+ document.querySelector<HTMLLinkElement>('link[rel="icon"]')?.setAttribute('href', getBrandLogoUrl());
55
+
56
+ const router = createHashRouter([
57
+ {
58
+ path: '/',
59
+ element: <AppShell />,
60
+ children: [
61
+ {
62
+ element: <AgentLayout />,
63
+ children: [
64
+ { index: true, element: <AgentPage /> },
65
+ { path: 'agent', element: <AgentPage /> },
66
+ { path: 'agent/:chatId', element: <AgentPage /> },
67
+ { path: 'settings', element: <SettingsPage /> },
68
+ ],
69
+ },
70
+ ...getPackRoutes().map(({ path, Component }) => ({
71
+ path,
72
+ element: <Component />,
73
+ })),
74
+ // Dev-only visual harness. Route is omitted entirely in prod builds so
75
+ // the chunk doesn't ship to end users; `ChatPanelPreviewPage` is
76
+ // intentionally not exported from any production code path either.
77
+ ...(ChatPanelPreviewPage
78
+ ? [
79
+ {
80
+ path: 'preview/chat',
81
+ element: (
82
+ <Suspense fallback={(<div>loading preview…</div>) as ReactNode}>
83
+ <ChatPanelPreviewPage />
84
+ </Suspense>
85
+ ),
86
+ },
87
+ ]
88
+ : []),
89
+ ],
90
+ },
91
+ ]);
92
+
93
+ const rootEl = document.getElementById('root');
94
+ if (!rootEl) {
95
+ throw new Error('Missing #root element in index.html');
96
+ }
97
+
98
+ createRoot(rootEl).render(
99
+ <StrictMode>
100
+ <RouterProvider router={router} />
101
+ </StrictMode>,
102
+ );
103
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 渲染层包注册表(0.3f)—— ScenarioPack.renderer 的 web 侧归宿。
3
+ *
4
+ * 包的渲染层代码住在 `packages/pack-<id>/web/`,经产品 web 入口
5
+ * (products/<id>/web/main.tsx,2.3 起)静态注册——产品编译单元只含
6
+ * 自己的包,未激活包代码物理上不进 bundle(不再依赖树摇正确性)。
7
+ *
8
+ * 槽位:routes / settingsPanels / chatSlots 均有真实包消费者(见各包
9
+ * 的 web/ 目录)。
10
+ */
11
+
12
+ import type {
13
+ PackChatSlotAutoRevealApi,
14
+ PackChatSlotContribution,
15
+ PackChatSlotProps,
16
+ PackRendererContributions,
17
+ PackRouteContribution,
18
+ PackSettingsPanelContribution,
19
+ } from '@steerable/pack-sdk/web';
20
+
21
+ // 类型单一真源在 @steerable/pack-sdk/web(阶段 2.2);re-export 兼容既有调用方。
22
+ export type {
23
+ PackChatSlotAutoRevealApi,
24
+ PackChatSlotContribution,
25
+ PackChatSlotProps,
26
+ PackRendererContributions,
27
+ PackRouteContribution,
28
+ PackSettingsPanelContribution,
29
+ };
30
+
31
+ const routes: PackRouteContribution[] = [];
32
+ const settingsPanels: PackSettingsPanelContribution[] = [];
33
+ const chatSlots: PackChatSlotContribution[] = [];
34
+ const hiddenSlashSkills = new Set<string>();
35
+ const registeredPacks = new Set<string>();
36
+
37
+ /** 注册一个包的渲染层贡献。重复注册同一包抛错(组装期笔误,fail fast)。 */
38
+ export function registerPackRenderer(packId: string, contributions: PackRendererContributions): void {
39
+ if (registeredPacks.has(packId)) {
40
+ throw new Error(`[pack-renderer] duplicate registration for pack: ${packId}`);
41
+ }
42
+ registeredPacks.add(packId);
43
+ routes.push(...(contributions.routes ?? []));
44
+ settingsPanels.push(...(contributions.settingsPanels ?? []));
45
+ chatSlots.push(...(contributions.chatSlots ?? []));
46
+ for (const name of contributions.hiddenSlashSkills ?? []) hiddenSlashSkills.add(name);
47
+ }
48
+
49
+ export function getPackRoutes(): readonly PackRouteContribution[] {
50
+ return routes;
51
+ }
52
+
53
+ export function getPackSettingsPanels(): readonly PackSettingsPanelContribution[] {
54
+ return settingsPanels;
55
+ }
56
+
57
+ export function getPackChatSlots(): readonly PackChatSlotContribution[] {
58
+ return chatSlots;
59
+ }
60
+
61
+ /** 包贡献的隐藏 slash 技能名集合(bootstrap 完成后稳定,渲染期只读)。 */
62
+ export function getPackHiddenSlashSkills(): ReadonlySet<string> {
63
+ return hiddenSlashSkills;
64
+ }