@hienlh/ppm 0.17.28 → 0.17.30
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/CHANGELOG.md +11 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{ai-resource-editor-BCgyv1KF.js → ai-resource-editor-D1efMh5f.js} +1 -1
- package/dist/web/assets/{audio-preview-BPuiX8ma.js → audio-preview-BfaIfDsY.js} +1 -1
- package/dist/web/assets/chat-tab-CVfcHLiQ.js +13 -0
- package/dist/web/assets/code-editor-hKFw2M2d.js +10 -0
- package/dist/web/assets/{conflict-editor-BuLnwxFE.js → conflict-editor-BRDEJEq6.js} +1 -1
- package/dist/web/assets/{csv-preview-DdA_IpTo.js → csv-preview-D32vaKWU.js} +1 -1
- package/dist/web/assets/{database-viewer-BcF3uuk5.js → database-viewer-B-ukNIhW.js} +1 -1
- package/dist/web/assets/{diff-viewer-tyMHX20V.js → diff-viewer-DfFzvZND.js} +1 -1
- package/dist/web/assets/{docx-preview-BTclTd2v.js → docx-preview-B1kTBbEc.js} +1 -1
- package/dist/web/assets/{extension-webview-CBLWXhjt.js → extension-webview-mQPWwlxb.js} +1 -1
- package/dist/web/assets/{git-log-panel-tmGgY2kK.js → git-log-panel-CIB6xuoc.js} +1 -1
- package/dist/web/assets/glide-data-grid-DM0v58AP.js +138 -0
- package/dist/web/assets/group-chat-tab-pfDZpGO_.js +1 -0
- package/dist/web/assets/{image-preview-CQ57RN6l.js → image-preview-Dxvk6fiw.js} +1 -1
- package/dist/web/assets/index-Cp6hb7uj.js +50 -0
- package/dist/web/assets/keybindings-store-BDQFSrPz.js +1 -0
- package/dist/web/assets/{markdown-renderer-devgS0zw.js → markdown-renderer-TCeFZDxj.js} +2 -2
- package/dist/web/assets/{markdown-renderer-C7clwuDe.js → markdown-renderer-zRn368SN.js} +1 -1
- package/dist/web/assets/notification-store-BcEM83wQ.js +1 -0
- package/dist/web/assets/{pdf-preview-Cw_GRd7e.js → pdf-preview-Bx_RsWdB.js} +1 -1
- package/dist/web/assets/{postgres-viewer-DN40Bdc-.js → postgres-viewer-DJXDXZbE.js} +2 -2
- package/dist/web/assets/{settings-tab-DkpOd_kU.js → settings-tab-CAJ4skP0.js} +1 -1
- package/dist/web/assets/{sql-query-editor-Df1pzXAj.js → sql-query-editor-UDQo7Bck.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-Dbh3LluT.js → sqlite-viewer-BaQq64OX.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-bbct9aEk.js → system-monitor-tab-P1uy84Os.js} +1 -1
- package/dist/web/assets/terminal-tab-mI-vAmOV.js +3 -0
- package/dist/web/assets/{tool-cards-BonPvpUI.js → tool-cards-B6jz0GZE.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-Cqo17yKA.js → use-monaco-theme-CZKIw2ZG.js} +1 -1
- package/dist/web/assets/{video-preview-kZgggFAn.js → video-preview-TGDSUs0J.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +2 -2
- package/src/services/supervisor.ts +45 -24
- package/src/services/windows-process-tree.ts +24 -3
- package/src/web/components/chat/account-dialogs.tsx +3 -3
- package/src/web/components/chat/chat-history-bar.tsx +4 -2
- package/src/web/components/chat/message-action-bar.tsx +2 -1
- package/src/web/components/database/connection-import-export.tsx +5 -2
- package/src/web/components/database/export-button.tsx +4 -3
- package/src/web/components/explorer/file-tree.tsx +3 -2
- package/src/web/components/layout/cloud-share-popover.tsx +2 -1
- package/src/web/components/layout/mobile-nav.tsx +3 -2
- package/src/web/components/layout/project-bottom-sheet.tsx +2 -1
- package/src/web/components/layout/project-switcher.tsx +2 -1
- package/src/web/components/layout/tab-bar.tsx +3 -2
- package/src/web/components/settings/accounts-settings-section.tsx +3 -3
- package/src/web/components/settings/proxy-settings-section.tsx +8 -7
- package/src/web/components/shared/bug-report-popup.tsx +2 -1
- package/src/web/components/shared/markdown-code-block.tsx +3 -2
- package/src/web/components/terminal/terminal-tab.tsx +2 -1
- package/src/web/components/tunnels/tunnel-manager-tab.tsx +4 -2
- package/src/web/lib/clipboard.ts +52 -0
- package/src/web/lib/report-bug.ts +0 -10
- package/dist/web/assets/chat-tab-WnnM6GKA.js +0 -13
- package/dist/web/assets/code-editor-CnGUyOuU.js +0 -10
- package/dist/web/assets/glide-data-grid-CkCW7sFw.js +0 -138
- package/dist/web/assets/group-chat-tab-D_x5CRae.js +0 -1
- package/dist/web/assets/index-Cp3e9olf.js +0 -50
- package/dist/web/assets/keybindings-store-CpgEwnFA.js +0 -1
- package/dist/web/assets/notification-store-BohufOcS.js +0 -1
- package/dist/web/assets/terminal-tab-Cqs7xZJt.js +0 -3
|
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
|
|
5
5
|
import { Label } from "@/components/ui/label";
|
|
6
6
|
import { Loader2, Download, Copy, Lock } from "lucide-react";
|
|
7
7
|
import { getAuthToken } from "../../lib/api-client";
|
|
8
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
8
9
|
import {
|
|
9
10
|
addAccount,
|
|
10
11
|
getOAuthUrl,
|
|
@@ -207,10 +208,9 @@ export function ExportAccountsDialog({ open, onOpenChange, accounts, preselectId
|
|
|
207
208
|
if (!res.ok) { const j = await res.json() as any; throw new Error(j.error ?? `Export failed: ${res.status}`); }
|
|
208
209
|
const text = await res.text();
|
|
209
210
|
if (toClipboard) {
|
|
210
|
-
|
|
211
|
-
await navigator.clipboard.writeText(text);
|
|
211
|
+
if (await copyToClipboard(text)) {
|
|
212
212
|
onMessage?.("Backup copied to clipboard!");
|
|
213
|
-
}
|
|
213
|
+
} else {
|
|
214
214
|
downloadBlob(text);
|
|
215
215
|
onMessage?.("Backup downloaded.");
|
|
216
216
|
}
|
|
@@ -17,6 +17,7 @@ import { CodexUsagePanel } from "./codex-usage-panel";
|
|
|
17
17
|
import { TeamActivityPanel } from "./team-activity-panel";
|
|
18
18
|
import { ProviderBadge } from "./provider-selector";
|
|
19
19
|
import { formatRelativeDate } from "@/lib/format-date";
|
|
20
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
20
21
|
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
|
21
22
|
import type { SessionInfo, SessionListResponse, ProjectTag } from "../../../types/chat";
|
|
22
23
|
import type { UsageInfo } from "../../../types/chat";
|
|
@@ -113,10 +114,11 @@ function DebugCopyButton({ sessionId, projectName }: { sessionId: string; projec
|
|
|
113
114
|
|
|
114
115
|
const copy = () => {
|
|
115
116
|
if (!info) return;
|
|
116
|
-
|
|
117
|
+
copyToClipboard(info).then((ok) => {
|
|
118
|
+
if (!ok) return;
|
|
117
119
|
setCopied(true);
|
|
118
120
|
setTimeout(() => setCopied(false), 1500);
|
|
119
|
-
})
|
|
121
|
+
});
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
const body = (
|
|
@@ -2,6 +2,7 @@ import { useState, type ReactNode } from "react";
|
|
|
2
2
|
import { Copy, Check } from "lucide-react";
|
|
3
3
|
import { cn } from "@/lib/utils";
|
|
4
4
|
import { formatRelativeDate } from "@/lib/format-date";
|
|
5
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
5
6
|
|
|
6
7
|
/** Full timestamp for the relative-time tooltip. */
|
|
7
8
|
function fullDate(iso: string): string {
|
|
@@ -50,7 +51,7 @@ function CopyButton({ content }: { content: string }) {
|
|
|
50
51
|
label={copied ? "Copied" : "Copy"}
|
|
51
52
|
title="Copy message to clipboard"
|
|
52
53
|
onClick={() => {
|
|
53
|
-
|
|
54
|
+
void copyToClipboard(content);
|
|
54
55
|
setCopied(true);
|
|
55
56
|
setTimeout(() => setCopied(false), 1500);
|
|
56
57
|
}}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef, useState } from "react";
|
|
2
2
|
import { MoreVertical, Download, Upload, Clipboard, ClipboardPaste } from "lucide-react";
|
|
3
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
onExport: () => Promise<{ version: number; exported_at: string; connections: unknown[] }>;
|
|
@@ -32,8 +33,10 @@ export function ConnectionImportExport({ onExport, onImport }: Props) {
|
|
|
32
33
|
close();
|
|
33
34
|
try {
|
|
34
35
|
const data = await onExport();
|
|
35
|
-
await
|
|
36
|
-
alert(
|
|
36
|
+
const ok = await copyToClipboard(JSON.stringify(data, null, 2));
|
|
37
|
+
alert(ok
|
|
38
|
+
? `Copied ${data.connections.length} connection(s) to clipboard`
|
|
39
|
+
: "Copy failed — clipboard unavailable");
|
|
37
40
|
} catch (e) {
|
|
38
41
|
alert(`Export failed: ${(e as Error).message}`);
|
|
39
42
|
}
|
|
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect, useCallback } from "react";
|
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { Download } from "lucide-react";
|
|
4
4
|
import { serializeCsv } from "@/lib/csv-parser";
|
|
5
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
5
6
|
|
|
6
7
|
interface ExportButtonProps {
|
|
7
8
|
columns: string[];
|
|
@@ -53,10 +54,10 @@ export function ExportButton({ columns, rows, filename = "export", exportAllUrl
|
|
|
53
54
|
setOpen(false);
|
|
54
55
|
};
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
+
const handleCopy = async () => {
|
|
57
58
|
const header = columns.join("\t");
|
|
58
59
|
const body = rows.map((r) => columns.map((c) => String(r[c] ?? "")).join("\t")).join("\n");
|
|
59
|
-
await
|
|
60
|
+
await copyToClipboard(header + "\n" + body);
|
|
60
61
|
setOpen(false);
|
|
61
62
|
};
|
|
62
63
|
|
|
@@ -90,7 +91,7 @@ export function ExportButton({ columns, rows, filename = "export", exportAllUrl
|
|
|
90
91
|
<div ref={dropdownRef}
|
|
91
92
|
style={{ position: "fixed", left: Math.min(rect.left, window.innerWidth - 170), top: rect.bottom + 4, zIndex: 10000 }}
|
|
92
93
|
className="bg-popover border border-border rounded-md shadow-md py-1 min-w-[160px] text-xs">
|
|
93
|
-
<button onClick={
|
|
94
|
+
<button onClick={handleCopy} className="w-full text-left px-3 py-1.5 hover:bg-muted transition-colors text-foreground">
|
|
94
95
|
Copy to Clipboard (TSV)
|
|
95
96
|
</button>
|
|
96
97
|
<button onClick={exportPageCsv} className="w-full text-left px-3 py-1.5 hover:bg-muted transition-colors text-foreground">
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Loader2,
|
|
14
14
|
} from "lucide-react";
|
|
15
15
|
import { SidebarHeader } from "@/components/ui/sidebar-header";
|
|
16
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
16
17
|
import { useShallow } from "zustand/react/shallow";
|
|
17
18
|
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
18
19
|
import { useProjectStore } from "@/stores/project-store";
|
|
@@ -244,12 +245,12 @@ export function FileTree({ onFileOpen }: FileTreeProps = {}) {
|
|
|
244
245
|
return;
|
|
245
246
|
}
|
|
246
247
|
if (action === "copy-path") {
|
|
247
|
-
|
|
248
|
+
void copyToClipboard(node.path);
|
|
248
249
|
return;
|
|
249
250
|
}
|
|
250
251
|
if (action === "copy-full-path") {
|
|
251
252
|
const root = activeProject?.path;
|
|
252
|
-
|
|
253
|
+
void copyToClipboard(root ? `${root}/${node.path}` : node.path);
|
|
253
254
|
return;
|
|
254
255
|
}
|
|
255
256
|
if (action === "select-for-compare") {
|
|
@@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from "react";
|
|
|
2
2
|
import { Cloud, Share2, Loader2, Copy, Check, X, LogOut, Link, Unlink, ExternalLink } from "lucide-react";
|
|
3
3
|
import { QRCodeSVG } from "qrcode.react";
|
|
4
4
|
import { api } from "@/lib/api-client";
|
|
5
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
5
6
|
|
|
6
7
|
interface CloudStatus {
|
|
7
8
|
logged_in: boolean;
|
|
@@ -54,7 +55,7 @@ export function CloudSharePopover({ onClose, variant = "popover" }: Props) {
|
|
|
54
55
|
}, []);
|
|
55
56
|
|
|
56
57
|
const handleCopy = useCallback((url: string) => {
|
|
57
|
-
|
|
58
|
+
void copyToClipboard(url);
|
|
58
59
|
setCopied(url);
|
|
59
60
|
setTimeout(() => setCopied(null), 2000);
|
|
60
61
|
}, []);
|
|
@@ -14,6 +14,7 @@ import { resolveProjectColor } from "@/lib/project-palette";
|
|
|
14
14
|
import { ProjectAvatar } from "@/components/layout/project-avatar";
|
|
15
15
|
import type { Tab } from "@/stores/tab-store";
|
|
16
16
|
import { cn, basename } from "@/lib/utils";
|
|
17
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
17
18
|
import { toast } from "sonner";
|
|
18
19
|
import { openCommandPalette } from "@/hooks/use-global-keybindings";
|
|
19
20
|
import { useNotificationStore } from "@/stores/notification-store";
|
|
@@ -188,12 +189,12 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
188
189
|
const projectName = tab.metadata?.projectName as string | undefined;
|
|
189
190
|
switch (action) {
|
|
190
191
|
case "copy-path":
|
|
191
|
-
if (filePath)
|
|
192
|
+
if (filePath) void copyToClipboard(filePath);
|
|
192
193
|
break;
|
|
193
194
|
case "copy-full-path": {
|
|
194
195
|
if (filePath) {
|
|
195
196
|
const project = projectName ? useProjectStore.getState().projects.find((p) => p.name === projectName) : null;
|
|
196
|
-
|
|
197
|
+
void copyToClipboard(project ? `${project.path}/${filePath}` : filePath);
|
|
197
198
|
}
|
|
198
199
|
break;
|
|
199
200
|
}
|
|
@@ -10,6 +10,7 @@ import { ProjectAvatar } from "@/components/layout/project-avatar";
|
|
|
10
10
|
import { buildUrl } from "@/hooks/use-url-sync";
|
|
11
11
|
import { formatRelativeDate } from "@/lib/format-date";
|
|
12
12
|
import { cn } from "@/lib/utils";
|
|
13
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
13
14
|
import { BottomSheet } from "@/components/ui/mobile-bottom-sheet";
|
|
14
15
|
|
|
15
16
|
interface ProjectBottomSheetProps {
|
|
@@ -181,7 +182,7 @@ export function ProjectBottomSheet({ isOpen, onClose }: ProjectBottomSheetProps)
|
|
|
181
182
|
label: "Copy Path",
|
|
182
183
|
icon: Copy,
|
|
183
184
|
onClick: () => {
|
|
184
|
-
if (actionProject)
|
|
185
|
+
if (actionProject) void copyToClipboard(actionProject.path);
|
|
185
186
|
setActionTarget(null);
|
|
186
187
|
},
|
|
187
188
|
},
|
|
@@ -6,6 +6,7 @@ import { useProjectStore, resolveOrder, loadRecentTimes, type ProjectInfo } from
|
|
|
6
6
|
import { SORT_OPTIONS, applySort } from "@/components/layout/project-sort";
|
|
7
7
|
import { buildUrl } from "@/hooks/use-url-sync";
|
|
8
8
|
import { formatRelativeDate } from "@/lib/format-date";
|
|
9
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
9
10
|
import { resolveProjectColor, PROJECT_PALETTE } from "@/lib/project-palette";
|
|
10
11
|
import { ProjectAvatar } from "@/components/layout/project-avatar";
|
|
11
12
|
import { useNotificationStore, selectProjectUrgentType, notificationColor } from "@/stores/notification-store";
|
|
@@ -358,7 +359,7 @@ export const ProjectSwitcher = memo(function ProjectSwitcher() {
|
|
|
358
359
|
<Trash2 className="size-3.5 mr-2" /> Remove Image
|
|
359
360
|
</ContextMenuItem>
|
|
360
361
|
)}
|
|
361
|
-
<ContextMenuItem onClick={() =>
|
|
362
|
+
<ContextMenuItem onClick={() => void copyToClipboard(p.path)}>
|
|
362
363
|
<Copy className="size-3.5 mr-2" /> Copy Path
|
|
363
364
|
</ContextMenuItem>
|
|
364
365
|
<ContextMenuSeparator />
|
|
@@ -43,6 +43,7 @@ import { DraggableTab } from "./draggable-tab";
|
|
|
43
43
|
import { cn } from "@/lib/utils";
|
|
44
44
|
import type { Tab } from "@/stores/tab-store";
|
|
45
45
|
import { downloadFile } from "@/lib/file-download";
|
|
46
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
46
47
|
import { FileActions } from "@/components/explorer/file-actions";
|
|
47
48
|
import {
|
|
48
49
|
ContextMenu as BarContextMenu,
|
|
@@ -278,7 +279,7 @@ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
|
|
|
278
279
|
}
|
|
279
280
|
case "copy-path": {
|
|
280
281
|
const filePath = tab.metadata?.filePath as string | undefined;
|
|
281
|
-
if (filePath)
|
|
282
|
+
if (filePath) void copyToClipboard(filePath);
|
|
282
283
|
break;
|
|
283
284
|
}
|
|
284
285
|
case "copy-full-path": {
|
|
@@ -286,7 +287,7 @@ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
|
|
|
286
287
|
const projectName = tab.metadata?.projectName as string | undefined;
|
|
287
288
|
if (filePath) {
|
|
288
289
|
const project = projectName ? useProjectStore.getState().projects.find((p) => p.name === projectName) : null;
|
|
289
|
-
|
|
290
|
+
void copyToClipboard(project ? `${project.path}/${filePath}` : filePath);
|
|
290
291
|
}
|
|
291
292
|
break;
|
|
292
293
|
}
|
|
@@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|
|
8
8
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
|
9
9
|
import { Eye, Loader2, Copy, X, Download, Upload, Lock, FlaskConical } from "lucide-react";
|
|
10
10
|
import { getAuthToken } from "../../lib/api-client";
|
|
11
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
11
12
|
import {
|
|
12
13
|
getAccounts,
|
|
13
14
|
getActiveAccount,
|
|
@@ -347,10 +348,9 @@ export function AccountsSettingsSection() {
|
|
|
347
348
|
if (!res.ok) { const j = await res.json() as any; throw new Error(j.error ?? `Export failed: ${res.status}`); }
|
|
348
349
|
const text = await res.text();
|
|
349
350
|
if (toClipboard) {
|
|
350
|
-
|
|
351
|
-
await navigator.clipboard.writeText(text);
|
|
351
|
+
if (await copyToClipboard(text)) {
|
|
352
352
|
showMessage({ type: "success", text: "Backup copied to clipboard!" });
|
|
353
|
-
}
|
|
353
|
+
} else {
|
|
354
354
|
const blob = new Blob([text], { type: "application/json" });
|
|
355
355
|
const a = document.createElement("a");
|
|
356
356
|
a.href = URL.createObjectURL(blob);
|
|
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
|
|
5
5
|
import { Label } from "@/components/ui/label";
|
|
6
6
|
import { Switch } from "@/components/ui/switch";
|
|
7
7
|
import { getProxySettings, updateProxySettings, type ProxySettings } from "@/lib/api-settings";
|
|
8
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
8
9
|
import { ProxyTestButton } from "./proxy-test-section";
|
|
9
10
|
|
|
10
11
|
export function ProxySettingsSection() {
|
|
@@ -37,8 +38,8 @@ export function ProxySettingsSection() {
|
|
|
37
38
|
}
|
|
38
39
|
};
|
|
39
40
|
|
|
40
|
-
const
|
|
41
|
-
|
|
41
|
+
const handleCopy = (text: string, label: string) => {
|
|
42
|
+
void copyToClipboard(text);
|
|
42
43
|
setCopied(label);
|
|
43
44
|
setTimeout(() => setCopied(null), 2000);
|
|
44
45
|
};
|
|
@@ -96,7 +97,7 @@ export function ProxySettingsSection() {
|
|
|
96
97
|
variant="outline"
|
|
97
98
|
size="sm"
|
|
98
99
|
className="h-7 px-2 cursor-pointer shrink-0"
|
|
99
|
-
onClick={() =>
|
|
100
|
+
onClick={() => handleCopy(settings.authKey!, "key")}
|
|
100
101
|
>
|
|
101
102
|
{copied === "key" ? "Copied!" : <Copy className="size-3" />}
|
|
102
103
|
</Button>
|
|
@@ -145,7 +146,7 @@ export function ProxySettingsSection() {
|
|
|
145
146
|
variant="ghost"
|
|
146
147
|
size="sm"
|
|
147
148
|
className="h-6 px-1.5 cursor-pointer shrink-0"
|
|
148
|
-
onClick={() =>
|
|
149
|
+
onClick={() => handleCopy(hasTunnel ? settings.proxyEndpoint! : localEndpoint, "anthropic")}
|
|
149
150
|
>
|
|
150
151
|
{copied === "anthropic" ? "Copied!" : <Copy className="size-3" />}
|
|
151
152
|
</Button>
|
|
@@ -163,7 +164,7 @@ export function ProxySettingsSection() {
|
|
|
163
164
|
variant="ghost"
|
|
164
165
|
size="sm"
|
|
165
166
|
className="h-6 px-1.5 cursor-pointer shrink-0"
|
|
166
|
-
onClick={() =>
|
|
167
|
+
onClick={() => handleCopy(
|
|
167
168
|
hasTunnel ? settings.openAiEndpoint! : settings.localOpenAiEndpoint, "openai",
|
|
168
169
|
)}
|
|
169
170
|
>
|
|
@@ -190,7 +191,7 @@ ANTHROPIC_API_KEY=${settings.authKey}`}
|
|
|
190
191
|
variant="ghost"
|
|
191
192
|
size="sm"
|
|
192
193
|
className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
|
|
193
|
-
onClick={() =>
|
|
194
|
+
onClick={() => handleCopy(
|
|
194
195
|
`ANTHROPIC_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy" : localBaseUrl + "/proxy"}\nANTHROPIC_API_KEY=${settings.authKey}`,
|
|
195
196
|
"anthropic-env",
|
|
196
197
|
)}
|
|
@@ -211,7 +212,7 @@ OPENAI_API_KEY=${settings.authKey}`}
|
|
|
211
212
|
variant="ghost"
|
|
212
213
|
size="sm"
|
|
213
214
|
className="absolute top-1 right-1 h-5 px-1 cursor-pointer"
|
|
214
|
-
onClick={() =>
|
|
215
|
+
onClick={() => handleCopy(
|
|
215
216
|
`OPENAI_BASE_URL=${hasTunnel ? settings.tunnelUrl + "/proxy/v1" : localBaseUrl + "/proxy/v1"}\nOPENAI_API_KEY=${settings.authKey}`,
|
|
216
217
|
"openai-env",
|
|
217
218
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { Copy, ExternalLink, X, Check } from "lucide-react";
|
|
3
|
-
import { openGithubIssue
|
|
3
|
+
import { openGithubIssue } from "@/lib/report-bug";
|
|
4
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
4
5
|
|
|
5
6
|
export function BugReportPopup() {
|
|
6
7
|
const [text, setText] = useState<string | null>(null);
|
|
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, type ReactNode } from "react";
|
|
|
2
2
|
import { useMdContext, FILE_EXT_RE, GLOB_CHARS_RE } from "./markdown-context";
|
|
3
3
|
import { useTabStore } from "@/stores/tab-store";
|
|
4
4
|
import { highlightToHtml, highlightSync, getActiveShikiTheme } from "@/theme/adapters/shiki-adapter";
|
|
5
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Shiki-highlighted HTML for a code block. Returns null while streaming or
|
|
@@ -88,13 +89,13 @@ export function MdPre({ children, node, ...rest }: any) {
|
|
|
88
89
|
: <pre {...rest}>{children}</pre>}
|
|
89
90
|
{codeActions && (
|
|
90
91
|
<div className="code-actions absolute top-1 right-1 flex gap-1">
|
|
91
|
-
<ActionBtn title="Copy" icon={<CopyIcon />} activeIcon={<CheckIcon />} onClick={() =>
|
|
92
|
+
<ActionBtn title="Copy" icon={<CopyIcon />} activeIcon={<CheckIcon />} onClick={() => void copyToClipboard(text)} />
|
|
92
93
|
{isBash && projectName && (
|
|
93
94
|
<ActionBtn
|
|
94
95
|
title="Run in terminal"
|
|
95
96
|
icon={<PlayIcon />}
|
|
96
97
|
onClick={() => {
|
|
97
|
-
|
|
98
|
+
void copyToClipboard(text.replace(/^\$\s*/gm, ""));
|
|
98
99
|
openTab({ type: "terminal", title: "Terminal", metadata: { projectName }, projectId: projectName, closable: true });
|
|
99
100
|
}}
|
|
100
101
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRef, useEffect, useState, useCallback, memo } from "react";
|
|
2
2
|
import { useTerminal } from "@/hooks/use-terminal";
|
|
3
3
|
import { cn } from "@/lib/utils";
|
|
4
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
4
5
|
import { Copy, ClipboardPaste, RotateCcw, MessageSquare } from "lucide-react";
|
|
5
6
|
import "@xterm/xterm/css/xterm.css";
|
|
6
7
|
import { toast } from "sonner";
|
|
@@ -88,7 +89,7 @@ export const TerminalTab = memo(function TerminalTab({ metadata, tabId }: Termin
|
|
|
88
89
|
const handleCopy = useCallback(async () => {
|
|
89
90
|
const selection = getSelection();
|
|
90
91
|
if (selection) {
|
|
91
|
-
await
|
|
92
|
+
await copyToClipboard(selection);
|
|
92
93
|
}
|
|
93
94
|
}, [getSelection]);
|
|
94
95
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { Check, Copy, ExternalLink, Globe, Loader2, Lock, RefreshCw, Square } from "lucide-react";
|
|
3
3
|
import { tunnelsApi, type TunnelEntry } from "@/lib/api-tunnels";
|
|
4
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
4
5
|
import { toast } from "sonner";
|
|
5
6
|
|
|
6
7
|
/** Badge label + style per tunnel source. */
|
|
@@ -73,11 +74,12 @@ export function TunnelManagerTab() {
|
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
const copyUrl = (pid: number, url: string) => {
|
|
76
|
-
|
|
77
|
+
copyToClipboard(url).then((ok) => {
|
|
78
|
+
if (!ok) { toast.error("Failed to copy URL"); return; }
|
|
77
79
|
setCopiedPid(pid);
|
|
78
80
|
toast.success("URL copied");
|
|
79
81
|
setTimeout(() => setCopiedPid(null), 2000);
|
|
80
|
-
})
|
|
82
|
+
});
|
|
81
83
|
};
|
|
82
84
|
|
|
83
85
|
const handleSubmit = (e: React.FormEvent) => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipboard write with an insecure-context fallback.
|
|
3
|
+
*
|
|
4
|
+
* `navigator.clipboard` only exists in a secure context (HTTPS or localhost).
|
|
5
|
+
* PPM is regularly reached over plain HTTP — LAN IP, Tailscale MagicDNS name —
|
|
6
|
+
* where the API is absent and every copy button silently no-ops. The legacy
|
|
7
|
+
* `execCommand` path keeps copy working on those origins.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Copy text to the clipboard. Returns false only if both paths fail. */
|
|
11
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
12
|
+
if (navigator.clipboard?.writeText) {
|
|
13
|
+
try {
|
|
14
|
+
await navigator.clipboard.writeText(text);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
// Denied permission or lost user activation — fall through to execCommand.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return legacyCopy(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function legacyCopy(text: string): boolean {
|
|
24
|
+
const textarea = document.createElement("textarea");
|
|
25
|
+
textarea.value = text;
|
|
26
|
+
textarea.setAttribute("readonly", "");
|
|
27
|
+
// Must stay rendered and non-zero sized: iOS Safari refuses to select a
|
|
28
|
+
// hidden or display:none element, which breaks the copy.
|
|
29
|
+
textarea.style.cssText =
|
|
30
|
+
"position:fixed;top:0;left:0;width:1px;height:1px;padding:0;border:none;opacity:0;";
|
|
31
|
+
document.body.appendChild(textarea);
|
|
32
|
+
|
|
33
|
+
const selection = document.getSelection();
|
|
34
|
+
const previousRange =
|
|
35
|
+
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
textarea.focus();
|
|
39
|
+
textarea.select();
|
|
40
|
+
// setSelectionRange is what actually selects on iOS; select() alone is ignored.
|
|
41
|
+
textarea.setSelectionRange(0, text.length);
|
|
42
|
+
return document.execCommand("copy");
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
} finally {
|
|
46
|
+
textarea.remove();
|
|
47
|
+
if (selection && previousRange) {
|
|
48
|
+
selection.removeAllRanges();
|
|
49
|
+
selection.addRange(previousRange);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -60,16 +60,6 @@ export function openGithubIssue(body: string) {
|
|
|
60
60
|
window.open(url, "_blank");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/** Copy text to clipboard */
|
|
64
|
-
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
65
|
-
try {
|
|
66
|
-
await navigator.clipboard.writeText(text);
|
|
67
|
-
return true;
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
63
|
/** Open bug report popup globally via custom event */
|
|
74
64
|
export function openBugReportPopup(
|
|
75
65
|
version: string | null,
|