@steerable/agent-shell-web 0.6.57 → 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
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;
|
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;
|