@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 +4 -4
- package/src/components/AgentSidebar.tsx +1 -0
- package/src/components/chat/ApprovalModal.tsx +10 -0
- package/src/components/chat/ExecutedActionsCard.tsx +39 -8
- package/src/components/chat/executed-actions-model.ts +119 -1
- package/src/layouts/AgentLayout.tsx +1 -1
- package/src/layouts/RightPanelTabBar.tsx +1 -1
- package/src/layouts/right-panel-tabs.ts +1 -1
- package/src/pages/SettingsPage.tsx +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell-web",
|
|
3
|
-
"version": "0.7.
|
|
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.
|
|
49
|
-
"@steerable/agent-ui": "0.7.
|
|
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.
|
|
62
|
+
"@steerable/pack-sdk": "0.7.4"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"gen": "echo agent-shell-web",
|
|
@@ -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
|
|
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 =
|
|
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={
|
|
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={
|
|
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
|
+
}
|
|
@@ -143,7 +143,7 @@ export function normalizeRightPanelEntry(
|
|
|
143
143
|
*
|
|
144
144
|
* 兼容这些历史格式:
|
|
145
145
|
* - v2:{"version":2,"sessions":{"<chatId>":{"tabs":[...],"activeTabId":"..."}}};
|
|
146
|
-
* - 旧多标签:{"<chatId>": { "tabs": ["
|
|
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>
|