@hienlh/ppm 0.17.38 → 0.17.39
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 +10 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +5 -1
- package/dist/web/assets/{ai-resource-editor-ChT9FzxT.js → ai-resource-editor-CAQ_0AXP.js} +1 -1
- package/dist/web/assets/{audio-preview-CSK6nxNy.js → audio-preview-BUBpG5oQ.js} +1 -1
- package/dist/web/assets/{chat-tab-DGM7js5J.js → chat-tab-BhjsCgzO.js} +3 -3
- package/dist/web/assets/{code-editor-BvLxm1bf.js → code-editor-CO_084SG.js} +2 -2
- package/dist/web/assets/{conflict-editor-Bc063EfD.js → conflict-editor-BsWmVy7f.js} +1 -1
- package/dist/web/assets/{csv-preview-ft27dubA.js → csv-preview-CfwxBxoL.js} +1 -1
- package/dist/web/assets/{database-viewer-Bdj1ferk.js → database-viewer-iR997ZF8.js} +1 -1
- package/dist/web/assets/{diff-viewer-C46_ZovZ.js → diff-viewer-DyOLq66H.js} +1 -1
- package/dist/web/assets/{docx-preview-CLDLTD8N.js → docx-preview-CwjJkYcL.js} +1 -1
- package/dist/web/assets/{extension-webview-BSvKnN1E.js → extension-webview-MI4F6BtU.js} +1 -1
- package/dist/web/assets/{git-log-panel-GP8XaTlA.js → git-log-panel-CYwATO2N.js} +1 -1
- package/dist/web/assets/{glide-data-grid-On_pS9Y7.js → glide-data-grid-B3DyD4fp.js} +1 -1
- package/dist/web/assets/{group-chat-tab-DmYt9GfT.js → group-chat-tab-CgWgatpe.js} +1 -1
- package/dist/web/assets/{image-preview-BWNTrufr.js → image-preview-CcPAUgwV.js} +1 -1
- package/dist/web/assets/index-BEB87kj8.css +2 -0
- package/dist/web/assets/index-CSpawh6a.js +50 -0
- package/dist/web/assets/{keybindings-store-B4EWnplE.js → keybindings-store-CT2XJy_J.js} +1 -1
- package/dist/web/assets/{markdown-renderer-suZuQvya.js → markdown-renderer-B54mHbyz.js} +1 -1
- package/dist/web/assets/{markdown-renderer-Bhv5w9Op.js → markdown-renderer-UboKVdKw.js} +1 -1
- package/dist/web/assets/{notification-store-B3zEJT3q.js → notification-store-Cw-106GW.js} +1 -1
- package/dist/web/assets/{pdf-preview-DcP6Tj7N.js → pdf-preview-B9munmj0.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BOlXoo4R.js → postgres-viewer-DMBDGrnZ.js} +1 -1
- package/dist/web/assets/{settings-tab-BqJJqL1l.js → settings-tab-CXqpAnZU.js} +1 -1
- package/dist/web/assets/{sql-query-editor-h4zuID_d.js → sql-query-editor-BT11M9Xs.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-D5X8iWUw.js → sqlite-viewer-BMrS0LPb.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-DL-viZmD.js → system-monitor-tab-CznKv8Bb.js} +1 -1
- package/dist/web/assets/terminal-tab-DLvj76hN.js +3 -0
- package/dist/web/assets/{tool-cards-zK7OP3DS.js → tool-cards-BgUD_NIq.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-Bk-dETGV.js → use-monaco-theme-BMgGJW1N.js} +1 -1
- package/dist/web/assets/{video-preview-C6bKCBvz.js → video-preview-Bkre8g0Y.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/index.ts +4 -0
- package/src/server/routes/oauth-loopback.ts +75 -0
- package/src/web/components/layout/dock-panel.tsx +8 -1
- package/src/web/components/layout/mobile-nav.tsx +16 -2
- package/src/web/components/terminal/terminal-finish-login.tsx +75 -0
- package/src/web/components/terminal/terminal-links-sheet.tsx +53 -30
- package/src/web/components/ui/mobile-bottom-sheet.tsx +29 -26
- package/src/web/hooks/use-terminal.ts +18 -7
- package/src/web/hooks/use-visual-viewport.ts +83 -0
- package/src/web/lib/oauth-loopback-url.ts +55 -0
- package/dist/web/assets/index-Crnizc26.js +0 -50
- package/dist/web/assets/index-opWDpE6A.css +0 -2
- package/dist/web/assets/terminal-tab-BVW9MfC3.js +0 -3
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* Also exports context-menu-specific sub-components (BottomSheetItem, etc.)
|
|
7
7
|
* used by adaptive-context-menu.tsx.
|
|
8
8
|
*/
|
|
9
|
-
import { createContext, useContext,
|
|
9
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
10
10
|
import { createPortal } from "react-dom";
|
|
11
11
|
import { cn } from "@/lib/utils";
|
|
12
12
|
import { useSwipeToDismiss } from "@/hooks/use-swipe-to-dismiss";
|
|
13
|
+
import { useVisualViewport } from "@/hooks/use-visual-viewport";
|
|
13
14
|
|
|
14
15
|
/* ------------------------------------------------------------------ */
|
|
15
16
|
/* Core BottomSheet — reusable everywhere */
|
|
@@ -33,44 +34,46 @@ export function BottomSheet({ open, onClose, children, className, zIndex = 50 }:
|
|
|
33
34
|
const { dragY, swipeHandlers, dragStyle, backdropOpacity, isDragging } =
|
|
34
35
|
useSwipeToDismiss(onClose);
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
const viewport = window.visualViewport;
|
|
43
|
-
if (!open || !viewport) return;
|
|
44
|
-
const update = () => setVv({ top: viewport.offsetTop, height: viewport.height });
|
|
45
|
-
update();
|
|
46
|
-
viewport.addEventListener("resize", update);
|
|
47
|
-
viewport.addEventListener("scroll", update);
|
|
48
|
-
return () => {
|
|
49
|
-
viewport.removeEventListener("resize", update);
|
|
50
|
-
viewport.removeEventListener("scroll", update);
|
|
51
|
-
};
|
|
52
|
-
}, [open]);
|
|
37
|
+
// Keyboard-aware geometry. The backdrop deliberately keeps covering the whole
|
|
38
|
+
// layout viewport while only the panel tracks the keyboard: if the two ever
|
|
39
|
+
// disagree by a few pixels, the seam shows as dimmed backdrop rather than a
|
|
40
|
+
// strip of untouched page.
|
|
41
|
+
const insets = useVisualViewport(open);
|
|
53
42
|
|
|
54
43
|
if (!open) return null;
|
|
55
44
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
// The panel keeps its footing on the bottom of the screen and only its
|
|
46
|
+
// *contents* are lifted clear of the keyboard, by padding. Moving the panel
|
|
47
|
+
// itself is what left a hole: the keyboard's height can only be estimated, and
|
|
48
|
+
// an estimate that is short drags the panel's background up with it, exposing
|
|
49
|
+
// the page. Padding an anchored panel degrades instead — a bad estimate puts
|
|
50
|
+
// the toolbar slightly off, over a background that still reaches the bottom.
|
|
51
|
+
//
|
|
52
|
+
// `--sheet-vh` is what is visible and `--sheet-kb` what the keyboard covers,
|
|
53
|
+
// so a panel can ask for "the visible area" and still span to the screen edge.
|
|
54
|
+
const frameStyle = {
|
|
55
|
+
zIndex,
|
|
56
|
+
"--sheet-vh": insets ? `${insets.height}px` : "100dvh",
|
|
57
|
+
"--sheet-kb": insets ? `${insets.keyboardInset}px` : "0px",
|
|
58
|
+
} as React.CSSProperties;
|
|
61
59
|
|
|
62
60
|
return createPortal(
|
|
63
|
-
<div className="fixed inset-0" style={
|
|
61
|
+
<div className="fixed inset-0" style={frameStyle} onClick={onClose}>
|
|
64
62
|
{/* Backdrop */}
|
|
65
63
|
<div
|
|
66
64
|
className="absolute inset-0 bg-black/40 animate-in fade-in-0 duration-200"
|
|
67
65
|
style={isDragging ? { opacity: backdropOpacity } : undefined}
|
|
68
66
|
/>
|
|
69
|
-
{/* Panel
|
|
67
|
+
{/* Panel. Padding does double duty: it clears the home indicator when the
|
|
68
|
+
keyboard is down, and the keyboard itself when it is up. A panel sized
|
|
69
|
+
in `--sheet-vh` must add `--sheet-kb` back, since the padding is inside
|
|
70
|
+
that height. */}
|
|
70
71
|
<div
|
|
71
72
|
className={cn(
|
|
72
73
|
"absolute bottom-0 left-0 right-0 rounded-t-2xl bg-popover text-popover-foreground border-t border-border",
|
|
73
|
-
|
|
74
|
+
insets?.keyboardOpen
|
|
75
|
+
? "pb-[var(--sheet-kb,0px)]"
|
|
76
|
+
: "pb-[max(0.5rem,env(safe-area-inset-bottom))]",
|
|
74
77
|
!isDragging && "animate-in slide-in-from-bottom duration-200",
|
|
75
78
|
className,
|
|
76
79
|
)}
|
|
@@ -44,6 +44,8 @@ const URL_TRAILING_PUNCT = /[).,;:'"\]}>]+$/;
|
|
|
44
44
|
const URL_PATTERN = /\bhttps?:\/\/[^\s<>"'`]+/g;
|
|
45
45
|
/** Cap the scan so a 50k-line scrollback never blocks the tap that triggered it. */
|
|
46
46
|
const URL_SCAN_MAX_ROWS = 5000;
|
|
47
|
+
/** Quiet period before refitting to a new container size, in ms. */
|
|
48
|
+
const RESIZE_SETTLE_MS = 100;
|
|
47
49
|
|
|
48
50
|
const RESIZE_PREFIX = "\x01RESIZE:";
|
|
49
51
|
const PING_MSG = "\x01PING";
|
|
@@ -363,16 +365,24 @@ export function useTerminal(
|
|
|
363
365
|
};
|
|
364
366
|
document.addEventListener("visibilitychange", onVisibility);
|
|
365
367
|
|
|
366
|
-
// ResizeObserver for auto-fit — skip when tab is hidden (0 dimensions)
|
|
368
|
+
// ResizeObserver for auto-fit — skip when tab is hidden (0 dimensions).
|
|
369
|
+
// Debounced: an on-screen keyboard resizes the container many times as it
|
|
370
|
+
// slides, and refitting on each one sends the shell a burst of size changes
|
|
371
|
+
// that it answers by redrawing, so the output visibly churns. One fit once
|
|
372
|
+
// the size settles gives the same result without the churn.
|
|
373
|
+
let fitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
367
374
|
const resizeObserver = new ResizeObserver((entries) => {
|
|
368
375
|
const entry = entries[0];
|
|
369
376
|
if (!entry || entry.contentRect.width === 0 || entry.contentRect.height === 0) return;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
377
|
+
if (fitTimer) clearTimeout(fitTimer);
|
|
378
|
+
fitTimer = setTimeout(() => {
|
|
379
|
+
try {
|
|
380
|
+
fitAddon.fit();
|
|
381
|
+
sendResize();
|
|
382
|
+
} catch {
|
|
383
|
+
// Ignore fit errors during teardown
|
|
384
|
+
}
|
|
385
|
+
}, RESIZE_SETTLE_MS);
|
|
376
386
|
});
|
|
377
387
|
resizeObserver.observe(container);
|
|
378
388
|
|
|
@@ -384,6 +394,7 @@ export function useTerminal(
|
|
|
384
394
|
|
|
385
395
|
return () => {
|
|
386
396
|
unsubTheme();
|
|
397
|
+
if (fitTimer) clearTimeout(fitTimer);
|
|
387
398
|
resizeObserver.disconnect();
|
|
388
399
|
clearInterval(heartbeatInterval);
|
|
389
400
|
document.removeEventListener("visibilitychange", onVisibility);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measures the part of the screen the user can actually see, and how much of it
|
|
3
|
+
* the on-screen keyboard is occupying.
|
|
4
|
+
*
|
|
5
|
+
* `vh`/`dvh` units and a `fixed` element both resolve against the layout
|
|
6
|
+
* viewport, which does not shrink when a keyboard opens — so anything anchored
|
|
7
|
+
* to the bottom ends up behind it. Only `visualViewport` reports the truth.
|
|
8
|
+
*
|
|
9
|
+
* Updates are applied on the frame they arrive rather than after a settle
|
|
10
|
+
* delay: iOS emits intermediate sizes throughout the keyboard's slide and its
|
|
11
|
+
* final event can land before the layout has finished moving, so deferring is
|
|
12
|
+
* what leaves geometry stale — visibly, as a strip of untouched page below a
|
|
13
|
+
* panel that should be flush with the keyboard.
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
|
|
17
|
+
export interface ViewportInsets {
|
|
18
|
+
/** Height of the visible area, in px. */
|
|
19
|
+
height: number;
|
|
20
|
+
/**
|
|
21
|
+
* Roughly how much the keyboard covers, in px.
|
|
22
|
+
*
|
|
23
|
+
* Approximate on purpose: `innerHeight` on iOS counts space behind browser
|
|
24
|
+
* chrome, so this can be off by a bar's height. Use it to inset content, never
|
|
25
|
+
* to place a background — an inset that is slightly wrong shifts content a
|
|
26
|
+
* little, whereas a background placed the same way leaves a visible hole.
|
|
27
|
+
*/
|
|
28
|
+
keyboardInset: number;
|
|
29
|
+
/** Whether the missing height is big enough to be a keyboard, not browser chrome. */
|
|
30
|
+
keyboardOpen: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Below this, the missing height is a collapsed address bar, not a keyboard. */
|
|
34
|
+
const KEYBOARD_MIN_INSET = 120;
|
|
35
|
+
|
|
36
|
+
function measure(viewport: VisualViewport): ViewportInsets {
|
|
37
|
+
const inset = Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop);
|
|
38
|
+
return {
|
|
39
|
+
height: viewport.height,
|
|
40
|
+
keyboardInset: inset,
|
|
41
|
+
keyboardOpen: inset >= KEYBOARD_MIN_INSET,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Live viewport insets, or null when unsupported or while `active` is false. */
|
|
46
|
+
export function useVisualViewport(active: boolean): ViewportInsets | null {
|
|
47
|
+
const [insets, setInsets] = useState<ViewportInsets | null>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const viewport = window.visualViewport;
|
|
51
|
+
if (!active || !viewport) {
|
|
52
|
+
setInsets(null);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Several resize/scroll events can fire per frame; one read per frame is
|
|
57
|
+
// enough and keeps React from rendering the same geometry repeatedly.
|
|
58
|
+
let frame: number | null = null;
|
|
59
|
+
const update = () => {
|
|
60
|
+
frame = null;
|
|
61
|
+
setInsets((prev) => {
|
|
62
|
+
const next = measure(viewport);
|
|
63
|
+
return prev && prev.height === next.height && prev.keyboardInset === next.keyboardInset
|
|
64
|
+
? prev
|
|
65
|
+
: next;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
const schedule = () => {
|
|
69
|
+
if (frame === null) frame = requestAnimationFrame(update);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
update();
|
|
73
|
+
viewport.addEventListener("resize", schedule);
|
|
74
|
+
viewport.addEventListener("scroll", schedule);
|
|
75
|
+
return () => {
|
|
76
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
77
|
+
viewport.removeEventListener("resize", schedule);
|
|
78
|
+
viewport.removeEventListener("scroll", schedule);
|
|
79
|
+
};
|
|
80
|
+
}, [active]);
|
|
81
|
+
|
|
82
|
+
return insets;
|
|
83
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognises OAuth authorization URLs that hand their result back to a port on
|
|
3
|
+
* the machine running the CLI, rather than to whatever device opened them.
|
|
4
|
+
*
|
|
5
|
+
* This is what strands a login started from a phone: the sign-in succeeds, the
|
|
6
|
+
* browser is redirected to 127.0.0.1:<port>, and on the phone that address is
|
|
7
|
+
* the phone itself — so the CLI waiting on the real machine never hears back.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const LOOPBACK_HOSTNAMES = new Set(["localhost", "::1"]);
|
|
11
|
+
|
|
12
|
+
/** Whether a hostname addresses the machine the browser is running on. */
|
|
13
|
+
export function isLoopbackHostname(hostname: string): boolean {
|
|
14
|
+
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
15
|
+
if (LOOPBACK_HOSTNAMES.has(host)) return true;
|
|
16
|
+
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The loopback port an authorization URL will redirect back to, or null when it
|
|
21
|
+
* redirects somewhere reachable from any device.
|
|
22
|
+
*/
|
|
23
|
+
export function loopbackRedirectPort(url: string): string | null {
|
|
24
|
+
let parsed: URL;
|
|
25
|
+
try {
|
|
26
|
+
parsed = new URL(url);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
// `redirect_uri` is the OAuth 2.0 name; some tools still send `redirectUri`.
|
|
31
|
+
const redirect =
|
|
32
|
+
parsed.searchParams.get("redirect_uri") ?? parsed.searchParams.get("redirectUri");
|
|
33
|
+
if (!redirect) return null;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const target = new URL(redirect);
|
|
37
|
+
if (!isLoopbackHostname(target.hostname)) return null;
|
|
38
|
+
return target.port || (target.protocol === "https:" ? "443" : "80");
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether a pasted string is a callback this machine could deliver. */
|
|
45
|
+
export function isDeliverableCallback(url: string): boolean {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = new URL(url.trim());
|
|
48
|
+
return (
|
|
49
|
+
(parsed.protocol === "http:" || parsed.protocol === "https:") &&
|
|
50
|
+
isLoopbackHostname(parsed.hostname)
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|