@steerable/agent-shell-web 0.6.50 → 0.6.52

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.
Files changed (37) hide show
  1. package/package.json +4 -4
  2. package/src/brand.ts +7 -0
  3. package/src/components/AgentSidebar.tsx +284 -188
  4. package/src/components/ConfirmDialog.tsx +90 -0
  5. package/src/components/CreateProjectModal.tsx +32 -16
  6. package/src/components/chat/ApprovalModal.tsx +2 -5
  7. package/src/components/chat/ApprovalPromptProvider.tsx +38 -22
  8. package/src/components/chat/AskUserPromptProvider.tsx +1 -32
  9. package/src/components/chat/ChatInput.tsx +47 -57
  10. package/src/components/chat/ChatProjectBadge.tsx +1 -1
  11. package/src/components/chat/EmptyChatHero.tsx +22 -0
  12. package/src/components/chat/ExecPolicyPicker.tsx +2 -2
  13. package/src/components/chat/ExecutedActionsCard.tsx +23 -13
  14. package/src/components/chat/LocalChatPanel.tsx +61 -18
  15. package/src/components/chat/MessageList.tsx +77 -5
  16. package/src/components/chat/ModelPicker.tsx +21 -1
  17. package/src/components/chat/OrchestrationFlowCard.tsx +484 -0
  18. package/src/components/chat/SessionOrchestrationFlow.tsx +573 -0
  19. package/src/components/chat/SessionTodoList.tsx +12 -3
  20. package/src/components/chat/UserMessage.tsx +116 -25
  21. package/src/components/chat/composer-status-stack.tsx +79 -0
  22. package/src/components/chat/orchestration-flow-model.ts +401 -0
  23. package/src/components/chat/prompt-chat-scope.ts +33 -0
  24. package/src/components/settings/AgentsSettingsPanel.tsx +33 -22
  25. package/src/components/settings/McpSettingsPanel.tsx +1 -1
  26. package/src/components/settings/OrchestrationSettingsPanel.tsx +67 -0
  27. package/src/components/settings/SettingsNavMenu.tsx +55 -0
  28. package/src/components/settings/SkillsSettingsPanel.tsx +1 -1
  29. package/src/hooks/useProjects.ts +82 -0
  30. package/src/lib/electron-bridge.ts +1 -0
  31. package/src/lib/exec-policy.ts +1 -1
  32. package/src/lib/local-api.ts +1 -1
  33. package/src/lib/orchestration-settings.ts +58 -0
  34. package/src/pages/AgentPage.tsx +116 -64
  35. package/src/pages/SettingsPage.tsx +338 -55
  36. package/src/vite-env.d.ts +2 -0
  37. package/vite.base.js +1 -0
@@ -6,7 +6,7 @@ import type { ExecPolicy } from '@/lib/exec-policy';
6
6
  * ExecPolicyPicker — 输入框上的命令沙箱切换(类 Codex 底部权限档)。
7
7
  *
8
8
  * 切档立刻记住,下一轮才生效:当前回合的 `execSandbox` 已经随流发出。
9
- * 工作区 = Seatbelt/bwrap 只允许写项目家目录或本对话工作区;完整权限
9
+ * 工作区 = Seatbelt/bwrap 只允许写项目家目录、源文件夹(含各自子目录)或本对话工作区;完整权限
10
10
  * = 不下发命令沙箱,本机路径(如 Downloads)不再被 Operation not permitted 拦住。
11
11
  */
12
12
 
@@ -19,7 +19,7 @@ const OPTIONS: Array<{
19
19
  id: 'workspace',
20
20
  label: '工作区',
21
21
  description:
22
- '命令只能写入当前项目或本对话工作区(文稿/<应用名>/conversations/);工作区外路径会被系统拒绝。',
22
+ '只能写入当前工作区,范围外的路径会被拒绝。',
23
23
  },
24
24
  {
25
25
  id: 'full',
@@ -59,9 +59,19 @@ export interface ExecutedAction {
59
59
  * port-only egress); none = no backend on this platform.
60
60
  */
61
61
  sandbox?: { backend?: string; enforcement: string };
62
+ /**
63
+ * 工具注册时声明的卡片。`declared` 为真时行标题用 `title`,
64
+ * 不再按工具名猜测。旧事件没有这个字段,仍走原来的摘要。
65
+ */
66
+ view?: {
67
+ card: 'generic' | 'terminal' | 'diff' | 'search' | 'read' | 'web';
68
+ kind: string;
69
+ title: string;
70
+ declared: boolean;
71
+ };
62
72
  }
63
73
 
64
- interface ExecutedActionsCardProps {
74
+ export interface ExecutedActionsCardProps {
65
75
  actions: ExecutedAction[];
66
76
  /** Drop the outer card chrome when nested in a turn-process group. */
67
77
  compact?: boolean;
@@ -124,18 +134,18 @@ function actionToTool(action: ExecutedAction, idx: number): ToolExecutionPayload
124
134
  ? humanizeDelegateError(env.error)
125
135
  : env.error
126
136
  : null;
127
- // W5-2: web_search/web_fetch 用结构化中文摘要(查询词/URL + 结果计数/
128
- // 状态码),其余工具走通用参数摘要。
129
- const summary =
130
- (action.tool === 'delegate_subagent'
131
- ? summarizeDelegateSubagent(action.arguments)
132
- : null) ??
133
- (isTaskFamilyTool(action.tool)
134
- ? summarizeTaskAction(action.tool, action.arguments, action.result)
135
- : null) ??
136
- summarizeRunCodeAction(action.tool, action.arguments, action.result) ??
137
- summarizeWebAction(action.tool, action.arguments, action.result) ??
138
- summarizeArguments(action.arguments);
137
+ // 声明过的工具用 view.title。没有 view 的旧事件仍按工具名做摘要。
138
+ const summary = action.view?.declared
139
+ ? action.view.title
140
+ : ((action.tool === 'delegate_subagent'
141
+ ? summarizeDelegateSubagent(action.arguments)
142
+ : null) ??
143
+ (isTaskFamilyTool(action.tool)
144
+ ? summarizeTaskAction(action.tool, action.arguments, action.result)
145
+ : null) ??
146
+ summarizeRunCodeAction(action.tool, action.arguments, action.result) ??
147
+ summarizeWebAction(action.tool, action.arguments, action.result) ??
148
+ summarizeArguments(action.arguments));
139
149
  const badge = sandboxBadge(action.sandbox);
140
150
  return {
141
151
  id: action.id ?? `${action.tool}-${idx}`,
@@ -11,6 +11,7 @@ import {
11
11
  type MentionReference,
12
12
  } from './ChatInput';
13
13
  import { EmptyChat } from './EmptyChat';
14
+ import { EmptyChatHero } from './EmptyChatHero';
14
15
  import { MessageList } from './MessageList';
15
16
  import type { ExecutedAction } from './ExecutedActionsCard';
16
17
  import type { InspectTaskInput } from './executed-actions-model';
@@ -18,8 +19,11 @@ import type { ChildInfo } from './OrchestrationChildrenCard';
18
19
  import type { TurnBlock } from './turn-timeline';
19
20
  import type { TurnFile } from './turn-files';
20
21
  import type { LlmSpeedSnapshot } from './process-status';
22
+ import { ComposerStatusStack } from './composer-status-stack';
21
23
  import { SessionTodoList } from './SessionTodoList';
22
24
  import { resolveLatestSessionTodos } from './todo-list-model';
25
+ import { SessionOrchestrationFlow } from './SessionOrchestrationFlow';
26
+ import { resolveLatestSessionOrchestrationFlow } from './orchestration-flow-model';
23
27
  import {
24
28
  appendAttachmentRefs,
25
29
  collectImageAttachments,
@@ -57,9 +61,9 @@ export interface LocalChatPanelProps {
57
61
  isStreaming?: boolean;
58
62
  onSubmit: (input: { content: string; metadata?: Record<string, unknown> }) => void | Promise<void>;
59
63
  onCancel?: () => void;
60
- /** 轮中转向(streaming 期间 Enter):注入失败由 hook 兜底为排队/直发,见 ChatInput。 */
64
+ /** 轮中插队(streaming 期间 ⌘/Ctrl+Enter):注入失败由 hook 兜底为排队/直发。 */
61
65
  onSteer?: (text: string) => Promise<SteerOutcome>;
62
- /** W6-2 follow-up 排队:streaming 期间 ⌘/Ctrl+Enter 把文本排入待发队列。 */
66
+ /** W6-2 follow-up 排队:streaming 期间 Enter 把文本排入待发队列。 */
63
67
  onFollowUp?: (text: string) => void;
64
68
  /** 排队待发的 follow-up 文本列表。 */
65
69
  pendingFollowUps?: string[];
@@ -76,11 +80,11 @@ export interface LocalChatPanelProps {
76
80
  /** Slot rendered above the message list (header / agent picker / …). */
77
81
  header?: ReactNode;
78
82
  /**
79
- * 空会话 hero 布局:提供后,当 messages 为空且未在流式时,整个面板渲染为
80
- * 居中首屏(品牌标题 + 居中输入框,无 header)——与 /agent 落地页
81
- * (EmptyChatGate)同款视觉。首条消息落地后自动切回常规布局。
83
+ * 空会话 hero 布局:为 true 时,当 messages 为空且未在流式时,整个面板渲染为
84
+ * 居中首屏(品牌 logo + brand.homeHint + 居中输入框,无 header)——与 /agent
85
+ * 落地页(EmptyChatGate)同款视觉。首条消息落地后自动切回常规布局。
82
86
  */
83
- emptyHero?: { title: string; subtitle?: string };
87
+ emptyHero?: boolean;
84
88
  /** Override the default `EmptyChat` empty-state. */
85
89
  emptyState?: ReactNode;
86
90
  /** Disable the input (e.g. during initial chat-history load). */
@@ -327,6 +331,30 @@ export function LocalChatPanel({
327
331
  ],
328
332
  );
329
333
 
334
+ const sessionOrchestrationFlow = useMemo(
335
+ () =>
336
+ resolveLatestSessionOrchestrationFlow({
337
+ messages,
338
+ executedActionsByMessageId,
339
+ currentTurnActions,
340
+ currentTurnChildren,
341
+ orchestrationChildrenByMessageId,
342
+ timelineByMessageId,
343
+ currentTurnTimeline,
344
+ turnActive: isStreaming,
345
+ }),
346
+ [
347
+ messages,
348
+ executedActionsByMessageId,
349
+ currentTurnActions,
350
+ currentTurnChildren,
351
+ orchestrationChildrenByMessageId,
352
+ timelineByMessageId,
353
+ currentTurnTimeline,
354
+ isStreaming,
355
+ ],
356
+ );
357
+
330
358
  // Sync `chat-input-box-width` CSS var so bubbles match the input width
331
359
  // (mirrors the rendered input box width so message bubbles + input visually align in a column).
332
360
  const containerRef = useRef<HTMLDivElement>(null);
@@ -354,6 +382,17 @@ export function LocalChatPanel({
354
382
  // inputBox 并重新 observe,否则气泡宽度变量停在旧节点上。
355
383
  }, [showEmptyHero]);
356
384
 
385
+ const handleCancel = useCallback(() => {
386
+ if (pendingFollowUps && pendingFollowUps.length > 0) {
387
+ setInputValue((draft) =>
388
+ [...pendingFollowUps, draft.trim()]
389
+ .filter(Boolean)
390
+ .join('\n\n'),
391
+ );
392
+ }
393
+ onCancel?.();
394
+ }, [onCancel, pendingFollowUps]);
395
+
357
396
  const chatInputNode = (
358
397
  <>
359
398
  {attachmentError && (
@@ -370,7 +409,7 @@ export function LocalChatPanel({
370
409
  value={inputValue}
371
410
  onChange={setInputValue}
372
411
  onSubmit={handleSubmit}
373
- onCancel={onCancel}
412
+ onCancel={handleCancel}
374
413
  onSteer={onSteer}
375
414
  onFollowUp={onFollowUp}
376
415
  pendingFollowUps={pendingFollowUps}
@@ -390,7 +429,20 @@ export function LocalChatPanel({
390
429
  onOpenSettings={onOpenSettings}
391
430
  toolbarExtras={inputToolbarExtras}
392
431
  leadingChrome={inputLeadingChrome}
393
- trailingChrome={sessionTodos ? <SessionTodoList todos={sessionTodos} /> : undefined}
432
+ trailingChrome={
433
+ sessionOrchestrationFlow || sessionTodos ? (
434
+ <ComposerStatusStack>
435
+ {sessionOrchestrationFlow ? (
436
+ <SessionOrchestrationFlow
437
+ flow={sessionOrchestrationFlow}
438
+ chatId={chatId}
439
+ onInspectTask={onInspectTask}
440
+ />
441
+ ) : null}
442
+ {sessionTodos ? <SessionTodoList todos={sessionTodos} /> : null}
443
+ </ComposerStatusStack>
444
+ ) : undefined
445
+ }
394
446
  mode={mode}
395
447
  onModeChange={onModeChange}
396
448
  execPolicy={execPolicy}
@@ -416,16 +468,7 @@ export function LocalChatPanel({
416
468
  {showEmptyHero ? (
417
469
  <div className="flex min-h-0 flex-1 items-center justify-center p-3 sm:p-5">
418
470
  <div className="flex w-full max-w-3xl flex-col items-center gap-4">
419
- <div className="text-center">
420
- <h1 className="text-xl font-semibold tracking-tight text-agent-foreground">
421
- {emptyHero?.title}
422
- </h1>
423
- {emptyHero?.subtitle && (
424
- <p className="mt-1.5 text-xs text-agent-muted-foreground">
425
- {emptyHero.subtitle}
426
- </p>
427
- )}
428
- </div>
471
+ <EmptyChatHero />
429
472
  <div className="w-full">
430
473
  {inputBanner}
431
474
  {chatInputNode}
@@ -2,6 +2,7 @@ import {
2
2
  useCallback,
3
3
  useEffect,
4
4
  useLayoutEffect,
5
+ useMemo,
5
6
  useRef,
6
7
  useState,
7
8
  type ReactNode,
@@ -245,6 +246,43 @@ export function MessageList({
245
246
  scrollToBottom('auto');
246
247
  }, [lastMessageId, lastContentLen, lastTimelineSig, suggestedSig, scrollToBottom]);
247
248
 
249
+ // Group consecutive user messages into turns (方案 A).
250
+ // A group of 2+ user messages renders as a single connected card with
251
+ // dividers and "↳ 追加" badges, instead of separate disconnected bubbles.
252
+ type MessageItem =
253
+ | { type: 'user-group'; messages: ChatMessage[]; key: string }
254
+ | { type: 'assistant'; message: ChatMessage; originalIndex: number };
255
+
256
+ const groupedItems = useMemo<MessageItem[]>(() => {
257
+ const items: MessageItem[] = [];
258
+ let currentUserGroup: ChatMessage[] = [];
259
+
260
+ for (let i = 0; i < visibleMessages.length; i++) {
261
+ const msg = visibleMessages[i];
262
+ if (msg.role === 'user') {
263
+ currentUserGroup.push(msg);
264
+ } else {
265
+ if (currentUserGroup.length > 0) {
266
+ items.push({
267
+ type: 'user-group',
268
+ messages: currentUserGroup,
269
+ key: `ug-${currentUserGroup[0].id}`,
270
+ });
271
+ currentUserGroup = [];
272
+ }
273
+ items.push({ type: 'assistant', message: msg, originalIndex: i });
274
+ }
275
+ }
276
+ if (currentUserGroup.length > 0) {
277
+ items.push({
278
+ type: 'user-group',
279
+ messages: currentUserGroup,
280
+ key: `ug-${currentUserGroup[0].id}`,
281
+ });
282
+ }
283
+ return items;
284
+ }, [visibleMessages]);
285
+
248
286
  return (
249
287
  <div className="relative flex-1 overflow-hidden">
250
288
  <div
@@ -255,11 +293,45 @@ export function MessageList({
255
293
  emptyState ?? null
256
294
  ) : (
257
295
  <div className="mx-auto w-full max-w-3xl space-y-4">
258
- {visibleMessages.map((message, index) => {
259
- const isLast = index === visibleMessages.length - 1;
260
- if (message.role === 'user') {
261
- return <UserMessage key={message.id} message={message} agents={agents} chats={chats} />;
296
+ {groupedItems.map((item) => {
297
+ if (item.type === 'user-group') {
298
+ if (item.messages.length === 1) {
299
+ return (
300
+ <UserMessage
301
+ key={item.messages[0].id}
302
+ message={item.messages[0]}
303
+ agents={agents}
304
+ chats={chats}
305
+ />
306
+ );
307
+ }
308
+ // Multiple consecutive user messages: render as a unified combined card.
309
+ return (
310
+ <div
311
+ key={item.key}
312
+ className="group/user-group"
313
+ data-message-role="user-group"
314
+ >
315
+ <div className="flex justify-start">
316
+ <div className="mx-auto w-full max-w-[var(--chat-input-box-width)] overflow-hidden rounded-agent-lg border border-agent-border/80 bg-agent-muted/70 shadow-sm">
317
+ {item.messages.map((msg, idx) => (
318
+ <UserMessage
319
+ key={msg.id}
320
+ message={msg}
321
+ agents={agents}
322
+ chats={chats}
323
+ isGrouped
324
+ isAppended={idx > 0}
325
+ />
326
+ ))}
327
+ </div>
328
+ </div>
329
+ </div>
330
+ );
262
331
  }
332
+
333
+ const message = item.message;
334
+ const isLast = item.originalIndex === visibleMessages.length - 1;
263
335
  // Action attribution: prefer the per-message map (set when the
264
336
  // backend's `message_id` event reconciles the stream-time queue
265
337
  // with the DB id). The framework keeps the in-flight assistant's
@@ -309,7 +381,7 @@ export function MessageList({
309
381
  : undefined;
310
382
  let previousUser: ChatMessage | undefined;
311
383
  let previousUserCreatedAt: string | undefined;
312
- for (let i = index - 1; i >= 0; i -= 1) {
384
+ for (let i = item.originalIndex - 1; i >= 0; i -= 1) {
313
385
  if (visibleMessages[i].role === 'user') {
314
386
  previousUser = visibleMessages[i];
315
387
  previousUserCreatedAt = previousUser.createdAt;
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { LuChevronDown, LuRefreshCw } from 'react-icons/lu';
2
+ import { LuChevronDown, LuRefreshCw, LuSettings } from 'react-icons/lu';
3
3
  import { ModelCapabilityChips } from '@/components/settings/ModelCapabilityChips';
4
4
  import {
5
5
  getLlmModels,
@@ -32,6 +32,8 @@ export interface ModelPickerProps {
32
32
  reasoningEffort: string | null;
33
33
  onSelectModel: (model: string | null) => void;
34
34
  onSelectEffort: (effort: string | null) => void;
35
+ /** 打开 LLM 设置弹窗。 */
36
+ onOpenSettings?: () => void;
35
37
  disabled?: boolean;
36
38
  }
37
39
 
@@ -40,6 +42,7 @@ export function ModelPicker({
40
42
  reasoningEffort,
41
43
  onSelectModel,
42
44
  onSelectEffort,
45
+ onOpenSettings,
43
46
  disabled = false,
44
47
  }: ModelPickerProps) {
45
48
  const [catalog, setCatalog] = useState<GatewayModelCatalog | null>(null);
@@ -109,6 +112,7 @@ export function ModelPicker({
109
112
  <div className="relative flex min-w-0">
110
113
  <button
111
114
  type="button"
115
+ data-testid="model-picker"
112
116
  onClick={() => {
113
117
  const next = !modelMenuOpen;
114
118
  setModelMenuOpen(next);
@@ -155,6 +159,22 @@ export function ModelPicker({
155
159
  >
156
160
  <LuRefreshCw className={`h-3 w-3 ${loading ? 'animate-spin' : ''}`} />
157
161
  </button>
162
+ {onOpenSettings && (
163
+ <button
164
+ type="button"
165
+ onClick={() => {
166
+ onOpenSettings();
167
+ setModelMenuOpen(false);
168
+ setEffortMenuOpen(false);
169
+ }}
170
+ className="flex h-4 w-4 items-center justify-center rounded text-agent-muted-foreground transition-colors hover:text-agent-foreground"
171
+ title="LLM 设置"
172
+ aria-label="LLM 设置"
173
+ data-testid="chat-llm-settings"
174
+ >
175
+ <LuSettings className="h-3 w-3" />
176
+ </button>
177
+ )}
158
178
  </span>
159
179
  </div>
160
180
  <button