@steerable/agent-shell-web 0.6.28 → 0.6.30
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 +3 -3
- package/src/components/BrandLockup.tsx +20 -4
- package/src/components/chat/AssistantMessage.tsx +83 -25
- package/src/components/chat/ChatInput.tsx +11 -3
- package/src/components/chat/ExecutedActionsCard.tsx +389 -27
- package/src/components/chat/LocalChatPanel.tsx +12 -16
- package/src/components/chat/MessageList.tsx +9 -4
- package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
- package/src/components/chat/SessionTodoList.tsx +117 -20
- package/src/components/chat/SuggestedReplies.tsx +11 -3
- package/src/components/chat/TaskProcessPanel.tsx +62 -17
- package/src/components/chat/TurnProcessGroup.tsx +27 -1
- package/src/components/chat/UserMessage.tsx +5 -5
- package/src/components/chat/executed-actions-model.ts +469 -0
- package/src/components/chat/orchestration-children-model.ts +120 -0
- package/src/components/chat/turn-timeline.ts +23 -0
- package/src/lib/local-api.ts +11 -0
- package/src/pages/AgentPage.tsx +27 -6
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.30",
|
|
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-ui": "0.6.30",
|
|
50
|
+
"@steerable/agent-protocol": "0.6.30"
|
|
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.30"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"gen": "echo agent-shell-web",
|
|
@@ -621,7 +621,7 @@ export function AgentSidebar({
|
|
|
621
621
|
<div className="flex h-full w-full flex-col border-r border-agent-border/60 bg-agent-muted/70 backdrop-blur-md">
|
|
622
622
|
{/* ───── Brand + actions ───── */}
|
|
623
623
|
<div className="flex h-9 flex-shrink-0 items-center justify-between px-2.5">
|
|
624
|
-
<BrandLockup />
|
|
624
|
+
<BrandLockup onClick={() => handleOpenNewChat()} />
|
|
625
625
|
<div className="flex items-center gap-1">
|
|
626
626
|
<button
|
|
627
627
|
type="button"
|
|
@@ -704,7 +704,7 @@ export function AgentSidebar({
|
|
|
704
704
|
<button
|
|
705
705
|
type="button"
|
|
706
706
|
onClick={() => setChatsExpanded((v) => !v)}
|
|
707
|
-
className="flex items-center text-xs font-semibold tracking-wider text-agent-muted-foreground transition-colors hover:text-agent-foreground"
|
|
707
|
+
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
708
|
>
|
|
709
709
|
<span className="mr-1">
|
|
710
710
|
{chatsExpanded ? (
|
|
@@ -739,7 +739,7 @@ export function AgentSidebar({
|
|
|
739
739
|
{chatsExpanded && (
|
|
740
740
|
<div
|
|
741
741
|
ref={chatScrollRef}
|
|
742
|
-
className="flex-1 overflow-y-auto px-2 pb-1"
|
|
742
|
+
className="flex-1 overflow-y-auto px-2.5 pb-1"
|
|
743
743
|
>
|
|
744
744
|
{isChatLoading && chats.length === 0 && projects.length === 0 ? (
|
|
745
745
|
<div className="flex items-center justify-center py-4 text-xs text-agent-muted-foreground">
|
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
import { BRAND_NAME, BRAND_TITLE, getBrandLogoUrl } from '@/brand';
|
|
2
2
|
|
|
3
|
+
export interface BrandLockupProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* 侧栏品牌锁头。配了 brand.title 就 logo + 标题;没配标题则只显示 logo,
|
|
5
10
|
* 并按原图宽高比缩放(一体字标不要压成方图)。
|
|
11
|
+
* 自带 px-2.5 内边距,与侧边栏下方各功能行、会话行的图标在一条垂直参考线上(left: 20px)精准对齐。
|
|
6
12
|
*/
|
|
7
|
-
export function BrandLockup() {
|
|
13
|
+
export function BrandLockup({ className, onClick }: BrandLockupProps = {}) {
|
|
8
14
|
const title = BRAND_TITLE.trim();
|
|
9
15
|
return (
|
|
10
|
-
<div
|
|
16
|
+
<div
|
|
17
|
+
onClick={onClick}
|
|
18
|
+
className={[
|
|
19
|
+
'flex min-w-0 items-center gap-2 px-2.5',
|
|
20
|
+
onClick ? 'cursor-pointer select-none transition-opacity hover:opacity-85' : '',
|
|
21
|
+
className,
|
|
22
|
+
]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join(' ')}
|
|
25
|
+
>
|
|
11
26
|
<img
|
|
12
27
|
src={getBrandLogoUrl()}
|
|
13
28
|
alt={title || BRAND_NAME}
|
|
14
|
-
className=
|
|
29
|
+
className={`${title ? 'h-5' : 'h-6'} w-auto max-w-[10rem] flex-shrink-0 select-none object-contain object-left`}
|
|
15
30
|
draggable={false}
|
|
16
31
|
/>
|
|
17
32
|
{title ? (
|
|
18
|
-
<span className="truncate text-xs font-semibold tracking-tight text-agent-foreground">
|
|
33
|
+
<span className="truncate text-xs font-semibold tracking-tight text-agent-foreground leading-none">
|
|
19
34
|
{title}
|
|
20
35
|
</span>
|
|
21
36
|
) : null}
|
|
22
37
|
</div>
|
|
23
38
|
);
|
|
24
39
|
}
|
|
40
|
+
|
|
@@ -5,7 +5,9 @@ import type { ChatMessage } from '@steerable/agent-protocol';
|
|
|
5
5
|
import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
|
|
6
6
|
import { Markdown } from './Markdown';
|
|
7
7
|
import { ExecutedActionsCard, type ExecutedAction } from './ExecutedActionsCard';
|
|
8
|
-
import {
|
|
8
|
+
import type { ChildInfo } from './OrchestrationChildrenCard';
|
|
9
|
+
import { collectTurnParticipants, extractMentionedAgentIds } from './executed-actions-model';
|
|
10
|
+
import type { InspectTaskInput } from './executed-actions-model';
|
|
9
11
|
import { StreamingStatus } from './StreamingStatus';
|
|
10
12
|
import { getFriendlyDate } from './timestamp';
|
|
11
13
|
import { useCopy } from './useCopy';
|
|
@@ -83,8 +85,15 @@ interface AssistantMessageProps {
|
|
|
83
85
|
* `content` keep the legacy stacked layout.
|
|
84
86
|
*/
|
|
85
87
|
timeline?: TurnBlock[];
|
|
86
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* P3.1 子代理生命周期。看板卡片已不再渲染——每个 `委派 · X` 工具行从
|
|
90
|
+
* 「执行中」到「已完成」本身就是同一份进度,还能展开看任务书与回报。
|
|
91
|
+
* 这份列表现在喂两处:顶栏参与者徽章(没有 `@提及` 时的回落来源),
|
|
92
|
+
* 以及委派行跳转右侧过程栏所需的子代理 record。
|
|
93
|
+
*/
|
|
87
94
|
orchestrationChildren?: ChildInfo[];
|
|
95
|
+
/** 上一轮用户消息:顶栏按里面的 `@提及` 画参与的智能体。 */
|
|
96
|
+
previousUser?: Pick<ChatMessage, 'content'> & { messageMetadata?: string | null };
|
|
88
97
|
/**
|
|
89
98
|
* Current round number for the in-flight turn (1-based). Only meaningful
|
|
90
99
|
* when `isStreaming === true` and this is the tail message — used by
|
|
@@ -121,37 +130,42 @@ interface AssistantMessageProps {
|
|
|
121
130
|
* 行上,跟复制 / 重新生成同一排。
|
|
122
131
|
*/
|
|
123
132
|
onShare?: () => Promise<boolean>;
|
|
133
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
function agentInitial(
|
|
127
|
-
|
|
128
|
-
return agent.name.trim()[0]?.toUpperCase() ?? 'A';
|
|
136
|
+
function agentInitial(name: string): string {
|
|
137
|
+
return name.trim()[0]?.toUpperCase() ?? 'A';
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
function AgentBadge({
|
|
132
|
-
|
|
141
|
+
name,
|
|
142
|
+
color,
|
|
143
|
+
title,
|
|
133
144
|
isStreaming,
|
|
134
145
|
}: {
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
name: string;
|
|
147
|
+
color: string;
|
|
148
|
+
title?: string;
|
|
149
|
+
isStreaming?: boolean;
|
|
137
150
|
}) {
|
|
138
151
|
return (
|
|
139
152
|
<div
|
|
140
153
|
className="inline-flex items-center gap-1.5 text-[11px] leading-none text-agent-muted-foreground"
|
|
141
|
-
title={
|
|
154
|
+
title={title || name}
|
|
142
155
|
>
|
|
143
156
|
<span
|
|
157
|
+
data-agent-color-dot=""
|
|
144
158
|
className="inline-flex items-center justify-center rounded-full text-[9px] font-semibold text-white"
|
|
145
159
|
style={{
|
|
146
160
|
width: 14,
|
|
147
161
|
height: 14,
|
|
148
|
-
backgroundColor:
|
|
162
|
+
backgroundColor: color,
|
|
149
163
|
}}
|
|
150
164
|
>
|
|
151
|
-
{agentInitial(
|
|
165
|
+
{agentInitial(name)}
|
|
152
166
|
</span>
|
|
153
167
|
<span className="max-w-[160px] truncate rounded bg-agent-canvas/90 px-1">
|
|
154
|
-
{
|
|
168
|
+
{name}
|
|
155
169
|
</span>
|
|
156
170
|
{isStreaming && (
|
|
157
171
|
<span className="ml-1 inline-flex h-1.5 w-1.5 animate-pulse rounded-full bg-agent-foreground/30" />
|
|
@@ -200,6 +214,17 @@ function readTurnFailure(
|
|
|
200
214
|
return null;
|
|
201
215
|
}
|
|
202
216
|
|
|
217
|
+
function readPersistedAgentId(message: ChatMessage): string | undefined {
|
|
218
|
+
const raw = (message as { messageMetadata?: unknown }).messageMetadata;
|
|
219
|
+
if (typeof raw !== 'string' || !raw) return undefined;
|
|
220
|
+
try {
|
|
221
|
+
const agentId = (JSON.parse(raw) as { agentId?: unknown }).agentId;
|
|
222
|
+
return typeof agentId === 'string' && agentId ? agentId : undefined;
|
|
223
|
+
} catch {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
203
228
|
function TurnErrorBubble({ reason }: { reason: string }) {
|
|
204
229
|
return (
|
|
205
230
|
<div className="rounded-agent-lg border border-agent-destructive/40 bg-agent-destructive/5 px-2.5 py-2 text-xs leading-relaxed text-agent-destructive shadow-sm">
|
|
@@ -235,6 +260,7 @@ export function AssistantMessage({
|
|
|
235
260
|
executedActions,
|
|
236
261
|
timeline,
|
|
237
262
|
orchestrationChildren,
|
|
263
|
+
previousUser,
|
|
238
264
|
currentRound = 1,
|
|
239
265
|
isPlanMode = false,
|
|
240
266
|
onRegenerate,
|
|
@@ -243,6 +269,7 @@ export function AssistantMessage({
|
|
|
243
269
|
llmSpeed,
|
|
244
270
|
turnFiles,
|
|
245
271
|
onShare,
|
|
272
|
+
onInspectTask,
|
|
246
273
|
}: AssistantMessageProps) {
|
|
247
274
|
const content = message.content || '';
|
|
248
275
|
const failure = readTurnFailure(message, content);
|
|
@@ -252,14 +279,28 @@ export function AssistantMessage({
|
|
|
252
279
|
const { copied, copy } = useCopy(stripNextStepsTags(content));
|
|
253
280
|
const [regenerating, setRegenerating] = useState(false);
|
|
254
281
|
const [regenerateError, setRegenerateError] = useState<string | null>(null);
|
|
255
|
-
const
|
|
256
|
-
|
|
282
|
+
const persistedAgentId =
|
|
283
|
+
typeof message.agentId === 'string' && message.agentId
|
|
284
|
+
? message.agentId
|
|
285
|
+
: readPersistedAgentId(message);
|
|
286
|
+
const persistedAgent = persistedAgentId
|
|
287
|
+
? agents.find((a) => a.id === persistedAgentId) ?? null
|
|
257
288
|
: null;
|
|
258
289
|
// Stream-time fallback: only allow the chat-level agent to "claim" a turn
|
|
259
290
|
// while it's actively streaming and hasn't flushed agentId to the DB yet.
|
|
260
291
|
const displayAgent = persistedAgent ?? (isStreaming ? currentAgent : null);
|
|
261
292
|
const useTimeline = timeline !== undefined;
|
|
262
293
|
const blocks = timeline ?? [];
|
|
294
|
+
const toolActions = [
|
|
295
|
+
...(executedActions ?? []),
|
|
296
|
+
...blocks.flatMap((block) => (block.type === 'tools' ? block.actions : [])),
|
|
297
|
+
];
|
|
298
|
+
const participants = collectTurnParticipants(displayAgent, {
|
|
299
|
+
agents,
|
|
300
|
+
children: orchestrationChildren,
|
|
301
|
+
actions: toolActions,
|
|
302
|
+
mentionedAgentIds: extractMentionedAgentIds(previousUser, agents),
|
|
303
|
+
});
|
|
263
304
|
const now = useLiveNow(Boolean(isStreaming && llmSpeed?.live));
|
|
264
305
|
const tokenSpeed = formatTokenSpeed(
|
|
265
306
|
llmSpeed?.tokens ?? 0,
|
|
@@ -268,7 +309,7 @@ export function AssistantMessage({
|
|
|
268
309
|
|
|
269
310
|
return (
|
|
270
311
|
<motion.div
|
|
271
|
-
className="group/message
|
|
312
|
+
className="group/message"
|
|
272
313
|
data-message-role="assistant"
|
|
273
314
|
data-message-id={message.id}
|
|
274
315
|
initial={{ opacity: 0, y: 8 }}
|
|
@@ -276,10 +317,22 @@ export function AssistantMessage({
|
|
|
276
317
|
transition={{ duration: 0.25 }}
|
|
277
318
|
>
|
|
278
319
|
<div className="mx-auto w-full max-w-[var(--chat-input-box-width)] px-1">
|
|
279
|
-
{(
|
|
280
|
-
<div className="mb-1 flex min-h-4 items-center justify-between">
|
|
281
|
-
{
|
|
282
|
-
<
|
|
320
|
+
{(participants.length > 0 || isPlanMode) && (
|
|
321
|
+
<div className="mb-1 flex min-h-4 items-center justify-between gap-2">
|
|
322
|
+
{participants.length > 0 ? (
|
|
323
|
+
<div
|
|
324
|
+
data-testid="turn-agent-badges"
|
|
325
|
+
className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"
|
|
326
|
+
>
|
|
327
|
+
{participants.map((agent, index) => (
|
|
328
|
+
<AgentBadge
|
|
329
|
+
key={agent.key}
|
|
330
|
+
name={agent.name}
|
|
331
|
+
color={agent.color}
|
|
332
|
+
isStreaming={isStreaming && index === 0}
|
|
333
|
+
/>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
283
336
|
) : (
|
|
284
337
|
<div />
|
|
285
338
|
)}
|
|
@@ -291,9 +344,6 @@ export function AssistantMessage({
|
|
|
291
344
|
)}
|
|
292
345
|
</div>
|
|
293
346
|
)}
|
|
294
|
-
{orchestrationChildren && orchestrationChildren.length > 0 && (
|
|
295
|
-
<OrchestrationChildrenCard children={orchestrationChildren} />
|
|
296
|
-
)}
|
|
297
347
|
{useTimeline ? (
|
|
298
348
|
<>
|
|
299
349
|
<TurnProcessGroup
|
|
@@ -304,6 +354,8 @@ export function AssistantMessage({
|
|
|
304
354
|
agents={agents}
|
|
305
355
|
chats={chats}
|
|
306
356
|
chatId={chatId}
|
|
357
|
+
onInspectTask={onInspectTask}
|
|
358
|
+
orchestrationChildren={orchestrationChildren}
|
|
307
359
|
emptyFallback={
|
|
308
360
|
failure ? (
|
|
309
361
|
<TurnErrorBubble reason={failure.reason} />
|
|
@@ -348,7 +400,13 @@ export function AssistantMessage({
|
|
|
348
400
|
) : (
|
|
349
401
|
<>
|
|
350
402
|
{executedActions && executedActions.length > 0 && (
|
|
351
|
-
<ExecutedActionsCard
|
|
403
|
+
<ExecutedActionsCard
|
|
404
|
+
actions={executedActions}
|
|
405
|
+
agents={agents}
|
|
406
|
+
chatId={chatId}
|
|
407
|
+
onInspectTask={onInspectTask}
|
|
408
|
+
orchestrationChildren={orchestrationChildren}
|
|
409
|
+
/>
|
|
352
410
|
)}
|
|
353
411
|
{displayContent ? (
|
|
354
412
|
<div className={bubbleClass(isPlanMode)}>
|
|
@@ -382,11 +440,11 @@ export function AssistantMessage({
|
|
|
382
440
|
{/* 回合产物文件列表:钉在回答之下、时间戳行之上(Codex 式收尾)。
|
|
383
441
|
回合收尾才有数据,流式期间天然为空。 */}
|
|
384
442
|
{!isStreaming && turnFiles && turnFiles.length > 0 && (
|
|
385
|
-
<div className="mt-
|
|
443
|
+
<div className="mt-2.5 mb-1">
|
|
386
444
|
<TurnFilesCard files={turnFiles} />
|
|
387
445
|
</div>
|
|
388
446
|
)}
|
|
389
|
-
<div className="mt-
|
|
447
|
+
<div className="mt-2 flex items-center gap-2 text-[11px] text-agent-muted-foreground/80">
|
|
390
448
|
<span>
|
|
391
449
|
{message.createdAt
|
|
392
450
|
? getFriendlyDate(new Date(message.createdAt))
|
|
@@ -141,6 +141,8 @@ export interface ChatInputProps {
|
|
|
141
141
|
/** Slot in the composer meta row above the input box, before the agent
|
|
142
142
|
* picker (e.g. the project badge). */
|
|
143
143
|
leadingChrome?: ReactNode;
|
|
144
|
+
/** Slot in the composer meta row above the input box, right-aligned (e.g. SessionTodoList). */
|
|
145
|
+
trailingChrome?: ReactNode;
|
|
144
146
|
/** Current chat mode. When unset the toggle is hidden (defaults to 'agent'). */
|
|
145
147
|
mode?: ChatMode;
|
|
146
148
|
/** Called when the user switches between Agent / Plan mode. */
|
|
@@ -696,6 +698,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
696
698
|
onOpenSettings,
|
|
697
699
|
toolbarExtras,
|
|
698
700
|
leadingChrome,
|
|
701
|
+
trailingChrome,
|
|
699
702
|
mode,
|
|
700
703
|
onModeChange,
|
|
701
704
|
execPolicy,
|
|
@@ -1549,10 +1552,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1549
1552
|
return (
|
|
1550
1553
|
// @container:底栏元素的显隐/宽度用容器查询(@sm = 384px)而不是
|
|
1551
1554
|
// 视口断点——侧边栏占宽后,视口够宽但输入框本身可能已经很窄。
|
|
1552
|
-
<div className="chat-input-container @container relative px-2.5 pb-2 pt-
|
|
1553
|
-
{(leadingChrome || selectedAgent) && (
|
|
1555
|
+
<div className="chat-input-container @container relative px-2.5 pb-2.5 pt-1">
|
|
1556
|
+
{(leadingChrome || selectedAgent || trailingChrome) && (
|
|
1554
1557
|
<div
|
|
1555
|
-
className="relative mb-1 flex min-w-0 items-center gap-1.5"
|
|
1558
|
+
className="relative mb-1.5 flex min-w-0 items-center gap-1.5"
|
|
1556
1559
|
data-testid="composer-meta-row"
|
|
1557
1560
|
>
|
|
1558
1561
|
{leadingChrome}
|
|
@@ -1568,6 +1571,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1568
1571
|
mentionReferences={mentionReferences}
|
|
1569
1572
|
/>
|
|
1570
1573
|
)}
|
|
1574
|
+
{trailingChrome && (
|
|
1575
|
+
<div className="ml-auto flex items-center min-w-0">
|
|
1576
|
+
{trailingChrome}
|
|
1577
|
+
</div>
|
|
1578
|
+
)}
|
|
1571
1579
|
</div>
|
|
1572
1580
|
)}
|
|
1573
1581
|
{mentionQuery && mentionSuggestions.length > 0 && (
|