@hienlh/ppm 0.17.29 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +5 -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-BCgyv1KF.js → ai-resource-editor-D1efMh5f.js} +1 -1
  5. package/dist/web/assets/{audio-preview-BPuiX8ma.js → audio-preview-BfaIfDsY.js} +1 -1
  6. package/dist/web/assets/chat-tab-CVfcHLiQ.js +13 -0
  7. package/dist/web/assets/code-editor-hKFw2M2d.js +10 -0
  8. package/dist/web/assets/{conflict-editor-BuLnwxFE.js → conflict-editor-BRDEJEq6.js} +1 -1
  9. package/dist/web/assets/{csv-preview-DdA_IpTo.js → csv-preview-D32vaKWU.js} +1 -1
  10. package/dist/web/assets/{database-viewer-BcF3uuk5.js → database-viewer-B-ukNIhW.js} +1 -1
  11. package/dist/web/assets/{diff-viewer-tyMHX20V.js → diff-viewer-DfFzvZND.js} +1 -1
  12. package/dist/web/assets/{docx-preview-BTclTd2v.js → docx-preview-B1kTBbEc.js} +1 -1
  13. package/dist/web/assets/{extension-webview-CBLWXhjt.js → extension-webview-mQPWwlxb.js} +1 -1
  14. package/dist/web/assets/{git-log-panel-tmGgY2kK.js → git-log-panel-CIB6xuoc.js} +1 -1
  15. package/dist/web/assets/glide-data-grid-DM0v58AP.js +138 -0
  16. package/dist/web/assets/group-chat-tab-pfDZpGO_.js +1 -0
  17. package/dist/web/assets/{image-preview-CQ57RN6l.js → image-preview-Dxvk6fiw.js} +1 -1
  18. package/dist/web/assets/index-Cp6hb7uj.js +50 -0
  19. package/dist/web/assets/keybindings-store-BDQFSrPz.js +1 -0
  20. package/dist/web/assets/{markdown-renderer-devgS0zw.js → markdown-renderer-TCeFZDxj.js} +2 -2
  21. package/dist/web/assets/{markdown-renderer-C7clwuDe.js → markdown-renderer-zRn368SN.js} +1 -1
  22. package/dist/web/assets/notification-store-BcEM83wQ.js +1 -0
  23. package/dist/web/assets/{pdf-preview-Cw_GRd7e.js → pdf-preview-Bx_RsWdB.js} +1 -1
  24. package/dist/web/assets/{postgres-viewer-DN40Bdc-.js → postgres-viewer-DJXDXZbE.js} +2 -2
  25. package/dist/web/assets/{settings-tab-DkpOd_kU.js → settings-tab-CAJ4skP0.js} +1 -1
  26. package/dist/web/assets/{sql-query-editor-Df1pzXAj.js → sql-query-editor-UDQo7Bck.js} +1 -1
  27. package/dist/web/assets/{sqlite-viewer-Dbh3LluT.js → sqlite-viewer-BaQq64OX.js} +1 -1
  28. package/dist/web/assets/{system-monitor-tab-bbct9aEk.js → system-monitor-tab-P1uy84Os.js} +1 -1
  29. package/dist/web/assets/terminal-tab-mI-vAmOV.js +3 -0
  30. package/dist/web/assets/{tool-cards-BonPvpUI.js → tool-cards-B6jz0GZE.js} +2 -2
  31. package/dist/web/assets/{use-monaco-theme-Cqo17yKA.js → use-monaco-theme-CZKIw2ZG.js} +1 -1
  32. package/dist/web/assets/{video-preview-kZgggFAn.js → video-preview-TGDSUs0J.js} +1 -1
  33. package/dist/web/index.html +1 -1
  34. package/dist/web/sw.js +1 -1
  35. package/package.json +1 -1
  36. package/src/web/components/chat/account-dialogs.tsx +3 -3
  37. package/src/web/components/chat/chat-history-bar.tsx +4 -2
  38. package/src/web/components/chat/message-action-bar.tsx +2 -1
  39. package/src/web/components/database/connection-import-export.tsx +5 -2
  40. package/src/web/components/database/export-button.tsx +4 -3
  41. package/src/web/components/explorer/file-tree.tsx +3 -2
  42. package/src/web/components/layout/cloud-share-popover.tsx +2 -1
  43. package/src/web/components/layout/mobile-nav.tsx +3 -2
  44. package/src/web/components/layout/project-bottom-sheet.tsx +2 -1
  45. package/src/web/components/layout/project-switcher.tsx +2 -1
  46. package/src/web/components/layout/tab-bar.tsx +3 -2
  47. package/src/web/components/settings/accounts-settings-section.tsx +3 -3
  48. package/src/web/components/settings/proxy-settings-section.tsx +8 -7
  49. package/src/web/components/shared/bug-report-popup.tsx +2 -1
  50. package/src/web/components/shared/markdown-code-block.tsx +3 -2
  51. package/src/web/components/terminal/terminal-tab.tsx +2 -1
  52. package/src/web/components/tunnels/tunnel-manager-tab.tsx +4 -2
  53. package/src/web/lib/clipboard.ts +52 -0
  54. package/src/web/lib/report-bug.ts +0 -10
  55. package/dist/web/assets/chat-tab-WnnM6GKA.js +0 -13
  56. package/dist/web/assets/code-editor-CnGUyOuU.js +0 -10
  57. package/dist/web/assets/glide-data-grid-CkCW7sFw.js +0 -138
  58. package/dist/web/assets/group-chat-tab-D_x5CRae.js +0 -1
  59. package/dist/web/assets/index-Cp3e9olf.js +0 -50
  60. package/dist/web/assets/keybindings-store-CpgEwnFA.js +0 -1
  61. package/dist/web/assets/notification-store-BohufOcS.js +0 -1
  62. package/dist/web/assets/terminal-tab-Cqs7xZJt.js +0 -3
  63. package/skills-lock.json +0 -11
  64. package/spike-memory-region-dump.ps1 +0 -69
@@ -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) navigator.clipboard.writeText(filePath).catch(() => {});
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
- navigator.clipboard.writeText(project ? `${project.path}/${filePath}` : filePath).catch(() => {});
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
- try {
351
- await navigator.clipboard.writeText(text);
351
+ if (await copyToClipboard(text)) {
352
352
  showMessage({ type: "success", text: "Backup copied to clipboard!" });
353
- } catch {
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 copyToClipboard = (text: string, label: string) => {
41
- navigator.clipboard.writeText(text);
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={() => copyToClipboard(settings.authKey!, "key")}
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={() => copyToClipboard(hasTunnel ? settings.proxyEndpoint! : localEndpoint, "anthropic")}
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={() => copyToClipboard(
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={() => copyToClipboard(
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={() => copyToClipboard(
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, copyToClipboard } from "@/lib/report-bug";
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={() => navigator.clipboard.writeText(text)} />
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
- navigator.clipboard.writeText(text.replace(/^\$\s*/gm, ""));
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 navigator.clipboard.writeText(selection);
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
- navigator.clipboard.writeText(url).then(() => {
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
- }).catch(() => toast.error("Failed to copy URL"));
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,