@steerable/agent-shell-web 0.6.47 → 0.6.49
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/src/components/AgentSidebar.tsx +423 -299
- package/src/components/ChatHeader.tsx +74 -4
- package/src/components/SidebarRelease.tsx +67 -0
- package/src/components/chat/ApprovalModal.tsx +8 -0
- package/src/components/chat/ApprovalPromptProvider.tsx +16 -1
- package/src/components/chat/AskUserPromptProvider.tsx +14 -0
- package/src/components/chat/AssistantMessage.tsx +1 -1
- package/src/components/chat/ChatInput.tsx +82 -31
- package/src/components/chat/SuggestedReplies.tsx +5 -2
- package/src/components/chat/TurnFilesCard.tsx +417 -69
- package/src/components/chat/turn-files.ts +273 -0
- package/src/components/settings/AppUpdateSettingsPanel.tsx +56 -0
- package/src/components/settings/PortableSettingsPanel.tsx +493 -0
- package/src/components/settings/PythonRunnerSettingsPanel.tsx +231 -0
- package/src/layouts/AgentLayout.tsx +23 -0
- package/src/lib/app-release.ts +66 -0
- package/src/lib/electron-bridge.ts +75 -0
- package/src/lib/host-paste.ts +85 -0
- package/src/lib/local-api.ts +18 -0
- package/src/lib/portable.ts +229 -0
- package/src/lib/tauri-bridge.ts +61 -1
- package/src/main.tsx +2 -0
- package/src/pages/AgentPage.tsx +17 -0
- package/src/pages/SettingsPage.tsx +19 -2
- package/src/styles/index.css +29 -0
- package/src/vite-env.d.ts +2 -0
- package/vite.base.js +1 -0
|
@@ -59,18 +59,18 @@
|
|
|
59
59
|
* │ ● 哲学家 │
|
|
60
60
|
* │ + 用「教练」新建对话 │ ← inline CTA when sel ≠ current
|
|
61
61
|
* ├──── divider ────────────────────┤
|
|
62
|
-
* │
|
|
62
|
+
* │ ✎ 新对话 │ ← 只打开落地页,有内容才落库
|
|
63
63
|
* │ ⬡ 智能体管理 │ ← /settings?section=agents(独立页)
|
|
64
64
|
* │ ⬡ Skill 设置 │ ← /settings?section=skills(独立页)
|
|
65
65
|
* │ 🔌 MCP 设置 │ ← /settings?section=mcp(独立页)
|
|
66
66
|
* │ 会话 v 📁+ │ ← 📁+ 打开新建项目弹窗
|
|
67
|
-
* │ v 📁 项目A
|
|
67
|
+
* │ v 📁 项目A (hover: ✎··)│ ← ✎ 新建对话;·· 菜单:重命名/换目录/访达/删
|
|
68
68
|
* │ ...(项目内对话) │
|
|
69
69
|
* │ 今天 │
|
|
70
70
|
* │ ...(无项目对话,按日期分组) │ ← 无项目排在项目分组之后
|
|
71
71
|
* ├─────────────────────────────────┤
|
|
72
72
|
* │ ▢_ 终端 ⌘T │
|
|
73
|
-
* │ ⚙ 设置
|
|
73
|
+
* │ ⚙ 设置 v0.2.2 │ ← /settings;右侧是当前版本,检查更新在设置页
|
|
74
74
|
* └─────────────────────────────────┘
|
|
75
75
|
*
|
|
76
76
|
* 项目模式:项目 = 名字 + 托管家目录(Documents/<应用名>/<项目名>/)+
|
|
@@ -82,14 +82,13 @@ import { createPortal } from "react-dom";
|
|
|
82
82
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
|
83
83
|
import {
|
|
84
84
|
LuChevronDown,
|
|
85
|
-
|
|
85
|
+
LuChevronRight,
|
|
86
86
|
LuMessageSquare,
|
|
87
87
|
LuCloudCog,
|
|
88
88
|
LuTrash2,
|
|
89
89
|
LuLoaderCircle,
|
|
90
|
-
LuTerminal,
|
|
91
90
|
LuSettings,
|
|
92
|
-
|
|
91
|
+
LuSquarePen,
|
|
93
92
|
LuPanelLeftClose,
|
|
94
93
|
LuBlocks,
|
|
95
94
|
LuBot,
|
|
@@ -99,7 +98,9 @@ import {
|
|
|
99
98
|
LuPencil,
|
|
100
99
|
LuPlug,
|
|
101
100
|
LuEllipsis,
|
|
101
|
+
LuCircleHelp,
|
|
102
102
|
} from "react-icons/lu";
|
|
103
|
+
import { RiPushpin2Fill, RiPushpin2Line } from "react-icons/ri";
|
|
103
104
|
import { parseChatTitle } from "@/lib/chat-title";
|
|
104
105
|
import { getDateGroupLabel, getDateGroupPriority } from "@/lib/date-groups";
|
|
105
106
|
import { getElectronBridge, isElectron } from "@/lib/electron-bridge";
|
|
@@ -109,41 +110,23 @@ import {
|
|
|
109
110
|
deleteProject,
|
|
110
111
|
listProjects,
|
|
111
112
|
openLocalPath,
|
|
113
|
+
setChatPinned,
|
|
112
114
|
updateProject,
|
|
113
115
|
type LocalChat,
|
|
114
116
|
type LocalChatAgent,
|
|
115
117
|
type LocalProject,
|
|
116
118
|
} from "@/lib/local-api";
|
|
119
|
+
import { usePendingAskUserChatIds } from "@/components/chat/AskUserPromptProvider";
|
|
120
|
+
import { usePendingApprovalChatIds } from "@/components/chat/ApprovalPromptProvider";
|
|
117
121
|
import type { UseChatsAndAgentsResult } from "@/hooks/useChatsAndAgents";
|
|
118
122
|
import type { RightPanelState } from "@/layouts/AgentLayout";
|
|
119
123
|
import { BrandLockup } from "@/components/BrandLockup";
|
|
124
|
+
import {
|
|
125
|
+
SidebarVersionLabel,
|
|
126
|
+
useAppRelease,
|
|
127
|
+
} from "@/components/SidebarRelease";
|
|
120
128
|
import { CreateProjectModal } from "@/components/CreateProjectModal";
|
|
121
129
|
|
|
122
|
-
const DEFAULT_DOT_COLOR = "#7c3aed";
|
|
123
|
-
|
|
124
|
-
function agentInitial(agent: LocalChatAgent | null): string {
|
|
125
|
-
const name = (agent?.name || "").trim();
|
|
126
|
-
return name ? name[0].toUpperCase() : "A";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function AgentDot({
|
|
130
|
-
agent,
|
|
131
|
-
size = 18,
|
|
132
|
-
}: {
|
|
133
|
-
agent: LocalChatAgent | null;
|
|
134
|
-
size?: number;
|
|
135
|
-
}) {
|
|
136
|
-
const color = agent?.color || DEFAULT_DOT_COLOR;
|
|
137
|
-
return (
|
|
138
|
-
<span
|
|
139
|
-
className="inline-flex flex-shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white shadow-sm"
|
|
140
|
-
style={{ width: size, height: size, backgroundColor: color }}
|
|
141
|
-
>
|
|
142
|
-
{agentInitial(agent)}
|
|
143
|
-
</span>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
130
|
function placeProjectMenu(anchor: HTMLElement): { top: number; left: number } {
|
|
148
131
|
const box = anchor.getBoundingClientRect();
|
|
149
132
|
const width = 224;
|
|
@@ -268,23 +251,24 @@ function ProjectOverflowMenu({
|
|
|
268
251
|
interface AgentSidebarProps {
|
|
269
252
|
data: UseChatsAndAgentsResult;
|
|
270
253
|
/** 右侧栏当前打开的面板:null / 'terminal' / 包槽位 id(互斥)。 */
|
|
271
|
-
rightPanel
|
|
254
|
+
rightPanel?: RightPanelState;
|
|
272
255
|
/** 切换右侧栏面板显隐——面板不是路由也不是独立窗口,只是布局里的一栏。 */
|
|
273
|
-
onToggleRightPanel
|
|
256
|
+
onToggleRightPanel?: (kind: string) => void;
|
|
274
257
|
/** 收起侧边栏(AgentLayout 换成窄 rail,展开按钮在 rail 上)。 */
|
|
275
258
|
onCollapse: () => void;
|
|
276
259
|
}
|
|
277
260
|
|
|
278
261
|
export function AgentSidebar({
|
|
279
262
|
data,
|
|
280
|
-
rightPanel,
|
|
281
|
-
onToggleRightPanel,
|
|
263
|
+
rightPanel: _rightPanel,
|
|
264
|
+
onToggleRightPanel: _onToggleRightPanel,
|
|
282
265
|
onCollapse,
|
|
283
266
|
}: AgentSidebarProps) {
|
|
284
267
|
const navigate = useNavigate();
|
|
285
268
|
const location = useLocation();
|
|
286
269
|
const { chatId: currentChatId } = useParams<{ chatId?: string }>();
|
|
287
270
|
const bridge = getElectronBridge();
|
|
271
|
+
const release = useAppRelease();
|
|
288
272
|
const onSettingsPage = location.pathname === "/settings";
|
|
289
273
|
// /settings?section=skills|mcp|agents|(缺省 = 综合设置)—— 各自高亮。
|
|
290
274
|
const settingsSection = useMemo(
|
|
@@ -300,9 +284,9 @@ export function AgentSidebar({
|
|
|
300
284
|
|
|
301
285
|
const {
|
|
302
286
|
chats,
|
|
303
|
-
agents,
|
|
304
287
|
isLoading: isChatLoading,
|
|
305
288
|
error,
|
|
289
|
+
refreshChats,
|
|
306
290
|
setSelectedAgentId,
|
|
307
291
|
deleteChat,
|
|
308
292
|
isLoadingMoreChats,
|
|
@@ -321,7 +305,43 @@ export function AgentSidebar({
|
|
|
321
305
|
null,
|
|
322
306
|
);
|
|
323
307
|
const [deletingChatId, setDeletingChatId] = useState<string | null>(null);
|
|
324
|
-
const [
|
|
308
|
+
const [pinningChatId, setPinningChatId] = useState<string | null>(null);
|
|
309
|
+
const [streamingChatIds, setStreamingChatIds] = useState<Set<string>>(
|
|
310
|
+
() => new Set(),
|
|
311
|
+
);
|
|
312
|
+
const pendingAskUserChatIds = usePendingAskUserChatIds();
|
|
313
|
+
const pendingApprovalChatIds = usePendingApprovalChatIds();
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
const handleStreamingChange = (event: Event) => {
|
|
317
|
+
const customEvent = event as CustomEvent<{
|
|
318
|
+
chatId: string;
|
|
319
|
+
isStreaming: boolean;
|
|
320
|
+
}>;
|
|
321
|
+
const { chatId, isStreaming } = customEvent.detail || {};
|
|
322
|
+
if (!chatId) return;
|
|
323
|
+
setStreamingChatIds((prev) => {
|
|
324
|
+
const next = new Set(prev);
|
|
325
|
+
if (isStreaming) {
|
|
326
|
+
next.add(chatId);
|
|
327
|
+
} else {
|
|
328
|
+
next.delete(chatId);
|
|
329
|
+
}
|
|
330
|
+
return next;
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
window.addEventListener("chat:streaming-change", handleStreamingChange);
|
|
334
|
+
return () => {
|
|
335
|
+
window.removeEventListener(
|
|
336
|
+
"chat:streaming-change",
|
|
337
|
+
handleStreamingChange,
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
}, []);
|
|
341
|
+
|
|
342
|
+
const [pinnedExpanded, setPinnedExpanded] = useState<boolean | null>(null);
|
|
343
|
+
const [projectsExpanded, setProjectsExpanded] = useState<boolean | null>(null);
|
|
344
|
+
const [recentsExpanded, setRecentsExpanded] = useState<boolean | null>(null);
|
|
325
345
|
const chatScrollRef = useRef<HTMLDivElement>(null);
|
|
326
346
|
|
|
327
347
|
// ───── 项目模式 ─────
|
|
@@ -537,6 +557,8 @@ export function AgentSidebar({
|
|
|
537
557
|
sortDate,
|
|
538
558
|
agentId: chat.agentId ?? null,
|
|
539
559
|
projectId: chat.projectId ?? null,
|
|
560
|
+
isStreaming: chat.isStreaming,
|
|
561
|
+
needsUserInput: chat.needsUserInput,
|
|
540
562
|
};
|
|
541
563
|
})
|
|
542
564
|
.sort((a, b) => {
|
|
@@ -574,7 +596,12 @@ export function AgentSidebar({
|
|
|
574
596
|
[projects, normalizedChats, showProjectsChrome],
|
|
575
597
|
);
|
|
576
598
|
|
|
577
|
-
const
|
|
599
|
+
const pinnedChats = useMemo(
|
|
600
|
+
() => normalizedChats.filter((c) => c.isPinned),
|
|
601
|
+
[normalizedChats],
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
const recentChatGroups = useMemo(() => {
|
|
578
605
|
const map = new Map<
|
|
579
606
|
string,
|
|
580
607
|
{
|
|
@@ -583,23 +610,26 @@ export function AgentSidebar({
|
|
|
583
610
|
items: typeof normalizedChats;
|
|
584
611
|
}
|
|
585
612
|
>();
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
});
|
|
613
|
+
noProjectChats
|
|
614
|
+
.filter((chat) => !chat.isPinned)
|
|
615
|
+
.forEach((chat) => {
|
|
616
|
+
const label = getDateGroupLabel(chat.sortDate);
|
|
617
|
+
if (!map.has(label)) {
|
|
618
|
+
map.set(label, {
|
|
619
|
+
label,
|
|
620
|
+
priority: getDateGroupPriority(label),
|
|
621
|
+
items: [],
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
map.get(label)!.items.push(chat);
|
|
625
|
+
});
|
|
600
626
|
return Array.from(map.values()).sort((a, b) => a.priority - b.priority);
|
|
601
627
|
}, [noProjectChats]);
|
|
602
628
|
|
|
629
|
+
const isPinnedExpanded = pinnedExpanded ?? pinnedChats.length > 0;
|
|
630
|
+
const isProjectsExpanded = projectsExpanded ?? true;
|
|
631
|
+
const isRecentsExpanded = recentsExpanded ?? true;
|
|
632
|
+
|
|
603
633
|
const handleDeleteChat = useCallback(
|
|
604
634
|
async (id: string) => {
|
|
605
635
|
if (deletingChatId === id) return;
|
|
@@ -624,12 +654,37 @@ export function AgentSidebar({
|
|
|
624
654
|
[confirmDeleteChatId, deleteChat, deletingChatId, currentChatId, navigate],
|
|
625
655
|
);
|
|
626
656
|
|
|
657
|
+
const handleTogglePinChat = useCallback(
|
|
658
|
+
async (id: string, nextPinned: boolean) => {
|
|
659
|
+
if (pinningChatId === id) return;
|
|
660
|
+
setPinningChatId(id);
|
|
661
|
+
try {
|
|
662
|
+
await setChatPinned(id, nextPinned);
|
|
663
|
+
await refreshChats();
|
|
664
|
+
} catch (err) {
|
|
665
|
+
console.error("切换会话置顶失败:", err);
|
|
666
|
+
} finally {
|
|
667
|
+
setPinningChatId(null);
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
[pinningChatId, refreshChats],
|
|
671
|
+
);
|
|
672
|
+
|
|
627
673
|
// 单条会话行——无项目日期分组和项目分组共用同一个渲染,避免两份 JSX。
|
|
628
674
|
const renderChatRow = (chat: (typeof normalizedChats)[number]) => {
|
|
629
675
|
const isCurrent = currentChatId === chat.id;
|
|
630
676
|
const isConfirmingDelete = confirmDeleteChatId === chat.id;
|
|
631
677
|
const isDeleting = deletingChatId === chat.id;
|
|
632
|
-
const
|
|
678
|
+
const isPinning = pinningChatId === chat.id;
|
|
679
|
+
const isStreaming = Boolean(
|
|
680
|
+
chat.isStreaming || streamingChatIds.has(chat.id),
|
|
681
|
+
);
|
|
682
|
+
const needsUserInput = Boolean(
|
|
683
|
+
chat.needsUserInput ||
|
|
684
|
+
pendingAskUserChatIds.has(chat.id) ||
|
|
685
|
+
pendingApprovalChatIds.has(chat.id),
|
|
686
|
+
);
|
|
687
|
+
|
|
633
688
|
return (
|
|
634
689
|
<div
|
|
635
690
|
key={chat.id}
|
|
@@ -653,61 +708,87 @@ export function AgentSidebar({
|
|
|
653
708
|
chat.isAutomation ? `[由自动化触发] ${chat.title}` : chat.title
|
|
654
709
|
}
|
|
655
710
|
>
|
|
656
|
-
{
|
|
657
|
-
<span
|
|
658
|
-
|
|
711
|
+
{needsUserInput ? (
|
|
712
|
+
<span
|
|
713
|
+
className="flex shrink-0 items-center justify-center text-amber-500 dark:text-amber-400"
|
|
714
|
+
title="有问题需要输入"
|
|
715
|
+
aria-label="等待用户输入"
|
|
716
|
+
>
|
|
717
|
+
<LuCircleHelp className="h-3.5 w-3.5 animate-pulse" />
|
|
659
718
|
</span>
|
|
660
|
-
)
|
|
719
|
+
) : isStreaming ? (
|
|
720
|
+
<span
|
|
721
|
+
className="flex shrink-0 items-center justify-center text-agent-foreground/70"
|
|
722
|
+
title="正在对话中"
|
|
723
|
+
aria-label="正在生成"
|
|
724
|
+
>
|
|
725
|
+
<LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
|
726
|
+
</span>
|
|
727
|
+
) : null}
|
|
661
728
|
{chat.isAutomation && (
|
|
662
729
|
<LuCloudCog
|
|
663
730
|
className="h-3 w-3 shrink-0 text-agent-muted-foreground"
|
|
664
731
|
aria-label="由自动化触发"
|
|
665
732
|
/>
|
|
666
733
|
)}
|
|
667
|
-
<AgentDot agent={chatAgent} size={16} />
|
|
668
734
|
<span className="min-w-0 truncate leading-none">{chat.title}</span>
|
|
669
735
|
</button>
|
|
670
|
-
{/*
|
|
671
|
-
|
|
672
|
-
own background (canvas when selected, muted when
|
|
673
|
-
hovered). */}
|
|
736
|
+
{/* Semi-transparent gradient container for action buttons:
|
|
737
|
+
fades long text smoothly to the left so buttons don't collide with text. */}
|
|
674
738
|
<div
|
|
675
|
-
aria-hidden="true"
|
|
676
|
-
className={[
|
|
677
|
-
"pointer-events-none absolute inset-y-0 right-0 w-12 rounded-r-full transition-opacity duration-200",
|
|
678
|
-
isConfirmingDelete
|
|
679
|
-
? isCurrent
|
|
680
|
-
? "bg-gradient-to-l from-agent-canvas via-agent-canvas/95 to-transparent opacity-100"
|
|
681
|
-
: "bg-gradient-to-l from-agent-muted via-agent-muted/95 to-transparent opacity-100"
|
|
682
|
-
: isCurrent
|
|
683
|
-
? "bg-gradient-to-l from-agent-canvas via-agent-canvas/95 to-transparent opacity-0 group-hover/item:opacity-100"
|
|
684
|
-
: "bg-gradient-to-l from-agent-muted via-agent-muted/95 to-transparent opacity-0 group-hover/item:opacity-100",
|
|
685
|
-
].join(" ")}
|
|
686
|
-
/>
|
|
687
|
-
<button
|
|
688
|
-
type="button"
|
|
689
|
-
onClick={(event) => {
|
|
690
|
-
event.stopPropagation();
|
|
691
|
-
void handleDeleteChat(chat.id);
|
|
692
|
-
}}
|
|
693
|
-
disabled={isDeleting}
|
|
694
739
|
className={[
|
|
695
|
-
"absolute
|
|
740
|
+
"absolute inset-y-0 right-0 flex items-center justify-end gap-0.5 pr-1 pl-8 rounded-r-full transition-all duration-200",
|
|
741
|
+
"bg-gradient-to-l from-agent-muted/95 via-agent-muted/80 to-transparent",
|
|
696
742
|
isConfirmingDelete
|
|
697
|
-
? "
|
|
698
|
-
: "
|
|
699
|
-
"disabled:cursor-not-allowed disabled:opacity-100",
|
|
743
|
+
? "opacity-100 pointer-events-auto"
|
|
744
|
+
: "opacity-0 group-hover/item:opacity-100 focus-within:opacity-100 pointer-events-none group-hover/item:pointer-events-auto focus-within:pointer-events-auto",
|
|
700
745
|
].join(" ")}
|
|
701
|
-
title={isConfirmingDelete ? "再次点击确认删除" : "删除会话"}
|
|
702
|
-
aria-label={isConfirmingDelete ? "再次点击确认删除" : "删除会话"}
|
|
703
|
-
data-testid="sidebar-chat-delete"
|
|
704
746
|
>
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
"
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
747
|
+
{!isConfirmingDelete && (
|
|
748
|
+
<button
|
|
749
|
+
type="button"
|
|
750
|
+
onClick={(event) => {
|
|
751
|
+
event.stopPropagation();
|
|
752
|
+
void handleTogglePinChat(chat.id, !chat.isPinned);
|
|
753
|
+
}}
|
|
754
|
+
disabled={isPinning}
|
|
755
|
+
className="flex h-6 w-6 items-center justify-center rounded-full text-agent-foreground/70 transition-all duration-200 hover:bg-agent-foreground/10 hover:text-agent-foreground disabled:cursor-not-allowed"
|
|
756
|
+
title={chat.isPinned ? "取消置顶" : "置顶会话"}
|
|
757
|
+
aria-label={chat.isPinned ? "取消置顶" : "置顶会话"}
|
|
758
|
+
data-testid="sidebar-chat-pin"
|
|
759
|
+
>
|
|
760
|
+
{chat.isPinned ? (
|
|
761
|
+
<RiPushpin2Fill className="h-3.5 w-3.5 text-agent-foreground" />
|
|
762
|
+
) : (
|
|
763
|
+
<RiPushpin2Line className="h-3.5 w-3.5 text-agent-foreground/75 hover:text-agent-foreground" />
|
|
764
|
+
)}
|
|
765
|
+
</button>
|
|
766
|
+
)}
|
|
767
|
+
<button
|
|
768
|
+
type="button"
|
|
769
|
+
onClick={(event) => {
|
|
770
|
+
event.stopPropagation();
|
|
771
|
+
void handleDeleteChat(chat.id);
|
|
772
|
+
}}
|
|
773
|
+
disabled={isDeleting}
|
|
774
|
+
className={[
|
|
775
|
+
"flex h-6 w-6 items-center justify-center rounded-full transition-all duration-200",
|
|
776
|
+
isConfirmingDelete
|
|
777
|
+
? "bg-agent-destructive/10 text-agent-destructive hover:bg-agent-destructive/20"
|
|
778
|
+
: "text-agent-foreground/70 hover:bg-agent-foreground/10 hover:text-agent-destructive",
|
|
779
|
+
"disabled:cursor-not-allowed disabled:opacity-100",
|
|
780
|
+
].join(" ")}
|
|
781
|
+
title={isConfirmingDelete ? "再次点击确认删除" : "删除会话"}
|
|
782
|
+
aria-label={isConfirmingDelete ? "再次点击确认删除" : "删除会话"}
|
|
783
|
+
data-testid="sidebar-chat-delete"
|
|
784
|
+
>
|
|
785
|
+
<LuTrash2
|
|
786
|
+
className={["h-3.5 w-3.5", isDeleting ? "animate-pulse" : ""].join(
|
|
787
|
+
" ",
|
|
788
|
+
)}
|
|
789
|
+
/>
|
|
790
|
+
</button>
|
|
791
|
+
</div>
|
|
711
792
|
</div>
|
|
712
793
|
);
|
|
713
794
|
};
|
|
@@ -729,7 +810,7 @@ export function AgentSidebar({
|
|
|
729
810
|
// Throttled by `isLoadingMoreChats` inside the hook.
|
|
730
811
|
useEffect(() => {
|
|
731
812
|
const container = chatScrollRef.current;
|
|
732
|
-
if (!container
|
|
813
|
+
if (!container) return;
|
|
733
814
|
|
|
734
815
|
const maybeLoadMore = () => {
|
|
735
816
|
if (!hasMoreChats || isLoadingMoreChats) return;
|
|
@@ -748,26 +829,10 @@ export function AgentSidebar({
|
|
|
748
829
|
hasMoreChats,
|
|
749
830
|
isLoadingMoreChats,
|
|
750
831
|
loadMoreChats,
|
|
751
|
-
|
|
832
|
+
isRecentsExpanded,
|
|
752
833
|
normalizedChats.length,
|
|
753
834
|
]);
|
|
754
835
|
|
|
755
|
-
// Cmd+T from the app menu — wired in src/main.ts:createMenu (sends
|
|
756
|
-
// `menu:open-terminal`). Same pattern as `menu:new-chat` above. The
|
|
757
|
-
// terminal is a toggleable panel beside the chat, so this just flips
|
|
758
|
-
// the layout state owned by AgentLayout.
|
|
759
|
-
const showTerminalChrome = hostToolChrome("terminal");
|
|
760
|
-
|
|
761
|
-
useEffect(() => {
|
|
762
|
-
if (!showTerminalChrome || !bridge?.onMenuOpenTerminal) return;
|
|
763
|
-
bridge.onMenuOpenTerminal(() => {
|
|
764
|
-
onToggleRightPanel("terminal");
|
|
765
|
-
});
|
|
766
|
-
return () => {
|
|
767
|
-
bridge.offMenuOpenTerminal?.();
|
|
768
|
-
};
|
|
769
|
-
}, [bridge, onToggleRightPanel, showTerminalChrome]);
|
|
770
|
-
|
|
771
836
|
const hasElectron = isElectron();
|
|
772
837
|
|
|
773
838
|
return (
|
|
@@ -804,7 +869,7 @@ export function AgentSidebar({
|
|
|
804
869
|
title="新建对话"
|
|
805
870
|
data-testid="sidebar-new-chat"
|
|
806
871
|
>
|
|
807
|
-
<
|
|
872
|
+
<LuSquarePen className="h-3.5 w-3.5" />
|
|
808
873
|
<span>新对话</span>
|
|
809
874
|
</button>
|
|
810
875
|
{settingsChrome("agents") && (
|
|
@@ -859,184 +924,257 @@ export function AgentSidebar({
|
|
|
859
924
|
</button>
|
|
860
925
|
)}
|
|
861
926
|
</div>
|
|
862
|
-
<div className="flex flex-shrink-0 items-center justify-between py-1 pl-2.5 pr-2.5">
|
|
863
|
-
<button
|
|
864
|
-
type="button"
|
|
865
|
-
onClick={() => setChatsExpanded((v) => !v)}
|
|
866
|
-
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"
|
|
867
|
-
>
|
|
868
|
-
会话
|
|
869
|
-
<span className="ml-1">
|
|
870
|
-
{chatsExpanded ? (
|
|
871
|
-
<LuChevronUp className="h-3 w-3" />
|
|
872
|
-
) : (
|
|
873
|
-
<LuChevronDown className="h-3 w-3" />
|
|
874
|
-
)}
|
|
875
|
-
</span>
|
|
876
|
-
</button>
|
|
877
|
-
<div className="flex items-center gap-1">
|
|
878
|
-
{hasElectron && showProjectsChrome && (
|
|
879
|
-
<button
|
|
880
|
-
type="button"
|
|
881
|
-
onClick={() => setCreateProjectOpen(true)}
|
|
882
|
-
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"
|
|
883
|
-
title="新建项目"
|
|
884
|
-
aria-label="新建项目"
|
|
885
|
-
>
|
|
886
|
-
<LuFolderPlus className="h-3.5 w-3.5" />
|
|
887
|
-
</button>
|
|
888
|
-
)}
|
|
889
|
-
</div>
|
|
890
|
-
</div>
|
|
891
927
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
<
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
928
|
+
<div
|
|
929
|
+
ref={chatScrollRef}
|
|
930
|
+
className="flex-1 overflow-y-auto px-2.5 pb-1"
|
|
931
|
+
>
|
|
932
|
+
{isChatLoading && chats.length === 0 && projects.length === 0 ? (
|
|
933
|
+
<div className="flex items-center justify-center py-4 text-xs text-agent-muted-foreground">
|
|
934
|
+
<LuLoaderCircle className="mr-1.5 h-3 w-3 animate-spin" />
|
|
935
|
+
加载中...
|
|
936
|
+
</div>
|
|
937
|
+
) : (
|
|
938
|
+
<>
|
|
939
|
+
{/* 1. 置顶 (Pinned):没有置顶会话时整组不出现 */}
|
|
940
|
+
{pinnedChats.length > 0 && (
|
|
941
|
+
<div className="mb-1.5">
|
|
942
|
+
<div className="group/section flex h-7 items-center justify-between rounded-agent-md px-1.5 text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground">
|
|
943
|
+
<button
|
|
944
|
+
type="button"
|
|
945
|
+
onClick={() => setPinnedExpanded(!isPinnedExpanded)}
|
|
946
|
+
className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
|
|
947
|
+
>
|
|
948
|
+
<span>置顶</span>
|
|
949
|
+
{isPinnedExpanded ? (
|
|
950
|
+
<LuChevronDown className="h-3 w-3 shrink-0 opacity-70" />
|
|
951
|
+
) : (
|
|
952
|
+
<LuChevronRight className="h-3 w-3 shrink-0 opacity-70" />
|
|
953
|
+
)}
|
|
954
|
+
</button>
|
|
955
|
+
</div>
|
|
956
|
+
{isPinnedExpanded && (
|
|
957
|
+
<div className="space-y-0.5">
|
|
958
|
+
{pinnedChats.map((chat) => renderChatRow(chat))}
|
|
959
|
+
</div>
|
|
960
|
+
)}
|
|
906
961
|
</div>
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
<div
|
|
913
|
-
<
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
962
|
+
)}
|
|
963
|
+
|
|
964
|
+
{/* 2. 项目 (Projects) */}
|
|
965
|
+
{showProjectsChrome && (
|
|
966
|
+
<div className="mb-1.5">
|
|
967
|
+
<div className="group/section flex h-7 items-center justify-between rounded-agent-md px-1.5 text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground">
|
|
968
|
+
<button
|
|
969
|
+
type="button"
|
|
970
|
+
onClick={() => setProjectsExpanded(!isProjectsExpanded)}
|
|
971
|
+
className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
|
|
972
|
+
>
|
|
973
|
+
<span>项目</span>
|
|
974
|
+
{isProjectsExpanded ? (
|
|
975
|
+
<LuChevronDown className="h-3 w-3 shrink-0 opacity-70" />
|
|
976
|
+
) : (
|
|
977
|
+
<LuChevronRight className="h-3 w-3 shrink-0 opacity-70" />
|
|
978
|
+
)}
|
|
979
|
+
</button>
|
|
980
|
+
{hasElectron && (
|
|
981
|
+
<button
|
|
982
|
+
type="button"
|
|
983
|
+
onClick={(e) => {
|
|
984
|
+
e.stopPropagation();
|
|
985
|
+
setCreateProjectOpen(true);
|
|
986
|
+
}}
|
|
987
|
+
className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground opacity-0 transition-opacity duration-200 hover:bg-agent-foreground/10 hover:text-agent-foreground group-hover/section:opacity-100 focus:opacity-100"
|
|
988
|
+
title="新建项目"
|
|
989
|
+
aria-label="新建项目"
|
|
990
|
+
>
|
|
991
|
+
<LuFolderPlus className="h-3.5 w-3.5" />
|
|
992
|
+
</button>
|
|
993
|
+
)}
|
|
994
|
+
</div>
|
|
995
|
+
{isProjectsExpanded && (
|
|
996
|
+
<div className="space-y-0.5">
|
|
997
|
+
{projectGroups.length === 0 ? (
|
|
998
|
+
<div className="px-2.5 py-1 text-[11px] text-agent-muted-foreground/60">
|
|
999
|
+
暂无项目
|
|
1000
|
+
</div>
|
|
936
1001
|
) : (
|
|
937
|
-
|
|
938
|
-
<
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1002
|
+
projectGroups.map(({ project, items }) => (
|
|
1003
|
+
<div key={project.id} className="mb-1">
|
|
1004
|
+
<div className="group/proj relative">
|
|
1005
|
+
{renamingProjectId === project.id ? (
|
|
1006
|
+
<form
|
|
1007
|
+
className="flex items-center px-2.5 pb-1 pt-1.5"
|
|
1008
|
+
onSubmit={(event) => {
|
|
1009
|
+
event.preventDefault();
|
|
1010
|
+
void handleRenameProject(project.id);
|
|
1011
|
+
}}
|
|
1012
|
+
>
|
|
1013
|
+
<input
|
|
1014
|
+
autoFocus
|
|
1015
|
+
value={renamingValue}
|
|
1016
|
+
onChange={(event) =>
|
|
1017
|
+
setRenamingValue(event.target.value)
|
|
1018
|
+
}
|
|
1019
|
+
onBlur={() => void handleRenameProject(project.id)}
|
|
1020
|
+
onKeyDown={(event) => {
|
|
1021
|
+
if (event.key === "Escape")
|
|
1022
|
+
setRenamingProjectId(null);
|
|
1023
|
+
}}
|
|
1024
|
+
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"
|
|
1025
|
+
/>
|
|
1026
|
+
</form>
|
|
951
1027
|
) : (
|
|
952
|
-
|
|
1028
|
+
<>
|
|
1029
|
+
<button
|
|
1030
|
+
type="button"
|
|
1031
|
+
onClick={() => toggleProjectCollapsed(project.id)}
|
|
1032
|
+
className="flex w-full min-w-0 items-center px-2 pb-0.5 pt-1 text-[11px] font-medium text-agent-muted-foreground/90 transition-colors hover:text-agent-foreground"
|
|
1033
|
+
title={`${project.name}\n${project.folderPath}`}
|
|
1034
|
+
>
|
|
1035
|
+
<LuFolder className="mr-1 h-3.5 w-3.5 shrink-0" />
|
|
1036
|
+
<span className="min-w-0 truncate normal-case">
|
|
1037
|
+
{project.name}
|
|
1038
|
+
</span>
|
|
1039
|
+
<span className="ml-1 shrink-0">
|
|
1040
|
+
{collapsedProjectIds.has(project.id) ? (
|
|
1041
|
+
<LuChevronRight className="h-3 w-3" />
|
|
1042
|
+
) : (
|
|
1043
|
+
<LuChevronDown className="h-3 w-3" />
|
|
1044
|
+
)}
|
|
1045
|
+
</span>
|
|
1046
|
+
</button>
|
|
1047
|
+
<div
|
|
1048
|
+
className={`absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 transition-opacity ${
|
|
1049
|
+
projectMenu?.id === project.id
|
|
1050
|
+
? "opacity-100"
|
|
1051
|
+
: "opacity-0 group-hover/proj:opacity-100"
|
|
1052
|
+
}`}
|
|
1053
|
+
>
|
|
1054
|
+
<button
|
|
1055
|
+
type="button"
|
|
1056
|
+
onClick={() => handleOpenNewChat(project.id)}
|
|
1057
|
+
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"
|
|
1058
|
+
title="在此项目下新建对话"
|
|
1059
|
+
>
|
|
1060
|
+
<LuSquarePen className="h-3 w-3" />
|
|
1061
|
+
</button>
|
|
1062
|
+
<button
|
|
1063
|
+
type="button"
|
|
1064
|
+
aria-label="项目菜单"
|
|
1065
|
+
aria-expanded={projectMenu?.id === project.id}
|
|
1066
|
+
onClick={(event) => {
|
|
1067
|
+
const button = event.currentTarget;
|
|
1068
|
+
setConfirmDeleteProjectId(null);
|
|
1069
|
+
setProjectMenu((current) =>
|
|
1070
|
+
current?.id === project.id
|
|
1071
|
+
? null
|
|
1072
|
+
: { id: project.id, anchor: button },
|
|
1073
|
+
);
|
|
1074
|
+
}}
|
|
1075
|
+
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"
|
|
1076
|
+
title="项目菜单"
|
|
1077
|
+
>
|
|
1078
|
+
<LuEllipsis className="h-3 w-3" />
|
|
1079
|
+
</button>
|
|
1080
|
+
</div>
|
|
1081
|
+
</>
|
|
953
1082
|
)}
|
|
954
|
-
</
|
|
955
|
-
{
|
|
956
|
-
<
|
|
957
|
-
|
|
958
|
-
|
|
1083
|
+
</div>
|
|
1084
|
+
{!collapsedProjectIds.has(project.id) && (
|
|
1085
|
+
<div className="space-y-0.5 pl-2">
|
|
1086
|
+
{items.length === 0 ? (
|
|
1087
|
+
<div className="px-2.5 py-0.5 text-[11px] text-agent-muted-foreground/60">
|
|
1088
|
+
暂无会话 — hover 项目名点 + 新建
|
|
1089
|
+
</div>
|
|
1090
|
+
) : (
|
|
1091
|
+
items.map((chat) => renderChatRow(chat))
|
|
1092
|
+
)}
|
|
1093
|
+
</div>
|
|
959
1094
|
)}
|
|
960
|
-
</button>
|
|
961
|
-
<div
|
|
962
|
-
className={`absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 transition-opacity ${
|
|
963
|
-
projectMenu?.id === project.id
|
|
964
|
-
? "opacity-100"
|
|
965
|
-
: "opacity-0 group-hover/proj:opacity-100"
|
|
966
|
-
}`}
|
|
967
|
-
>
|
|
968
|
-
<button
|
|
969
|
-
type="button"
|
|
970
|
-
onClick={() => handleOpenNewChat(project.id)}
|
|
971
|
-
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"
|
|
972
|
-
title="在此项目下新建对话"
|
|
973
|
-
>
|
|
974
|
-
<LuPlus className="h-3 w-3" />
|
|
975
|
-
</button>
|
|
976
|
-
<button
|
|
977
|
-
type="button"
|
|
978
|
-
aria-label="项目菜单"
|
|
979
|
-
aria-expanded={projectMenu?.id === project.id}
|
|
980
|
-
onClick={(event) => {
|
|
981
|
-
const button = event.currentTarget;
|
|
982
|
-
setConfirmDeleteProjectId(null);
|
|
983
|
-
setProjectMenu((current) =>
|
|
984
|
-
current?.id === project.id
|
|
985
|
-
? null
|
|
986
|
-
: { id: project.id, anchor: button },
|
|
987
|
-
);
|
|
988
|
-
}}
|
|
989
|
-
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"
|
|
990
|
-
title="项目菜单"
|
|
991
|
-
>
|
|
992
|
-
<LuEllipsis className="h-3 w-3" />
|
|
993
|
-
</button>
|
|
994
1095
|
</div>
|
|
995
|
-
|
|
1096
|
+
))
|
|
996
1097
|
)}
|
|
997
1098
|
</div>
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1099
|
+
)}
|
|
1100
|
+
</div>
|
|
1101
|
+
)}
|
|
1102
|
+
|
|
1103
|
+
{/* 3. 最近 (Recents) */}
|
|
1104
|
+
<div className="mb-1.5">
|
|
1105
|
+
<div className="group/section flex h-7 items-center justify-between rounded-agent-md px-1.5 text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground">
|
|
1106
|
+
<button
|
|
1107
|
+
type="button"
|
|
1108
|
+
onClick={() => setRecentsExpanded(!isRecentsExpanded)}
|
|
1109
|
+
className="flex flex-1 min-w-0 items-center gap-1 text-left font-medium text-agent-muted-foreground hover:text-agent-foreground"
|
|
1110
|
+
>
|
|
1111
|
+
<span>最近</span>
|
|
1112
|
+
{isRecentsExpanded ? (
|
|
1113
|
+
<LuChevronDown className="h-3 w-3 shrink-0 opacity-70" />
|
|
1114
|
+
) : (
|
|
1115
|
+
<LuChevronRight className="h-3 w-3 shrink-0 opacity-70" />
|
|
1008
1116
|
)}
|
|
1117
|
+
</button>
|
|
1118
|
+
<div className="flex items-center gap-0.5 opacity-0 transition-opacity duration-200 group-hover/section:opacity-100 focus-within:opacity-100">
|
|
1119
|
+
<button
|
|
1120
|
+
type="button"
|
|
1121
|
+
onClick={(e) => {
|
|
1122
|
+
e.stopPropagation();
|
|
1123
|
+
handleOpenNewChat();
|
|
1124
|
+
}}
|
|
1125
|
+
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"
|
|
1126
|
+
title="新对话"
|
|
1127
|
+
aria-label="新对话"
|
|
1128
|
+
>
|
|
1129
|
+
<LuSquarePen className="h-3 w-3" />
|
|
1130
|
+
</button>
|
|
1009
1131
|
</div>
|
|
1010
|
-
|
|
1011
|
-
{
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1132
|
+
</div>
|
|
1133
|
+
{isRecentsExpanded && (
|
|
1134
|
+
<div className="space-y-0.5">
|
|
1135
|
+
{recentChatGroups.length === 0 ? (
|
|
1136
|
+
chats.length === 0 && projects.length === 0 ? (
|
|
1137
|
+
<div className="flex flex-col items-center gap-1.5 py-4 text-xs text-agent-muted-foreground">
|
|
1138
|
+
<LuMessageSquare className="h-4 w-4 text-agent-muted-foreground/60" />
|
|
1139
|
+
暂无会话
|
|
1140
|
+
</div>
|
|
1141
|
+
) : (
|
|
1142
|
+
<div className="px-2.5 py-1 text-[11px] text-agent-muted-foreground/60">
|
|
1143
|
+
暂无会话
|
|
1144
|
+
</div>
|
|
1145
|
+
)
|
|
1146
|
+
) : (
|
|
1147
|
+
recentChatGroups.map((group) => (
|
|
1148
|
+
<div key={group.label} className="mb-1">
|
|
1149
|
+
<div className="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-agent-muted-foreground/80">
|
|
1150
|
+
{group.label}
|
|
1151
|
+
</div>
|
|
1152
|
+
<div className="space-y-0.5">
|
|
1153
|
+
{group.items.map((chat) => renderChatRow(chat))}
|
|
1154
|
+
</div>
|
|
1155
|
+
</div>
|
|
1156
|
+
))
|
|
1157
|
+
)}
|
|
1020
1158
|
</div>
|
|
1021
|
-
))}
|
|
1022
|
-
</>
|
|
1023
|
-
)}
|
|
1024
|
-
{(chatGroups.length > 0 || projectGroups.length > 0) && (
|
|
1025
|
-
<div className="px-2 py-2 text-center text-[11px] text-agent-muted-foreground">
|
|
1026
|
-
{isLoadingMoreChats ? (
|
|
1027
|
-
<span className="inline-flex items-center gap-1">
|
|
1028
|
-
<LuLoaderCircle className="h-3 w-3 animate-spin" />
|
|
1029
|
-
加载更多...
|
|
1030
|
-
</span>
|
|
1031
|
-
) : hasMoreChats ? (
|
|
1032
|
-
"继续下滑加载更多"
|
|
1033
|
-
) : (
|
|
1034
|
-
`共 ${normalizedChats.length} 个会话`
|
|
1035
1159
|
)}
|
|
1036
1160
|
</div>
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1161
|
+
</>
|
|
1162
|
+
)}
|
|
1163
|
+
{normalizedChats.length > 0 && (
|
|
1164
|
+
<div className="px-2 py-2 text-center text-[11px] text-agent-muted-foreground">
|
|
1165
|
+
{isLoadingMoreChats ? (
|
|
1166
|
+
<span className="inline-flex items-center gap-1">
|
|
1167
|
+
<LuLoaderCircle className="h-3 w-3 animate-spin" />
|
|
1168
|
+
加载更多...
|
|
1169
|
+
</span>
|
|
1170
|
+
) : hasMoreChats ? (
|
|
1171
|
+
"继续下滑加载更多"
|
|
1172
|
+
) : (
|
|
1173
|
+
`共 ${normalizedChats.length} 个会话`
|
|
1174
|
+
)}
|
|
1175
|
+
</div>
|
|
1176
|
+
)}
|
|
1177
|
+
</div>
|
|
1040
1178
|
</div>
|
|
1041
1179
|
|
|
1042
1180
|
{(error || projectError) && (
|
|
@@ -1048,34 +1186,14 @@ export function AgentSidebar({
|
|
|
1048
1186
|
</div>
|
|
1049
1187
|
)}
|
|
1050
1188
|
|
|
1051
|
-
{/* ───── Footer:
|
|
1189
|
+
{/* ───── Footer: 设置 ───── */}
|
|
1052
1190
|
<div className="flex-shrink-0 border-t border-agent-border/40 px-2.5 py-1.5">
|
|
1053
|
-
{showTerminalChrome ? (
|
|
1054
|
-
<button
|
|
1055
|
-
type="button"
|
|
1056
|
-
onClick={() => onToggleRightPanel("terminal")}
|
|
1057
|
-
className={[
|
|
1058
|
-
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
1059
|
-
rightPanel === "terminal"
|
|
1060
|
-
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
1061
|
-
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-foreground",
|
|
1062
|
-
].join(" ")}
|
|
1063
|
-
title={`${rightPanel === "terminal" ? "关闭" : "打开"}终端面板 (${isMac ? "⌘T" : "Ctrl+T"})`}
|
|
1064
|
-
data-testid="sidebar-terminal"
|
|
1065
|
-
>
|
|
1066
|
-
<LuTerminal className="h-3.5 w-3.5" />
|
|
1067
|
-
<span>终端</span>
|
|
1068
|
-
<span className="ml-auto text-[10px] text-agent-muted-foreground/70">
|
|
1069
|
-
{isMac ? "⌘T" : "Ctrl+T"}
|
|
1070
|
-
</span>
|
|
1071
|
-
</button>
|
|
1072
|
-
) : null}
|
|
1073
1191
|
{hasGeneralSettingsChrome() && (
|
|
1074
1192
|
<button
|
|
1075
1193
|
type="button"
|
|
1076
1194
|
onClick={() => navigate("/settings")}
|
|
1077
1195
|
className={[
|
|
1078
|
-
"
|
|
1196
|
+
"flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors",
|
|
1079
1197
|
onGeneralSettings
|
|
1080
1198
|
? "bg-agent-foreground/10 font-medium text-agent-foreground"
|
|
1081
1199
|
: "text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground",
|
|
@@ -1085,8 +1203,14 @@ export function AgentSidebar({
|
|
|
1085
1203
|
>
|
|
1086
1204
|
<LuSettings className="h-3.5 w-3.5" />
|
|
1087
1205
|
<span>设置</span>
|
|
1206
|
+
<SidebarVersionLabel release={release} />
|
|
1088
1207
|
</button>
|
|
1089
1208
|
)}
|
|
1209
|
+
{!hasGeneralSettingsChrome() && release.version ? (
|
|
1210
|
+
<div className="mt-0.5 flex h-7 w-full items-center px-2.5">
|
|
1211
|
+
<SidebarVersionLabel release={release} />
|
|
1212
|
+
</div>
|
|
1213
|
+
) : null}
|
|
1090
1214
|
</div>
|
|
1091
1215
|
|
|
1092
1216
|
{projectMenu &&
|