@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.57",
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-ui": "0.6.57",
49
- "@steerable/agent-protocol": "0.6.57"
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.57"
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
- if (nearBottom) void loadMoreChats();
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
- void navigator.clipboard
107
- .readText()
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
- void navigator.clipboard
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
- if (
1575
- hostClipboardAvailable() &&
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;
@@ -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
- const read = readClipboard
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 instanceof HTMLInputElement ||
71
- target instanceof HTMLTextAreaElement ||
72
- (target instanceof HTMLElement && target.isContentEditable)
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;