@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.
- package/package.json +15 -8
- package/src/components/chat/ApprovalModal.tsx +21 -41
- package/src/components/chat/ApprovalPromptProvider.tsx +98 -0
- package/src/components/chat/AskUserPromptProvider.tsx +99 -0
- package/src/components/chat/AssistantMessage.tsx +25 -2
- package/src/components/chat/ChatInput.tsx +116 -13
- package/src/components/chat/FilePathCode.tsx +65 -0
- package/src/components/chat/LocalChatPanel.tsx +19 -0
- package/src/components/chat/Markdown.tsx +54 -25
- package/src/components/chat/MessageList.tsx +66 -18
- package/src/components/chat/ModelPicker.tsx +5 -3
- package/src/components/chat/SuggestedReplies.tsx +34 -0
- package/src/components/chat/TurnFilesCard.tsx +113 -0
- package/src/components/chat/TurnProcessGroup.tsx +7 -2
- package/src/components/chat/path-mentions.ts +148 -0
- package/src/components/chat/suggested-replies-model.ts +40 -0
- package/src/components/chat/turn-files.ts +50 -0
- package/src/layouts/AgentLayout.tsx +15 -26
- package/src/lib/browser-dev-electron-mock.ts +15 -0
- package/src/lib/electron-bridge.ts +13 -7
- package/src/lib/http-bridge.ts +8 -2
- package/src/lib/local-api.ts +31 -0
- package/src/pages/AgentPage.tsx +126 -1
- package/src/pages/ChatPanelPreviewPage.tsx +4 -0
- package/vite.base.d.ts +7 -0
- package/vite.base.js +83 -0
- package/src/brand.test.ts +0 -22
- package/src/components/AgentSidebar.test.tsx +0 -791
- package/src/components/ChatHeader.test.tsx +0 -230
- package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
- package/src/components/chat/ApprovalModal.test.tsx +0 -83
- package/src/components/chat/AskUserModalHost.test.tsx +0 -176
- package/src/components/chat/AskUserModalHost.tsx +0 -61
- package/src/components/chat/AskUserQuestionMenu.test.tsx +0 -190
- package/src/components/chat/AssistantMessage.test.tsx +0 -68
- package/src/components/chat/ChatInput.test.tsx +0 -609
- package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
- package/src/components/chat/Markdown.test.tsx +0 -170
- package/src/components/chat/ModelPicker.test.tsx +0 -149
- package/src/components/chat/SessionTreeModal.test.tsx +0 -161
- package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
- package/src/components/chat/TaskPanelModal.test.tsx +0 -198
- package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
- package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
- package/src/components/chat/elapsed.test.ts +0 -42
- package/src/components/chat/executed-actions-model.test.ts +0 -134
- package/src/components/chat/orchestration-children-model.test.ts +0 -115
- package/src/components/chat/turn-timeline.test.ts +0 -97
- package/src/components/chat/useChatTasks.test.ts +0 -136
- package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
- package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
- package/src/components/settings/LlmSettingsPanel.test.tsx +0 -461
- package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
- package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
- package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
- package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
- package/src/components/settings/compat-flags-model.test.ts +0 -71
- package/src/components/settings/llm-vendors.test.ts +0 -171
- package/src/components/settings/model-capabilities.test.ts +0 -88
- package/src/components/settings/preset-choice-model.test.ts +0 -96
- package/src/hooks/useChatsAndAgents.test.ts +0 -165
- package/src/lib/attachments.test.ts +0 -127
- package/src/lib/chat-title.test.ts +0 -53
- package/src/lib/chat-transport.test.ts +0 -282
- package/src/lib/date-groups.test.ts +0 -62
- package/src/lib/demo-flag.test.ts +0 -17
- package/src/lib/electron-bridge.test.ts +0 -54
- package/src/lib/exec-policy.test.ts +0 -71
- package/src/lib/http-bridge.test.ts +0 -283
- package/src/lib/insights.test.ts +0 -56
- package/src/lib/local-api.test.ts +0 -353
- package/src/lib/pending-first-message.test.ts +0 -17
- package/src/lib/slash-sources.test.ts +0 -161
- package/src/pages/AgentPage.test.tsx +0 -627
- package/src/pages/SettingsPage.test.tsx +0 -102
- package/vite.base.ts +0 -94
package/src/pages/AgentPage.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
});
|