@steerable/agent-shell-web 0.6.32 → 0.6.34
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 +5 -8
- package/src/components/AgentSidebar.tsx +46 -17
- package/src/components/CreateProjectModal.tsx +10 -8
- package/src/components/chat/ChatInput.tsx +14 -3
- package/src/components/chat/FilePathCode.tsx +6 -2
- package/src/components/chat/LocalChatPanel.tsx +4 -15
- package/src/components/chat/TurnFilesCard.tsx +14 -3
- package/src/layouts/AgentLayout.tsx +16 -9
- package/src/lib/attachments.ts +27 -3
- package/src/lib/exec-policy.ts +3 -0
- package/src/lib/host-tools.ts +206 -0
- package/src/pages/AgentPage.tsx +61 -45
- package/src/pages/SettingsPage.tsx +21 -2
- package/src/vite-env.d.ts +16 -0
- package/vite.base.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell-web",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -15,10 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
17
|
"./auth/gate": "./src/auth/gate.ts",
|
|
18
|
-
"./vite.base":
|
|
19
|
-
"types": "./vite.base.d.ts",
|
|
20
|
-
"default": "./vite.base.js"
|
|
21
|
-
},
|
|
18
|
+
"./vite.base": "./vite.base.ts",
|
|
22
19
|
"./package.json": "./package.json"
|
|
23
20
|
},
|
|
24
21
|
"files": [
|
|
@@ -46,8 +43,8 @@
|
|
|
46
43
|
"react-router-dom": "^6.30.0",
|
|
47
44
|
"rehype-raw": "^7.0.0",
|
|
48
45
|
"remark-gfm": "^4.0.1",
|
|
49
|
-
"@steerable/agent-
|
|
50
|
-
"@steerable/agent-
|
|
46
|
+
"@steerable/agent-ui": "0.6.34",
|
|
47
|
+
"@steerable/agent-protocol": "0.6.34"
|
|
51
48
|
},
|
|
52
49
|
"devDependencies": {
|
|
53
50
|
"@tailwindcss/vite": "^4.3.0",
|
|
@@ -60,7 +57,7 @@
|
|
|
60
57
|
"typescript": "^5.8.3",
|
|
61
58
|
"vite": "^5.4.21",
|
|
62
59
|
"vitest": "^3.2.4",
|
|
63
|
-
"@steerable/pack-sdk": "0.6.
|
|
60
|
+
"@steerable/pack-sdk": "0.6.34"
|
|
64
61
|
},
|
|
65
62
|
"scripts": {
|
|
66
63
|
"gen": "echo agent-shell-web",
|
|
@@ -103,6 +103,7 @@ import {
|
|
|
103
103
|
import { parseChatTitle } from "@/lib/chat-title";
|
|
104
104
|
import { getDateGroupLabel, getDateGroupPriority } from "@/lib/date-groups";
|
|
105
105
|
import { getElectronBridge, isElectron } from "@/lib/electron-bridge";
|
|
106
|
+
import { hasGeneralSettingsChrome, hostToolChrome, settingsChrome } from "@/lib/host-tools";
|
|
106
107
|
import {
|
|
107
108
|
createProject,
|
|
108
109
|
deleteProject,
|
|
@@ -173,7 +174,7 @@ function ProjectOverflowMenu({
|
|
|
173
174
|
onClose: () => void;
|
|
174
175
|
onRename: () => void;
|
|
175
176
|
onChangeFolder: () => void;
|
|
176
|
-
onReveal
|
|
177
|
+
onReveal?: () => void;
|
|
177
178
|
onDelete: () => void;
|
|
178
179
|
}) {
|
|
179
180
|
const pos = placeProjectMenu(anchor);
|
|
@@ -229,6 +230,7 @@ function ProjectOverflowMenu({
|
|
|
229
230
|
<LuFolderOpen className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
230
231
|
编辑项目
|
|
231
232
|
</button>
|
|
233
|
+
{onReveal && (
|
|
232
234
|
<button
|
|
233
235
|
type="button"
|
|
234
236
|
role="menuitem"
|
|
@@ -239,6 +241,7 @@ function ProjectOverflowMenu({
|
|
|
239
241
|
<LuFolder className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
240
242
|
{revealLabel}
|
|
241
243
|
</button>
|
|
244
|
+
)}
|
|
242
245
|
<div className="mx-1 my-1 border-t border-agent-border/60" />
|
|
243
246
|
<button
|
|
244
247
|
type="button"
|
|
@@ -351,8 +354,10 @@ export function AgentSidebar({
|
|
|
351
354
|
anchor: HTMLElement;
|
|
352
355
|
} | null>(null);
|
|
353
356
|
|
|
357
|
+
const showProjectsChrome = hostToolChrome("projects");
|
|
358
|
+
|
|
354
359
|
const fetchProjects = useCallback(async () => {
|
|
355
|
-
if (!isElectron()) return;
|
|
360
|
+
if (!isElectron() || !hostToolChrome("projects")) return;
|
|
356
361
|
try {
|
|
357
362
|
const res = await listProjects();
|
|
358
363
|
setProjects(res.projects || []);
|
|
@@ -547,8 +552,11 @@ export function AgentSidebar({
|
|
|
547
552
|
// 顶层按项目分组:无项目对话(含项目已被删但列表还没刷新的孤儿会话)
|
|
548
553
|
// 保持原有的日期分组;每个项目一个分组,组内按 pin + 时间排序。
|
|
549
554
|
const knownProjectIds = useMemo(
|
|
550
|
-
() =>
|
|
551
|
-
|
|
555
|
+
() =>
|
|
556
|
+
showProjectsChrome
|
|
557
|
+
? new Set(projects.map((p) => p.id))
|
|
558
|
+
: new Set<string>(),
|
|
559
|
+
[projects, showProjectsChrome],
|
|
552
560
|
);
|
|
553
561
|
|
|
554
562
|
const noProjectChats = useMemo(
|
|
@@ -561,11 +569,13 @@ export function AgentSidebar({
|
|
|
561
569
|
|
|
562
570
|
const projectGroups = useMemo(
|
|
563
571
|
() =>
|
|
564
|
-
|
|
565
|
-
project
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
572
|
+
showProjectsChrome
|
|
573
|
+
? projects.map((project) => ({
|
|
574
|
+
project,
|
|
575
|
+
items: normalizedChats.filter((c) => c.projectId === project.id),
|
|
576
|
+
}))
|
|
577
|
+
: [],
|
|
578
|
+
[projects, normalizedChats, showProjectsChrome],
|
|
569
579
|
);
|
|
570
580
|
|
|
571
581
|
const chatGroups = useMemo(() => {
|
|
@@ -750,15 +760,17 @@ export function AgentSidebar({
|
|
|
750
760
|
// `menu:open-terminal`). Same pattern as `menu:new-chat` above. The
|
|
751
761
|
// terminal is a toggleable panel beside the chat, so this just flips
|
|
752
762
|
// the layout state owned by AgentLayout.
|
|
763
|
+
const showTerminalChrome = hostToolChrome("terminal");
|
|
764
|
+
|
|
753
765
|
useEffect(() => {
|
|
754
|
-
if (!bridge?.onMenuOpenTerminal) return;
|
|
766
|
+
if (!showTerminalChrome || !bridge?.onMenuOpenTerminal) return;
|
|
755
767
|
bridge.onMenuOpenTerminal(() => {
|
|
756
768
|
onToggleRightPanel("terminal");
|
|
757
769
|
});
|
|
758
770
|
return () => {
|
|
759
771
|
bridge.offMenuOpenTerminal?.();
|
|
760
772
|
};
|
|
761
|
-
}, [bridge, onToggleRightPanel]);
|
|
773
|
+
}, [bridge, onToggleRightPanel, showTerminalChrome]);
|
|
762
774
|
|
|
763
775
|
const hasElectron = isElectron();
|
|
764
776
|
|
|
@@ -799,6 +811,7 @@ export function AgentSidebar({
|
|
|
799
811
|
<LuPlus className="h-3.5 w-3.5" />
|
|
800
812
|
<span>新对话</span>
|
|
801
813
|
</button>
|
|
814
|
+
{settingsChrome("agents") && (
|
|
802
815
|
<button
|
|
803
816
|
type="button"
|
|
804
817
|
onClick={() => navigate("/settings?section=agents")}
|
|
@@ -814,6 +827,8 @@ export function AgentSidebar({
|
|
|
814
827
|
<LuBot className="h-3.5 w-3.5" />
|
|
815
828
|
<span>智能体管理</span>
|
|
816
829
|
</button>
|
|
830
|
+
)}
|
|
831
|
+
{settingsChrome("skills") && (
|
|
817
832
|
<button
|
|
818
833
|
type="button"
|
|
819
834
|
onClick={() => navigate("/settings?section=skills")}
|
|
@@ -829,6 +844,8 @@ export function AgentSidebar({
|
|
|
829
844
|
<LuBlocks className="h-3.5 w-3.5" />
|
|
830
845
|
<span>Skill 设置</span>
|
|
831
846
|
</button>
|
|
847
|
+
)}
|
|
848
|
+
{settingsChrome("mcp") && (
|
|
832
849
|
<button
|
|
833
850
|
type="button"
|
|
834
851
|
onClick={() => navigate("/settings?section=mcp")}
|
|
@@ -844,6 +861,7 @@ export function AgentSidebar({
|
|
|
844
861
|
<LuPlug className="h-3.5 w-3.5" />
|
|
845
862
|
<span>MCP 设置</span>
|
|
846
863
|
</button>
|
|
864
|
+
)}
|
|
847
865
|
</div>
|
|
848
866
|
<div className="flex flex-shrink-0 items-center justify-between py-1 pl-2.5 pr-2.5">
|
|
849
867
|
<button
|
|
@@ -861,7 +879,7 @@ export function AgentSidebar({
|
|
|
861
879
|
</span>
|
|
862
880
|
</button>
|
|
863
881
|
<div className="flex items-center gap-1">
|
|
864
|
-
{hasElectron && (
|
|
882
|
+
{hasElectron && showProjectsChrome && (
|
|
865
883
|
<button
|
|
866
884
|
type="button"
|
|
867
885
|
onClick={() => setCreateProjectOpen(true)}
|
|
@@ -893,7 +911,8 @@ export function AgentSidebar({
|
|
|
893
911
|
) : (
|
|
894
912
|
<>
|
|
895
913
|
{/* 项目分组在前:组头可折叠,hover 出 + 新建对话 / ·· 菜单 */}
|
|
896
|
-
{
|
|
914
|
+
{showProjectsChrome &&
|
|
915
|
+
projectGroups.map(({ project, items }) => (
|
|
897
916
|
<div key={project.id} className="mb-1">
|
|
898
917
|
<div className="group/proj relative">
|
|
899
918
|
{renamingProjectId === project.id ? (
|
|
@@ -1036,6 +1055,7 @@ export function AgentSidebar({
|
|
|
1036
1055
|
{/* ───── Footer: 右侧面板切换(终端 | 包槽位)+ 设置 ───── */}
|
|
1037
1056
|
<div className="flex-shrink-0 border-t border-agent-border/40 px-2.5 py-1.5">
|
|
1038
1057
|
{chatSlots.length === 0 ? (
|
|
1058
|
+
showTerminalChrome ? (
|
|
1039
1059
|
<button
|
|
1040
1060
|
type="button"
|
|
1041
1061
|
onClick={() => onToggleRightPanel("terminal")}
|
|
@@ -1054,12 +1074,14 @@ export function AgentSidebar({
|
|
|
1054
1074
|
{isMac ? "⌘T" : "Ctrl+T"}
|
|
1055
1075
|
</span>
|
|
1056
1076
|
</button>
|
|
1077
|
+
) : null
|
|
1057
1078
|
) : (
|
|
1058
1079
|
<div
|
|
1059
1080
|
role="group"
|
|
1060
1081
|
aria-label="右侧面板切换"
|
|
1061
1082
|
className="flex h-7 w-full items-center gap-0.5 rounded-full bg-agent-foreground/5 p-0.5"
|
|
1062
1083
|
>
|
|
1084
|
+
{showTerminalChrome ? (
|
|
1063
1085
|
<button
|
|
1064
1086
|
type="button"
|
|
1065
1087
|
onClick={() => onToggleRightPanel("terminal")}
|
|
@@ -1075,6 +1097,7 @@ export function AgentSidebar({
|
|
|
1075
1097
|
<LuTerminal className="h-3.5 w-3.5" />
|
|
1076
1098
|
<span>终端</span>
|
|
1077
1099
|
</button>
|
|
1100
|
+
) : null}
|
|
1078
1101
|
{chatSlots.map((slot) => (
|
|
1079
1102
|
<button
|
|
1080
1103
|
key={slot.slotId}
|
|
@@ -1095,6 +1118,7 @@ export function AgentSidebar({
|
|
|
1095
1118
|
))}
|
|
1096
1119
|
</div>
|
|
1097
1120
|
)}
|
|
1121
|
+
{hasGeneralSettingsChrome() && (
|
|
1098
1122
|
<button
|
|
1099
1123
|
type="button"
|
|
1100
1124
|
onClick={() => navigate("/settings")}
|
|
@@ -1110,6 +1134,7 @@ export function AgentSidebar({
|
|
|
1110
1134
|
<LuSettings className="h-3.5 w-3.5" />
|
|
1111
1135
|
<span>设置</span>
|
|
1112
1136
|
</button>
|
|
1137
|
+
)}
|
|
1113
1138
|
</div>
|
|
1114
1139
|
|
|
1115
1140
|
{projectMenu &&
|
|
@@ -1137,10 +1162,14 @@ export function AgentSidebar({
|
|
|
1137
1162
|
setProjectMenu(null);
|
|
1138
1163
|
setEditingProjectId(project.id);
|
|
1139
1164
|
}}
|
|
1140
|
-
onReveal={
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1165
|
+
onReveal={
|
|
1166
|
+
hostToolChrome("local-fs")
|
|
1167
|
+
? () => {
|
|
1168
|
+
setProjectMenu(null);
|
|
1169
|
+
void handleRevealProjectFolder(project.folderPath);
|
|
1170
|
+
}
|
|
1171
|
+
: undefined
|
|
1172
|
+
}
|
|
1144
1173
|
onDelete={() => void handleDeleteProject(project.id)}
|
|
1145
1174
|
/>
|
|
1146
1175
|
);
|
|
@@ -77,16 +77,18 @@ export function CreateProjectModal({
|
|
|
77
77
|
addSourceFolder(typed);
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!result || result.canceled || result.filePaths.length === 0) {
|
|
84
|
-
if (!getElectronBridge()?.local?.selectDirectory) {
|
|
85
|
-
setError('请输入文件夹路径');
|
|
86
|
-
}
|
|
80
|
+
const picker = getElectronBridge()?.local?.selectDirectory;
|
|
81
|
+
if (!picker) {
|
|
82
|
+
setError('请输入文件夹路径');
|
|
87
83
|
return;
|
|
88
84
|
}
|
|
89
|
-
|
|
85
|
+
try {
|
|
86
|
+
const result = await picker({ title: '添加源文件夹' });
|
|
87
|
+
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
88
|
+
addSourceFolder(result.filePaths[0]);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
91
|
+
}
|
|
90
92
|
};
|
|
91
93
|
|
|
92
94
|
const handleSubmit = async () => {
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from 'react-icons/lu';
|
|
33
33
|
import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
|
|
34
34
|
import type { ExecPolicy } from '@/lib/exec-policy';
|
|
35
|
+
import { getWebChatModes, hostToolCapability, settingsChrome } from '@/lib/host-tools';
|
|
35
36
|
import type { AttachmentFile } from '@/lib/attachments';
|
|
36
37
|
import type { SteerOutcome } from '@steerable/agent-ui';
|
|
37
38
|
import {
|
|
@@ -1112,8 +1113,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1112
1113
|
const trimmed = value.trim();
|
|
1113
1114
|
const canSend = (trimmed.length > 0 || actualFiles.length > 0) && !disabled && !isStreaming;
|
|
1114
1115
|
|
|
1116
|
+
const allowFileAttach = hostToolCapability('local-fs');
|
|
1117
|
+
|
|
1115
1118
|
const handleDragOver = (event: React.DragEvent) => {
|
|
1116
1119
|
event.preventDefault();
|
|
1120
|
+
if (!allowFileAttach) return;
|
|
1117
1121
|
if (event.dataTransfer.types.includes('Files') && !disabled) {
|
|
1118
1122
|
setIsDragging(true);
|
|
1119
1123
|
}
|
|
@@ -1143,7 +1147,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1143
1147
|
// 按钮点击 → 触发隐藏的 <input type="file" multiple>,由浏览器/Electron
|
|
1144
1148
|
// 直接弹出系统文件选择框(不依赖 IPC,避免「点击无反应」)。
|
|
1145
1149
|
const handlePickFiles = () => {
|
|
1146
|
-
if (disabled) return;
|
|
1150
|
+
if (disabled || !allowFileAttach) return;
|
|
1147
1151
|
fileInputRef.current?.click();
|
|
1148
1152
|
};
|
|
1149
1153
|
const handleFileInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -1158,7 +1162,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1158
1162
|
event.preventDefault();
|
|
1159
1163
|
setIsDragging(false);
|
|
1160
1164
|
|
|
1161
|
-
if (disabled) return;
|
|
1165
|
+
if (disabled || !allowFileAttach) return;
|
|
1162
1166
|
|
|
1163
1167
|
const droppedFiles = Array.from(event.dataTransfer.files);
|
|
1164
1168
|
if (droppedFiles.length > 0) {
|
|
@@ -1816,7 +1820,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1816
1820
|
Agent picker lives in the meta row above the box, next to
|
|
1817
1821
|
the project badge. min-w-0 允许窄屏时胶囊截断收缩。 */}
|
|
1818
1822
|
<div ref={toolbarLeftRef} className="flex min-w-0 items-center gap-1">
|
|
1819
|
-
{mode && onModeChange && (
|
|
1823
|
+
{mode && onModeChange && getWebChatModes().length > 1 && (
|
|
1820
1824
|
<ModeToggle
|
|
1821
1825
|
mode={mode}
|
|
1822
1826
|
disabled={disabled || isStreaming}
|
|
@@ -1831,6 +1835,8 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1831
1835
|
/>
|
|
1832
1836
|
)}
|
|
1833
1837
|
{toolbarExtras}
|
|
1838
|
+
{allowFileAttach && (
|
|
1839
|
+
<>
|
|
1834
1840
|
<button
|
|
1835
1841
|
type="button"
|
|
1836
1842
|
onClick={handlePickFiles}
|
|
@@ -1838,6 +1844,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1838
1844
|
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
1839
1845
|
title="上传文件(可多选)"
|
|
1840
1846
|
aria-label="上传文件"
|
|
1847
|
+
data-testid="chat-attach"
|
|
1841
1848
|
>
|
|
1842
1849
|
<LuPaperclip className="h-3 w-3" />
|
|
1843
1850
|
</button>
|
|
@@ -1851,6 +1858,8 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1851
1858
|
tabIndex={-1}
|
|
1852
1859
|
onChange={handleFileInputChange}
|
|
1853
1860
|
/>
|
|
1861
|
+
</>
|
|
1862
|
+
)}
|
|
1854
1863
|
{onOpenSettings && (
|
|
1855
1864
|
<button
|
|
1856
1865
|
type="button"
|
|
@@ -2142,6 +2151,7 @@ function AgentSelect({
|
|
|
2142
2151
|
</button>
|
|
2143
2152
|
);
|
|
2144
2153
|
})}
|
|
2154
|
+
{settingsChrome('agents') && (
|
|
2145
2155
|
<button
|
|
2146
2156
|
type="button"
|
|
2147
2157
|
role="menuitem"
|
|
@@ -2155,6 +2165,7 @@ function AgentSelect({
|
|
|
2155
2165
|
<LuBot className="h-3.5 w-3.5 shrink-0" />
|
|
2156
2166
|
<span className="text-xs font-medium">管理智能体</span>
|
|
2157
2167
|
</button>
|
|
2168
|
+
)}
|
|
2158
2169
|
</div>
|
|
2159
2170
|
)}
|
|
2160
2171
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
import { openLocalPath, type ResolvedLocalPath } from '@/lib/local-api';
|
|
3
|
+
import { hostToolChrome } from '@/lib/host-tools';
|
|
3
4
|
import { peekResolvedPath, subscribeResolvedPath } from './path-mentions';
|
|
4
5
|
import { splitTurnFilePath } from './turn-files';
|
|
5
6
|
|
|
@@ -27,12 +28,15 @@ export function FilePathCode({ candidate, chatId, children, ...rest }: FilePathC
|
|
|
27
28
|
);
|
|
28
29
|
const [openError, setOpenError] = useState<string | null>(null);
|
|
29
30
|
|
|
31
|
+
const allowOpenPath = hostToolChrome('local-fs');
|
|
32
|
+
|
|
30
33
|
useEffect(() => {
|
|
31
34
|
setOpenError(null);
|
|
35
|
+
if (!allowOpenPath) return;
|
|
32
36
|
return subscribeResolvedPath(candidate, chatId, (entry) => setResolved(entry));
|
|
33
|
-
}, [candidate, chatId]);
|
|
37
|
+
}, [allowOpenPath, candidate, chatId]);
|
|
34
38
|
|
|
35
|
-
if (!resolved) {
|
|
39
|
+
if (!resolved || !allowOpenPath) {
|
|
36
40
|
return (
|
|
37
41
|
<code {...rest} className={INLINE_CODE_CLASS}>
|
|
38
42
|
{children}
|
|
@@ -21,7 +21,7 @@ import type { LlmSpeedSnapshot } from './process-status';
|
|
|
21
21
|
import { SessionTodoList } from './SessionTodoList';
|
|
22
22
|
import { resolveLatestSessionTodos } from './todo-list-model';
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
composeAttachmentUserContent,
|
|
25
25
|
saveChatAttachments,
|
|
26
26
|
type AttachmentFile,
|
|
27
27
|
} from '@/lib/attachments';
|
|
@@ -235,16 +235,8 @@ export function LocalChatPanel({
|
|
|
235
235
|
// 自行持久化,这里 chatId 为空时原样用源路径。
|
|
236
236
|
const resolvedFiles =
|
|
237
237
|
chatId && files.length > 0 ? await saveChatAttachments(chatId, files) : files;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (resolvedFiles.length > 0) {
|
|
241
|
-
const fileRefs = resolvedFiles.map(f => `- \`${f.path}\``).join('\n');
|
|
242
|
-
if (trimmed) {
|
|
243
|
-
trimmed = `${trimmed}\n\n---\n关联文件:\n${fileRefs}`;
|
|
244
|
-
} else {
|
|
245
|
-
trimmed = `关联文件:\n${fileRefs}`;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
238
|
+
const assembled = composeAttachmentUserContent(trimmed, resolvedFiles);
|
|
239
|
+
trimmed = assembled.content;
|
|
248
240
|
|
|
249
241
|
const mentionedAgentIds = mentionReferences
|
|
250
242
|
.filter((ref) => ref.type === 'agent')
|
|
@@ -255,16 +247,13 @@ export function LocalChatPanel({
|
|
|
255
247
|
// W6-3: image attachments ride as metadata so the main process reads the
|
|
256
248
|
// bytes into base64 ImageParts; the text path refs above stay so the
|
|
257
249
|
// persisted record reflects that an image was attached.
|
|
258
|
-
const imageAttachments = resolvedFiles
|
|
259
|
-
.filter((f) => isImageFile(f.path))
|
|
260
|
-
.map((f) => ({ path: f.path, name: f.name }));
|
|
261
250
|
const metadata = {
|
|
262
251
|
...(mode === 'plan' ? { mode: 'plan' as const } : {}),
|
|
263
252
|
...(execPolicy === 'full' ? { execPolicy: 'full' as const } : {}),
|
|
264
253
|
...(selectedAgentId ? { agentId: selectedAgentId } : {}),
|
|
265
254
|
...(mentionedAgentIds.length > 0 ? { mentionedAgentIds } : {}),
|
|
266
255
|
...(referencedChatIds.length > 0 ? { referencedChatIds } : {}),
|
|
267
|
-
...(
|
|
256
|
+
...(assembled.images.length > 0 ? { images: assembled.images } : {}),
|
|
268
257
|
};
|
|
269
258
|
|
|
270
259
|
// Snapshot what we're about to clear so a failed submit can restore it —
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
LuLoaderCircle,
|
|
7
7
|
} from 'react-icons/lu';
|
|
8
8
|
import { openLocalPath } from '@/lib/local-api';
|
|
9
|
+
import { hostToolChrome } from '@/lib/host-tools';
|
|
9
10
|
import {
|
|
10
11
|
formatFileSize,
|
|
11
12
|
splitTurnFilePath,
|
|
@@ -30,10 +31,12 @@ interface TurnFilesCardProps {
|
|
|
30
31
|
export function TurnFilesCard({ files }: TurnFilesCardProps) {
|
|
31
32
|
const [openingPath, setOpeningPath] = useState<string | null>(null);
|
|
32
33
|
const [openError, setOpenError] = useState<{ path: string; message: string } | null>(null);
|
|
34
|
+
const allowOpenPath = hostToolChrome('local-fs');
|
|
33
35
|
|
|
34
36
|
if (files.length === 0) return null;
|
|
35
37
|
|
|
36
38
|
const handleOpen = async (file: TurnFile) => {
|
|
39
|
+
if (!allowOpenPath) return;
|
|
37
40
|
if (openingPath) return;
|
|
38
41
|
setOpeningPath(file.path);
|
|
39
42
|
setOpenError(null);
|
|
@@ -59,7 +62,11 @@ export function TurnFilesCard({ files }: TurnFilesCardProps) {
|
|
|
59
62
|
>
|
|
60
63
|
<div className="flex items-center gap-1.5 border-b border-agent-border/60 px-2.5 py-1 text-[11px] text-agent-muted-foreground">
|
|
61
64
|
<LuFiles className="h-3.5 w-3.5" />
|
|
62
|
-
<span
|
|
65
|
+
<span>
|
|
66
|
+
{allowOpenPath
|
|
67
|
+
? `本轮产生了 ${files.length} 个文件,点击打开`
|
|
68
|
+
: `本轮产生了 ${files.length} 个文件`}
|
|
69
|
+
</span>
|
|
63
70
|
</div>
|
|
64
71
|
<ul className="max-h-64 overflow-y-auto py-1">
|
|
65
72
|
{files.map((file) => {
|
|
@@ -72,9 +79,13 @@ export function TurnFilesCard({ files }: TurnFilesCardProps) {
|
|
|
72
79
|
<button
|
|
73
80
|
type="button"
|
|
74
81
|
onClick={() => void handleOpen(file)}
|
|
75
|
-
disabled={openingPath !== null}
|
|
82
|
+
disabled={!allowOpenPath || openingPath !== null}
|
|
76
83
|
title={file.path}
|
|
77
|
-
className=
|
|
84
|
+
className={`flex w-full items-center gap-2 px-2.5 py-1 text-left text-xs transition-colors ${
|
|
85
|
+
allowOpenPath
|
|
86
|
+
? 'hover:bg-agent-foreground/5 disabled:cursor-wait'
|
|
87
|
+
: 'cursor-default'
|
|
88
|
+
}`}
|
|
78
89
|
data-turn-file=""
|
|
79
90
|
data-kind={file.kind}
|
|
80
91
|
>
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
useChatsAndAgents,
|
|
16
16
|
type UseChatsAndAgentsResult,
|
|
17
17
|
} from '@/hooks/useChatsAndAgents';
|
|
18
|
+
import { hostToolChrome, sanitizeRightPanelKind, settingsChrome } from '@/lib/host-tools';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* AgentLayout wraps `/agent`, `/agent/:chatId`, and `/` (default) with a
|
|
@@ -272,14 +273,16 @@ function AgentLayoutContent() {
|
|
|
272
273
|
try {
|
|
273
274
|
const saved = window.localStorage.getItem(RIGHT_PANEL_KEY);
|
|
274
275
|
if (saved !== null) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
const raw =
|
|
277
|
+
saved === 'terminal' || packChatSlots.some((s) => s.slotId === saved)
|
|
278
|
+
? saved
|
|
279
|
+
: null;
|
|
280
|
+
return sanitizeRightPanelKind(raw);
|
|
278
281
|
}
|
|
279
282
|
// 新 key 不存在时才迁移老版本仅持久化终端的旧 key。
|
|
280
|
-
return
|
|
281
|
-
? 'terminal'
|
|
282
|
-
|
|
283
|
+
return sanitizeRightPanelKind(
|
|
284
|
+
window.localStorage.getItem(TERMINAL_OPEN_KEY) === '1' ? 'terminal' : null,
|
|
285
|
+
);
|
|
283
286
|
} catch {
|
|
284
287
|
return null;
|
|
285
288
|
}
|
|
@@ -311,7 +314,8 @@ function AgentLayoutContent() {
|
|
|
311
314
|
打开任一面板即离开任务 dock(栏位同一时刻只呈现一个内容)。 */
|
|
312
315
|
const toggleRightPanel = useCallback(
|
|
313
316
|
(kind: string) => {
|
|
314
|
-
const
|
|
317
|
+
const raw: RightPanelState = rightPanelRef.current === kind ? null : kind;
|
|
318
|
+
const next = sanitizeRightPanelKind(raw);
|
|
315
319
|
rightPanelRef.current = next;
|
|
316
320
|
if (next !== null) setInspectedTask(null);
|
|
317
321
|
setRightPanelState(next);
|
|
@@ -363,6 +367,7 @@ function AgentLayoutContent() {
|
|
|
363
367
|
}, []);
|
|
364
368
|
|
|
365
369
|
const showTerminalFromTask = useCallback(() => {
|
|
370
|
+
if (!hostToolChrome('terminal')) return;
|
|
366
371
|
setInspectedTask(null);
|
|
367
372
|
rightPanelRef.current = 'terminal';
|
|
368
373
|
setRightPanelState('terminal');
|
|
@@ -499,7 +504,7 @@ function AgentLayoutContent() {
|
|
|
499
504
|
} satisfies AgentOutletContext}
|
|
500
505
|
/>
|
|
501
506
|
</div>
|
|
502
|
-
<InsightsConsentBanner />
|
|
507
|
+
{settingsChrome('insights') && <InsightsConsentBanner />}
|
|
503
508
|
</div>
|
|
504
509
|
{(rightPanel !== null || inspectedTask !== null) && (
|
|
505
510
|
<>
|
|
@@ -523,7 +528,9 @@ function AgentLayoutContent() {
|
|
|
523
528
|
<TaskProcessPanel
|
|
524
529
|
inspected={inspectedTask}
|
|
525
530
|
onClose={closeTaskProcess}
|
|
526
|
-
onShowTerminal={
|
|
531
|
+
onShowTerminal={
|
|
532
|
+
hostToolChrome('terminal') ? showTerminalFromTask : undefined
|
|
533
|
+
}
|
|
527
534
|
/>
|
|
528
535
|
) : rightPanel === 'terminal' ? (
|
|
529
536
|
<TerminalPanel
|
package/src/lib/attachments.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - BS/browser:File 没有路径,这里把字节读成 base64 走 HTTP 端点落盘。
|
|
10
10
|
* 返回的落盘路径会重写消息正文里的文件引用与图像附件元数据。
|
|
11
11
|
*/
|
|
12
|
-
import { getElectronBridge
|
|
12
|
+
import { getElectronBridge } from './electron-bridge';
|
|
13
13
|
|
|
14
14
|
export interface AttachmentFile {
|
|
15
15
|
name: string;
|
|
@@ -39,17 +39,41 @@ export async function fileToBase64(file: File): Promise<string> {
|
|
|
39
39
|
return btoa(binary);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** 正文引用:优先落盘路径,浏览器未落盘时退回文件名(避免写成空的 ``)。 */
|
|
43
|
+
export function attachmentRefLabel(file: AttachmentFile): string {
|
|
44
|
+
return file.path || file.name;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 把附件写进用户消息:正文「关联文件」列表 + 图像 metadata。
|
|
49
|
+
* 只收录有路径或文件名的项。
|
|
50
|
+
*/
|
|
51
|
+
export function composeAttachmentUserContent(
|
|
52
|
+
text: string,
|
|
53
|
+
files: AttachmentFile[],
|
|
54
|
+
): { content: string; images: Array<{ path: string; name: string }> } {
|
|
55
|
+
const usable = files.filter((file) => attachmentRefLabel(file));
|
|
56
|
+
let content = text.trim();
|
|
57
|
+
if (usable.length > 0) {
|
|
58
|
+
const fileRefs = usable.map((file) => `- \`${attachmentRefLabel(file)}\``).join('\n');
|
|
59
|
+
content = content ? `${content}\n\n---\n关联文件:\n${fileRefs}` : `关联文件:\n${fileRefs}`;
|
|
60
|
+
}
|
|
61
|
+
const images = usable
|
|
62
|
+
.filter((file) => isImageFile(file.path || file.name))
|
|
63
|
+
.map((file) => ({ path: attachmentRefLabel(file), name: file.name }));
|
|
64
|
+
return { content, images };
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
/**
|
|
43
68
|
* 把一批附件持久化到会话附件目录,返回「提交时应使用」的文件列表:
|
|
44
69
|
* 成功项用落盘路径,失败项退回原文件(不让用户的选择因单个文件失败而丢失)。
|
|
45
|
-
*
|
|
70
|
+
* 无 attachments 桥 / 空 chatId 时原样返回。
|
|
46
71
|
*/
|
|
47
72
|
export async function saveChatAttachments(
|
|
48
73
|
chatId: string | null | undefined,
|
|
49
74
|
files: AttachmentFile[],
|
|
50
75
|
): Promise<AttachmentFile[]> {
|
|
51
76
|
if (!chatId || files.length === 0) return files;
|
|
52
|
-
if (!isElectron()) return files;
|
|
53
77
|
const bridge = getElectronBridge();
|
|
54
78
|
if (!bridge?.attachments?.save) return files;
|
|
55
79
|
try {
|
package/src/lib/exec-policy.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* = 关闭逐条命令沙箱。工具审批(允许/拒绝)仍按原策略生效。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { hostToolChrome } from './host-tools';
|
|
10
|
+
|
|
9
11
|
export type ExecPolicy = 'workspace' | 'full';
|
|
10
12
|
|
|
11
13
|
export const EXEC_POLICY_STORAGE_KEY = 'agent-exec-policy';
|
|
@@ -16,6 +18,7 @@ export function parseExecPolicy(value: unknown): ExecPolicy {
|
|
|
16
18
|
|
|
17
19
|
export function readStoredExecPolicy(): ExecPolicy {
|
|
18
20
|
if (typeof localStorage === 'undefined') return 'workspace';
|
|
21
|
+
if (!hostToolChrome('local-fs')) return 'workspace';
|
|
19
22
|
return parseExecPolicy(localStorage.getItem(EXEC_POLICY_STORAGE_KEY));
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 渲染层读构建期注入的宿主工具族 / 安全询问。
|
|
3
|
+
* 解析规则与 ts/src/host-tools.ts 对齐:缺省全开;对象必须显式 chrome:true。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type HostToolFamilyId =
|
|
7
|
+
| 'terminal'
|
|
8
|
+
| 'local-fs'
|
|
9
|
+
| 'projects'
|
|
10
|
+
| 'background-tasks'
|
|
11
|
+
| 'mcp'
|
|
12
|
+
| 'web'
|
|
13
|
+
| 'plugins';
|
|
14
|
+
|
|
15
|
+
export interface HostToolFamilySurface {
|
|
16
|
+
capability: boolean;
|
|
17
|
+
chrome: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type HostToolsConfig = Partial<
|
|
21
|
+
Record<HostToolFamilyId, boolean | { capability?: boolean; chrome?: boolean }>
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export type ResolvedHostTools = Record<HostToolFamilyId, HostToolFamilySurface>;
|
|
25
|
+
|
|
26
|
+
const FAMILY_IDS: readonly HostToolFamilyId[] = [
|
|
27
|
+
'terminal',
|
|
28
|
+
'local-fs',
|
|
29
|
+
'projects',
|
|
30
|
+
'background-tasks',
|
|
31
|
+
'mcp',
|
|
32
|
+
'web',
|
|
33
|
+
'plugins',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function resolveSurface(
|
|
37
|
+
value: boolean | { capability?: boolean; chrome?: boolean } | undefined,
|
|
38
|
+
): HostToolFamilySurface {
|
|
39
|
+
if (value === undefined || value === true) return { capability: true, chrome: true };
|
|
40
|
+
if (value === false) return { capability: false, chrome: false };
|
|
41
|
+
return {
|
|
42
|
+
capability: value.capability !== false,
|
|
43
|
+
chrome: value.chrome === true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveWebHostTools(config?: HostToolsConfig | null): ResolvedHostTools {
|
|
48
|
+
const out = {} as ResolvedHostTools;
|
|
49
|
+
for (const id of FAMILY_IDS) {
|
|
50
|
+
out[id] = resolveSurface(config?.[id]);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readHostToolsConfig(): HostToolsConfig {
|
|
56
|
+
const raw = import.meta.env.VITE_HOST_TOOLS;
|
|
57
|
+
if (!raw) return {};
|
|
58
|
+
try {
|
|
59
|
+
const parsed = typeof raw === 'string' ? (JSON.parse(raw) as unknown) : raw;
|
|
60
|
+
return parsed && typeof parsed === 'object' ? (parsed as HostToolsConfig) : {};
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let cached: ResolvedHostTools | null = null;
|
|
67
|
+
|
|
68
|
+
export function getWebHostTools(): ResolvedHostTools {
|
|
69
|
+
cached ??= resolveWebHostTools(readHostToolsConfig());
|
|
70
|
+
return cached;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function hostToolChrome(id: HostToolFamilyId): boolean {
|
|
74
|
+
return getWebHostTools()[id].chrome;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function hostToolCapability(id: HostToolFamilyId): boolean {
|
|
78
|
+
return getWebHostTools()[id].capability;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isWebApprovalEnabled(): boolean {
|
|
82
|
+
return import.meta.env.VITE_APPROVAL !== 'off';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type ChatModeId = 'agent' | 'plan';
|
|
86
|
+
|
|
87
|
+
const DEFAULT_CHAT_MODES: ChatModeId[] = ['agent', 'plan'];
|
|
88
|
+
|
|
89
|
+
export function resolveWebChatModes(value?: unknown): ChatModeId[] {
|
|
90
|
+
if (!Array.isArray(value)) return [...DEFAULT_CHAT_MODES];
|
|
91
|
+
const allowed = DEFAULT_CHAT_MODES.filter((id) => value.includes(id));
|
|
92
|
+
return allowed.length > 0 ? allowed : ['agent'];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readChatModesConfig(): unknown {
|
|
96
|
+
const raw = import.meta.env.VITE_CHAT_MODES;
|
|
97
|
+
if (!raw) return undefined;
|
|
98
|
+
try {
|
|
99
|
+
return typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
100
|
+
} catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let cachedModes: ChatModeId[] | null = null;
|
|
106
|
+
|
|
107
|
+
export function getWebChatModes(): ChatModeId[] {
|
|
108
|
+
cachedModes ??= resolveWebChatModes(readChatModesConfig());
|
|
109
|
+
return cachedModes;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function clampWebChatMode(requested: unknown): ChatModeId {
|
|
113
|
+
return requested === 'plan' && getWebChatModes().includes('plan') ? 'plan' : 'agent';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const SETTINGS_ITEM_IDS = [
|
|
117
|
+
'agents',
|
|
118
|
+
'skills',
|
|
119
|
+
'mcp',
|
|
120
|
+
'appearance',
|
|
121
|
+
'llm',
|
|
122
|
+
'web-search',
|
|
123
|
+
'usage',
|
|
124
|
+
'diagnose',
|
|
125
|
+
'security',
|
|
126
|
+
'insights',
|
|
127
|
+
'telemetry',
|
|
128
|
+
] as const;
|
|
129
|
+
|
|
130
|
+
export type SettingsItemId = (typeof SETTINGS_ITEM_IDS)[number];
|
|
131
|
+
|
|
132
|
+
export const GENERAL_SETTINGS_ITEM_IDS = [
|
|
133
|
+
'appearance',
|
|
134
|
+
'llm',
|
|
135
|
+
'web-search',
|
|
136
|
+
'usage',
|
|
137
|
+
'diagnose',
|
|
138
|
+
'security',
|
|
139
|
+
'insights',
|
|
140
|
+
'telemetry',
|
|
141
|
+
] as const;
|
|
142
|
+
|
|
143
|
+
export type SettingsChromeConfig = Partial<Record<SettingsItemId, boolean>>;
|
|
144
|
+
|
|
145
|
+
export type ResolvedSettingsChrome = Record<SettingsItemId, boolean>;
|
|
146
|
+
|
|
147
|
+
function settingsFollowsHostTool(id: SettingsItemId, tools: ResolvedHostTools): boolean {
|
|
148
|
+
if (id === 'skills') return tools.plugins.chrome;
|
|
149
|
+
if (id === 'mcp') return tools.mcp.chrome;
|
|
150
|
+
if (id === 'web-search') return tools.web.chrome;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function resolveWebSettingsChrome(
|
|
155
|
+
value?: unknown,
|
|
156
|
+
tools: ResolvedHostTools = resolveWebHostTools(),
|
|
157
|
+
): ResolvedSettingsChrome {
|
|
158
|
+
const config =
|
|
159
|
+
value && typeof value === 'object' && !Array.isArray(value)
|
|
160
|
+
? (value as Record<string, unknown>)
|
|
161
|
+
: {};
|
|
162
|
+
const out = {} as ResolvedSettingsChrome;
|
|
163
|
+
for (const id of SETTINGS_ITEM_IDS) {
|
|
164
|
+
out[id] = config[id] !== false && settingsFollowsHostTool(id, tools);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function readSettingsConfig(): unknown {
|
|
170
|
+
const raw = import.meta.env.VITE_SETTINGS;
|
|
171
|
+
if (!raw) return undefined;
|
|
172
|
+
try {
|
|
173
|
+
return typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
174
|
+
} catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let cachedSettings: ResolvedSettingsChrome | null = null;
|
|
180
|
+
|
|
181
|
+
export function getWebSettingsChrome(): ResolvedSettingsChrome {
|
|
182
|
+
cachedSettings ??= resolveWebSettingsChrome(readSettingsConfig(), getWebHostTools());
|
|
183
|
+
return cachedSettings;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function settingsChrome(id: SettingsItemId): boolean {
|
|
187
|
+
return getWebSettingsChrome()[id];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function hasGeneralSettingsChrome(): boolean {
|
|
191
|
+
const chrome = getWebSettingsChrome();
|
|
192
|
+
return GENERAL_SETTINGS_ITEM_IDS.some((id) => chrome[id]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function sanitizeRightPanelKind(kind: string | null | undefined): string | null {
|
|
196
|
+
if (!kind) return null;
|
|
197
|
+
if (kind === 'terminal') return hostToolChrome('terminal') ? 'terminal' : null;
|
|
198
|
+
return kind;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 测试用:清掉缓存,让下次按新 env 重读。 */
|
|
202
|
+
export function resetWebHostToolsForTests(): void {
|
|
203
|
+
cached = null;
|
|
204
|
+
cachedModes = null;
|
|
205
|
+
cachedSettings = null;
|
|
206
|
+
}
|
package/src/pages/AgentPage.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
readStoredExecPolicy,
|
|
23
23
|
type ExecPolicy,
|
|
24
24
|
} from "@/lib/exec-policy";
|
|
25
|
+
import { clampWebChatMode, hostToolChrome, settingsChrome } from "@/lib/host-tools";
|
|
25
26
|
import type { ExecutedAction } from "@/components/chat/ExecutedActionsCard";
|
|
26
27
|
import { inspectTaskTitle, type InspectTaskInput } from "@/components/chat/executed-actions-model";
|
|
27
28
|
import type { ChildInfo } from "@/components/chat/OrchestrationChildrenCard";
|
|
@@ -60,6 +61,10 @@ import {
|
|
|
60
61
|
setPendingFirstMessage,
|
|
61
62
|
takePendingFirstMessage,
|
|
62
63
|
} from "@/lib/pending-first-message";
|
|
64
|
+
import {
|
|
65
|
+
composeAttachmentUserContent,
|
|
66
|
+
saveChatAttachments,
|
|
67
|
+
} from "@/lib/attachments";
|
|
63
68
|
import { BRAND_NAME, pickDefaultAgentId } from "@/brand";
|
|
64
69
|
|
|
65
70
|
/**
|
|
@@ -85,10 +90,8 @@ type ChatMessageWithMetadata = ChatMessage & LocalChatMessage;
|
|
|
85
90
|
const CHAT_MODE_STORAGE_KEY = "agent-chat-mode";
|
|
86
91
|
|
|
87
92
|
function readStoredMode(): ChatMode {
|
|
88
|
-
if (typeof localStorage === "undefined") return "agent";
|
|
89
|
-
return localStorage.getItem(CHAT_MODE_STORAGE_KEY)
|
|
90
|
-
? "plan"
|
|
91
|
-
: "agent";
|
|
93
|
+
if (typeof localStorage === "undefined") return clampWebChatMode("agent");
|
|
94
|
+
return clampWebChatMode(localStorage.getItem(CHAT_MODE_STORAGE_KEY));
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
function extractPersistedActions(
|
|
@@ -374,8 +377,9 @@ function AgentChatView({
|
|
|
374
377
|
// 当前会话绑定的项目 — Codex 式 cwd 指示:输入框上方显示项目名徽章,
|
|
375
378
|
// 点击可关联到其他项目 / 修改项目目录 / 移出项目。无项目会话不显示。
|
|
376
379
|
const [projects, setProjects] = useState<LocalProject[]>([]);
|
|
380
|
+
const showProjectsChrome = hostToolChrome("projects");
|
|
377
381
|
const fetchProjects = useCallback(async () => {
|
|
378
|
-
if (!isElectron()) return;
|
|
382
|
+
if (!isElectron() || !hostToolChrome("projects")) return;
|
|
379
383
|
try {
|
|
380
384
|
const res = await listProjects();
|
|
381
385
|
setProjects(res.projects ?? []);
|
|
@@ -516,9 +520,10 @@ function AgentChatView({
|
|
|
516
520
|
);
|
|
517
521
|
|
|
518
522
|
const handleModeChange = useCallback((next: ChatMode) => {
|
|
519
|
-
|
|
523
|
+
const clamped = clampWebChatMode(next);
|
|
524
|
+
setMode(clamped);
|
|
520
525
|
if (typeof localStorage !== "undefined") {
|
|
521
|
-
localStorage.setItem(CHAT_MODE_STORAGE_KEY,
|
|
526
|
+
localStorage.setItem(CHAT_MODE_STORAGE_KEY, clamped);
|
|
522
527
|
}
|
|
523
528
|
}, []);
|
|
524
529
|
|
|
@@ -1121,7 +1126,9 @@ function AgentChatView({
|
|
|
1121
1126
|
suggestedReplies={visibleSuggestedReplies}
|
|
1122
1127
|
onSelectSuggestion={handleSelectSuggestion}
|
|
1123
1128
|
onOpenSettings={
|
|
1124
|
-
isElectron()
|
|
1129
|
+
isElectron() && settingsChrome("llm")
|
|
1130
|
+
? () => setLlmSettingsOpen(true)
|
|
1131
|
+
: undefined
|
|
1125
1132
|
}
|
|
1126
1133
|
inputToolbarExtras={
|
|
1127
1134
|
<ModelPicker
|
|
@@ -1134,25 +1141,31 @@ function AgentChatView({
|
|
|
1134
1141
|
}
|
|
1135
1142
|
mode={mode}
|
|
1136
1143
|
onModeChange={handleModeChange}
|
|
1137
|
-
execPolicy={execPolicy}
|
|
1138
|
-
onExecPolicyChange={
|
|
1144
|
+
execPolicy={hostToolChrome("local-fs") ? execPolicy : "workspace"}
|
|
1145
|
+
onExecPolicyChange={
|
|
1146
|
+
hostToolChrome("local-fs") ? handleExecPolicyChange : undefined
|
|
1147
|
+
}
|
|
1139
1148
|
inputLeadingChrome={
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1149
|
+
showProjectsChrome ? (
|
|
1150
|
+
<ChatProjectBadge
|
|
1151
|
+
chatId={chatId}
|
|
1152
|
+
project={chatProject}
|
|
1153
|
+
projects={projects}
|
|
1154
|
+
onProjectsChanged={fetchProjects}
|
|
1155
|
+
onChatProjectChanged={ctx.refreshChats}
|
|
1156
|
+
/>
|
|
1157
|
+
) : undefined
|
|
1147
1158
|
}
|
|
1148
1159
|
inputBanner={
|
|
1149
1160
|
<>
|
|
1150
1161
|
{/* W6-5 项目信任门控:项目含规则文件但未信任时提示授权。 */}
|
|
1162
|
+
{showProjectsChrome ? (
|
|
1151
1163
|
<ProjectTrustBanner
|
|
1152
1164
|
chatId={chatId}
|
|
1153
1165
|
project={chatProject}
|
|
1154
1166
|
onTrustChanged={fetchProjects}
|
|
1155
1167
|
/>
|
|
1168
|
+
) : null}
|
|
1156
1169
|
{planReady && !isStreaming ? (
|
|
1157
1170
|
<div className="mx-2.5 mb-1 flex items-center justify-between gap-2 rounded-agent-md border border-amber-400/50 bg-amber-400/10 px-2.5 py-1.5 text-xs">
|
|
1158
1171
|
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
|
|
@@ -1183,7 +1196,7 @@ function AgentChatView({
|
|
|
1183
1196
|
</>
|
|
1184
1197
|
}
|
|
1185
1198
|
/>
|
|
1186
|
-
{isElectron() && (
|
|
1199
|
+
{isElectron() && settingsChrome("llm") && (
|
|
1187
1200
|
<LocalLlmSettingsModal
|
|
1188
1201
|
open={llmSettingsOpen}
|
|
1189
1202
|
onClose={() => setLlmSettingsOpen(false)}
|
|
@@ -1234,8 +1247,9 @@ function EmptyChatGate() {
|
|
|
1234
1247
|
useEffect(() => {
|
|
1235
1248
|
setSelectedProjectId(projectIdFromUrl);
|
|
1236
1249
|
}, [projectIdFromUrl]);
|
|
1250
|
+
const showProjectsChrome = hostToolChrome("projects");
|
|
1237
1251
|
const fetchProjects = useCallback(async () => {
|
|
1238
|
-
if (!isElectron()) return;
|
|
1252
|
+
if (!isElectron() || !hostToolChrome("projects")) return;
|
|
1239
1253
|
try {
|
|
1240
1254
|
const res = await listProjects();
|
|
1241
1255
|
setProjects(res.projects ?? []);
|
|
@@ -1258,9 +1272,10 @@ function EmptyChatGate() {
|
|
|
1258
1272
|
null;
|
|
1259
1273
|
|
|
1260
1274
|
const handleModeChange = useCallback((next: ChatMode) => {
|
|
1261
|
-
|
|
1275
|
+
const clamped = clampWebChatMode(next);
|
|
1276
|
+
setMode(clamped);
|
|
1262
1277
|
if (typeof localStorage !== "undefined") {
|
|
1263
|
-
localStorage.setItem(CHAT_MODE_STORAGE_KEY,
|
|
1278
|
+
localStorage.setItem(CHAT_MODE_STORAGE_KEY, clamped);
|
|
1264
1279
|
}
|
|
1265
1280
|
}, []);
|
|
1266
1281
|
|
|
@@ -1288,41 +1303,38 @@ function EmptyChatGate() {
|
|
|
1288
1303
|
length: trimmed.length,
|
|
1289
1304
|
});
|
|
1290
1305
|
|
|
1291
|
-
// 与 LocalChatPanel.handleSubmit 相同的装配规则:文件路径附到正文,
|
|
1292
|
-
// @ 引用进 metadata,plan 模式打 metadata.mode。
|
|
1293
|
-
if (files.length > 0) {
|
|
1294
|
-
const fileRefs = files.map((f) => `- \`${f.path}\``).join("\n");
|
|
1295
|
-
trimmed = trimmed
|
|
1296
|
-
? `${trimmed}\n\n---\n关联文件:\n${fileRefs}`
|
|
1297
|
-
: `关联文件:\n${fileRefs}`;
|
|
1298
|
-
}
|
|
1299
1306
|
const mentionedAgentIds = mentionReferences
|
|
1300
1307
|
.filter((ref) => ref.type === "agent")
|
|
1301
1308
|
.map((ref) => ref.id);
|
|
1302
1309
|
const referencedChatIds = mentionReferences
|
|
1303
1310
|
.filter((ref) => ref.type === "chat")
|
|
1304
1311
|
.map((ref) => ref.id);
|
|
1305
|
-
const metadata = {
|
|
1306
|
-
...(mode === "plan" ? { mode: "plan" as const } : {}),
|
|
1307
|
-
...(execPolicy === "full" ? { execPolicy: "full" as const } : {}),
|
|
1308
|
-
...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
|
|
1309
|
-
...(mentionedAgentIds.length > 0 ? { mentionedAgentIds } : {}),
|
|
1310
|
-
...(referencedChatIds.length > 0 ? { referencedChatIds } : {}),
|
|
1311
|
-
...(modelOverride ? { model: modelOverride } : {}),
|
|
1312
|
-
...(effortOverride ? { reasoningEffort: effortOverride } : {}),
|
|
1313
|
-
};
|
|
1314
1312
|
|
|
1315
1313
|
setIsCreating(true);
|
|
1316
1314
|
setCreateError(null);
|
|
1317
1315
|
try {
|
|
1318
1316
|
const id = await ctx.createChat({
|
|
1319
|
-
...(selectedProjectId ? { projectId: selectedProjectId } : {}),
|
|
1317
|
+
...(selectedProjectId && showProjectsChrome ? { projectId: selectedProjectId } : {}),
|
|
1320
1318
|
...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
|
|
1321
1319
|
});
|
|
1322
1320
|
if (!id) throw new Error("创建对话失败,请重试");
|
|
1321
|
+
// 落地页提交时还没有 chatId;先建会话再落盘,正文用落盘路径。
|
|
1322
|
+
const resolvedFiles =
|
|
1323
|
+
files.length > 0 ? await saveChatAttachments(id, files) : files;
|
|
1324
|
+
const assembled = composeAttachmentUserContent(trimmed, resolvedFiles);
|
|
1325
|
+
const metadata = {
|
|
1326
|
+
...(mode === "plan" ? { mode: "plan" as const } : {}),
|
|
1327
|
+
...(execPolicy === "full" ? { execPolicy: "full" as const } : {}),
|
|
1328
|
+
...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
|
|
1329
|
+
...(mentionedAgentIds.length > 0 ? { mentionedAgentIds } : {}),
|
|
1330
|
+
...(referencedChatIds.length > 0 ? { referencedChatIds } : {}),
|
|
1331
|
+
...(modelOverride ? { model: modelOverride } : {}),
|
|
1332
|
+
...(effortOverride ? { reasoningEffort: effortOverride } : {}),
|
|
1333
|
+
...(assembled.images.length > 0 ? { images: assembled.images } : {}),
|
|
1334
|
+
};
|
|
1323
1335
|
setPendingFirstMessage({
|
|
1324
1336
|
chatId: id,
|
|
1325
|
-
content:
|
|
1337
|
+
content: assembled.content,
|
|
1326
1338
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1327
1339
|
});
|
|
1328
1340
|
navigate(`/agent/${id}`);
|
|
@@ -1365,7 +1377,9 @@ function EmptyChatGate() {
|
|
|
1365
1377
|
selectedAgentId={ctx.selectedAgentId}
|
|
1366
1378
|
onSelectAgent={handleSelectAgent}
|
|
1367
1379
|
onOpenSettings={
|
|
1368
|
-
isElectron()
|
|
1380
|
+
isElectron() && settingsChrome("llm")
|
|
1381
|
+
? () => setLlmSettingsOpen(true)
|
|
1382
|
+
: undefined
|
|
1369
1383
|
}
|
|
1370
1384
|
toolbarExtras={
|
|
1371
1385
|
<ModelPicker
|
|
@@ -1377,7 +1391,7 @@ function EmptyChatGate() {
|
|
|
1377
1391
|
/>
|
|
1378
1392
|
}
|
|
1379
1393
|
leadingChrome={
|
|
1380
|
-
isElectron() ? (
|
|
1394
|
+
isElectron() && showProjectsChrome ? (
|
|
1381
1395
|
<ProjectPickerButton
|
|
1382
1396
|
projects={projects}
|
|
1383
1397
|
value={selectedProjectId}
|
|
@@ -1388,8 +1402,10 @@ function EmptyChatGate() {
|
|
|
1388
1402
|
}
|
|
1389
1403
|
mode={mode}
|
|
1390
1404
|
onModeChange={handleModeChange}
|
|
1391
|
-
execPolicy={execPolicy}
|
|
1392
|
-
onExecPolicyChange={
|
|
1405
|
+
execPolicy={hostToolChrome("local-fs") ? execPolicy : "workspace"}
|
|
1406
|
+
onExecPolicyChange={
|
|
1407
|
+
hostToolChrome("local-fs") ? handleExecPolicyChange : undefined
|
|
1408
|
+
}
|
|
1393
1409
|
files={files}
|
|
1394
1410
|
onFilesChange={setFiles}
|
|
1395
1411
|
onMentionReferencesChange={setMentionReferences}
|
|
@@ -1411,7 +1427,7 @@ function EmptyChatGate() {
|
|
|
1411
1427
|
</p>
|
|
1412
1428
|
)}
|
|
1413
1429
|
</div>
|
|
1414
|
-
{isElectron() && (
|
|
1430
|
+
{isElectron() && settingsChrome("llm") && (
|
|
1415
1431
|
<LocalLlmSettingsModal
|
|
1416
1432
|
open={llmSettingsOpen}
|
|
1417
1433
|
onClose={() => setLlmSettingsOpen(false)}
|
|
@@ -19,12 +19,15 @@ import { TelemetrySettingsPanel } from '@/components/settings/TelemetrySettingsP
|
|
|
19
19
|
import { UsagePanel } from '@/components/settings/UsagePanel';
|
|
20
20
|
import { WebSearchSettingsPanel } from '@/components/settings/WebSearchSettingsPanel';
|
|
21
21
|
import { getPackSettingsPanels } from '@/packs/registry';
|
|
22
|
+
import { settingsChrome } from '@/lib/host-tools';
|
|
22
23
|
import type { AgentOutletContext } from '@/layouts/AgentLayout';
|
|
23
24
|
|
|
24
25
|
type SettingsSection = 'skills' | 'mcp' | 'agents' | 'general';
|
|
25
26
|
|
|
26
27
|
function resolveSection(raw: string | null): SettingsSection {
|
|
27
|
-
if (raw === 'skills'
|
|
28
|
+
if (raw === 'skills' && settingsChrome('skills')) return 'skills';
|
|
29
|
+
if (raw === 'mcp' && settingsChrome('mcp')) return 'mcp';
|
|
30
|
+
if (raw === 'agents' && settingsChrome('agents')) return 'agents';
|
|
28
31
|
return 'general';
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -61,7 +64,7 @@ export function SettingsPage() {
|
|
|
61
64
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
|
62
65
|
<header className="flex h-9 flex-shrink-0 items-center justify-between gap-2 border-b border-agent-border px-2.5">
|
|
63
66
|
<h1 className="text-xs font-semibold text-agent-foreground">{title}</h1>
|
|
64
|
-
{section === 'general' && (
|
|
67
|
+
{section === 'general' && settingsChrome('llm') && (
|
|
65
68
|
<SettingsSaveButton
|
|
66
69
|
testId="settings-header-save"
|
|
67
70
|
saving={llmSaveUi.saving}
|
|
@@ -112,6 +115,7 @@ export function SettingsPage() {
|
|
|
112
115
|
|
|
113
116
|
{section === 'general' && (
|
|
114
117
|
<>
|
|
118
|
+
{settingsChrome('appearance') && (
|
|
115
119
|
<section className="space-y-2" data-testid="settings-section-appearance">
|
|
116
120
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
117
121
|
<LuMonitor className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
@@ -119,7 +123,9 @@ export function SettingsPage() {
|
|
|
119
123
|
</h2>
|
|
120
124
|
<AppearanceSettingsPanel />
|
|
121
125
|
</section>
|
|
126
|
+
)}
|
|
122
127
|
|
|
128
|
+
{settingsChrome('llm') && (
|
|
123
129
|
<section className="space-y-2" data-testid="settings-section-llm">
|
|
124
130
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
125
131
|
<LuSettings className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
@@ -131,7 +137,9 @@ export function SettingsPage() {
|
|
|
131
137
|
onSaveUiChange={setLlmSaveUi}
|
|
132
138
|
/>
|
|
133
139
|
</section>
|
|
140
|
+
)}
|
|
134
141
|
|
|
142
|
+
{settingsChrome('web-search') && (
|
|
135
143
|
<section className="space-y-2" data-testid="settings-section-web-search">
|
|
136
144
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
137
145
|
<LuSearch className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
@@ -139,7 +147,9 @@ export function SettingsPage() {
|
|
|
139
147
|
</h2>
|
|
140
148
|
<WebSearchSettingsPanel />
|
|
141
149
|
</section>
|
|
150
|
+
)}
|
|
142
151
|
|
|
152
|
+
{settingsChrome('usage') && (
|
|
143
153
|
<section className="space-y-2" data-testid="settings-section-usage">
|
|
144
154
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
145
155
|
<LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
@@ -147,7 +157,9 @@ export function SettingsPage() {
|
|
|
147
157
|
</h2>
|
|
148
158
|
<UsagePanel />
|
|
149
159
|
</section>
|
|
160
|
+
)}
|
|
150
161
|
|
|
162
|
+
{settingsChrome('diagnose') && (
|
|
151
163
|
<section className="space-y-2" data-testid="settings-section-diagnose">
|
|
152
164
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
153
165
|
<LuNetwork className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
@@ -155,7 +167,9 @@ export function SettingsPage() {
|
|
|
155
167
|
</h2>
|
|
156
168
|
<DiagnoseSettingsPanel />
|
|
157
169
|
</section>
|
|
170
|
+
)}
|
|
158
171
|
|
|
172
|
+
{settingsChrome('security') && (
|
|
159
173
|
<section className="space-y-2" data-testid="settings-section-security">
|
|
160
174
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
161
175
|
<LuShieldCheck className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
@@ -163,7 +177,9 @@ export function SettingsPage() {
|
|
|
163
177
|
</h2>
|
|
164
178
|
<SecuritySettingsPanel />
|
|
165
179
|
</section>
|
|
180
|
+
)}
|
|
166
181
|
|
|
182
|
+
{settingsChrome('insights') && (
|
|
167
183
|
<section className="space-y-2" data-testid="settings-section-insights">
|
|
168
184
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
169
185
|
<LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
@@ -171,7 +187,9 @@ export function SettingsPage() {
|
|
|
171
187
|
</h2>
|
|
172
188
|
<InsightsSettingsPanel />
|
|
173
189
|
</section>
|
|
190
|
+
)}
|
|
174
191
|
|
|
192
|
+
{settingsChrome('telemetry') && (
|
|
175
193
|
<section className="space-y-2" data-testid="settings-section-telemetry">
|
|
176
194
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
177
195
|
<LuActivity className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
@@ -179,6 +197,7 @@ export function SettingsPage() {
|
|
|
179
197
|
</h2>
|
|
180
198
|
<TelemetrySettingsPanel />
|
|
181
199
|
</section>
|
|
200
|
+
)}
|
|
182
201
|
|
|
183
202
|
{/* 场景包设置面板(1.2 起由包渲染层贡献)。
|
|
184
203
|
包经 packs/registry 注册,未激活 flavor 的产物里没有包组件。 */}
|
package/src/vite-env.d.ts
CHANGED
|
@@ -1 +1,17 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
readonly VITE_APP_FLAVOR?: string;
|
|
5
|
+
readonly VITE_BRAND_NAME?: string;
|
|
6
|
+
readonly VITE_BRAND_TITLE?: string;
|
|
7
|
+
readonly VITE_BRAND_TAGLINE?: string;
|
|
8
|
+
readonly VITE_DEFAULT_AGENT_ID?: string;
|
|
9
|
+
readonly VITE_HOST_TOOLS?: string;
|
|
10
|
+
readonly VITE_APPROVAL?: string;
|
|
11
|
+
readonly VITE_CHAT_MODES?: string;
|
|
12
|
+
readonly VITE_SETTINGS?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ImportMeta {
|
|
16
|
+
readonly env: ImportMetaEnv;
|
|
17
|
+
}
|
package/vite.base.js
CHANGED
|
@@ -53,6 +53,11 @@ export function createProductViteConfig(options) {
|
|
|
53
53
|
'import.meta.env.VITE_BRAND_TITLE': JSON.stringify(brand?.title ?? ''),
|
|
54
54
|
'import.meta.env.VITE_BRAND_TAGLINE': JSON.stringify(brand?.tagline ?? '一款本地桌面 AI 伙伴'),
|
|
55
55
|
'import.meta.env.VITE_DEFAULT_AGENT_ID': JSON.stringify(brand?.defaultAgentId ?? 'local-assistant'),
|
|
56
|
+
// 宿主工具族 / 安全询问:与 node 侧 setProductConfig 读同一份 product.json。
|
|
57
|
+
'import.meta.env.VITE_HOST_TOOLS': JSON.stringify(JSON.stringify(product.hostTools ?? {})),
|
|
58
|
+
'import.meta.env.VITE_APPROVAL': JSON.stringify(product.approval ?? 'host'),
|
|
59
|
+
'import.meta.env.VITE_CHAT_MODES': JSON.stringify(JSON.stringify(product.chatModes ?? ['agent', 'plan'])),
|
|
60
|
+
'import.meta.env.VITE_SETTINGS': JSON.stringify(JSON.stringify(product.settings ?? {})),
|
|
56
61
|
},
|
|
57
62
|
resolve: {
|
|
58
63
|
alias: {
|