@steerable/agent-shell-web 0.6.56 → 0.6.58
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 +8 -2
- package/src/components/TerminalView.tsx +15 -12
- package/src/components/chat/ChatInput.tsx +6 -4
- package/src/components/settings/CliInstallSettingsPanel.tsx +67 -0
- package/src/layouts/AgentLayout.tsx +8 -0
- package/src/lib/host-bridge.ts +5 -0
- package/src/lib/host-paste.ts +38 -7
- package/src/lib/http-bridge.ts +2 -0
- package/src/lib/local-api.ts +8 -0
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.58",
|
|
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-
|
|
49
|
-
"@steerable/agent-
|
|
48
|
+
"@steerable/agent-protocol": "0.6.58",
|
|
49
|
+
"@steerable/agent-ui": "0.6.58"
|
|
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.6.
|
|
62
|
+
"@steerable/pack-sdk": "0.6.58"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"gen": "echo agent-shell-web",
|
|
@@ -881,7 +881,9 @@ export function AgentSidebar({
|
|
|
881
881
|
};
|
|
882
882
|
}, [bridge, handleOpenNewChat]);
|
|
883
883
|
|
|
884
|
-
// Auto-load next page when the chat scroller approaches the bottom.
|
|
884
|
+
// Auto-load next page when the chat scroller approaches the bottom. Project
|
|
885
|
+
// groups need the complete list: otherwise an older project chat outside the
|
|
886
|
+
// first page makes its project incorrectly render as empty.
|
|
885
887
|
// Throttled by `isLoadingMoreChats` inside the hook.
|
|
886
888
|
useEffect(() => {
|
|
887
889
|
const container = chatScrollRef.current;
|
|
@@ -892,7 +894,9 @@ export function AgentSidebar({
|
|
|
892
894
|
const nearBottom =
|
|
893
895
|
container.scrollTop + container.clientHeight >=
|
|
894
896
|
container.scrollHeight - 60;
|
|
895
|
-
|
|
897
|
+
const needsCompleteProjectGroups =
|
|
898
|
+
showProjectsChrome && projects.length > 0;
|
|
899
|
+
if (nearBottom || needsCompleteProjectGroups) void loadMoreChats();
|
|
896
900
|
};
|
|
897
901
|
container.addEventListener("scroll", maybeLoadMore, { passive: true });
|
|
898
902
|
const tickId = window.requestAnimationFrame(maybeLoadMore);
|
|
@@ -906,6 +910,8 @@ export function AgentSidebar({
|
|
|
906
910
|
loadMoreChats,
|
|
907
911
|
isRecentsExpanded,
|
|
908
912
|
normalizedChats.length,
|
|
913
|
+
projects.length,
|
|
914
|
+
showProjectsChrome,
|
|
909
915
|
]);
|
|
910
916
|
|
|
911
917
|
const hasElectron = hasHostBridge();
|
|
@@ -85,6 +85,18 @@ export function TerminalView({
|
|
|
85
85
|
// Cmd+V / Ctrl+Shift+V → 从剪贴板读出来 paste 到 PTY
|
|
86
86
|
// Cmd/Ctrl+A → 全选
|
|
87
87
|
const isMac = navigator.platform.toUpperCase().includes('MAC');
|
|
88
|
+
// WKWebView's navigator.clipboard.readText() is empty or prompts; the
|
|
89
|
+
// desktop host reads the OS pasteboard directly.
|
|
90
|
+
const pasteFromClipboard = () => {
|
|
91
|
+
const readText = bridge?.readClipboardText
|
|
92
|
+
? () => bridge.readClipboardText!()
|
|
93
|
+
: () => navigator.clipboard.readText();
|
|
94
|
+
void readText()
|
|
95
|
+
.then((text) => {
|
|
96
|
+
if (text) term.paste(text);
|
|
97
|
+
})
|
|
98
|
+
.catch(() => {});
|
|
99
|
+
};
|
|
88
100
|
term.attachCustomKeyEventHandler((event) => {
|
|
89
101
|
if (event.type !== 'keydown') return true;
|
|
90
102
|
const mod = isMac ? event.metaKey : event.ctrlKey;
|
|
@@ -103,12 +115,8 @@ export function TerminalView({
|
|
|
103
115
|
(isMac && mod && key === 'v') ||
|
|
104
116
|
(!isMac && event.ctrlKey && event.shiftKey && key === 'v')
|
|
105
117
|
) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.then((text) => {
|
|
109
|
-
if (text) term.paste(text);
|
|
110
|
-
})
|
|
111
|
-
.catch(() => {});
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
pasteFromClipboard();
|
|
112
120
|
return false;
|
|
113
121
|
}
|
|
114
122
|
if (mod && key === 'a' && !event.shiftKey) {
|
|
@@ -125,12 +133,7 @@ export function TerminalView({
|
|
|
125
133
|
void navigator.clipboard.writeText(sel).catch(() => {});
|
|
126
134
|
term.clearSelection();
|
|
127
135
|
} else {
|
|
128
|
-
|
|
129
|
-
.readText()
|
|
130
|
-
.then((text) => {
|
|
131
|
-
if (text) term.paste(text);
|
|
132
|
-
})
|
|
133
|
-
.catch(() => {});
|
|
136
|
+
pasteFromClipboard();
|
|
134
137
|
}
|
|
135
138
|
};
|
|
136
139
|
containerRef.current.addEventListener('contextmenu', onContextMenu);
|
|
@@ -1571,12 +1571,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
|
|
|
1571
1571
|
};
|
|
1572
1572
|
|
|
1573
1573
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
1574
|
-
|
|
1575
|
-
|
|
1574
|
+
// Cmd/Ctrl+V never reaches a paste event when the app menu owns the
|
|
1575
|
+
// shortcut, and WKWebView leaves that event's clipboard empty. Read the
|
|
1576
|
+
// OS pasteboard instead. Match `code` too: with an IME, `key` may not be "v".
|
|
1577
|
+
const pasteShortcut =
|
|
1576
1578
|
(event.metaKey || event.ctrlKey) &&
|
|
1577
1579
|
!event.altKey &&
|
|
1578
|
-
event.key.toLowerCase() === 'v'
|
|
1579
|
-
) {
|
|
1580
|
+
(event.key.toLowerCase() === 'v' || event.key === '\u0016' || event.code === 'KeyV');
|
|
1581
|
+
if (hostClipboardAvailable() && pasteShortcut) {
|
|
1580
1582
|
event.preventDefault();
|
|
1581
1583
|
requestHostPaste(event.currentTarget);
|
|
1582
1584
|
return;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { LuLoader, LuTerminal } from 'react-icons/lu';
|
|
3
|
+
import { t } from '@/i18n';
|
|
4
|
+
import { installCommandLineTool } from '@/lib/local-api';
|
|
5
|
+
|
|
6
|
+
export function CliInstallSettingsPanel(props: {
|
|
7
|
+
binName: string;
|
|
8
|
+
install?: () => Promise<{ path: string; onPath: boolean }>;
|
|
9
|
+
}) {
|
|
10
|
+
const install = props.install ?? (() => installCommandLineTool(props.binName));
|
|
11
|
+
const [running, setRunning] = useState(false);
|
|
12
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const onClick = async () => {
|
|
16
|
+
setRunning(true);
|
|
17
|
+
setMessage(null);
|
|
18
|
+
setError(null);
|
|
19
|
+
try {
|
|
20
|
+
const result = await install();
|
|
21
|
+
setMessage(result.onPath
|
|
22
|
+
? t('Installed {path}', { path: result.path })
|
|
23
|
+
: t('Installed {path}. Add its directory to PATH.', { path: result.path }));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
26
|
+
} finally {
|
|
27
|
+
setRunning(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-2 rounded-agent-md border border-agent-border bg-agent-card p-2.5" data-testid="cli-install-settings">
|
|
33
|
+
<p className="text-xs text-agent-muted-foreground">
|
|
34
|
+
{t('Install the command-line tool on your PATH so you can run it from a terminal.')}
|
|
35
|
+
</p>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
data-testid="cli-install-button"
|
|
39
|
+
onClick={() => void onClick()}
|
|
40
|
+
disabled={running}
|
|
41
|
+
className="inline-flex items-center gap-1.5 rounded-agent-md bg-agent-primary px-3 py-1.5 text-xs font-medium text-agent-primary-foreground transition hover:bg-agent-primary/90 disabled:opacity-50"
|
|
42
|
+
>
|
|
43
|
+
{running ? (
|
|
44
|
+
<>
|
|
45
|
+
<LuLoader className="h-3.5 w-3.5 animate-spin" />
|
|
46
|
+
{t('Installing…')}
|
|
47
|
+
</>
|
|
48
|
+
) : (
|
|
49
|
+
<>
|
|
50
|
+
<LuTerminal className="h-3.5 w-3.5" />
|
|
51
|
+
{t('Install command-line tool')}
|
|
52
|
+
</>
|
|
53
|
+
)}
|
|
54
|
+
</button>
|
|
55
|
+
{message && (
|
|
56
|
+
<p data-testid="cli-install-message" className="text-xs text-agent-foreground">
|
|
57
|
+
{message}
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
{error && (
|
|
61
|
+
<p data-testid="cli-install-error" className="text-xs text-agent-destructive">
|
|
62
|
+
{t('Install failed: {error}', { error })}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -188,6 +188,14 @@ function AgentLayoutContent() {
|
|
|
188
188
|
});
|
|
189
189
|
return unsubscribe;
|
|
190
190
|
}, []);
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const bridge = getHostBridge();
|
|
193
|
+
if (!bridge?.onStoreChanged) return;
|
|
194
|
+
const unsubscribe = bridge.onStoreChanged(() => {
|
|
195
|
+
void refreshChatsRef.current();
|
|
196
|
+
});
|
|
197
|
+
return unsubscribe;
|
|
198
|
+
}, []);
|
|
191
199
|
useEffect(() => {
|
|
192
200
|
const onImported = () => {
|
|
193
201
|
void refreshChatsRef.current();
|
package/src/lib/host-bridge.ts
CHANGED
|
@@ -239,6 +239,11 @@ export interface HostBridge {
|
|
|
239
239
|
onChatCreated?: (
|
|
240
240
|
callback: (payload: { chatId: string; agentId?: string | null }) => void,
|
|
241
241
|
) => () => void;
|
|
242
|
+
/**
|
|
243
|
+
* 另一个进程提交了宿主库。侧栏据此重新拉会话列表。
|
|
244
|
+
* 载荷为空;渲染端不依赖里面的字段。
|
|
245
|
+
*/
|
|
246
|
+
onStoreChanged?: (callback: () => void) => () => void;
|
|
242
247
|
/**
|
|
243
248
|
* 4.6a 后台任务状态推送。任务到达终态或 worktree 合并/丢弃完成时主进程
|
|
244
249
|
* 广播;载荷只有 chatId/taskId,面板收到后重新拉列表。
|
package/src/lib/host-paste.ts
CHANGED
|
@@ -42,10 +42,7 @@ export function requestHostPaste(target?: HTMLElement | null): void {
|
|
|
42
42
|
const readText = bridge?.readClipboardText;
|
|
43
43
|
if ((!readClipboard && !readText) || pending) return;
|
|
44
44
|
pending = true;
|
|
45
|
-
|
|
46
|
-
? readClipboard()
|
|
47
|
-
: readText!().then((text) => ({ text, files: [] as HostClipboardFile[] }));
|
|
48
|
-
void read
|
|
45
|
+
void readHostClipboard(readClipboard, readText)
|
|
49
46
|
.then((clip) => {
|
|
50
47
|
if (clip.files.length > 0) insertHostFiles(clip.files, target ?? null);
|
|
51
48
|
if (clip.text) insertHostText(clip.text, target ?? null);
|
|
@@ -58,6 +55,30 @@ export function requestHostPaste(target?: HTMLElement | null): void {
|
|
|
58
55
|
});
|
|
59
56
|
}
|
|
60
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Prefer the rich pasteboard (text, files, screenshots). Desktop capabilities
|
|
60
|
+
* may allow only `host_read_clipboard_text`; a denial of the rich command must
|
|
61
|
+
* still insert plain text.
|
|
62
|
+
*/
|
|
63
|
+
function readHostClipboard(
|
|
64
|
+
readClipboard: (() => Promise<HostClipboard>) | undefined,
|
|
65
|
+
readText: (() => Promise<string>) | undefined,
|
|
66
|
+
): Promise<HostClipboard> {
|
|
67
|
+
const plain = (): Promise<HostClipboard> => {
|
|
68
|
+
if (!readText) return Promise.reject(new Error('clipboard text is unavailable'));
|
|
69
|
+
return readText().then((text) => ({ text, files: [] }));
|
|
70
|
+
};
|
|
71
|
+
if (!readClipboard) return plain();
|
|
72
|
+
return Promise.resolve()
|
|
73
|
+
.then(() => readClipboard())
|
|
74
|
+
.catch((error: unknown) => {
|
|
75
|
+
if (!readText) throw error;
|
|
76
|
+
return plain().catch(() => {
|
|
77
|
+
throw error;
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
export function hostClipboardAvailable(): boolean {
|
|
62
83
|
const bridge = getHostBridge();
|
|
63
84
|
return (
|
|
@@ -67,13 +88,23 @@ export function hostClipboardAvailable(): boolean {
|
|
|
67
88
|
|
|
68
89
|
function isEditable(target: EventTarget | null): target is HTMLElement {
|
|
69
90
|
return (
|
|
70
|
-
target
|
|
71
|
-
target instanceof
|
|
72
|
-
|
|
91
|
+
!isTerminalInput(target) &&
|
|
92
|
+
(target instanceof HTMLInputElement ||
|
|
93
|
+
target instanceof HTMLTextAreaElement ||
|
|
94
|
+
(target instanceof HTMLElement && target.isContentEditable))
|
|
73
95
|
);
|
|
74
96
|
}
|
|
75
97
|
|
|
98
|
+
/**
|
|
99
|
+
* xterm's hidden textarea. The terminal pastes into its PTY itself; text
|
|
100
|
+
* written into this textarea never reaches the shell.
|
|
101
|
+
*/
|
|
102
|
+
function isTerminalInput(target: EventTarget | null): boolean {
|
|
103
|
+
return target instanceof HTMLTextAreaElement && target.closest('.xterm') !== null;
|
|
104
|
+
}
|
|
105
|
+
|
|
76
106
|
function focusedEditable(): HTMLElement | null {
|
|
107
|
+
if (isTerminalInput(document.activeElement)) return null;
|
|
77
108
|
if (isEditable(document.activeElement)) return document.activeElement;
|
|
78
109
|
if (lastEditable?.isConnected) return lastEditable;
|
|
79
110
|
return null;
|
package/src/lib/http-bridge.ts
CHANGED
|
@@ -266,6 +266,8 @@ export function createHttpBridge(): HostBridge {
|
|
|
266
266
|
onChatCreated: (callback) =>
|
|
267
267
|
subscribeChannel<{ chatId: string; agentId?: string | null }>('chat-created', callback),
|
|
268
268
|
|
|
269
|
+
onStoreChanged: (callback) => subscribeChannel('store:changed', () => callback()),
|
|
270
|
+
|
|
269
271
|
onTaskUpdated: (callback) =>
|
|
270
272
|
subscribeChannel<{ chatId: string; taskId: string }>(
|
|
271
273
|
'task-updated',
|
package/src/lib/local-api.ts
CHANGED
|
@@ -767,6 +767,14 @@ export interface DiagnoseResult {
|
|
|
767
767
|
hint: string | null;
|
|
768
768
|
}
|
|
769
769
|
|
|
770
|
+
export async function installCommandLineTool(name: string) {
|
|
771
|
+
return bridge().localBackend.request<{ path: string; onPath: boolean }>({
|
|
772
|
+
method: 'POST',
|
|
773
|
+
path: '/api/v2/cli/install',
|
|
774
|
+
body: { name },
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
770
778
|
export async function diagnoseLlmConnection(input: {
|
|
771
779
|
baseUrl?: string;
|
|
772
780
|
apiKey?: string;
|