@steerable/agent-shell-web 0.7.1 → 0.7.4

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.7.1",
3
+ "version": "0.7.4",
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.7.1",
49
- "@steerable/agent-ui": "0.7.1"
48
+ "@steerable/agent-protocol": "0.7.4",
49
+ "@steerable/agent-ui": "0.7.4"
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.7.1"
62
+ "@steerable/pack-sdk": "0.7.4"
63
63
  },
64
64
  "scripts": {
65
65
  "gen": "echo agent-shell-web",
@@ -1932,6 +1932,7 @@ export function AgentSidebar({
1932
1932
  void forkThisChat(target);
1933
1933
  }}
1934
1934
  onDelete={() => {
1935
+ chatMenu.anchor.focus();
1935
1936
  setPendingDeleteChat({ id: chatMenu.id, title: chatMenu.title });
1936
1937
  setChatMenu(null);
1937
1938
  }}
@@ -167,6 +167,16 @@ export function ApprovalPromptMenu({
167
167
  </span>
168
168
  ) : null}
169
169
  </>
170
+ ) : isBrowser && current.toolName === 'browser_navigate' ? (
171
+ <>
172
+ {t('Agent requests to open a page')}
173
+ <span className="mx-1 font-mono">{current.toolName}</span>
174
+ {current.categoryLabel ? (
175
+ <span className="text-agent-muted-foreground">
176
+ {t('({category})', { category: categoryName(current.categoryLabel) })}
177
+ </span>
178
+ ) : null}
179
+ </>
170
180
  ) : (
171
181
  <>
172
182
  {t('Agent requests to run')}
@@ -21,6 +21,9 @@ import {
21
21
  isTaskFamilyTool,
22
22
  inspectableTaskFromAction,
23
23
  shortTaskId,
24
+ askReplay,
25
+ askRowSummary,
26
+ egressRowSummary,
24
27
  type InspectTaskInput,
25
28
  } from './executed-actions-model';
26
29
  import {
@@ -60,8 +63,8 @@ export interface ExecutedAction {
60
63
  * port-only egress); none = no backend on this platform.
61
64
  */
62
65
  sandbox?: { backend?: string; enforcement: string };
63
- /** Set when a session allow covered this call, so the row can say so. */
64
- approval?: { kind?: string; category?: string; cached?: boolean; waitMs?: number };
66
+ /** Set when a session allow or an unattended allow covered this call, so the row can say so. */
67
+ approval?: { kind?: string; category?: string; cached?: boolean; waitMs?: number; auto?: boolean };
65
68
  /**
66
69
  * 工具注册时声明的卡片。`declared` 为真时行标题用 `title`,
67
70
  * 不再按工具名猜测。旧事件没有这个字段,仍走原来的摘要。
@@ -108,7 +111,7 @@ function summarizeArguments(args: unknown): string | null {
108
111
  if (typeof args === 'string') return args.length > 60 ? args.slice(0, 57) + '…' : args;
109
112
  if (typeof args !== 'object') return String(args);
110
113
  const obj = args as Record<string, unknown>;
111
- for (const key of ['command', 'cmd', 'query', 'path', 'file', 'target', 'message']) {
114
+ for (const key of ['command', 'cmd', 'url', 'query', 'path', 'file', 'target', 'message']) {
112
115
  const v = obj[key];
113
116
  if (typeof v === 'string' && v.length > 0) {
114
117
  return v.length > 60 ? v.slice(0, 57) + '…' : v;
@@ -129,10 +132,16 @@ function sandboxBadge(sandbox: ExecutedAction['sandbox']): string | null {
129
132
  }
130
133
  }
131
134
 
132
- function sessionAllowNote(action: ExecutedAction): string | undefined {
135
+ function toolProgressNote(action: ExecutedAction): string | undefined {
133
136
  if (action.approval?.kind === 'allow_for_session' && action.approval.cached === true) {
134
137
  return t('Allowed for this chat');
135
138
  }
139
+ if (action.tool === 'ask_user' && askReplay(action.result)?.unattended && deriveStatus(action) !== 'failed') {
140
+ return t('Answered automatically');
141
+ }
142
+ if (action.approval?.auto === true && deriveStatus(action) !== 'failed') {
143
+ return t('Auto');
144
+ }
136
145
  return undefined;
137
146
  }
138
147
 
@@ -145,8 +154,13 @@ function actionToTool(action: ExecutedAction, idx: number): ToolExecutionPayload
145
154
  ? humanizeDelegateError(env.error)
146
155
  : env.error
147
156
  : null;
157
+ // 追问行用第一条问题。出网行用主机名。已保存的 view.title 往往只是开场白或工具名。
158
+ const asked = askRowSummary(action);
159
+ const egress = egressRowSummary(action);
148
160
  // 声明过的工具用 view.title。没有 view 的旧事件仍按工具名做摘要。
149
- const summary = action.view?.declared
161
+ const summary = asked
162
+ || egress
163
+ || (action.view?.declared
150
164
  ? action.view.title
151
165
  : ((action.tool === 'delegate_subagent'
152
166
  ? summarizeDelegateSubagent(action.arguments)
@@ -156,7 +170,7 @@ function actionToTool(action: ExecutedAction, idx: number): ToolExecutionPayload
156
170
  : null) ??
157
171
  summarizeRunCodeAction(action.tool, action.arguments, action.result) ??
158
172
  summarizeWebAction(action.tool, action.arguments, action.result) ??
159
- summarizeArguments(action.arguments));
173
+ summarizeArguments(action.arguments)));
160
174
  const badge = sandboxBadge(action.sandbox);
161
175
  return {
162
176
  id: action.id ?? `${action.tool}-${idx}`,
@@ -177,6 +191,20 @@ function actionToTool(action: ExecutedAction, idx: number): ToolExecutionPayload
177
191
  * unified `diff` (local_edit_file), render it as a coloured diff; otherwise
178
192
  * fall back to the default JSON/text view.
179
193
  */
194
+ function renderAskOutput(output: unknown): ReactNode {
195
+ const replay = askReplay(output);
196
+ if (!replay) return renderActionOutput(output);
197
+ return (
198
+ <div className="space-y-1 text-[11px] text-agent-foreground">
199
+ {replay.lines.map((line, index) => (
200
+ <p key={`${index}-${line}`} className="whitespace-pre-wrap break-words">
201
+ {line}
202
+ </p>
203
+ ))}
204
+ </div>
205
+ );
206
+ }
207
+
180
208
  function renderActionOutput(output: unknown): ReactNode {
181
209
  if (output && typeof output === 'object') {
182
210
  const obj = output as Record<string, unknown>;
@@ -444,6 +472,9 @@ function toolCardExtras(
444
472
  renderOutput: renderDelegateOutput,
445
473
  };
446
474
  }
475
+ if (action.tool === 'ask_user') {
476
+ return { renderOutput: renderAskOutput };
477
+ }
447
478
  if (isTaskFamilyTool(action.tool)) {
448
479
  return {
449
480
  label: taskToolLabel(action.tool, action.arguments) ?? undefined,
@@ -543,7 +574,7 @@ export function ToolsFlow({
543
574
  renderOutput={extras.renderOutput ?? renderActionOutput}
544
575
  onActivate={inspect.onActivate}
545
576
  activateTitle={inspect.activateTitle}
546
- note={sessionAllowNote(action)}
577
+ note={toolProgressNote(action)}
547
578
  className="rounded-none border-0 border-t border-agent-border first:border-t-0"
548
579
  />
549
580
  );
@@ -606,7 +637,7 @@ export function ExecutedActionsCard({
606
637
  renderOutput={extras.renderOutput ?? renderActionOutput}
607
638
  onActivate={inspect.onActivate}
608
639
  activateTitle={inspect.activateTitle}
609
- note={sessionAllowNote(action)}
640
+ note={toolProgressNote(action)}
610
641
  className="rounded-none border-0 border-t border-agent-border first:border-t-0"
611
642
  />
612
643
  );
@@ -121,7 +121,7 @@ export interface ExecutedActionLike {
121
121
  arguments?: unknown;
122
122
  result?: unknown;
123
123
  sandbox?: { backend?: string; enforcement: string };
124
- approval?: { kind?: string; cached?: boolean };
124
+ approval?: { kind?: string; cached?: boolean; auto?: boolean };
125
125
  view?: { title?: string; declared?: boolean };
126
126
  durationMs?: number;
127
127
  }
@@ -618,6 +618,7 @@ function approvalFromNestedResult(result: unknown): ExecutedActionLike['approval
618
618
  return {
619
619
  kind: marker.kind,
620
620
  ...(marker.cached === true ? { cached: true } : {}),
621
+ ...(marker.auto === true ? { auto: true } : {}),
621
622
  };
622
623
  }
623
624
 
@@ -644,3 +645,120 @@ export function expandRunCodeActions<T extends ExecutedActionLike>(actions: T[])
644
645
  }
645
646
  return out;
646
647
  }
648
+
649
+ export interface AskReplay {
650
+ unattended: boolean;
651
+ lines: string[];
652
+ }
653
+
654
+ /** First question, used as the folded tool-row title. The intro is only a fallback. */
655
+ export function askRowSummary(action: { tool?: string; arguments?: unknown; result?: unknown }): string {
656
+ if (action.tool !== 'ask_user') return '';
657
+ return firstQuestionText(action.result) || firstQuestionText(action.arguments);
658
+ }
659
+
660
+ /** Host on a settled egress row. Ports 80 and 443 are omitted. */
661
+ export function egressRowSummary(action: { tool?: string; arguments?: unknown }): string {
662
+ if (action.tool !== 'network_egress') return '';
663
+ const value = action.arguments;
664
+ if (!value || typeof value !== 'object') return '';
665
+ const record = value as { host?: unknown; port?: unknown };
666
+ const host = typeof record.host === 'string' ? record.host : '';
667
+ if (!host) return '';
668
+ const port = typeof record.port === 'number' ? record.port : null;
669
+ if (port === null || port === 80 || port === 443) return host;
670
+ return `${host}:${port}`;
671
+ }
672
+
673
+ /** Questions and answers from an ask_user result, including a JSON preview. */
674
+ export function askReplay(result: unknown): AskReplay | null {
675
+ const record = unwrapAsk(result);
676
+ if (!record) return null;
677
+ const asked: string[] = [];
678
+ const rendered: string[] = [];
679
+ for (const question of record.questions) {
680
+ if (!question || typeof question !== 'object') continue;
681
+ const row = question as { id?: unknown; text?: unknown };
682
+ const text = typeof row.text === 'string' ? row.text.trim() : '';
683
+ if (!text) continue;
684
+ asked.push(text);
685
+ const id = typeof row.id === 'string' ? row.id : '';
686
+ rendered.push(id ? answerText(record.answers[id]) : '');
687
+ }
688
+ if (asked.length === 0) return null;
689
+ const filled = rendered.filter((value) => value.length > 0);
690
+ const first = filled[0];
691
+ const shared = first !== undefined && filled.length === asked.length && filled.every((value) => value === first);
692
+ const lines = shared
693
+ ? [...asked.map((text) => t('Question {text}', { text })), t('Answer {text}', { text: first })]
694
+ : asked.flatMap((text, index) => {
695
+ const answer = rendered[index] ?? '';
696
+ return answer
697
+ ? [t('Question {text}', { text }), t('Answer {text}', { text: answer })]
698
+ : [t('Question {text}', { text })];
699
+ });
700
+ return { unattended: record.unattended === true, lines };
701
+ }
702
+
703
+ function unwrapAsk(result: unknown): { questions: unknown[]; answers: Record<string, unknown>; unattended?: boolean } | null {
704
+ if (typeof result === 'string') {
705
+ const trimmed = result.trim();
706
+ if (!trimmed.startsWith('{')) return null;
707
+ try {
708
+ const parsed: unknown = JSON.parse(trimmed);
709
+ return unwrapAsk(parsed);
710
+ } catch {
711
+ return null;
712
+ }
713
+ }
714
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return null;
715
+ const record = result as {
716
+ questions?: unknown;
717
+ answers?: unknown;
718
+ data?: unknown;
719
+ value?: unknown;
720
+ unattended?: unknown;
721
+ };
722
+ if (Array.isArray(record.questions) && record.answers && typeof record.answers === 'object' && !Array.isArray(record.answers)) {
723
+ return {
724
+ questions: record.questions,
725
+ answers: record.answers as Record<string, unknown>,
726
+ ...(record.unattended === true ? { unattended: true } : {}),
727
+ };
728
+ }
729
+ if (record.value && typeof record.value === 'object') {
730
+ const inner = unwrapAsk(record.value);
731
+ if (inner) return record.unattended === true ? { ...inner, unattended: true } : inner;
732
+ }
733
+ if (record.data && typeof record.data === 'object') {
734
+ const inner = unwrapAsk(record.data);
735
+ if (inner) return record.unattended === true ? { ...inner, unattended: true } : inner;
736
+ }
737
+ return null;
738
+ }
739
+
740
+ function firstQuestionText(value: unknown): string {
741
+ const unwrapped = unwrapAsk(value);
742
+ const questions = unwrapped?.questions ?? questionList(value);
743
+ for (const question of questions) {
744
+ if (!question || typeof question !== 'object') continue;
745
+ const text = (question as { text?: unknown }).text;
746
+ if (typeof text === 'string' && text.trim()) return text.trim();
747
+ }
748
+ return '';
749
+ }
750
+
751
+ function questionList(value: unknown): unknown[] {
752
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
753
+ const questions = (value as { questions?: unknown }).questions;
754
+ return Array.isArray(questions) ? questions : [];
755
+ }
756
+
757
+ function answerText(value: unknown): string {
758
+ if (typeof value === 'string') return value.trim();
759
+ if (!Array.isArray(value)) return '';
760
+ return value
761
+ .filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
762
+ .map((item) => item.trim())
763
+ .join(', ');
764
+ }
@@ -365,7 +365,7 @@ function AgentLayoutContent() {
365
365
  return {
366
366
  kind,
367
367
  contentId,
368
- title: request.title || contentTitle(contentId, slot.title),
368
+ title: request.title || contentTitle(contentId, t(slot.title)),
369
369
  };
370
370
  }, [packChatSlots]);
371
371
 
@@ -192,7 +192,7 @@ function unopenedPanels(
192
192
  const Icon = slot.Icon;
193
193
  entries.push({
194
194
  id: slot.slotId,
195
- title: slot.title,
195
+ title: t(slot.title),
196
196
  testId: `header-slot-${slot.slotId}`,
197
197
  icon: <Icon className="h-3.5 w-3.5 shrink-0" />,
198
198
  });
@@ -143,7 +143,7 @@ export function normalizeRightPanelEntry(
143
143
  *
144
144
  * 兼容这些历史格式:
145
145
  * - v2:{"version":2,"sessions":{"<chatId>":{"tabs":[...],"activeTabId":"..."}}};
146
- * - 旧多标签:{"<chatId>": { "tabs": ["ppt", "terminal"], "active": "ppt" }};
146
+ * - 旧多标签:{"<chatId>": { "tabs": ["<slotId>", "terminal"], "active": "<slotId>" }};
147
147
  * - 单值映射:{"<chatId>": "terminal" | "<slotId>"} → 一个标签;
148
148
  * - 整个值就是单个字符串 → 迁到当前会话;
149
149
  * - 更老:只记录终端是否打开过的布尔 key。
@@ -220,7 +220,7 @@ export function SettingsPage() {
220
220
  for (const panel of packPanels) {
221
221
  list.push({
222
222
  id: panel.panelId,
223
- label: panel.title,
223
+ label: t(panel.title),
224
224
  Icon: panel.Icon ?? LuSettings,
225
225
  });
226
226
  }
@@ -503,7 +503,7 @@ export function SettingsPage() {
503
503
  {panel.Icon ? (
504
504
  <panel.Icon className="h-3.5 w-3.5 text-agent-muted-foreground" />
505
505
  ) : null}
506
- {panel.title}
506
+ {t(panel.title)}
507
507
  </h2>
508
508
  <panel.Component />
509
509
  </section>