@hienlh/ppm 0.17.37 → 0.17.38

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/http-api.md +1 -1
  4. package/dist/web/assets/{ai-resource-editor-DAIaB4mX.js → ai-resource-editor-ChT9FzxT.js} +1 -1
  5. package/dist/web/assets/{audio-preview-Df6_blMs.js → audio-preview-CSK6nxNy.js} +1 -1
  6. package/dist/web/assets/chat-tab-DGM7js5J.js +13 -0
  7. package/dist/web/assets/code-editor-BvLxm1bf.js +10 -0
  8. package/dist/web/assets/{conflict-editor-BqMdj782.js → conflict-editor-Bc063EfD.js} +3 -3
  9. package/dist/web/assets/context-menu-DIiZdfbE.js +1 -0
  10. package/dist/web/assets/{csv-preview-BC_BuOzK.js → csv-preview-ft27dubA.js} +1 -1
  11. package/dist/web/assets/{database-viewer-DZQDQOye.js → database-viewer-Bdj1ferk.js} +1 -1
  12. package/dist/web/assets/{diff-viewer-CaKD5pR5.js → diff-viewer-C46_ZovZ.js} +1 -1
  13. package/dist/web/assets/{docx-preview-MBY4op8y.js → docx-preview-CLDLTD8N.js} +1 -1
  14. package/dist/web/assets/{extension-webview-CMtucoeg.js → extension-webview-BSvKnN1E.js} +1 -1
  15. package/dist/web/assets/{git-log-panel-DWZUIupS.js → git-log-panel-GP8XaTlA.js} +1 -1
  16. package/dist/web/assets/{glide-data-grid-SLs8Xp6G.js → glide-data-grid-On_pS9Y7.js} +4 -4
  17. package/dist/web/assets/group-chat-tab-DmYt9GfT.js +1 -0
  18. package/dist/web/assets/{image-preview-lRcpEoEk.js → image-preview-BWNTrufr.js} +1 -1
  19. package/dist/web/assets/index-Crnizc26.js +50 -0
  20. package/dist/web/assets/index-opWDpE6A.css +2 -0
  21. package/dist/web/assets/keybindings-store-B4EWnplE.js +1 -0
  22. package/dist/web/assets/{markdown-renderer-DkYo6o9f.js → markdown-renderer-Bhv5w9Op.js} +1 -1
  23. package/dist/web/assets/{markdown-renderer-D9r_4-qe.js → markdown-renderer-suZuQvya.js} +2 -2
  24. package/dist/web/assets/notification-store-B3zEJT3q.js +1 -0
  25. package/dist/web/assets/{pdf-preview-D17y9l8U.js → pdf-preview-DcP6Tj7N.js} +1 -1
  26. package/dist/web/assets/{postgres-viewer-DgpX_Kyl.js → postgres-viewer-BOlXoo4R.js} +2 -2
  27. package/dist/web/assets/settings-tab-BqJJqL1l.js +1 -0
  28. package/dist/web/assets/{sql-query-editor-BIeAQego.js → sql-query-editor-h4zuID_d.js} +1 -1
  29. package/dist/web/assets/{sqlite-viewer-SipxlqWM.js → sqlite-viewer-D5X8iWUw.js} +1 -1
  30. package/dist/web/assets/{system-monitor-tab-Cp-rP5HZ.js → system-monitor-tab-DL-viZmD.js} +1 -1
  31. package/dist/web/assets/terminal-tab-BVW9MfC3.js +3 -0
  32. package/dist/web/assets/tool-cards-zK7OP3DS.js +7 -0
  33. package/dist/web/assets/{use-monaco-theme-Bd1GBgB3.js → use-monaco-theme-Bk-dETGV.js} +1 -1
  34. package/dist/web/assets/{video-preview-DT8B2-re.js → video-preview-C6bKCBvz.js} +1 -1
  35. package/dist/web/index.html +3 -3
  36. package/dist/web/sw.js +1 -1
  37. package/package.json +1 -1
  38. package/src/web/app.tsx +6 -2
  39. package/src/web/components/layout/mobile-nav.tsx +6 -3
  40. package/src/web/components/terminal/terminal-links-sheet.tsx +66 -0
  41. package/src/web/components/terminal/terminal-mobile-toolbar.tsx +107 -0
  42. package/src/web/components/terminal/terminal-tab.tsx +57 -86
  43. package/src/web/components/ui/mobile-bottom-sheet.tsx +20 -3
  44. package/src/web/hooks/use-terminal-touch-selection.ts +87 -0
  45. package/src/web/hooks/use-terminal.ts +53 -1
  46. package/dist/web/assets/chat-tab-yPXWmX01.js +0 -13
  47. package/dist/web/assets/code-editor-CSDB2W8N.js +0 -10
  48. package/dist/web/assets/group-chat-tab-Celp6ihW.js +0 -1
  49. package/dist/web/assets/index-BkH2b_6W.js +0 -50
  50. package/dist/web/assets/index-eW0VEaRF.css +0 -2
  51. package/dist/web/assets/keybindings-store-CtfEQL3U.js +0 -1
  52. package/dist/web/assets/mobile-bottom-sheet-BwPBKZON.js +0 -1
  53. package/dist/web/assets/notification-store-OF6L8lWu.js +0 -1
  54. package/dist/web/assets/settings-tab-Cou9P1gO.js +0 -1
  55. package/dist/web/assets/terminal-tab-C9T_X4oo.js +0 -3
  56. package/dist/web/assets/tool-cards-BFVmoF_j.js +0 -7
@@ -1,51 +1,32 @@
1
1
  import { useRef, useEffect, useState, useCallback, memo } from "react";
2
2
  import { useTerminal } from "@/hooks/use-terminal";
3
+ import { useTerminalTouchSelection } from "@/hooks/use-terminal-touch-selection";
3
4
  import { cn } from "@/lib/utils";
4
5
  import { copyToClipboard } from "@/lib/clipboard";
5
- import { Copy, ClipboardPaste, RotateCcw, MessageSquare } from "lucide-react";
6
+ import { RotateCcw, MessageSquare } from "lucide-react";
6
7
  import "@xterm/xterm/css/xterm.css";
7
8
  import { toast } from "sonner";
8
9
 
9
10
  import { usePanelStore } from "@/stores/panel-store";
11
+ import { TerminalMobileToolbar } from "./terminal-mobile-toolbar";
12
+ import { TerminalLinksSheet } from "./terminal-links-sheet";
10
13
 
11
14
  interface TerminalTabProps {
12
15
  metadata?: Record<string, unknown>;
13
16
  tabId?: string;
14
17
  }
15
18
 
16
- const MOBILE_KEYS = [
17
- { label: "Tab", value: "\t" },
18
- { label: "Esc", value: "\x1b" },
19
- { label: "Ctrl", value: null, isModifier: true },
20
- { label: "\u2191", value: "\x1b[A" },
21
- { label: "\u2193", value: "\x1b[B" },
22
- { label: "\u2190", value: "\x1b[D" },
23
- { label: "\u2192", value: "\x1b[C" },
24
- { label: "^C", value: "\x03" },
25
- ] as const;
26
-
27
19
  export const TerminalTab = memo(function TerminalTab({ metadata, tabId }: TerminalTabProps) {
28
20
  const sessionId = (metadata?.sessionId as string) ?? "new";
29
21
  const projectName = metadata?.projectName as string | undefined;
30
22
  const containerRef = useRef<HTMLDivElement>(null);
31
23
 
32
- const { connected, reconnecting, exited, sendData, getSelection, getLastCommandOutput, restart } = useTerminal({ sessionId, projectName, containerRef, tabId });
24
+ const { connected, reconnecting, exited, sendData, getSelection, getLastCommandOutput, getBufferUrls, restart } = useTerminal({ sessionId, projectName, containerRef, tabId });
33
25
  const [ctrlMode, setCtrlMode] = useState(false);
34
- const [viewportHeight, setViewportHeight] = useState<number | null>(null);
35
-
36
- // Adjust height when mobile keyboard opens
37
- useEffect(() => {
38
- const vv = window.visualViewport;
39
- if (!vv) return;
40
-
41
- function handleResize() {
42
- if (!vv) return;
43
- setViewportHeight(vv.height);
44
- }
26
+ const [selectMode, setSelectMode] = useState(false);
27
+ const [linksOpen, setLinksOpen] = useState(false);
45
28
 
46
- vv.addEventListener("resize", handleResize);
47
- return () => vv.removeEventListener("resize", handleResize);
48
- }, []);
29
+ useTerminalTouchSelection(containerRef, selectMode);
49
30
 
50
31
  const focusTerminal = useCallback(() => {
51
32
  const termElement = containerRef.current?.querySelector(
@@ -54,18 +35,19 @@ export const TerminalTab = memo(function TerminalTab({ metadata, tabId }: Termin
54
35
  termElement?.focus();
55
36
  }, []);
56
37
 
57
- // On touch devices xterm swallows the synthesized `click`, so an onClick
58
- // handler never fires and the soft keyboard never opens. Bind `touchend` in
59
- // the capture phase (runs before xterm's own handlers) and focus the hidden
60
- // textarea there this happens inside a real user gesture, so mobile
61
- // browsers reliably raise the on-screen keyboard.
38
+ // Raising the soft keyboard needs focus() to run inside a gesture the browser
39
+ // recognises, and xterm's own mousedown handling does not reliably qualify.
40
+ // Capture-phase `touchend` runs before xterm's handlers and is unambiguously a
41
+ // gesture, so the keyboard opens even when the click path is unreliable.
42
+ // Skipped in select mode: a drag there is a selection gesture, and raising the
43
+ // keyboard would both shrink the viewport mid-drag and clear the selection.
62
44
  useEffect(() => {
63
45
  const container = containerRef.current;
64
- if (!container) return;
46
+ if (!container || selectMode) return;
65
47
  const onTouchEnd = () => focusTerminal();
66
48
  container.addEventListener("touchend", onTouchEnd, { capture: true });
67
49
  return () => container.removeEventListener("touchend", onTouchEnd, { capture: true });
68
- }, [focusTerminal]);
50
+ }, [focusTerminal, selectMode]);
69
51
 
70
52
  const sendKey = useCallback(
71
53
  (value: string) => {
@@ -86,12 +68,22 @@ export const TerminalTab = memo(function TerminalTab({ metadata, tabId }: Termin
86
68
  [ctrlMode, sendData, focusTerminal],
87
69
  );
88
70
 
71
+ // xterm starts a selection from mousedown and has no touch equivalent, so on a
72
+ // phone getSelection() is always empty. Falling back to the last command's
73
+ // output keeps the button useful instead of silently doing nothing.
89
74
  const handleCopy = useCallback(async () => {
90
75
  const selection = getSelection();
91
- if (selection) {
92
- await copyToClipboard(selection);
76
+ const text = selection || getLastCommandOutput();
77
+ if (!text.trim()) {
78
+ toast("Nothing to copy", { duration: 1500 });
79
+ return;
93
80
  }
94
- }, [getSelection]);
81
+ const ok = await copyToClipboard(text);
82
+ toast[ok ? "success" : "error"](
83
+ ok ? (selection ? "Selection copied" : "Last output copied") : "Could not copy",
84
+ { duration: 1500 },
85
+ );
86
+ }, [getSelection, getLastCommandOutput]);
95
87
 
96
88
  const handlePaste = useCallback(async () => {
97
89
  try {
@@ -101,7 +93,10 @@ export const TerminalTab = memo(function TerminalTab({ metadata, tabId }: Termin
101
93
  focusTerminal();
102
94
  }
103
95
  } catch {
104
- // Clipboard permission denied
96
+ // readText() needs a secure context and an explicit permission grant, and
97
+ // Safari rejects it outright when the tap is not treated as a gesture.
98
+ // Say so — a silent catch reads as a dead button.
99
+ toast.error("Clipboard blocked — paste with the keyboard instead", { duration: 2500 });
105
100
  }
106
101
  }, [sendData, focusTerminal]);
107
102
 
@@ -136,10 +131,7 @@ export const TerminalTab = memo(function TerminalTab({ metadata, tabId }: Termin
136
131
  const isMobile = typeof window !== "undefined" && "ontouchstart" in window;
137
132
 
138
133
  return (
139
- <div
140
- className="flex flex-col h-full"
141
- style={viewportHeight ? { maxHeight: `${viewportHeight}px` } : undefined}
142
- >
134
+ <div className="flex flex-col h-full min-h-0">
143
135
  {/* Status bar */}
144
136
  <div className="flex items-center gap-2 px-3 py-1 bg-surface border-b border-border text-xs">
145
137
  <span
@@ -180,52 +172,31 @@ export const TerminalTab = memo(function TerminalTab({ metadata, tabId }: Termin
180
172
  {/* Terminal container. onClick focuses xterm's hidden textarea — on mobile
181
173
  the soft keyboard only opens when focus() runs inside a recognized tap
182
174
  gesture, which xterm's internal mousedown handling doesn't reliably do. */}
183
- <div ref={containerRef} onClick={focusTerminal} className="flex-1 min-h-0 bg-background p-1" />
175
+ <div
176
+ ref={containerRef}
177
+ onClick={selectMode ? undefined : focusTerminal}
178
+ className={cn("flex-1 min-h-0 bg-background p-1", selectMode && "touch-none")}
179
+ />
184
180
 
185
- {/* Mobile toolbar */}
186
181
  {isMobile && (
187
- <div className="flex items-center gap-1 px-2 py-1.5 bg-surface border-t border-border overflow-x-auto">
188
- <button
189
- onClick={handleCopy}
190
- className="px-2 py-1.5 rounded text-xs min-w-[36px] min-h-[32px] bg-surface-elevated text-text-primary active:bg-primary active:text-primary-foreground transition-colors select-none"
191
- >
192
- <Copy size={14} />
193
- </button>
194
- <button
195
- onClick={handlePaste}
196
- className="px-2 py-1.5 rounded text-xs min-w-[36px] min-h-[32px] bg-surface-elevated text-text-primary active:bg-primary active:text-primary-foreground transition-colors select-none"
197
- >
198
- <ClipboardPaste size={14} />
199
- </button>
200
- <button
201
- onClick={handleSendToChat}
202
- className="px-2 py-1.5 rounded text-xs min-w-[36px] min-h-[32px] bg-surface-elevated text-text-primary active:bg-primary active:text-primary-foreground transition-colors select-none"
203
- aria-label="Send to Chat"
204
- >
205
- <MessageSquare size={14} />
206
- </button>
207
- <div className="w-px h-5 bg-border mx-0.5" />
208
- {MOBILE_KEYS.map((key) => (
209
- <button
210
- key={key.label}
211
- onClick={() => {
212
- if (key.label === "Ctrl") {
213
- setCtrlMode(!ctrlMode);
214
- } else if (key.value) {
215
- sendKey(key.value);
216
- }
217
- }}
218
- className={cn(
219
- "px-3 py-1.5 rounded text-xs font-mono min-w-[36px] min-h-[32px]",
220
- "bg-surface-elevated text-text-primary active:bg-primary active:text-primary-foreground",
221
- "transition-colors select-none",
222
- key.label === "Ctrl" && ctrlMode && "bg-primary text-primary-foreground",
223
- )}
224
- >
225
- {key.label}
226
- </button>
227
- ))}
228
- </div>
182
+ <>
183
+ <TerminalMobileToolbar
184
+ ctrlMode={ctrlMode}
185
+ onToggleCtrl={() => setCtrlMode(!ctrlMode)}
186
+ selectMode={selectMode}
187
+ onToggleSelect={() => setSelectMode(!selectMode)}
188
+ onKey={sendKey}
189
+ onCopy={() => void handleCopy()}
190
+ onPaste={() => void handlePaste()}
191
+ onSendToChat={handleSendToChat}
192
+ onOpenLinks={() => setLinksOpen(true)}
193
+ />
194
+ <TerminalLinksSheet
195
+ open={linksOpen}
196
+ onClose={() => setLinksOpen(false)}
197
+ urls={linksOpen ? getBufferUrls() : []}
198
+ />
199
+ </>
229
200
  )}
230
201
  </div>
231
202
  );
@@ -78,17 +78,34 @@ export function BottomSheet({ open, onClose, children, className, zIndex = 50 }:
78
78
  onClick={(e) => e.stopPropagation()}
79
79
  {...swipeHandlers}
80
80
  >
81
- {/* Drag handle */}
82
- <div className="flex justify-center pt-3 pb-1">
81
+ {/* Drag handle. shrink-0 so a flex-column sheet gives it its own height
82
+ instead of letting `h-full`/`flex-1` content overflow past it. */}
83
+ <div className="flex justify-center pt-3 pb-1 shrink-0">
83
84
  <div className="w-10 h-1 rounded-full bg-border" />
84
85
  </div>
85
86
  {children}
86
87
  </div>
87
88
  </div>,
88
- document.body,
89
+ sheetPortalTarget(),
89
90
  );
90
91
  }
91
92
 
93
+ /**
94
+ * Portal target for every sheet: the React root, never `document.body`.
95
+ *
96
+ * TabPool reparents live tab DOM into panel slots with `appendChild`, and on
97
+ * mobile the dock's slot lives inside a sheet. A body-level portal would put
98
+ * that node outside the root container React binds its delegated listeners to,
99
+ * so React resolves the node's root container (`#root`) against the container
100
+ * the event fired on (`body`), finds no match, and drops the event — every
101
+ * button inside a dock tab goes dead while native listeners still fire.
102
+ * `#root` carries no styles, so keeping the portal inside it leaves stacking
103
+ * order untouched.
104
+ */
105
+ function sheetPortalTarget(): HTMLElement {
106
+ return document.getElementById("root") ?? document.body;
107
+ }
108
+
92
109
  /* ------------------------------------------------------------------ */
93
110
  /* Context-menu-specific helpers (used by adaptive-context-menu) */
94
111
  /* ------------------------------------------------------------------ */
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Drag-to-select for xterm on touch devices.
3
+ *
4
+ * xterm has no touch selection at all: `SelectionService` is driven by a
5
+ * `mousedown` on the `.xterm` element followed by `mousemove`/`mouseup` on the
6
+ * owning document, and nothing translates touch into those. This hook feeds it
7
+ * synthetic mouse events from a one-finger drag, so selection, its highlight and
8
+ * `term.getSelection()` all behave exactly as they do with a mouse.
9
+ *
10
+ * Only active while the caller has select mode switched on — the same gesture
11
+ * otherwise scrolls the viewport, and silently stealing it would strand the user
12
+ * with no way to scroll back.
13
+ */
14
+ import { useEffect, type RefObject } from "react";
15
+
16
+ function mouseEventFrom(type: string, touch: Touch, buttons: number): MouseEvent {
17
+ return new MouseEvent(type, {
18
+ bubbles: true,
19
+ cancelable: true,
20
+ view: window,
21
+ clientX: touch.clientX,
22
+ clientY: touch.clientY,
23
+ screenX: touch.screenX,
24
+ screenY: touch.screenY,
25
+ button: 0,
26
+ buttons,
27
+ // Click count. xterm branches on it to pick single/word/line selection and
28
+ // ignores the event entirely at the default of 0, so a drag would start no
29
+ // selection at all without this.
30
+ detail: 1,
31
+ });
32
+ }
33
+
34
+ export function useTerminalTouchSelection(
35
+ containerRef: RefObject<HTMLDivElement | null>,
36
+ enabled: boolean,
37
+ ) {
38
+ useEffect(() => {
39
+ const container = containerRef.current;
40
+ if (!enabled || !container) return;
41
+
42
+ // `.xterm` is where SelectionService listens for mousedown; dispatching on
43
+ // the screen element would work too but this keeps the target explicit.
44
+ const xterm = container.querySelector(".xterm");
45
+ if (!xterm) return;
46
+
47
+ // Capture phase so these run before xterm's own document-level touch
48
+ // gesture handler, and stopPropagation keeps that handler from scrolling
49
+ // the viewport out from under the drag.
50
+ const onTouchStart = (e: TouchEvent) => {
51
+ const touch = e.touches[0];
52
+ if (!touch || e.touches.length > 1) return;
53
+ e.preventDefault();
54
+ e.stopPropagation();
55
+ xterm.dispatchEvent(mouseEventFrom("mousedown", touch, 1));
56
+ };
57
+
58
+ const onTouchMove = (e: TouchEvent) => {
59
+ const touch = e.touches[0];
60
+ if (!touch) return;
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ document.dispatchEvent(mouseEventFrom("mousemove", touch, 1));
64
+ };
65
+
66
+ const onTouchEnd = (e: TouchEvent) => {
67
+ const touch = e.changedTouches[0];
68
+ if (!touch) return;
69
+ e.preventDefault();
70
+ e.stopPropagation();
71
+ document.dispatchEvent(mouseEventFrom("mouseup", touch, 0));
72
+ };
73
+
74
+ const opts = { capture: true, passive: false } as const;
75
+ container.addEventListener("touchstart", onTouchStart, opts);
76
+ container.addEventListener("touchmove", onTouchMove, opts);
77
+ container.addEventListener("touchend", onTouchEnd, opts);
78
+ container.addEventListener("touchcancel", onTouchEnd, opts);
79
+
80
+ return () => {
81
+ container.removeEventListener("touchstart", onTouchStart, opts);
82
+ container.removeEventListener("touchmove", onTouchMove, opts);
83
+ container.removeEventListener("touchend", onTouchEnd, opts);
84
+ container.removeEventListener("touchcancel", onTouchEnd, opts);
85
+ };
86
+ }, [containerRef, enabled]);
87
+ }
@@ -34,9 +34,17 @@ interface UseTerminalReturn {
34
34
  getSelection: () => string;
35
35
  /** Read buffer from last command start to current cursor (for "Send to Chat"). */
36
36
  getLastCommandOutput: () => string;
37
+ /** URLs present in the scrollback, most recent first. */
38
+ getBufferUrls: () => string[];
37
39
  restart: () => void;
38
40
  }
39
41
 
42
+ /** Trailing punctuation that ends a sentence rather than the URL itself. */
43
+ const URL_TRAILING_PUNCT = /[).,;:'"\]}>]+$/;
44
+ const URL_PATTERN = /\bhttps?:\/\/[^\s<>"'`]+/g;
45
+ /** Cap the scan so a 50k-line scrollback never blocks the tap that triggered it. */
46
+ const URL_SCAN_MAX_ROWS = 5000;
47
+
40
48
  const RESIZE_PREFIX = "\x01RESIZE:";
41
49
  const PING_MSG = "\x01PING";
42
50
  const PONG_MSG = "\x01PONG";
@@ -101,6 +109,50 @@ export function useTerminal(
101
109
  return lines.join("\n");
102
110
  }, []);
103
111
 
112
+ /**
113
+ * Collect URLs from the scrollback.
114
+ *
115
+ * xterm's link addon activates a link from a mouse hover followed by mouseup,
116
+ * neither of which a touch device produces, so on mobile this list is the only
117
+ * way to reach a URL the shell printed (an `aws sso login` code, for example).
118
+ * Wrapped lines are joined first — a URL long enough to matter is usually the
119
+ * one that got split across rows.
120
+ */
121
+ const getBufferUrls = useCallback(() => {
122
+ const term = termRef.current;
123
+ if (!term) return [];
124
+ const buf = term.buffer.active;
125
+ const endRow = buf.baseY + buf.cursorY;
126
+ const startRow = Math.max(0, endRow - URL_SCAN_MAX_ROWS);
127
+
128
+ const logical: string[] = [];
129
+ let current = "";
130
+ for (let i = startRow; i <= endRow; i++) {
131
+ const line = buf.getLine(i);
132
+ if (!line) continue;
133
+ current += line.translateToString(true);
134
+ // isWrapped marks a row as the continuation of the previous one, so the
135
+ // break is a display artefact and must not split the URL.
136
+ const next = buf.getLine(i + 1);
137
+ if (next?.isWrapped) continue;
138
+ logical.push(current);
139
+ current = "";
140
+ }
141
+ if (current) logical.push(current);
142
+
143
+ const seen = new Set<string>();
144
+ const urls: string[] = [];
145
+ for (const text of logical) {
146
+ for (const match of text.match(URL_PATTERN) ?? []) {
147
+ const url = match.replace(URL_TRAILING_PUNCT, "");
148
+ if (!url || seen.has(url)) continue;
149
+ seen.add(url);
150
+ urls.push(url);
151
+ }
152
+ }
153
+ return urls.reverse();
154
+ }, []);
155
+
104
156
  const sendResize = useCallback(() => {
105
157
  const term = termRef.current;
106
158
  const ws = wsRef.current;
@@ -344,5 +396,5 @@ export function useTerminal(
344
396
  };
345
397
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
346
398
 
347
- return { connected, reconnecting, exited, sendData, getSelection, getLastCommandOutput, restart };
399
+ return { connected, reconnecting, exited, sendData, getSelection, getLastCommandOutput, getBufferUrls, restart };
348
400
  }