@steerable/agent-shell-web 0.6.28 → 0.6.29

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.28",
3
+ "version": "0.6.29",
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-protocol": "0.6.28",
50
- "@steerable/agent-ui": "0.6.28"
49
+ "@steerable/agent-ui": "0.6.29",
50
+ "@steerable/agent-protocol": "0.6.29"
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.28"
63
+ "@steerable/pack-sdk": "0.6.29"
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 className="flex min-w-0 items-center gap-2">
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="h-6 w-auto max-w-[10rem] flex-shrink-0 select-none object-contain object-left"
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 { OrchestrationChildrenCard, type ChildInfo } from './OrchestrationChildrenCard';
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
- /** P3.1: live child-agent list for this turn (orchestration card). */
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(agent: LocalChatAgent | null): string {
127
- if (!agent?.name) return 'A';
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
- agent,
141
+ name,
142
+ color,
143
+ title,
133
144
  isStreaming,
134
145
  }: {
135
- agent: LocalChatAgent;
136
- isStreaming: boolean;
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={agent.description || agent.name}
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: agent.color || '#7c3aed',
162
+ backgroundColor: color,
149
163
  }}
150
164
  >
151
- {agentInitial(agent)}
165
+ {agentInitial(name)}
152
166
  </span>
153
167
  <span className="max-w-[160px] truncate rounded bg-agent-canvas/90 px-1">
154
- {agent.name}
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 persistedAgent = message.agentId
256
- ? agents.find((a) => a.id === message.agentId) ?? null
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 mb-1"
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
- {(displayAgent || isPlanMode) && (
280
- <div className="mb-1 flex min-h-4 items-center justify-between">
281
- {displayAgent ? (
282
- <AgentBadge agent={displayAgent} isStreaming={isStreaming} />
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 actions={executedActions} />
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-1.5">
443
+ <div className="mt-2.5 mb-1">
386
444
  <TurnFilesCard files={turnFiles} />
387
445
  </div>
388
446
  )}
389
- <div className="mt-1 flex items-center gap-2 text-[11px] text-agent-muted-foreground">
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-0.5">
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 && (