@steerable/agent-shell-web 0.6.19 → 0.6.21

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 (76) hide show
  1. package/package.json +15 -8
  2. package/src/components/chat/ApprovalModal.tsx +21 -41
  3. package/src/components/chat/ApprovalPromptProvider.tsx +98 -0
  4. package/src/components/chat/AskUserPromptProvider.tsx +99 -0
  5. package/src/components/chat/AssistantMessage.tsx +25 -2
  6. package/src/components/chat/ChatInput.tsx +116 -13
  7. package/src/components/chat/FilePathCode.tsx +65 -0
  8. package/src/components/chat/LocalChatPanel.tsx +19 -0
  9. package/src/components/chat/Markdown.tsx +54 -25
  10. package/src/components/chat/MessageList.tsx +66 -18
  11. package/src/components/chat/ModelPicker.tsx +5 -3
  12. package/src/components/chat/SuggestedReplies.tsx +34 -0
  13. package/src/components/chat/TurnFilesCard.tsx +113 -0
  14. package/src/components/chat/TurnProcessGroup.tsx +7 -2
  15. package/src/components/chat/path-mentions.ts +148 -0
  16. package/src/components/chat/suggested-replies-model.ts +40 -0
  17. package/src/components/chat/turn-files.ts +50 -0
  18. package/src/layouts/AgentLayout.tsx +15 -26
  19. package/src/lib/browser-dev-electron-mock.ts +15 -0
  20. package/src/lib/electron-bridge.ts +13 -7
  21. package/src/lib/http-bridge.ts +8 -2
  22. package/src/lib/local-api.ts +31 -0
  23. package/src/pages/AgentPage.tsx +126 -1
  24. package/src/pages/ChatPanelPreviewPage.tsx +4 -0
  25. package/vite.base.d.ts +7 -0
  26. package/vite.base.js +83 -0
  27. package/src/brand.test.ts +0 -22
  28. package/src/components/AgentSidebar.test.tsx +0 -791
  29. package/src/components/ChatHeader.test.tsx +0 -230
  30. package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
  31. package/src/components/chat/ApprovalModal.test.tsx +0 -83
  32. package/src/components/chat/AskUserModalHost.test.tsx +0 -176
  33. package/src/components/chat/AskUserModalHost.tsx +0 -61
  34. package/src/components/chat/AskUserQuestionMenu.test.tsx +0 -190
  35. package/src/components/chat/AssistantMessage.test.tsx +0 -68
  36. package/src/components/chat/ChatInput.test.tsx +0 -609
  37. package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
  38. package/src/components/chat/Markdown.test.tsx +0 -170
  39. package/src/components/chat/ModelPicker.test.tsx +0 -149
  40. package/src/components/chat/SessionTreeModal.test.tsx +0 -161
  41. package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
  42. package/src/components/chat/TaskPanelModal.test.tsx +0 -198
  43. package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
  44. package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
  45. package/src/components/chat/elapsed.test.ts +0 -42
  46. package/src/components/chat/executed-actions-model.test.ts +0 -134
  47. package/src/components/chat/orchestration-children-model.test.ts +0 -115
  48. package/src/components/chat/turn-timeline.test.ts +0 -97
  49. package/src/components/chat/useChatTasks.test.ts +0 -136
  50. package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
  51. package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
  52. package/src/components/settings/LlmSettingsPanel.test.tsx +0 -461
  53. package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
  54. package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
  55. package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
  56. package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
  57. package/src/components/settings/compat-flags-model.test.ts +0 -71
  58. package/src/components/settings/llm-vendors.test.ts +0 -171
  59. package/src/components/settings/model-capabilities.test.ts +0 -88
  60. package/src/components/settings/preset-choice-model.test.ts +0 -96
  61. package/src/hooks/useChatsAndAgents.test.ts +0 -165
  62. package/src/lib/attachments.test.ts +0 -127
  63. package/src/lib/chat-title.test.ts +0 -53
  64. package/src/lib/chat-transport.test.ts +0 -282
  65. package/src/lib/date-groups.test.ts +0 -62
  66. package/src/lib/demo-flag.test.ts +0 -17
  67. package/src/lib/electron-bridge.test.ts +0 -54
  68. package/src/lib/exec-policy.test.ts +0 -71
  69. package/src/lib/http-bridge.test.ts +0 -283
  70. package/src/lib/insights.test.ts +0 -56
  71. package/src/lib/local-api.test.ts +0 -353
  72. package/src/lib/pending-first-message.test.ts +0 -17
  73. package/src/lib/slash-sources.test.ts +0 -161
  74. package/src/pages/AgentPage.test.tsx +0 -627
  75. package/src/pages/SettingsPage.test.tsx +0 -102
  76. package/vite.base.ts +0 -94
@@ -26,7 +26,12 @@ import type { ExecutedAction } from "@/components/chat/ExecutedActionsCard";
26
26
  import type { ChildInfo } from "@/components/chat/OrchestrationChildrenCard";
27
27
  import { foldOrchestrationChildEvents } from "@/components/chat/orchestration-children-model";
28
28
  import { parseTurnBlocks, type TurnBlock } from "@/components/chat/turn-timeline";
29
+ import { parseTurnFiles, type TurnFile } from "@/components/chat/turn-files";
29
30
  import { readPersistedDurationMs } from "@/components/chat/elapsed";
31
+ import {
32
+ extractLatestSuggestedReplies,
33
+ type SuggestedRepliesState,
34
+ } from "@/components/chat/suggested-replies-model";
30
35
  import { useChatTasks } from "@/components/chat/useChatTasks";
31
36
  import { LuListChecks, LuArrowRight } from "react-icons/lu";
32
37
  import { LocalLlmSettingsModal } from "@/components/LocalLlmSettingsModal";
@@ -136,6 +141,26 @@ function extractPersistedDurations(
136
141
  return seeded;
137
142
  }
138
143
 
144
+ function extractPersistedTurnFiles(
145
+ messages: ChatMessageWithMetadata[] | undefined,
146
+ ): Record<string, TurnFile[]> {
147
+ const seeded: Record<string, TurnFile[]> = {};
148
+ if (!messages || messages.length === 0) return seeded;
149
+ for (const message of messages) {
150
+ if (message.role !== "assistant" || !message.messageMetadata) continue;
151
+ try {
152
+ const metadata = JSON.parse(message.messageMetadata) as {
153
+ turnFiles?: unknown;
154
+ };
155
+ const files = parseTurnFiles(metadata.turnFiles);
156
+ if (files) seeded[message.id] = files;
157
+ } catch {
158
+ // Ignore malformed legacy metadata.
159
+ }
160
+ }
161
+ return seeded;
162
+ }
163
+
139
164
  /**
140
165
  * Routing layer for `/agent/:chatId`.
141
166
  *
@@ -205,6 +230,11 @@ function AgentChatLoader({
205
230
  const [initialDurations, setInitialDurations] = useState<
206
231
  Record<string, number>
207
232
  >({});
233
+ const [initialTurnFiles, setInitialTurnFiles] = useState<
234
+ Record<string, TurnFile[]>
235
+ >({});
236
+ const [initialSuggestedReplies, setInitialSuggestedReplies] =
237
+ useState<SuggestedRepliesState | null>(null);
208
238
  // W7-1: 后端在 messages 响应里下发 interrupted(上一轮崩溃/强杀中断)。
209
239
  const [initialInterrupted, setInitialInterrupted] = useState(false);
210
240
  // 运行中回合的实时快照:切走再切回时用它在历史消息之上叠出「正在运行」
@@ -234,10 +264,17 @@ function AgentChatLoader({
234
264
  getChatLiveStream(chatId),
235
265
  ]);
236
266
  if (cancelled) return;
237
- setInitialMessages(chronological(response.messages));
267
+ const ordered = chronological(response.messages);
268
+ setInitialMessages(ordered);
238
269
  setInitialExecutedActions(extractPersistedActions(response.messages));
239
270
  setInitialTimelines(extractPersistedTimelines(response.messages));
240
271
  setInitialDurations(extractPersistedDurations(response.messages));
272
+ setInitialTurnFiles(extractPersistedTurnFiles(response.messages));
273
+ setInitialSuggestedReplies(
274
+ extractLatestSuggestedReplies(
275
+ ordered as ChatMessageWithMetadata[],
276
+ ),
277
+ );
241
278
  setInitialInterrupted(response.interrupted === true);
242
279
  setInitialLiveStream(live ?? { active: false });
243
280
  } catch (err) {
@@ -247,6 +284,8 @@ function AgentChatLoader({
247
284
  setInitialExecutedActions({});
248
285
  setInitialTimelines({});
249
286
  setInitialDurations({});
287
+ setInitialTurnFiles({});
288
+ setInitialSuggestedReplies(null);
250
289
  setInitialInterrupted(false);
251
290
  setInitialLiveStream({ active: false });
252
291
  }
@@ -271,6 +310,8 @@ function AgentChatLoader({
271
310
  initialExecutedActions={initialExecutedActions}
272
311
  initialTimelines={initialTimelines}
273
312
  initialDurations={initialDurations}
313
+ initialTurnFiles={initialTurnFiles}
314
+ initialSuggestedReplies={initialSuggestedReplies}
274
315
  initialInterrupted={initialInterrupted}
275
316
  initialLiveStream={initialLiveStream}
276
317
  hydrationError={hydrationError}
@@ -285,6 +326,8 @@ interface AgentChatViewProps {
285
326
  initialExecutedActions: Record<string, ExecutedAction[]>;
286
327
  initialTimelines: Record<string, TurnBlock[]>;
287
328
  initialDurations: Record<string, number>;
329
+ initialTurnFiles: Record<string, TurnFile[]>;
330
+ initialSuggestedReplies: SuggestedRepliesState | null;
288
331
  /** W7-1: 打开会话时上一轮处于中断态(崩溃/强杀,无完成记录)。 */
289
332
  initialInterrupted: boolean;
290
333
  /** 打开会话时的运行中回合快照(切回恢复运行状态用)。 */
@@ -299,6 +342,8 @@ function AgentChatView({
299
342
  initialExecutedActions,
300
343
  initialTimelines,
301
344
  initialDurations,
345
+ initialTurnFiles,
346
+ initialSuggestedReplies,
302
347
  initialInterrupted,
303
348
  initialLiveStream,
304
349
  hydrationError,
@@ -389,6 +434,16 @@ function AgentChatView({
389
434
  const [durationByMessageId, setDurationByMessageId] = useState<
390
435
  Record<string, number>
391
436
  >(initialDurations);
437
+ // 回合产物文件列表:与 executedActions 同款 reconcile 模式——live 回合
438
+ // 先挂 pendingTurnFilesRef,`message_id` 事件到达时归档到落库 id 上;
439
+ // 历史回合由 initialTurnFiles 从 messageMetadata.turnFiles 水合。
440
+ const [turnFilesByMessageId, setTurnFilesByMessageId] = useState<
441
+ Record<string, TurnFile[]>
442
+ >(initialTurnFiles);
443
+ // 当轮产物:turn_files 事件到达(流尾声)到 message_id 归档之间,以及
444
+ // 归档后占位 id 消息仍在屏上的这段时间,都靠它渲染尾部消息的文件列表。
445
+ const [currentTurnFiles, setCurrentTurnFiles] = useState<TurnFile[]>([]);
446
+ const pendingTurnFilesRef = useRef<TurnFile[]>([]);
392
447
  const turnStartedAtRef = useRef<number | null>(null);
393
448
  const [currentTurnStartedAtMs, setCurrentTurnStartedAtMs] = useState<
394
449
  number | undefined
@@ -425,6 +480,11 @@ function AgentChatView({
425
480
  const [planReady, setPlanReady] = useState(false);
426
481
  const planTurnRef = useRef(false);
427
482
  const wasStreamingRef = useRef(false);
483
+ const [suggestedReplies, setSuggestedReplies] =
484
+ useState<SuggestedRepliesState | null>(initialSuggestedReplies);
485
+ const [tailAssistantDbId, setTailAssistantDbId] = useState<string | null>(
486
+ initialSuggestedReplies?.messageId ?? null,
487
+ );
428
488
 
429
489
  const handleModeChange = useCallback((next: ChatMode) => {
430
490
  setMode(next);
@@ -459,6 +519,16 @@ function AgentChatView({
459
519
  }
460
520
  return;
461
521
  }
522
+ if (ev.event === "turn_files") {
523
+ // 回合收尾时后端发一次(在 message_id 之前);先挂 pending,
524
+ // message_id 到达时随其他队列一起归档。
525
+ const files = parseTurnFiles(ev.payload?.files);
526
+ if (files) {
527
+ pendingTurnFilesRef.current = files;
528
+ setCurrentTurnFiles(files);
529
+ }
530
+ return;
531
+ }
462
532
  if (ev.event === "round_end") {
463
533
  // chat-transport surfaces `completion executing` as round_end. Bumping
464
534
  // the counter here is safe even if the round emits no tools — the
@@ -537,6 +607,15 @@ function AgentChatView({
537
607
  }));
538
608
  }
539
609
  pendingChildrenRef.current = [];
610
+ const queuedTurnFiles = pendingTurnFilesRef.current;
611
+ if (queuedTurnFiles.length > 0) {
612
+ setTurnFilesByMessageId((prev) => ({
613
+ ...prev,
614
+ [messageId]: queuedTurnFiles,
615
+ }));
616
+ }
617
+ pendingTurnFilesRef.current = [];
618
+ setTailAssistantDbId(messageId);
540
619
  }
541
620
  // AI 标题更新走的是后台 IPC 广播(chat-title-updated)而不是 SSE——
542
621
  // 见 AgentLayout 里的订阅。SSE 通道在 [DONE] 之后就不再监听了,title-gen
@@ -684,6 +763,8 @@ function AgentChatView({
684
763
  setCurrentTurnTimeline([]);
685
764
  pendingChildrenRef.current = [];
686
765
  setCurrentTurnChildren([]);
766
+ pendingTurnFilesRef.current = [];
767
+ setCurrentTurnFiles([]);
687
768
  setCurrentRound(1);
688
769
  const started = Date.now();
689
770
  turnStartedAtRef.current = started;
@@ -694,6 +775,8 @@ function AgentChatView({
694
775
  setPlanReady(false);
695
776
  // 手动发起新一轮即取代中断态——用户已经继续往前走了。
696
777
  setInterrupted(false);
778
+ setSuggestedReplies(null);
779
+ setTailAssistantDbId(null);
697
780
  // 模型选择器的每轮覆盖随 metadata 下发:model 覆盖设置里的全局
698
781
  // 模型;reasoningEffort 由 sidecar 按目录严格校验(不支持即报错,
699
782
  // 不静默丢弃)。都为 null 时保持设置/预制默认。
@@ -711,6 +794,26 @@ function AgentChatView({
711
794
  [sendUserMessage, modelOverride, effortOverride, execPolicy],
712
795
  );
713
796
 
797
+ const handleSelectSuggestion = useCallback(
798
+ (text: string) => {
799
+ void handleSubmit({ content: text });
800
+ },
801
+ [handleSubmit],
802
+ );
803
+
804
+ useEffect(() => {
805
+ const bridge = getElectronBridge();
806
+ if (!bridge?.onSuggestedReplies) return;
807
+ return bridge.onSuggestedReplies((payload) => {
808
+ if (payload.chatId !== chatId) return;
809
+ if (!Array.isArray(payload.suggestions) || payload.suggestions.length === 0) return;
810
+ setSuggestedReplies({
811
+ messageId: payload.messageId,
812
+ suggestions: payload.suggestions,
813
+ });
814
+ });
815
+ }, [chatId]);
816
+
714
817
  // 包槽位 fallback 发送通道(如文档包的单页修改在 sidecar 未就绪时把
715
818
  // 后端拼好的指令退回主聊天):AgentLayout 持有发送器 ref,这里挂载时
716
819
  // 注册、卸载时注销,包面板经 sendChatMessage 调到当前注册的 handleSubmit。
@@ -732,6 +835,8 @@ function AgentChatView({
732
835
  setCurrentTurnTimeline([]);
733
836
  pendingChildrenRef.current = [];
734
837
  setCurrentTurnChildren([]);
838
+ pendingTurnFilesRef.current = [];
839
+ setCurrentTurnFiles([]);
735
840
  setCurrentRound(1);
736
841
  const started = Date.now();
737
842
  turnStartedAtRef.current = started;
@@ -863,6 +968,22 @@ function AgentChatView({
863
968
  )
864
969
  : currentTurnChildren;
865
970
 
971
+ let lastAssistantId: string | undefined;
972
+ for (let i = effectiveMessages.length - 1; i >= 0; i -= 1) {
973
+ if (effectiveMessages[i].role === "assistant") {
974
+ lastAssistantId = effectiveMessages[i].id;
975
+ break;
976
+ }
977
+ }
978
+ const visibleSuggestedReplies =
979
+ !effectiveIsStreaming &&
980
+ !interrupted &&
981
+ suggestedReplies &&
982
+ (suggestedReplies.messageId === lastAssistantId ||
983
+ suggestedReplies.messageId === tailAssistantDbId)
984
+ ? suggestedReplies.suggestions
985
+ : undefined;
986
+
866
987
  return (
867
988
  <div className="flex h-full w-full flex-col">
868
989
  {hydrationError && (
@@ -915,6 +1036,8 @@ function AgentChatView({
915
1036
  currentTurnTimeline={effectiveCurrentTurnTimeline}
916
1037
  currentTurnStartedAtMs={currentTurnStartedAtMs}
917
1038
  durationByMessageId={durationByMessageId}
1039
+ turnFilesByMessageId={turnFilesByMessageId}
1040
+ currentTurnFiles={currentTurnFiles}
918
1041
  currentTurnChildren={effectiveCurrentTurnChildren}
919
1042
  orchestrationChildrenByMessageId={orchestrationChildrenByMessageId}
920
1043
  currentRound={currentRound}
@@ -925,6 +1048,8 @@ function AgentChatView({
925
1048
  onInspectTask={handleInspectTask}
926
1049
  onDismissFinishedTask={dismissFinished}
927
1050
  onShare={isElectron() ? handleShare : undefined}
1051
+ suggestedReplies={visibleSuggestedReplies}
1052
+ onSelectSuggestion={handleSelectSuggestion}
928
1053
  onOpenSettings={
929
1054
  isElectron() ? () => setLlmSettingsOpen(true) : undefined
930
1055
  }
@@ -148,6 +148,7 @@ export function ChatPanelPreviewPage() {
148
148
  currentTurnStartedAtMs?: number;
149
149
  durationByMessageId?: Record<string, number>;
150
150
  currentRound: number;
151
+ suggestedReplies?: string[];
151
152
  }
152
153
  const config = useMemo<SceneConfig>(() => {
153
154
  switch (scene) {
@@ -203,6 +204,7 @@ export function ChatPanelPreviewPage() {
203
204
  timelineByMsgId: { m2: MOCK_TIMELINE },
204
205
  durationByMessageId: { m2: 83_000 },
205
206
  currentRound: 1,
207
+ suggestedReplies: ['修一下失败的断言颜色', '把测试再跑一遍', '解释这次失败的原因'],
206
208
  };
207
209
  }
208
210
  }, [scene]);
@@ -252,6 +254,8 @@ export function ChatPanelPreviewPage() {
252
254
  currentTurnStartedAtMs={config.currentTurnStartedAtMs}
253
255
  durationByMessageId={config.durationByMessageId}
254
256
  currentRound={config.currentRound}
257
+ suggestedReplies={config.suggestedReplies}
258
+ onSelectSuggestion={(text) => console.log('[preview] suggestion:', text)}
255
259
  onOpenSettings={() =>
256
260
  console.log('[preview] open llm settings (noop in harness)')
257
261
  }
package/vite.base.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export interface ProductViteConfigOptions {
2
+ /** 产品目录(products/<id>/web 的上一级…即 products/<id>)。 */
3
+ productDir: string;
4
+ /** 产品 flavor(决定品牌 define 与激活包集合)。 */
5
+ flavor: string;
6
+ }
7
+ export declare function createProductViteConfig(options: ProductViteConfigOptions): import("vite").UserConfig;
package/vite.base.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * 产品 web 构建的共享 vite 配置工厂(2.3)。
3
+ *
4
+ * 每个产品(products/<id>/web/)有自己的 vite 入口:index.html +
5
+ * main.tsx(静态注册本产品的包渲染层贡献)+ vite.config.ts(调本工厂)。
6
+ * 产品的编译单元只含自己的包——未激活包代码不进 bundle,不依赖树摇
7
+ * 正确性(物理隔离与 node 侧对称)。
8
+ *
9
+ * Electron 加载产物时走 file:// 协议,必须用相对路径,否则资源 404。
10
+ * dev server 跑在固定端口,主进程的 IS_DEV 分支 loadURL('http://localhost:5173')。
11
+ */
12
+ import { defineConfig } from 'vite';
13
+ import react from '@vitejs/plugin-react';
14
+ import tailwindcss from '@tailwindcss/vite';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
19
+ export function createProductViteConfig(options) {
20
+ const { productDir, flavor } = options;
21
+ const repoRoot = path.resolve(productDir, '../..');
22
+ // shell 渲染层源码锚到本包自身(3.2 起住在框架仓库
23
+ // packages/agent-shell/web/src),产品经 `@/` 别名引用。
24
+ const webSrcDir = path.resolve(HERE, 'src');
25
+ const readJson = (p) => JSON.parse(readFileSync(p, 'utf8'));
26
+ // 产品品牌(3.1):product.json 的 brand 字段优先(无包产品的品牌
27
+ // 来源);否则取激活包 pack.json 的 brand(0.3g 机制,读
28
+ // products/manifest.json 的 flavor→包映射,第一个带 brand 的包)。
29
+ // 都没有则用 shell 中性默认(Agent Shell)。
30
+ const productJsonPath = path.join(productDir, 'product.json');
31
+ const product = existsSync(productJsonPath) ? readJson(productJsonPath) : {};
32
+ const manifest = readJson(path.join(repoRoot, 'products/manifest.json'));
33
+ let packBrand = null;
34
+ for (const packId of manifest.flavors?.[flavor] ?? []) {
35
+ const pack = readJson(path.join(repoRoot, 'packages', `pack-${packId}`, 'pack.json'));
36
+ if (pack.brand) {
37
+ packBrand = pack.brand;
38
+ break;
39
+ }
40
+ }
41
+ const brand = product.brand ?? packBrand;
42
+ return defineConfig({
43
+ // root 默认取 config 文件所在目录(products/<id>/web)。
44
+ base: './',
45
+ plugins: [react(), tailwindcss()],
46
+ define: {
47
+ // 显式 define 让 flavor 条件在构建期可静态折叠。
48
+ 'import.meta.env.VITE_APP_FLAVOR': JSON.stringify(flavor),
49
+ // 品牌 define:产品/包声明覆盖 shell 中性默认(Agent Shell)。
50
+ // logo 不走 define——资产必须静态 import 才进 bundle,由包 web 模块
51
+ // 在注册时经 setBrandLogoUrl 注入(本包 src/brand.ts)。
52
+ 'import.meta.env.VITE_BRAND_NAME': JSON.stringify(brand?.displayName ?? 'Steerable Shell'),
53
+ 'import.meta.env.VITE_BRAND_TAGLINE': JSON.stringify(brand?.tagline ?? '一款本地桌面 AI 伙伴'),
54
+ 'import.meta.env.VITE_DEFAULT_AGENT_ID': JSON.stringify(brand?.defaultAgentId ?? 'local-assistant'),
55
+ },
56
+ resolve: {
57
+ alias: {
58
+ // shell 渲染层源码(产品入口与包 web 模块都以 `@/` 引用它)。
59
+ '@': webSrcDir,
60
+ },
61
+ },
62
+ // 静态资产(favicon 等中性默认)随 shell web 包分发;产品可在自己的
63
+ // web root 放 public/ 覆盖(vite 不合并双 publicDir——需要产品级资产
64
+ // 时在产品 vite.config 里显式覆盖本字段)。
65
+ publicDir: path.resolve(HERE, 'public'),
66
+ server: {
67
+ port: 5173,
68
+ strictPort: true,
69
+ host: '127.0.0.1',
70
+ // 产品 root 在 products/<id>/web;shell 渲染层源码在框架仓库
71
+ // (3.2 起经 file: 依赖符号链接)——两个根都放行。
72
+ fs: { allow: [repoRoot, path.resolve(webSrcDir, '..')] },
73
+ },
74
+ build: {
75
+ // 产物落在产品目录:products/<id>/web/dist(打包期被 electron-builder
76
+ // 映射为包内 web-dist/)。
77
+ outDir: 'dist',
78
+ emptyOutDir: true,
79
+ sourcemap: true,
80
+ target: 'chrome120',
81
+ },
82
+ });
83
+ }
package/src/brand.test.ts DELETED
@@ -1,22 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { LOCAL_ASSISTANT_AGENT_ID, pickDefaultAgentId } from './brand';
3
-
4
- describe('pickDefaultAgentId', () => {
5
- const catalog = [
6
- { id: LOCAL_ASSISTANT_AGENT_ID },
7
- { id: 'data-analyst' },
8
- { id: 'all-round-assistant' },
9
- ];
10
-
11
- it('returns null when the catalog is empty', () => {
12
- expect(pickDefaultAgentId([])).toBeNull();
13
- });
14
-
15
- it('prefers the build-time default (shell: 电脑操作员) even when it is not first', () => {
16
- expect(pickDefaultAgentId(catalog)).toBe(LOCAL_ASSISTANT_AGENT_ID);
17
- });
18
-
19
- it('falls back to the first visible agent when the build default is absent', () => {
20
- expect(pickDefaultAgentId([{ id: 'writer' }, { id: 'reader' }])).toBe('writer');
21
- });
22
- });