@steerable/agent-shell-web 0.6.30 → 0.6.31
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 +4 -4
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +14 -0
- package/src/assets/logo-steerable.svg +14 -0
- package/src/brand.ts +3 -3
- package/src/components/AgentSidebar.tsx +161 -168
- package/src/components/BrandLockup.tsx +1 -1
- package/src/components/CreateProjectModal.tsx +182 -0
- package/src/components/chat/ChatProjectBadge.tsx +97 -11
- package/src/layouts/AgentLayout.tsx +12 -10
- package/src/lib/local-api.ts +7 -1
- package/src/pages/AgentPage.tsx +11 -10
- package/src/assets/logo-generic.png +0 -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.31",
|
|
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",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"react-router-dom": "^6.30.0",
|
|
47
47
|
"rehype-raw": "^7.0.0",
|
|
48
48
|
"remark-gfm": "^4.0.1",
|
|
49
|
-
"@steerable/agent-
|
|
50
|
-
"@steerable/agent-
|
|
49
|
+
"@steerable/agent-protocol": "0.6.31",
|
|
50
|
+
"@steerable/agent-ui": "0.6.31"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@tailwindcss/vite": "^4.3.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"typescript": "^5.8.3",
|
|
61
61
|
"vite": "^5.4.21",
|
|
62
62
|
"vitest": "^3.2.4",
|
|
63
|
-
"@steerable/pack-sdk": "0.6.
|
|
63
|
+
"@steerable/pack-sdk": "0.6.31"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"gen": "echo agent-shell-web",
|
package/public/favicon.ico
CHANGED
|
Binary file
|
package/public/favicon.png
CHANGED
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
|
2
|
+
<circle cx="24" cy="24" r="17" stroke="#5c6bc0" stroke-width="3.5"/>
|
|
3
|
+
<circle cx="24" cy="24" r="5" fill="#5c6bc0"/>
|
|
4
|
+
<g stroke="#5c6bc0" stroke-width="3.5" stroke-linecap="round">
|
|
5
|
+
<line x1="24" y1="2.5" x2="24" y2="12"/>
|
|
6
|
+
<line x1="24" y1="36" x2="24" y2="45.5"/>
|
|
7
|
+
<line x1="2.5" y1="24" x2="12" y2="24"/>
|
|
8
|
+
<line x1="36" y1="24" x2="45.5" y2="24"/>
|
|
9
|
+
<line x1="8.8" y1="8.8" x2="15.5" y2="15.5"/>
|
|
10
|
+
<line x1="32.5" y1="32.5" x2="39.2" y2="39.2"/>
|
|
11
|
+
<line x1="39.2" y1="8.8" x2="32.5" y2="15.5"/>
|
|
12
|
+
<line x1="15.5" y1="32.5" x2="8.8" y2="39.2"/>
|
|
13
|
+
</g>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
|
2
|
+
<circle cx="24" cy="24" r="17" stroke="#5c6bc0" stroke-width="3.5"/>
|
|
3
|
+
<circle cx="24" cy="24" r="5" fill="#5c6bc0"/>
|
|
4
|
+
<g stroke="#5c6bc0" stroke-width="3.5" stroke-linecap="round">
|
|
5
|
+
<line x1="24" y1="2.5" x2="24" y2="12"/>
|
|
6
|
+
<line x1="24" y1="36" x2="24" y2="45.5"/>
|
|
7
|
+
<line x1="2.5" y1="24" x2="12" y2="24"/>
|
|
8
|
+
<line x1="36" y1="24" x2="45.5" y2="24"/>
|
|
9
|
+
<line x1="8.8" y1="8.8" x2="15.5" y2="15.5"/>
|
|
10
|
+
<line x1="32.5" y1="32.5" x2="39.2" y2="39.2"/>
|
|
11
|
+
<line x1="39.2" y1="8.8" x2="32.5" y2="15.5"/>
|
|
12
|
+
<line x1="15.5" y1="32.5" x2="8.8" y2="39.2"/>
|
|
13
|
+
</g>
|
|
14
|
+
</svg>
|
package/src/brand.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* (VITE_BRAND_* define:product.json brand 优先,其次激活包 pack.json
|
|
4
4
|
* brand,3.1);logo 资产由激活包的 web 模块在注册时经
|
|
5
5
|
* setBrandLogoUrl() 注入(资产静态 import 在包内,只进本产品 bundle)。
|
|
6
|
-
* 未注入时一律回落到 shell
|
|
6
|
+
* 未注入时一律回落到 shell 默认(Steerable Shell / 舵轮标)。
|
|
7
7
|
* 主进程侧对应物见 src/brand.ts。
|
|
8
8
|
*/
|
|
9
|
-
import shellLogoUrl from '@/assets/logo-
|
|
9
|
+
import shellLogoUrl from '@/assets/logo-steerable.svg';
|
|
10
10
|
|
|
11
11
|
/** 当前构建的 flavor(开放字符串,0.3g 起不再是二值联合)。 */
|
|
12
12
|
export const APP_FLAVOR: string = import.meta.env.VITE_APP_FLAVOR ?? 'generic';
|
|
@@ -23,7 +23,7 @@ export const BRAND_NAME: string = import.meta.env.VITE_BRAND_NAME ?? 'Steerable
|
|
|
23
23
|
export const BRAND_TITLE: string =
|
|
24
24
|
import.meta.env.VITE_BRAND_TITLE ?? import.meta.env.VITE_BRAND_NAME ?? 'Steerable Shell';
|
|
25
25
|
|
|
26
|
-
/** shell 默认 logo
|
|
26
|
+
/** shell 默认 logo(Steerable 舵轮标);包品牌 logo 由包 web 模块注册覆盖。 */
|
|
27
27
|
let brandLogoUrl: string = shellLogoUrl;
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
* │ ⬡ 智能体管理 │ ← /settings?section=agents(独立页)
|
|
64
64
|
* │ ⬡ Skill 设置 │ ← /settings?section=skills(独立页)
|
|
65
65
|
* │ 🔌 MCP 设置 │ ← /settings?section=mcp(独立页)
|
|
66
|
-
* │
|
|
66
|
+
* │ 会话 v 📁+ │ ← 📁+ 打开新建项目弹窗
|
|
67
67
|
* │ v 📁 项目A · 3 (hover: +✏📂🗑)│ ← 项目组:折叠/新建/重命名/换文件夹/删
|
|
68
68
|
* │ ...(项目内对话) │
|
|
69
69
|
* │ 今天 │
|
|
@@ -73,19 +73,12 @@
|
|
|
73
73
|
* │ ⚙ 设置 │ ← /settings(模型 + 洞察/遥测/用量/搜索/安全)
|
|
74
74
|
* └─────────────────────────────────┘
|
|
75
75
|
*
|
|
76
|
-
* 项目模式:项目 = 名字 +
|
|
77
|
-
*
|
|
78
|
-
* tool-router.ts ToolExecContext.projectRoot);无项目对话不沙箱。
|
|
76
|
+
* 项目模式:项目 = 名字 + 托管家目录(Documents/<应用名>/<项目名>/)+
|
|
77
|
+
* 可选源文件夹。写入/命令围栏在家目录;源文件夹只放宽读取。
|
|
79
78
|
*/
|
|
80
79
|
|
|
81
|
-
import {
|
|
82
|
-
|
|
83
|
-
useEffect,
|
|
84
|
-
useMemo,
|
|
85
|
-
useRef,
|
|
86
|
-
useState,
|
|
87
|
-
} from 'react';
|
|
88
|
-
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
80
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
81
|
+
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
|
89
82
|
import {
|
|
90
83
|
LuChevronDown,
|
|
91
84
|
LuChevronUp,
|
|
@@ -104,10 +97,10 @@ import {
|
|
|
104
97
|
LuFolderPlus,
|
|
105
98
|
LuPencil,
|
|
106
99
|
LuPlug,
|
|
107
|
-
} from
|
|
108
|
-
import { parseChatTitle } from
|
|
109
|
-
import { getDateGroupLabel, getDateGroupPriority } from
|
|
110
|
-
import { getElectronBridge, isElectron } from
|
|
100
|
+
} from "react-icons/lu";
|
|
101
|
+
import { parseChatTitle } from "@/lib/chat-title";
|
|
102
|
+
import { getDateGroupLabel, getDateGroupPriority } from "@/lib/date-groups";
|
|
103
|
+
import { getElectronBridge, isElectron } from "@/lib/electron-bridge";
|
|
111
104
|
import {
|
|
112
105
|
createProject,
|
|
113
106
|
deleteProject,
|
|
@@ -116,17 +109,18 @@ import {
|
|
|
116
109
|
type LocalChat,
|
|
117
110
|
type LocalChatAgent,
|
|
118
111
|
type LocalProject,
|
|
119
|
-
} from
|
|
120
|
-
import type { UseChatsAndAgentsResult } from
|
|
121
|
-
import type { PackChatSlotContribution } from
|
|
122
|
-
import type { RightPanelState } from
|
|
123
|
-
import { BrandLockup } from
|
|
112
|
+
} from "@/lib/local-api";
|
|
113
|
+
import type { UseChatsAndAgentsResult } from "@/hooks/useChatsAndAgents";
|
|
114
|
+
import type { PackChatSlotContribution } from "@/packs/registry";
|
|
115
|
+
import type { RightPanelState } from "@/layouts/AgentLayout";
|
|
116
|
+
import { BrandLockup } from "@/components/BrandLockup";
|
|
117
|
+
import { CreateProjectModal } from "@/components/CreateProjectModal";
|
|
124
118
|
|
|
125
|
-
const DEFAULT_DOT_COLOR =
|
|
119
|
+
const DEFAULT_DOT_COLOR = "#7c3aed";
|
|
126
120
|
|
|
127
121
|
function agentInitial(agent: LocalChatAgent | null): string {
|
|
128
|
-
const name = (agent?.name ||
|
|
129
|
-
return name ? name[0].toUpperCase() :
|
|
122
|
+
const name = (agent?.name || "").trim();
|
|
123
|
+
return name ? name[0].toUpperCase() : "A";
|
|
130
124
|
}
|
|
131
125
|
|
|
132
126
|
function AgentDot({
|
|
@@ -170,17 +164,17 @@ export function AgentSidebar({
|
|
|
170
164
|
const location = useLocation();
|
|
171
165
|
const { chatId: currentChatId } = useParams<{ chatId?: string }>();
|
|
172
166
|
const bridge = getElectronBridge();
|
|
173
|
-
const onSettingsPage = location.pathname ===
|
|
167
|
+
const onSettingsPage = location.pathname === "/settings";
|
|
174
168
|
// /settings?section=skills|mcp|agents|(缺省 = 综合设置)—— 各自高亮。
|
|
175
169
|
const settingsSection = useMemo(
|
|
176
|
-
() => new URLSearchParams(location.search).get(
|
|
170
|
+
() => new URLSearchParams(location.search).get("section"),
|
|
177
171
|
[location.search],
|
|
178
172
|
);
|
|
179
173
|
const onGeneralSettings =
|
|
180
174
|
onSettingsPage &&
|
|
181
|
-
settingsSection !==
|
|
182
|
-
settingsSection !==
|
|
183
|
-
settingsSection !==
|
|
175
|
+
settingsSection !== "skills" &&
|
|
176
|
+
settingsSection !== "mcp" &&
|
|
177
|
+
settingsSection !== "agents";
|
|
184
178
|
const onNewChatHome = !currentChatId && !onSettingsPage;
|
|
185
179
|
|
|
186
180
|
const {
|
|
@@ -198,7 +192,7 @@ export function AgentSidebar({
|
|
|
198
192
|
const [isMac, setIsMac] = useState(false);
|
|
199
193
|
|
|
200
194
|
useEffect(() => {
|
|
201
|
-
if (typeof navigator !==
|
|
195
|
+
if (typeof navigator !== "undefined") {
|
|
202
196
|
setIsMac(/Mac|iPod|iPhone|iPad/.test(navigator.platform));
|
|
203
197
|
}
|
|
204
198
|
}, []);
|
|
@@ -217,11 +211,18 @@ export function AgentSidebar({
|
|
|
217
211
|
const [collapsedProjectIds, setCollapsedProjectIds] = useState<Set<string>>(
|
|
218
212
|
() => new Set(),
|
|
219
213
|
);
|
|
220
|
-
const [renamingProjectId, setRenamingProjectId] = useState<string | null>(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const [
|
|
214
|
+
const [renamingProjectId, setRenamingProjectId] = useState<string | null>(
|
|
215
|
+
null,
|
|
216
|
+
);
|
|
217
|
+
const [renamingValue, setRenamingValue] = useState("");
|
|
218
|
+
const [confirmDeleteProjectId, setConfirmDeleteProjectId] = useState<
|
|
219
|
+
string | null
|
|
220
|
+
>(null);
|
|
221
|
+
const [deletingProjectId, setDeletingProjectId] = useState<string | null>(
|
|
222
|
+
null,
|
|
223
|
+
);
|
|
224
224
|
const [projectError, setProjectError] = useState<string | null>(null);
|
|
225
|
+
const [createProjectOpen, setCreateProjectOpen] = useState(false);
|
|
225
226
|
|
|
226
227
|
const fetchProjects = useCallback(async () => {
|
|
227
228
|
if (!isElectron()) return;
|
|
@@ -229,7 +230,7 @@ export function AgentSidebar({
|
|
|
229
230
|
const res = await listProjects();
|
|
230
231
|
setProjects(res.projects || []);
|
|
231
232
|
} catch (err) {
|
|
232
|
-
console.error(
|
|
233
|
+
console.error("获取项目列表失败:", err);
|
|
233
234
|
setProjectError(err instanceof Error ? err.message : String(err));
|
|
234
235
|
}
|
|
235
236
|
}, []);
|
|
@@ -238,24 +239,25 @@ export function AgentSidebar({
|
|
|
238
239
|
void fetchProjects();
|
|
239
240
|
}, [fetchProjects]);
|
|
240
241
|
|
|
241
|
-
const handleCreateProject = useCallback(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
242
|
+
const handleCreateProject = useCallback(
|
|
243
|
+
async (input: { name: string; sourceFolders: string[] }) => {
|
|
244
|
+
setProjectError(null);
|
|
245
|
+
try {
|
|
246
|
+
await createProject({
|
|
247
|
+
name: input.name,
|
|
248
|
+
...(input.sourceFolders.length > 0
|
|
249
|
+
? { sourceFolders: input.sourceFolders }
|
|
250
|
+
: {}),
|
|
251
|
+
});
|
|
252
|
+
await fetchProjects();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
255
|
+
setProjectError(message);
|
|
256
|
+
throw err instanceof Error ? err : new Error(message);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
[fetchProjects],
|
|
260
|
+
);
|
|
259
261
|
|
|
260
262
|
const handleRenameProject = useCallback(
|
|
261
263
|
async (projectId: string) => {
|
|
@@ -278,7 +280,7 @@ export function AgentSidebar({
|
|
|
278
280
|
setProjectError(null);
|
|
279
281
|
try {
|
|
280
282
|
const result = await bridge?.local?.selectDirectory({
|
|
281
|
-
title:
|
|
283
|
+
title: "重新选择项目文件夹",
|
|
282
284
|
});
|
|
283
285
|
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
284
286
|
await updateProject(projectId, { folderPath: result.filePaths[0] });
|
|
@@ -350,7 +352,7 @@ export function AgentSidebar({
|
|
|
350
352
|
navigate(`/agent?projectId=${encodeURIComponent(projectId)}`);
|
|
351
353
|
return;
|
|
352
354
|
}
|
|
353
|
-
navigate(
|
|
355
|
+
navigate("/agent");
|
|
354
356
|
},
|
|
355
357
|
[navigate],
|
|
356
358
|
);
|
|
@@ -369,7 +371,7 @@ export function AgentSidebar({
|
|
|
369
371
|
const { displayTitle, isAutomation } = parseChatTitle(chat.title);
|
|
370
372
|
return {
|
|
371
373
|
id: chat.id,
|
|
372
|
-
title: displayTitle ||
|
|
374
|
+
title: displayTitle || "新会话",
|
|
373
375
|
isAutomation,
|
|
374
376
|
isPinned: chat.isPinned,
|
|
375
377
|
sortDate,
|
|
@@ -420,7 +422,7 @@ export function AgentSidebar({
|
|
|
420
422
|
// 分组,「5 天前置顶的会话」会排在「今天」的普通会话之后,置顶语义
|
|
421
423
|
// 就只剩组内有效——与用户点图钉时的预期不符。
|
|
422
424
|
noProjectChats.forEach((chat) => {
|
|
423
|
-
const label = chat.isPinned ?
|
|
425
|
+
const label = chat.isPinned ? "置顶" : getDateGroupLabel(chat.sortDate);
|
|
424
426
|
if (!map.has(label)) {
|
|
425
427
|
map.set(label, {
|
|
426
428
|
label,
|
|
@@ -448,7 +450,7 @@ export function AgentSidebar({
|
|
|
448
450
|
const ok = await deleteChat(id);
|
|
449
451
|
if (ok) {
|
|
450
452
|
setConfirmDeleteChatId(null);
|
|
451
|
-
if (id === currentChatId) navigate(
|
|
453
|
+
if (id === currentChatId) navigate("/agent");
|
|
452
454
|
}
|
|
453
455
|
} finally {
|
|
454
456
|
setDeletingChatId(null);
|
|
@@ -477,22 +479,17 @@ export function AgentSidebar({
|
|
|
477
479
|
navigate(`/agent/${chat.id}`);
|
|
478
480
|
}}
|
|
479
481
|
className={[
|
|
480
|
-
|
|
482
|
+
"flex h-7 w-full min-w-0 items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors duration-200",
|
|
481
483
|
isCurrent
|
|
482
|
-
?
|
|
483
|
-
:
|
|
484
|
-
].join(
|
|
484
|
+
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
485
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
486
|
+
].join(" ")}
|
|
485
487
|
title={
|
|
486
|
-
chat.isAutomation
|
|
487
|
-
? `[由自动化触发] ${chat.title}`
|
|
488
|
-
: chat.title
|
|
488
|
+
chat.isAutomation ? `[由自动化触发] ${chat.title}` : chat.title
|
|
489
489
|
}
|
|
490
490
|
>
|
|
491
491
|
{chat.isPinned && (
|
|
492
|
-
<span
|
|
493
|
-
className="shrink-0 text-[10px]"
|
|
494
|
-
aria-label="已置顶"
|
|
495
|
-
>
|
|
492
|
+
<span className="shrink-0 text-[10px]" aria-label="已置顶">
|
|
496
493
|
📌
|
|
497
494
|
</span>
|
|
498
495
|
)}
|
|
@@ -503,9 +500,7 @@ export function AgentSidebar({
|
|
|
503
500
|
/>
|
|
504
501
|
)}
|
|
505
502
|
<AgentDot agent={chatAgent} size={16} />
|
|
506
|
-
<span className="min-w-0 truncate leading-none">
|
|
507
|
-
{chat.title}
|
|
508
|
-
</span>
|
|
503
|
+
<span className="min-w-0 truncate leading-none">{chat.title}</span>
|
|
509
504
|
</button>
|
|
510
505
|
{/* Gradient mask so the trash button doesn't paint
|
|
511
506
|
over the chat title — fade matches the row's
|
|
@@ -514,15 +509,15 @@ export function AgentSidebar({
|
|
|
514
509
|
<div
|
|
515
510
|
aria-hidden="true"
|
|
516
511
|
className={[
|
|
517
|
-
|
|
512
|
+
"pointer-events-none absolute inset-y-0 right-0 w-12 rounded-r-full transition-opacity duration-200",
|
|
518
513
|
isConfirmingDelete
|
|
519
514
|
? isCurrent
|
|
520
|
-
?
|
|
521
|
-
:
|
|
515
|
+
? "bg-gradient-to-l from-agent-canvas via-agent-canvas/95 to-transparent opacity-100"
|
|
516
|
+
: "bg-gradient-to-l from-agent-muted via-agent-muted/95 to-transparent opacity-100"
|
|
522
517
|
: isCurrent
|
|
523
|
-
?
|
|
524
|
-
:
|
|
525
|
-
].join(
|
|
518
|
+
? "bg-gradient-to-l from-agent-canvas via-agent-canvas/95 to-transparent opacity-0 group-hover/item:opacity-100"
|
|
519
|
+
: "bg-gradient-to-l from-agent-muted via-agent-muted/95 to-transparent opacity-0 group-hover/item:opacity-100",
|
|
520
|
+
].join(" ")}
|
|
526
521
|
/>
|
|
527
522
|
<button
|
|
528
523
|
type="button"
|
|
@@ -532,29 +527,20 @@ export function AgentSidebar({
|
|
|
532
527
|
}}
|
|
533
528
|
disabled={isDeleting}
|
|
534
529
|
className={[
|
|
535
|
-
|
|
536
|
-
isConfirmingDelete
|
|
537
|
-
? 'bg-agent-destructive/10 text-agent-destructive opacity-100 hover:bg-agent-destructive/20'
|
|
538
|
-
: 'text-agent-muted-foreground opacity-0 hover:bg-agent-foreground/5 hover:text-agent-destructive group-hover/item:opacity-100 focus:opacity-100',
|
|
539
|
-
'disabled:cursor-not-allowed disabled:opacity-100',
|
|
540
|
-
].join(' ')}
|
|
541
|
-
title={
|
|
542
|
-
isConfirmingDelete
|
|
543
|
-
? '再次点击确认删除'
|
|
544
|
-
: '删除会话'
|
|
545
|
-
}
|
|
546
|
-
aria-label={
|
|
530
|
+
"absolute right-1 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full transition-all duration-200",
|
|
547
531
|
isConfirmingDelete
|
|
548
|
-
?
|
|
549
|
-
:
|
|
550
|
-
|
|
532
|
+
? "bg-agent-destructive/10 text-agent-destructive opacity-100 hover:bg-agent-destructive/20"
|
|
533
|
+
: "text-agent-muted-foreground opacity-0 hover:bg-agent-foreground/5 hover:text-agent-destructive group-hover/item:opacity-100 focus:opacity-100",
|
|
534
|
+
"disabled:cursor-not-allowed disabled:opacity-100",
|
|
535
|
+
].join(" ")}
|
|
536
|
+
title={isConfirmingDelete ? "再次点击确认删除" : "删除会话"}
|
|
537
|
+
aria-label={isConfirmingDelete ? "再次点击确认删除" : "删除会话"}
|
|
551
538
|
data-testid="sidebar-chat-delete"
|
|
552
539
|
>
|
|
553
540
|
<LuTrash2
|
|
554
|
-
className={[
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
].join(' ')}
|
|
541
|
+
className={["h-3.5 w-3.5", isDeleting ? "animate-pulse" : ""].join(
|
|
542
|
+
" ",
|
|
543
|
+
)}
|
|
558
544
|
/>
|
|
559
545
|
</button>
|
|
560
546
|
</div>
|
|
@@ -587,10 +573,10 @@ export function AgentSidebar({
|
|
|
587
573
|
container.scrollHeight - 60;
|
|
588
574
|
if (nearBottom) void loadMoreChats();
|
|
589
575
|
};
|
|
590
|
-
container.addEventListener(
|
|
576
|
+
container.addEventListener("scroll", maybeLoadMore, { passive: true });
|
|
591
577
|
const tickId = window.requestAnimationFrame(maybeLoadMore);
|
|
592
578
|
return () => {
|
|
593
|
-
container.removeEventListener(
|
|
579
|
+
container.removeEventListener("scroll", maybeLoadMore);
|
|
594
580
|
window.cancelAnimationFrame(tickId);
|
|
595
581
|
};
|
|
596
582
|
}, [
|
|
@@ -608,7 +594,7 @@ export function AgentSidebar({
|
|
|
608
594
|
useEffect(() => {
|
|
609
595
|
if (!bridge?.onMenuOpenTerminal) return;
|
|
610
596
|
bridge.onMenuOpenTerminal(() => {
|
|
611
|
-
onToggleRightPanel(
|
|
597
|
+
onToggleRightPanel("terminal");
|
|
612
598
|
});
|
|
613
599
|
return () => {
|
|
614
600
|
bridge.offMenuOpenTerminal?.();
|
|
@@ -620,7 +606,7 @@ export function AgentSidebar({
|
|
|
620
606
|
return (
|
|
621
607
|
<div className="flex h-full w-full flex-col border-r border-agent-border/60 bg-agent-muted/70 backdrop-blur-md">
|
|
622
608
|
{/* ───── Brand + actions ───── */}
|
|
623
|
-
<div className="flex h-
|
|
609
|
+
<div className="flex h-11 flex-shrink-0 items-center justify-between px-2.5">
|
|
624
610
|
<BrandLockup onClick={() => handleOpenNewChat()} />
|
|
625
611
|
<div className="flex items-center gap-1">
|
|
626
612
|
<button
|
|
@@ -643,11 +629,11 @@ export function AgentSidebar({
|
|
|
643
629
|
type="button"
|
|
644
630
|
onClick={() => handleOpenNewChat()}
|
|
645
631
|
className={[
|
|
646
|
-
|
|
632
|
+
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
647
633
|
onNewChatHome
|
|
648
|
-
?
|
|
649
|
-
:
|
|
650
|
-
].join(
|
|
634
|
+
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
635
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
636
|
+
].join(" ")}
|
|
651
637
|
title="新建对话"
|
|
652
638
|
data-testid="sidebar-new-chat"
|
|
653
639
|
>
|
|
@@ -656,13 +642,13 @@ export function AgentSidebar({
|
|
|
656
642
|
</button>
|
|
657
643
|
<button
|
|
658
644
|
type="button"
|
|
659
|
-
onClick={() => navigate(
|
|
645
|
+
onClick={() => navigate("/settings?section=agents")}
|
|
660
646
|
className={[
|
|
661
|
-
|
|
662
|
-
onSettingsPage && settingsSection ===
|
|
663
|
-
?
|
|
664
|
-
:
|
|
665
|
-
].join(
|
|
647
|
+
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
648
|
+
onSettingsPage && settingsSection === "agents"
|
|
649
|
+
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
650
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
651
|
+
].join(" ")}
|
|
666
652
|
title="智能体管理"
|
|
667
653
|
data-testid="sidebar-agent-settings"
|
|
668
654
|
>
|
|
@@ -671,13 +657,13 @@ export function AgentSidebar({
|
|
|
671
657
|
</button>
|
|
672
658
|
<button
|
|
673
659
|
type="button"
|
|
674
|
-
onClick={() => navigate(
|
|
660
|
+
onClick={() => navigate("/settings?section=skills")}
|
|
675
661
|
className={[
|
|
676
|
-
|
|
677
|
-
onSettingsPage && settingsSection ===
|
|
678
|
-
?
|
|
679
|
-
:
|
|
680
|
-
].join(
|
|
662
|
+
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
663
|
+
onSettingsPage && settingsSection === "skills"
|
|
664
|
+
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
665
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
666
|
+
].join(" ")}
|
|
681
667
|
title="Skill 设置"
|
|
682
668
|
data-testid="sidebar-skill-settings"
|
|
683
669
|
>
|
|
@@ -686,13 +672,13 @@ export function AgentSidebar({
|
|
|
686
672
|
</button>
|
|
687
673
|
<button
|
|
688
674
|
type="button"
|
|
689
|
-
onClick={() => navigate(
|
|
675
|
+
onClick={() => navigate("/settings?section=mcp")}
|
|
690
676
|
className={[
|
|
691
|
-
|
|
692
|
-
onSettingsPage && settingsSection ===
|
|
693
|
-
?
|
|
694
|
-
:
|
|
695
|
-
].join(
|
|
677
|
+
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
678
|
+
onSettingsPage && settingsSection === "mcp"
|
|
679
|
+
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
680
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
681
|
+
].join(" ")}
|
|
696
682
|
title="MCP 设置"
|
|
697
683
|
data-testid="sidebar-mcp-settings"
|
|
698
684
|
>
|
|
@@ -706,28 +692,22 @@ export function AgentSidebar({
|
|
|
706
692
|
onClick={() => setChatsExpanded((v) => !v)}
|
|
707
693
|
className="flex h-6 items-center rounded-full px-2.5 text-xs font-semibold tracking-wider text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
|
|
708
694
|
>
|
|
709
|
-
|
|
695
|
+
会话
|
|
696
|
+
<span className="ml-1">
|
|
710
697
|
{chatsExpanded ? (
|
|
711
698
|
<LuChevronUp className="h-3 w-3" />
|
|
712
699
|
) : (
|
|
713
700
|
<LuChevronDown className="h-3 w-3" />
|
|
714
701
|
)}
|
|
715
702
|
</span>
|
|
716
|
-
会话
|
|
717
|
-
{normalizedChats.length > 0 && (
|
|
718
|
-
<span className="ml-1.5 text-[10px] font-normal text-agent-muted-foreground/70">
|
|
719
|
-
· {normalizedChats.length}
|
|
720
|
-
</span>
|
|
721
|
-
)}
|
|
722
703
|
</button>
|
|
723
704
|
<div className="flex items-center gap-1">
|
|
724
|
-
{/* 新建项目:选文件夹 → 以文件夹名建项目,之后可在组头重命名 */}
|
|
725
705
|
{hasElectron && (
|
|
726
706
|
<button
|
|
727
707
|
type="button"
|
|
728
|
-
onClick={() =>
|
|
708
|
+
onClick={() => setCreateProjectOpen(true)}
|
|
729
709
|
className="flex h-6 w-6 items-center justify-center rounded-full text-agent-muted-foreground transition-colors duration-200 hover:bg-agent-foreground/5 hover:text-agent-foreground"
|
|
730
|
-
title="
|
|
710
|
+
title="新建项目"
|
|
731
711
|
aria-label="新建项目"
|
|
732
712
|
>
|
|
733
713
|
<LuFolderPlus className="h-3.5 w-3.5" />
|
|
@@ -768,10 +748,13 @@ export function AgentSidebar({
|
|
|
768
748
|
<input
|
|
769
749
|
autoFocus
|
|
770
750
|
value={renamingValue}
|
|
771
|
-
onChange={(event) =>
|
|
751
|
+
onChange={(event) =>
|
|
752
|
+
setRenamingValue(event.target.value)
|
|
753
|
+
}
|
|
772
754
|
onBlur={() => void handleRenameProject(project.id)}
|
|
773
755
|
onKeyDown={(event) => {
|
|
774
|
-
if (event.key ===
|
|
756
|
+
if (event.key === "Escape")
|
|
757
|
+
setRenamingProjectId(null);
|
|
775
758
|
}}
|
|
776
759
|
className="h-6 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-2 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
|
|
777
760
|
/>
|
|
@@ -824,7 +807,9 @@ export function AgentSidebar({
|
|
|
824
807
|
</button>
|
|
825
808
|
<button
|
|
826
809
|
type="button"
|
|
827
|
-
onClick={() =>
|
|
810
|
+
onClick={() =>
|
|
811
|
+
void handleChangeProjectFolder(project.id)
|
|
812
|
+
}
|
|
828
813
|
className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground hover:bg-agent-foreground/10 hover:text-agent-foreground"
|
|
829
814
|
title="更换绑定文件夹"
|
|
830
815
|
>
|
|
@@ -832,21 +817,23 @@ export function AgentSidebar({
|
|
|
832
817
|
</button>
|
|
833
818
|
<button
|
|
834
819
|
type="button"
|
|
835
|
-
onClick={() =>
|
|
820
|
+
onClick={() =>
|
|
821
|
+
void handleDeleteProject(project.id)
|
|
822
|
+
}
|
|
836
823
|
disabled={deletingProjectId === project.id}
|
|
837
824
|
className={`flex h-5 w-5 items-center justify-center rounded-full transition-colors ${
|
|
838
825
|
confirmDeleteProjectId === project.id
|
|
839
|
-
?
|
|
840
|
-
:
|
|
826
|
+
? "bg-agent-destructive/10 text-agent-destructive hover:bg-agent-destructive/20"
|
|
827
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/10 hover:text-agent-destructive"
|
|
841
828
|
}`}
|
|
842
829
|
title={
|
|
843
830
|
confirmDeleteProjectId === project.id
|
|
844
|
-
?
|
|
845
|
-
:
|
|
831
|
+
? "再次点击确认删除(会话会保留为无项目对话)"
|
|
832
|
+
: "删除项目(会话保留为无项目对话)"
|
|
846
833
|
}
|
|
847
834
|
>
|
|
848
835
|
<LuTrash2
|
|
849
|
-
className={`h-3 w-3 ${deletingProjectId === project.id ?
|
|
836
|
+
className={`h-3 w-3 ${deletingProjectId === project.id ? "animate-pulse" : ""}`}
|
|
850
837
|
/>
|
|
851
838
|
</button>
|
|
852
839
|
</div>
|
|
@@ -887,7 +874,7 @@ export function AgentSidebar({
|
|
|
887
874
|
加载更多...
|
|
888
875
|
</span>
|
|
889
876
|
) : hasMoreChats ? (
|
|
890
|
-
|
|
877
|
+
"继续下滑加载更多"
|
|
891
878
|
) : (
|
|
892
879
|
`共 ${normalizedChats.length} 个会话`
|
|
893
880
|
)}
|
|
@@ -911,20 +898,20 @@ export function AgentSidebar({
|
|
|
911
898
|
{chatSlots.length === 0 ? (
|
|
912
899
|
<button
|
|
913
900
|
type="button"
|
|
914
|
-
onClick={() => onToggleRightPanel(
|
|
901
|
+
onClick={() => onToggleRightPanel("terminal")}
|
|
915
902
|
className={[
|
|
916
|
-
|
|
917
|
-
rightPanel ===
|
|
918
|
-
?
|
|
919
|
-
:
|
|
920
|
-
].join(
|
|
921
|
-
title={`${rightPanel ===
|
|
903
|
+
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
904
|
+
rightPanel === "terminal"
|
|
905
|
+
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
906
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-foreground",
|
|
907
|
+
].join(" ")}
|
|
908
|
+
title={`${rightPanel === "terminal" ? "关闭" : "打开"}终端面板 (${isMac ? "⌘T" : "Ctrl+T"})`}
|
|
922
909
|
data-testid="sidebar-terminal"
|
|
923
910
|
>
|
|
924
911
|
<LuTerminal className="h-3.5 w-3.5" />
|
|
925
912
|
<span>终端</span>
|
|
926
913
|
<span className="ml-auto text-[10px] text-agent-muted-foreground/70">
|
|
927
|
-
{isMac ?
|
|
914
|
+
{isMac ? "⌘T" : "Ctrl+T"}
|
|
928
915
|
</span>
|
|
929
916
|
</button>
|
|
930
917
|
) : (
|
|
@@ -935,14 +922,14 @@ export function AgentSidebar({
|
|
|
935
922
|
>
|
|
936
923
|
<button
|
|
937
924
|
type="button"
|
|
938
|
-
onClick={() => onToggleRightPanel(
|
|
925
|
+
onClick={() => onToggleRightPanel("terminal")}
|
|
939
926
|
className={[
|
|
940
|
-
|
|
941
|
-
rightPanel ===
|
|
942
|
-
?
|
|
943
|
-
:
|
|
944
|
-
].join(
|
|
945
|
-
title={`${rightPanel ===
|
|
927
|
+
"flex h-7 flex-1 items-center justify-center gap-1.5 rounded-full text-xs font-medium transition-colors",
|
|
928
|
+
rightPanel === "terminal"
|
|
929
|
+
? "bg-agent-foreground/10 text-agent-foreground"
|
|
930
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
931
|
+
].join(" ")}
|
|
932
|
+
title={`${rightPanel === "terminal" ? "关闭" : "打开"}终端面板`}
|
|
946
933
|
data-testid="sidebar-terminal"
|
|
947
934
|
>
|
|
948
935
|
<LuTerminal className="h-3.5 w-3.5" />
|
|
@@ -954,12 +941,12 @@ export function AgentSidebar({
|
|
|
954
941
|
type="button"
|
|
955
942
|
onClick={() => onToggleRightPanel(slot.slotId)}
|
|
956
943
|
className={[
|
|
957
|
-
|
|
944
|
+
"flex h-7 flex-1 items-center justify-center gap-1.5 rounded-full text-xs font-medium transition-colors",
|
|
958
945
|
rightPanel === slot.slotId
|
|
959
|
-
?
|
|
960
|
-
:
|
|
961
|
-
].join(
|
|
962
|
-
title={`${rightPanel === slot.slotId ?
|
|
946
|
+
? "bg-agent-foreground/10 text-agent-foreground"
|
|
947
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
948
|
+
].join(" ")}
|
|
949
|
+
title={`${rightPanel === slot.slotId ? "关闭" : "打开"}${slot.title}`}
|
|
963
950
|
data-testid={`sidebar-slot-${slot.slotId}`}
|
|
964
951
|
>
|
|
965
952
|
<slot.Icon className="h-3.5 w-3.5" />
|
|
@@ -970,13 +957,13 @@ export function AgentSidebar({
|
|
|
970
957
|
)}
|
|
971
958
|
<button
|
|
972
959
|
type="button"
|
|
973
|
-
onClick={() => navigate(
|
|
960
|
+
onClick={() => navigate("/settings")}
|
|
974
961
|
className={[
|
|
975
|
-
|
|
962
|
+
"mt-0.5 flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
976
963
|
onGeneralSettings
|
|
977
|
-
?
|
|
978
|
-
:
|
|
979
|
-
].join(
|
|
964
|
+
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
965
|
+
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
966
|
+
].join(" ")}
|
|
980
967
|
title="设置"
|
|
981
968
|
data-testid="sidebar-llm-settings"
|
|
982
969
|
>
|
|
@@ -984,6 +971,12 @@ export function AgentSidebar({
|
|
|
984
971
|
<span>设置</span>
|
|
985
972
|
</button>
|
|
986
973
|
</div>
|
|
974
|
+
|
|
975
|
+
<CreateProjectModal
|
|
976
|
+
open={createProjectOpen}
|
|
977
|
+
onClose={() => setCreateProjectOpen(false)}
|
|
978
|
+
onCreate={handleCreateProject}
|
|
979
|
+
/>
|
|
987
980
|
</div>
|
|
988
981
|
);
|
|
989
982
|
}
|
|
@@ -30,7 +30,7 @@ export function BrandLockup({ className, onClick }: BrandLockupProps = {}) {
|
|
|
30
30
|
draggable={false}
|
|
31
31
|
/>
|
|
32
32
|
{title ? (
|
|
33
|
-
<span className="truncate text-
|
|
33
|
+
<span className="truncate text-sm font-semibold tracking-tight text-agent-foreground leading-none">
|
|
34
34
|
{title}
|
|
35
35
|
</span>
|
|
36
36
|
) : null}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 新建项目弹窗(Codex 式):项目是带名字的容器,不是「选中的那个文件夹」。
|
|
3
|
+
* 家目录由后端建在 Documents/<应用名>/<项目名>/;这里可选附加源文件夹。
|
|
4
|
+
*/
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
7
|
+
import { LuFolder, LuFolderPlus, LuX } from 'react-icons/lu';
|
|
8
|
+
import { BRAND_NAME } from '@/brand';
|
|
9
|
+
import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
|
|
10
|
+
|
|
11
|
+
export interface CreateProjectModalProps {
|
|
12
|
+
open: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onCreate: (input: { name: string; sourceFolders: string[] }) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CreateProjectModal({ open, onClose, onCreate }: CreateProjectModalProps) {
|
|
18
|
+
const [name, setName] = useState('');
|
|
19
|
+
const [sourceFolders, setSourceFolders] = useState<string[]>([]);
|
|
20
|
+
const [submitting, setSubmitting] = useState(false);
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!open) return;
|
|
25
|
+
setName('');
|
|
26
|
+
setSourceFolders([]);
|
|
27
|
+
setSubmitting(false);
|
|
28
|
+
setError(null);
|
|
29
|
+
const onKey = (event: KeyboardEvent) => {
|
|
30
|
+
if (event.key === 'Escape') onClose();
|
|
31
|
+
};
|
|
32
|
+
window.addEventListener('keydown', onKey);
|
|
33
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
34
|
+
}, [open, onClose]);
|
|
35
|
+
|
|
36
|
+
if (!open) return null;
|
|
37
|
+
if (typeof document === 'undefined') return null;
|
|
38
|
+
|
|
39
|
+
const trimmed = name.trim();
|
|
40
|
+
const homePreview = `~/Documents/${BRAND_NAME}/${trimmed || '项目名'}`;
|
|
41
|
+
|
|
42
|
+
const handleAddFolder = async () => {
|
|
43
|
+
if (!isElectron()) return;
|
|
44
|
+
const result = await getElectronBridge()?.local?.selectDirectory({
|
|
45
|
+
title: '添加源文件夹',
|
|
46
|
+
});
|
|
47
|
+
if (!result || result.canceled || result.filePaths.length === 0) return;
|
|
48
|
+
const next = result.filePaths[0];
|
|
49
|
+
setSourceFolders((prev) => (prev.includes(next) ? prev : [...prev, next]));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleSubmit = async () => {
|
|
53
|
+
if (!trimmed || submitting) return;
|
|
54
|
+
setSubmitting(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
try {
|
|
57
|
+
await onCreate({ name: trimmed, sourceFolders });
|
|
58
|
+
onClose();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
61
|
+
setSubmitting(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return createPortal(
|
|
66
|
+
<div
|
|
67
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
|
68
|
+
onMouseDown={(event) => {
|
|
69
|
+
if (event.target === event.currentTarget) onClose();
|
|
70
|
+
}}
|
|
71
|
+
role="dialog"
|
|
72
|
+
aria-modal="true"
|
|
73
|
+
aria-labelledby="create-project-title"
|
|
74
|
+
data-testid="create-project-dialog"
|
|
75
|
+
>
|
|
76
|
+
<div className="flex w-[440px] max-w-[92vw] flex-col overflow-hidden rounded-2xl border border-agent-border bg-agent-canvas shadow-2xl">
|
|
77
|
+
<div className="flex items-center justify-between px-5 pb-1 pt-4">
|
|
78
|
+
<h2 id="create-project-title" className="text-base font-semibold text-agent-foreground">
|
|
79
|
+
新建项目
|
|
80
|
+
</h2>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={onClose}
|
|
84
|
+
className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
|
|
85
|
+
aria-label="关闭"
|
|
86
|
+
>
|
|
87
|
+
<LuX className="h-4 w-4" />
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="space-y-4 px-5 py-3">
|
|
92
|
+
<label className="flex h-10 items-center gap-2 rounded-full border border-agent-border bg-agent-canvas px-3 focus-within:border-agent-foreground/40">
|
|
93
|
+
<LuFolder className="h-4 w-4 shrink-0 text-agent-muted-foreground" />
|
|
94
|
+
<input
|
|
95
|
+
type="text"
|
|
96
|
+
value={name}
|
|
97
|
+
onChange={(event) => setName(event.target.value)}
|
|
98
|
+
onKeyDown={(event) => {
|
|
99
|
+
if (event.key === 'Enter') void handleSubmit();
|
|
100
|
+
}}
|
|
101
|
+
placeholder="项目名称"
|
|
102
|
+
autoFocus
|
|
103
|
+
className="h-full min-w-0 flex-1 bg-transparent text-sm text-agent-foreground outline-none placeholder:text-agent-muted-foreground"
|
|
104
|
+
data-testid="create-project-name"
|
|
105
|
+
/>
|
|
106
|
+
</label>
|
|
107
|
+
<p className="px-1 text-[11px] text-agent-muted-foreground">
|
|
108
|
+
默认位置:{homePreview}
|
|
109
|
+
</p>
|
|
110
|
+
|
|
111
|
+
<div>
|
|
112
|
+
<div className="mb-2 text-sm text-agent-foreground">源文件夹</div>
|
|
113
|
+
{sourceFolders.length > 0 && (
|
|
114
|
+
<ul className="mb-2 space-y-1">
|
|
115
|
+
{sourceFolders.map((folder) => (
|
|
116
|
+
<li
|
|
117
|
+
key={folder}
|
|
118
|
+
className="flex items-center gap-2 rounded-lg bg-agent-muted/40 px-2.5 py-1.5 text-xs text-agent-foreground"
|
|
119
|
+
>
|
|
120
|
+
<LuFolder className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
121
|
+
<span className="min-w-0 flex-1 truncate font-mono" title={folder}>
|
|
122
|
+
{folder}
|
|
123
|
+
</span>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={() =>
|
|
127
|
+
setSourceFolders((prev) => prev.filter((item) => item !== folder))
|
|
128
|
+
}
|
|
129
|
+
className="rounded-full p-0.5 text-agent-muted-foreground hover:text-agent-foreground"
|
|
130
|
+
aria-label={`移除 ${folder}`}
|
|
131
|
+
>
|
|
132
|
+
<LuX className="h-3.5 w-3.5" />
|
|
133
|
+
</button>
|
|
134
|
+
</li>
|
|
135
|
+
))}
|
|
136
|
+
</ul>
|
|
137
|
+
)}
|
|
138
|
+
<div className="flex flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-agent-border px-4 py-8">
|
|
139
|
+
<span className="text-sm text-agent-muted-foreground">添加此电脑上的文件夹</span>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={() => void handleAddFolder()}
|
|
143
|
+
disabled={!isElectron()}
|
|
144
|
+
className="flex h-8 items-center gap-1.5 rounded-full border border-agent-border px-4 text-xs font-medium text-agent-foreground transition-colors hover:bg-agent-muted disabled:cursor-not-allowed disabled:opacity-50"
|
|
145
|
+
data-testid="create-project-add-folder"
|
|
146
|
+
>
|
|
147
|
+
<LuFolderPlus className="h-3.5 w-3.5" />
|
|
148
|
+
添加
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{error && (
|
|
154
|
+
<p className="text-xs text-agent-destructive" role="alert">
|
|
155
|
+
{error}
|
|
156
|
+
</p>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="flex items-center justify-end gap-2 px-5 pb-4 pt-1">
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={onClose}
|
|
164
|
+
className="h-9 rounded-full px-4 text-sm text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
|
|
165
|
+
>
|
|
166
|
+
取消
|
|
167
|
+
</button>
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={() => void handleSubmit()}
|
|
171
|
+
disabled={!trimmed || submitting}
|
|
172
|
+
className="h-9 rounded-full bg-agent-foreground px-4 text-sm font-medium text-agent-canvas transition-opacity disabled:opacity-40"
|
|
173
|
+
data-testid="create-project-submit"
|
|
174
|
+
>
|
|
175
|
+
{submitting ? '创建中…' : '创建项目'}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>,
|
|
180
|
+
document.body,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -7,9 +7,12 @@ import {
|
|
|
7
7
|
LuFolderPlus,
|
|
8
8
|
LuFolderX,
|
|
9
9
|
LuLoaderCircle,
|
|
10
|
+
LuPlus,
|
|
10
11
|
} from 'react-icons/lu';
|
|
12
|
+
import { CreateProjectModal } from '@/components/CreateProjectModal';
|
|
11
13
|
import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
|
|
12
14
|
import {
|
|
15
|
+
createProject,
|
|
13
16
|
updateChatProject,
|
|
14
17
|
updateProject,
|
|
15
18
|
type LocalProject,
|
|
@@ -93,14 +96,47 @@ function MenuShell({
|
|
|
93
96
|
);
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
function MenuSectionLabel({
|
|
99
|
+
function MenuSectionLabel({
|
|
100
|
+
children,
|
|
101
|
+
onAdd,
|
|
102
|
+
}: {
|
|
103
|
+
children: ReactNode;
|
|
104
|
+
onAdd?: () => void;
|
|
105
|
+
}) {
|
|
97
106
|
return (
|
|
98
|
-
<div className="
|
|
99
|
-
|
|
107
|
+
<div className="flex items-center justify-between gap-1 px-2 py-1">
|
|
108
|
+
<div className="text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
|
|
109
|
+
{children}
|
|
110
|
+
</div>
|
|
111
|
+
{onAdd && (
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={onAdd}
|
|
115
|
+
className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/10 hover:text-agent-foreground"
|
|
116
|
+
title="新建项目"
|
|
117
|
+
aria-label="新建项目"
|
|
118
|
+
>
|
|
119
|
+
<LuPlus className="h-3 w-3" />
|
|
120
|
+
</button>
|
|
121
|
+
)}
|
|
100
122
|
</div>
|
|
101
123
|
);
|
|
102
124
|
}
|
|
103
125
|
|
|
126
|
+
function CreateProjectMenuItem({ onClick }: { onClick: () => void }) {
|
|
127
|
+
return (
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
role="menuitem"
|
|
131
|
+
onClick={onClick}
|
|
132
|
+
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
|
|
133
|
+
>
|
|
134
|
+
<LuFolderPlus className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
135
|
+
新建项目
|
|
136
|
+
</button>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
104
140
|
function ProjectMenuRow({
|
|
105
141
|
project,
|
|
106
142
|
selected,
|
|
@@ -147,9 +183,15 @@ export function ChatProjectBadge({
|
|
|
147
183
|
onChatProjectChanged: () => void | Promise<void>;
|
|
148
184
|
}) {
|
|
149
185
|
const [open, setOpen] = useState(false);
|
|
186
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
150
187
|
const [busy, setBusy] = useState(false);
|
|
151
188
|
const [error, setError] = useState<string | null>(null);
|
|
152
189
|
|
|
190
|
+
const openCreate = () => {
|
|
191
|
+
setOpen(false);
|
|
192
|
+
setCreateOpen(true);
|
|
193
|
+
};
|
|
194
|
+
|
|
153
195
|
const otherProjects = project
|
|
154
196
|
? projects.filter((p) => p.id !== project.id)
|
|
155
197
|
: projects;
|
|
@@ -226,7 +268,7 @@ export function ChatProjectBadge({
|
|
|
226
268
|
</div>
|
|
227
269
|
</>
|
|
228
270
|
) : (
|
|
229
|
-
<MenuSectionLabel>关联到项目</MenuSectionLabel>
|
|
271
|
+
<MenuSectionLabel onAdd={openCreate}>关联到项目</MenuSectionLabel>
|
|
230
272
|
)}
|
|
231
273
|
|
|
232
274
|
{otherProjects.length > 0 && (
|
|
@@ -247,9 +289,12 @@ export function ChatProjectBadge({
|
|
|
247
289
|
</>
|
|
248
290
|
)}
|
|
249
291
|
{!project && projects.length === 0 && (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
292
|
+
<>
|
|
293
|
+
<div className="px-2 py-1.5 text-[11px] text-agent-muted-foreground/70">
|
|
294
|
+
还没有项目
|
|
295
|
+
</div>
|
|
296
|
+
<CreateProjectMenuItem onClick={openCreate} />
|
|
297
|
+
</>
|
|
253
298
|
)}
|
|
254
299
|
|
|
255
300
|
{project && (
|
|
@@ -283,6 +328,22 @@ export function ChatProjectBadge({
|
|
|
283
328
|
)}
|
|
284
329
|
</MenuShell>
|
|
285
330
|
)}
|
|
331
|
+
|
|
332
|
+
<CreateProjectModal
|
|
333
|
+
open={createOpen}
|
|
334
|
+
onClose={() => setCreateOpen(false)}
|
|
335
|
+
onCreate={async (input) => {
|
|
336
|
+
const res = await createProject({
|
|
337
|
+
name: input.name,
|
|
338
|
+
...(input.sourceFolders.length > 0 ? { sourceFolders: input.sourceFolders } : {}),
|
|
339
|
+
});
|
|
340
|
+
await onProjectsChanged();
|
|
341
|
+
if (res.project?.id) {
|
|
342
|
+
await updateChatProject(chatId, res.project.id);
|
|
343
|
+
await onChatProjectChanged();
|
|
344
|
+
}
|
|
345
|
+
}}
|
|
346
|
+
/>
|
|
286
347
|
</div>
|
|
287
348
|
);
|
|
288
349
|
}
|
|
@@ -293,15 +354,24 @@ export function ProjectPickerButton({
|
|
|
293
354
|
projects,
|
|
294
355
|
value,
|
|
295
356
|
onChange,
|
|
357
|
+
onProjectsChanged,
|
|
296
358
|
}: {
|
|
297
359
|
projects: LocalProject[];
|
|
298
360
|
/** 当前选中的 projectId;null = 无项目。 */
|
|
299
361
|
value: string | null;
|
|
300
362
|
onChange: (projectId: string | null) => void;
|
|
363
|
+
/** 新建项目后刷新列表。 */
|
|
364
|
+
onProjectsChanged: () => void | Promise<void>;
|
|
301
365
|
}) {
|
|
302
366
|
const [open, setOpen] = useState(false);
|
|
367
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
303
368
|
const selected = value ? (projects.find((p) => p.id === value) ?? null) : null;
|
|
304
369
|
|
|
370
|
+
const openCreate = () => {
|
|
371
|
+
setOpen(false);
|
|
372
|
+
setCreateOpen(true);
|
|
373
|
+
};
|
|
374
|
+
|
|
305
375
|
return (
|
|
306
376
|
<div className="relative">
|
|
307
377
|
<BadgeButton
|
|
@@ -319,7 +389,7 @@ export function ProjectPickerButton({
|
|
|
319
389
|
|
|
320
390
|
{open && (
|
|
321
391
|
<MenuShell onClose={() => setOpen(false)}>
|
|
322
|
-
<MenuSectionLabel>新对话所属项目</MenuSectionLabel>
|
|
392
|
+
<MenuSectionLabel onAdd={openCreate}>新对话所属项目</MenuSectionLabel>
|
|
323
393
|
<button
|
|
324
394
|
type="button"
|
|
325
395
|
role="menuitem"
|
|
@@ -349,12 +419,28 @@ export function ProjectPickerButton({
|
|
|
349
419
|
))}
|
|
350
420
|
</div>
|
|
351
421
|
{projects.length === 0 && (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
422
|
+
<>
|
|
423
|
+
<div className="px-2 py-1.5 text-[11px] text-agent-muted-foreground/70">
|
|
424
|
+
还没有项目
|
|
425
|
+
</div>
|
|
426
|
+
<CreateProjectMenuItem onClick={openCreate} />
|
|
427
|
+
</>
|
|
355
428
|
)}
|
|
356
429
|
</MenuShell>
|
|
357
430
|
)}
|
|
431
|
+
|
|
432
|
+
<CreateProjectModal
|
|
433
|
+
open={createOpen}
|
|
434
|
+
onClose={() => setCreateOpen(false)}
|
|
435
|
+
onCreate={async (input) => {
|
|
436
|
+
const res = await createProject({
|
|
437
|
+
name: input.name,
|
|
438
|
+
...(input.sourceFolders.length > 0 ? { sourceFolders: input.sourceFolders } : {}),
|
|
439
|
+
});
|
|
440
|
+
await onProjectsChanged();
|
|
441
|
+
if (res.project?.id) onChange(res.project.id);
|
|
442
|
+
}}
|
|
443
|
+
/>
|
|
358
444
|
</div>
|
|
359
445
|
);
|
|
360
446
|
}
|
|
@@ -450,16 +450,18 @@ function AgentLayoutContent() {
|
|
|
450
450
|
<div className="relative flex h-full w-full bg-agent-muted/30">
|
|
451
451
|
{sidebarCollapsed ? (
|
|
452
452
|
// 收起态:窄 rail 只放展开按钮。新建对话仍可用 Cmd+N / 菜单触发。
|
|
453
|
-
<div className="flex h-full w-10 flex-shrink-0 flex-col items-center border-r border-agent-border/60 bg-agent-muted/70
|
|
454
|
-
<
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
453
|
+
<div className="flex h-full w-10 flex-shrink-0 flex-col items-center border-r border-agent-border/60 bg-agent-muted/70 backdrop-blur-md">
|
|
454
|
+
<div className="flex h-11 w-full items-center justify-center">
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
457
|
+
onClick={toggleSidebarCollapsed}
|
|
458
|
+
className="flex h-7 w-7 items-center justify-center rounded-full text-agent-muted-foreground transition-colors duration-200 hover:bg-agent-foreground/5 hover:text-agent-foreground"
|
|
459
|
+
title="展开侧边栏"
|
|
460
|
+
aria-label="展开侧边栏"
|
|
461
|
+
>
|
|
462
|
+
<LuPanelLeftOpen className="h-3.5 w-3.5" />
|
|
463
|
+
</button>
|
|
464
|
+
</div>
|
|
463
465
|
</div>
|
|
464
466
|
) : (
|
|
465
467
|
<>
|
package/src/lib/local-api.ts
CHANGED
|
@@ -369,6 +369,8 @@ export interface LocalProject {
|
|
|
369
369
|
id: string;
|
|
370
370
|
name: string;
|
|
371
371
|
folderPath: string;
|
|
372
|
+
/** 附加源文件夹(只读)。缺省空。 */
|
|
373
|
+
sourceFolders?: string[];
|
|
372
374
|
/** W6-5: 信任后该项目目录里的规则文件才会注入模型上下文。缺省 false。 */
|
|
373
375
|
trusted?: boolean;
|
|
374
376
|
createdAt: string;
|
|
@@ -473,7 +475,11 @@ export async function listProjects() {
|
|
|
473
475
|
});
|
|
474
476
|
}
|
|
475
477
|
|
|
476
|
-
export async function createProject(input: {
|
|
478
|
+
export async function createProject(input: {
|
|
479
|
+
name: string;
|
|
480
|
+
folderPath?: string;
|
|
481
|
+
sourceFolders?: string[];
|
|
482
|
+
}) {
|
|
477
483
|
return bridge().localBackend.request<{ success: boolean; project: LocalProject }>({
|
|
478
484
|
method: 'POST',
|
|
479
485
|
path: '/api/v2/projects',
|
package/src/pages/AgentPage.tsx
CHANGED
|
@@ -1234,18 +1234,18 @@ function EmptyChatGate() {
|
|
|
1234
1234
|
useEffect(() => {
|
|
1235
1235
|
setSelectedProjectId(projectIdFromUrl);
|
|
1236
1236
|
}, [projectIdFromUrl]);
|
|
1237
|
-
|
|
1237
|
+
const fetchProjects = useCallback(async () => {
|
|
1238
1238
|
if (!isElectron()) return;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
return () => {
|
|
1246
|
-
cancelled = true;
|
|
1247
|
-
};
|
|
1239
|
+
try {
|
|
1240
|
+
const res = await listProjects();
|
|
1241
|
+
setProjects(res.projects ?? []);
|
|
1242
|
+
} catch {
|
|
1243
|
+
/* 列表失败保持旧数据 */
|
|
1244
|
+
}
|
|
1248
1245
|
}, []);
|
|
1246
|
+
useEffect(() => {
|
|
1247
|
+
void fetchProjects();
|
|
1248
|
+
}, [fetchProjects]);
|
|
1249
1249
|
|
|
1250
1250
|
useEffect(() => {
|
|
1251
1251
|
inputRef.current?.focusAtEnd();
|
|
@@ -1382,6 +1382,7 @@ function EmptyChatGate() {
|
|
|
1382
1382
|
projects={projects}
|
|
1383
1383
|
value={selectedProjectId}
|
|
1384
1384
|
onChange={setSelectedProjectId}
|
|
1385
|
+
onProjectsChanged={fetchProjects}
|
|
1385
1386
|
/>
|
|
1386
1387
|
) : undefined
|
|
1387
1388
|
}
|
|
Binary file
|