@steerable/agent-shell-web 0.6.64 → 0.6.66
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 +278 -18
- package/src/components/ChatHeader.tsx +666 -277
- package/src/components/chat/MessageList.tsx +58 -1
- package/src/components/chat/TaskOutcomeCards.tsx +2 -2
- package/src/components/chat/turn-files.ts +57 -0
- package/src/hooks/useProjects.ts +30 -1
- package/src/layouts/AgentLayout.tsx +279 -127
- package/src/layouts/PanelToggleButton.tsx +35 -0
- package/src/layouts/RightPanelTabBar.tsx +214 -0
- package/src/layouts/conversation-preview.ts +87 -0
- package/src/layouts/request-chat-slot.ts +65 -0
- package/src/layouts/right-panel-tabs.ts +309 -0
- package/src/lib/local-api.ts +11 -0
- package/src/lib/project-order.ts +48 -0
- package/src/packs/registry.ts +2 -0
- package/src/pages/AgentPage.tsx +13 -2
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.66",
|
|
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",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"react-router-dom": "^6.30.0",
|
|
46
46
|
"rehype-raw": "^7.0.0",
|
|
47
47
|
"remark-gfm": "^4.0.1",
|
|
48
|
-
"@steerable/agent-protocol": "0.6.
|
|
49
|
-
"@steerable/agent-ui": "0.6.
|
|
48
|
+
"@steerable/agent-protocol": "0.6.66",
|
|
49
|
+
"@steerable/agent-ui": "0.6.66"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@tailwindcss/vite": "^4.3.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"typescript": "^5.8.3",
|
|
60
60
|
"vite": "^5.4.21",
|
|
61
61
|
"vitest": "^3.2.4",
|
|
62
|
-
"@steerable/pack-sdk": "0.6.
|
|
62
|
+
"@steerable/pack-sdk": "0.6.66"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"gen": "echo agent-shell-web",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
* │ ✎ 新对话 │ ← 只打开落地页,有内容才落库
|
|
57
57
|
* │ 🧩 插件 │ ← /settings?section=plugins(智能体 / Skills / MCP / 网络搜索)
|
|
58
58
|
* │ 会话 v 📁+ │ ← 📁+ 打开新建项目弹窗
|
|
59
|
-
* │ v 📁 项目A (hover: ✎··)│ ←
|
|
60
|
-
* │
|
|
59
|
+
* │ v 📁 项目A (hover: ✎··)│ ← 拖拽组头排序;✎ 新建对话;·· 菜单:重命名/换目录/访达/删
|
|
60
|
+
* │ ...(项目内对话,超过 5 条再展开)│
|
|
61
61
|
* │ 今天 │
|
|
62
62
|
* │ ...(无项目对话,按日期分组) │ ← 无项目排在项目分组之后
|
|
63
63
|
* ├─────────────────────────────────┤
|
|
@@ -109,7 +109,11 @@ import {
|
|
|
109
109
|
import { usePendingAskUserChatIds } from "@/components/chat/AskUserPromptProvider";
|
|
110
110
|
import { usePendingApprovalChatIds } from "@/components/chat/ApprovalPromptProvider";
|
|
111
111
|
import type { UseChatsAndAgentsResult } from "@/hooks/useChatsAndAgents";
|
|
112
|
-
import { useProjects } from "@/hooks/useProjects";
|
|
112
|
+
import { persistProjectOrder, useProjects } from "@/hooks/useProjects";
|
|
113
|
+
import {
|
|
114
|
+
nextProjectOrder,
|
|
115
|
+
type ProjectDropPlace,
|
|
116
|
+
} from "@/lib/project-order";
|
|
113
117
|
import type { RightPanelState } from "@/layouts/AgentLayout";
|
|
114
118
|
import { BrandLockup } from "@/components/BrandLockup";
|
|
115
119
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
@@ -121,6 +125,48 @@ import { SidebarAccount } from "@/components/SidebarAccount";
|
|
|
121
125
|
import { CreateProjectModal } from "@/components/CreateProjectModal";
|
|
122
126
|
import { t } from "@/i18n";
|
|
123
127
|
|
|
128
|
+
function projectDropPlace(element: HTMLElement, clientY: number): ProjectDropPlace {
|
|
129
|
+
const rect = element.getBoundingClientRect();
|
|
130
|
+
return clientY < rect.top + rect.height / 2 ? "before" : "after";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 项目里先露出的普通会话条数。每一次「显示更多」再露出同样多条。 */
|
|
134
|
+
const PROJECT_CHAT_PAGE = 5;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 进行中、等待输入的会话保持原位,不占用这 5 条名额。
|
|
138
|
+
* 当前打开的会话如果落在名额之外,仍单独留在列表里。
|
|
139
|
+
*/
|
|
140
|
+
function visibleProjectChats<T extends { id: string }>(
|
|
141
|
+
items: readonly T[],
|
|
142
|
+
limit: number,
|
|
143
|
+
options: {
|
|
144
|
+
currentId?: string;
|
|
145
|
+
isAlwaysVisible: (chat: T) => boolean;
|
|
146
|
+
},
|
|
147
|
+
): { rows: T[]; hiddenCount: number } {
|
|
148
|
+
let ordinary = 0;
|
|
149
|
+
const kept = new Set<string>();
|
|
150
|
+
for (const chat of items) {
|
|
151
|
+
if (options.isAlwaysVisible(chat)) {
|
|
152
|
+
kept.add(chat.id);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ordinary < limit) {
|
|
156
|
+
kept.add(chat.id);
|
|
157
|
+
ordinary += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
options.currentId &&
|
|
162
|
+
items.some((chat) => chat.id === options.currentId)
|
|
163
|
+
) {
|
|
164
|
+
kept.add(options.currentId);
|
|
165
|
+
}
|
|
166
|
+
const rows = items.filter((chat) => kept.has(chat.id));
|
|
167
|
+
return { rows, hiddenCount: items.length - rows.length };
|
|
168
|
+
}
|
|
169
|
+
|
|
124
170
|
function placeProjectMenu(anchor: HTMLElement): { top: number; left: number } {
|
|
125
171
|
const box = anchor.getBoundingClientRect();
|
|
126
172
|
const width = 224;
|
|
@@ -481,6 +527,10 @@ export function AgentSidebar({
|
|
|
481
527
|
const [collapsedProjectIds, setCollapsedProjectIds] = useState<Set<string>>(
|
|
482
528
|
() => new Set(),
|
|
483
529
|
);
|
|
530
|
+
// 每个项目已展开的普通会话名额。缺省是 PROJECT_CHAT_PAGE;Infinity 表示已全部展开。
|
|
531
|
+
const [projectChatLimits, setProjectChatLimits] = useState<
|
|
532
|
+
Record<string, number>
|
|
533
|
+
>({});
|
|
484
534
|
const [renamingProjectId, setRenamingProjectId] = useState<string | null>(
|
|
485
535
|
null,
|
|
486
536
|
);
|
|
@@ -497,6 +547,37 @@ export function AgentSidebar({
|
|
|
497
547
|
id: string;
|
|
498
548
|
anchor: HTMLElement;
|
|
499
549
|
} | null>(null);
|
|
550
|
+
const draggingProjectIdRef = useRef<string | null>(null);
|
|
551
|
+
const projectDragMovedRef = useRef(false);
|
|
552
|
+
const [draggingProjectId, setDraggingProjectId] = useState<string | null>(
|
|
553
|
+
null,
|
|
554
|
+
);
|
|
555
|
+
const [dropTarget, setDropTarget] = useState<{
|
|
556
|
+
id: string;
|
|
557
|
+
place: ProjectDropPlace;
|
|
558
|
+
} | null>(null);
|
|
559
|
+
|
|
560
|
+
const endProjectDrag = useCallback(() => {
|
|
561
|
+
draggingProjectIdRef.current = null;
|
|
562
|
+
setDraggingProjectId(null);
|
|
563
|
+
setDropTarget(null);
|
|
564
|
+
// dragend 之后浏览器有时还会补一次 click。等这次 click 过去再清标记,
|
|
565
|
+
// 避免松手把项目折叠掉;下一次真正的点击仍能折叠。
|
|
566
|
+
window.setTimeout(() => {
|
|
567
|
+
projectDragMovedRef.current = false;
|
|
568
|
+
}, 0);
|
|
569
|
+
}, []);
|
|
570
|
+
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
if (!draggingProjectId) return;
|
|
573
|
+
const { cursor, userSelect } = document.body.style;
|
|
574
|
+
document.body.style.cursor = "grabbing";
|
|
575
|
+
document.body.style.userSelect = "none";
|
|
576
|
+
return () => {
|
|
577
|
+
document.body.style.cursor = cursor;
|
|
578
|
+
document.body.style.userSelect = userSelect;
|
|
579
|
+
};
|
|
580
|
+
}, [draggingProjectId]);
|
|
500
581
|
|
|
501
582
|
const showProjectsChrome = hostToolChrome("projects");
|
|
502
583
|
|
|
@@ -592,14 +673,49 @@ export function AgentSidebar({
|
|
|
592
673
|
}
|
|
593
674
|
}, []);
|
|
594
675
|
|
|
595
|
-
const toggleProjectCollapsed = useCallback(
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
676
|
+
const toggleProjectCollapsed = useCallback(
|
|
677
|
+
(projectId: string) => {
|
|
678
|
+
const collapsing = !collapsedProjectIds.has(projectId);
|
|
679
|
+
setCollapsedProjectIds((prev) => {
|
|
680
|
+
const next = new Set(prev);
|
|
681
|
+
if (next.has(projectId)) next.delete(projectId);
|
|
682
|
+
else next.add(projectId);
|
|
683
|
+
return next;
|
|
684
|
+
});
|
|
685
|
+
// 收起项目后再打开,回到最初的 5 条,不记住上次展开到第几页。
|
|
686
|
+
if (collapsing) {
|
|
687
|
+
setProjectChatLimits((limits) => {
|
|
688
|
+
if (limits[projectId] === undefined) return limits;
|
|
689
|
+
const next = { ...limits };
|
|
690
|
+
delete next[projectId];
|
|
691
|
+
return next;
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
[collapsedProjectIds],
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const revealMoreProjectChats = useCallback(
|
|
699
|
+
(projectId: string, hiddenCount: number) => {
|
|
700
|
+
setProjectChatLimits((limits) => {
|
|
701
|
+
if (hiddenCount === 0) {
|
|
702
|
+
if (limits[projectId] === undefined) return limits;
|
|
703
|
+
const next = { ...limits };
|
|
704
|
+
delete next[projectId];
|
|
705
|
+
return next;
|
|
706
|
+
}
|
|
707
|
+
const current = limits[projectId] ?? PROJECT_CHAT_PAGE;
|
|
708
|
+
return {
|
|
709
|
+
...limits,
|
|
710
|
+
[projectId]:
|
|
711
|
+
hiddenCount <= PROJECT_CHAT_PAGE
|
|
712
|
+
? Number.POSITIVE_INFINITY
|
|
713
|
+
: current + PROJECT_CHAT_PAGE,
|
|
714
|
+
};
|
|
715
|
+
});
|
|
716
|
+
},
|
|
717
|
+
[],
|
|
718
|
+
);
|
|
603
719
|
|
|
604
720
|
// Keep the shared "next new chat" agent aligned with the current chat. The
|
|
605
721
|
// actual picker now lives in ChatInput, so the sidebar no longer renders the
|
|
@@ -918,6 +1034,22 @@ export function AgentSidebar({
|
|
|
918
1034
|
|
|
919
1035
|
const hasElectron = hasHostBridge();
|
|
920
1036
|
|
|
1037
|
+
const projectChatWindow = (
|
|
1038
|
+
items: typeof normalizedChats,
|
|
1039
|
+
limit: number,
|
|
1040
|
+
) =>
|
|
1041
|
+
visibleProjectChats(items, limit, {
|
|
1042
|
+
currentId: currentChatId,
|
|
1043
|
+
isAlwaysVisible: (chat) =>
|
|
1044
|
+
Boolean(
|
|
1045
|
+
chat.isStreaming ||
|
|
1046
|
+
streamingChatIds.has(chat.id) ||
|
|
1047
|
+
chat.needsUserInput ||
|
|
1048
|
+
pendingAskUserChatIds.has(chat.id) ||
|
|
1049
|
+
pendingApprovalChatIds.has(chat.id),
|
|
1050
|
+
),
|
|
1051
|
+
});
|
|
1052
|
+
|
|
921
1053
|
return (
|
|
922
1054
|
<div className="flex h-full w-full flex-col border-r border-agent-border/60 bg-agent-muted/70 backdrop-blur-md">
|
|
923
1055
|
{/* ───── Brand + actions ───── */}
|
|
@@ -1059,8 +1191,75 @@ export function AgentSidebar({
|
|
|
1059
1191
|
</div>
|
|
1060
1192
|
) : (
|
|
1061
1193
|
projectGroups.map(({ project, items }) => (
|
|
1062
|
-
<div
|
|
1063
|
-
|
|
1194
|
+
<div
|
|
1195
|
+
key={project.id}
|
|
1196
|
+
data-testid="sidebar-project"
|
|
1197
|
+
data-project-id={project.id}
|
|
1198
|
+
className="relative pb-1"
|
|
1199
|
+
onDragOver={(event) => {
|
|
1200
|
+
if (!draggingProjectIdRef.current) return;
|
|
1201
|
+
event.preventDefault();
|
|
1202
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
1203
|
+
const place = projectDropPlace(
|
|
1204
|
+
event.currentTarget,
|
|
1205
|
+
event.clientY,
|
|
1206
|
+
);
|
|
1207
|
+
setDropTarget((current) =>
|
|
1208
|
+
current?.id === project.id && current.place === place
|
|
1209
|
+
? current
|
|
1210
|
+
: { id: project.id, place },
|
|
1211
|
+
);
|
|
1212
|
+
}}
|
|
1213
|
+
onDragLeave={(event) => {
|
|
1214
|
+
const next = event.relatedTarget;
|
|
1215
|
+
if (
|
|
1216
|
+
next instanceof Node &&
|
|
1217
|
+
event.currentTarget.contains(next)
|
|
1218
|
+
) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
setDropTarget((current) =>
|
|
1222
|
+
current?.id === project.id ? null : current,
|
|
1223
|
+
);
|
|
1224
|
+
}}
|
|
1225
|
+
onDrop={(event) => {
|
|
1226
|
+
event.preventDefault();
|
|
1227
|
+
const fromId = draggingProjectIdRef.current;
|
|
1228
|
+
const place = projectDropPlace(
|
|
1229
|
+
event.currentTarget,
|
|
1230
|
+
event.clientY,
|
|
1231
|
+
);
|
|
1232
|
+
endProjectDrag();
|
|
1233
|
+
if (!fromId) return;
|
|
1234
|
+
const next = nextProjectOrder(
|
|
1235
|
+
projectGroups.map((group) => group.project.id),
|
|
1236
|
+
fromId,
|
|
1237
|
+
project.id,
|
|
1238
|
+
place,
|
|
1239
|
+
);
|
|
1240
|
+
if (!next) return;
|
|
1241
|
+
void persistProjectOrder(next).catch((err) => {
|
|
1242
|
+
setProjectError(
|
|
1243
|
+
err instanceof Error ? err.message : String(err),
|
|
1244
|
+
);
|
|
1245
|
+
});
|
|
1246
|
+
}}
|
|
1247
|
+
>
|
|
1248
|
+
{dropTarget?.id === project.id && draggingProjectId ? (
|
|
1249
|
+
<div
|
|
1250
|
+
className={[
|
|
1251
|
+
"pointer-events-none absolute inset-x-2 z-10 h-0.5 rounded-full bg-agent-foreground",
|
|
1252
|
+
dropTarget.place === "before" ? "top-0" : "bottom-1",
|
|
1253
|
+
].join(" ")}
|
|
1254
|
+
data-testid="sidebar-project-drop-indicator"
|
|
1255
|
+
/>
|
|
1256
|
+
) : null}
|
|
1257
|
+
<div
|
|
1258
|
+
className={[
|
|
1259
|
+
"group/proj relative",
|
|
1260
|
+
draggingProjectId === project.id ? "opacity-40" : "",
|
|
1261
|
+
].join(" ")}
|
|
1262
|
+
>
|
|
1064
1263
|
{renamingProjectId === project.id ? (
|
|
1065
1264
|
<form
|
|
1066
1265
|
className="flex items-center px-2.5 pb-1 pt-1.5"
|
|
@@ -1087,8 +1286,31 @@ export function AgentSidebar({
|
|
|
1087
1286
|
<>
|
|
1088
1287
|
<button
|
|
1089
1288
|
type="button"
|
|
1090
|
-
|
|
1091
|
-
|
|
1289
|
+
draggable
|
|
1290
|
+
data-testid="sidebar-project-handle"
|
|
1291
|
+
data-project-id={project.id}
|
|
1292
|
+
onDragStart={(event) => {
|
|
1293
|
+
projectDragMovedRef.current = true;
|
|
1294
|
+
draggingProjectIdRef.current = project.id;
|
|
1295
|
+
setDraggingProjectId(project.id);
|
|
1296
|
+
const transfer = event.dataTransfer;
|
|
1297
|
+
if (!transfer) return;
|
|
1298
|
+
transfer.effectAllowed = "move";
|
|
1299
|
+
try {
|
|
1300
|
+
transfer.setData("text/plain", project.name);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
// 排序状态记在 ref 里。个别环境拒绝写入 DataTransfer。
|
|
1303
|
+
void error;
|
|
1304
|
+
}
|
|
1305
|
+
}}
|
|
1306
|
+
onDragEnd={() => {
|
|
1307
|
+
endProjectDrag();
|
|
1308
|
+
}}
|
|
1309
|
+
onClick={() => {
|
|
1310
|
+
if (projectDragMovedRef.current) return;
|
|
1311
|
+
toggleProjectCollapsed(project.id);
|
|
1312
|
+
}}
|
|
1313
|
+
className="flex w-full min-w-0 cursor-grab items-center px-2 pb-0.5 pt-1 text-[11px] font-medium text-agent-muted-foreground/90 transition-colors hover:text-agent-foreground active:cursor-grabbing"
|
|
1092
1314
|
title={`${project.name}\n${project.folderPath}`}
|
|
1093
1315
|
>
|
|
1094
1316
|
<LuFolder className="mr-1 h-3.5 w-3.5 shrink-0" />
|
|
@@ -1106,8 +1328,8 @@ export function AgentSidebar({
|
|
|
1106
1328
|
<div
|
|
1107
1329
|
className={`absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 transition-opacity ${
|
|
1108
1330
|
projectMenu?.id === project.id
|
|
1109
|
-
? "opacity-100"
|
|
1110
|
-
: "opacity-0 group-hover/proj:opacity-100"
|
|
1331
|
+
? "pointer-events-auto opacity-100"
|
|
1332
|
+
: "pointer-events-none opacity-0 group-hover/proj:pointer-events-auto group-hover/proj:opacity-100"
|
|
1111
1333
|
}`}
|
|
1112
1334
|
>
|
|
1113
1335
|
<button
|
|
@@ -1146,7 +1368,45 @@ export function AgentSidebar({
|
|
|
1146
1368
|
{t("No chats yet. Hover the project name and click + to start one.")}
|
|
1147
1369
|
</div>
|
|
1148
1370
|
) : (
|
|
1149
|
-
|
|
1371
|
+
(() => {
|
|
1372
|
+
const limit =
|
|
1373
|
+
projectChatLimits[project.id] ??
|
|
1374
|
+
PROJECT_CHAT_PAGE;
|
|
1375
|
+
const folded = projectChatWindow(
|
|
1376
|
+
items,
|
|
1377
|
+
PROJECT_CHAT_PAGE,
|
|
1378
|
+
);
|
|
1379
|
+
const shown = projectChatWindow(items, limit);
|
|
1380
|
+
const fullyShown = shown.hiddenCount === 0;
|
|
1381
|
+
return (
|
|
1382
|
+
<>
|
|
1383
|
+
{shown.rows.map((chat) =>
|
|
1384
|
+
renderChatRow(chat),
|
|
1385
|
+
)}
|
|
1386
|
+
{folded.hiddenCount > 0 && (
|
|
1387
|
+
<button
|
|
1388
|
+
type="button"
|
|
1389
|
+
data-testid="sidebar-project-show-more"
|
|
1390
|
+
data-project-id={project.id}
|
|
1391
|
+
aria-expanded={fullyShown}
|
|
1392
|
+
onClick={() =>
|
|
1393
|
+
revealMoreProjectChats(
|
|
1394
|
+
project.id,
|
|
1395
|
+
shown.hiddenCount,
|
|
1396
|
+
)
|
|
1397
|
+
}
|
|
1398
|
+
className="flex h-7 w-full items-center rounded-full px-2.5 text-left text-[11px] text-agent-muted-foreground/80 transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
|
|
1399
|
+
>
|
|
1400
|
+
{fullyShown
|
|
1401
|
+
? t("Show less")
|
|
1402
|
+
: t("Show {count} more", {
|
|
1403
|
+
count: shown.hiddenCount,
|
|
1404
|
+
})}
|
|
1405
|
+
</button>
|
|
1406
|
+
)}
|
|
1407
|
+
</>
|
|
1408
|
+
);
|
|
1409
|
+
})()
|
|
1150
1410
|
)}
|
|
1151
1411
|
</div>
|
|
1152
1412
|
)}
|