@hienlh/ppm 0.8.54 → 0.8.55
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 +19 -0
- package/bun.lock +250 -1
- package/dist/web/assets/_basePickBy-CZovQgWd.js +1 -0
- package/dist/web/assets/_baseUniq-ClnvscgW.js +1 -0
- package/dist/web/assets/{api-client-TUmacMRS.js → api-client-DpGMOZNf.js} +1 -1
- package/dist/web/assets/api-settings--eVrUeZM.js +1 -0
- package/dist/web/assets/arc-C2Qaz-ch.js +1 -0
- package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +1 -0
- package/dist/web/assets/architectureDiagram-2XIMDMQ5-Jq91S_rs.js +36 -0
- package/dist/web/assets/array-BGFCBI0e.js +1 -0
- package/dist/web/assets/blockDiagram-WCTKOSBZ-CKGufRTy.js +132 -0
- package/dist/web/assets/c4Diagram-IC4MRINW-BNP2L9r_.js +10 -0
- package/dist/web/assets/channel-w7yboq56.js +1 -0
- package/dist/web/assets/chat-tab-BUOCxR2G.js +7 -0
- package/dist/web/assets/chunk-4BX2VUAB-BptTlTyl.js +1 -0
- package/dist/web/assets/chunk-55IACEB6-C4mUdyio.js +1 -0
- package/dist/web/assets/chunk-7E7YKBS2-6xAQfBwa.js +1 -0
- package/dist/web/assets/chunk-7R4GIKGN-DXaGAn_K.js +80 -0
- package/dist/web/assets/chunk-C72U2L5F-DOtEiN5f.js +1 -0
- package/dist/web/assets/chunk-CFjPhJqf.js +1 -0
- package/dist/web/assets/chunk-EGIJ26TM-D0KJTa_T.js +1 -0
- package/dist/web/assets/chunk-FMBD7UC4-C_1aG0eb.js +15 -0
- package/dist/web/assets/chunk-GEFDOKGD-DwVPiYfW.js +2 -0
- package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +1 -0
- package/dist/web/assets/chunk-JSJVCQXG-BSrqCL_3.js +1 -0
- package/dist/web/assets/chunk-KX2RTZJC-BCxGmbzy.js +1 -0
- package/dist/web/assets/chunk-KYZI473N-BKO5gMeU.js +53 -0
- package/dist/web/assets/chunk-L3YUKLVL-3wBgkSvL.js +1 -0
- package/dist/web/assets/chunk-MX3YWQON-BgjSEzus.js +1 -0
- package/dist/web/assets/chunk-NQ4KR5QH-DLrZwBEm.js +220 -0
- package/dist/web/assets/chunk-O4XLMI2P-BurQy8tt.js +7 -0
- package/dist/web/assets/chunk-OZEHJAEY-YTn24bGg.js +1 -0
- package/dist/web/assets/chunk-PQ6SQG4A-BxtUGYhW.js +1 -0
- package/dist/web/assets/chunk-PU5JKC2W-B66ELkQm.js +70 -0
- package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +1 -0
- package/dist/web/assets/chunk-R5LLSJPH-euR2RxLN.js +1 -0
- package/dist/web/assets/chunk-WL4C6EOR-_2CBOJdI.js +189 -0
- package/dist/web/assets/chunk-XIRO2GV7-kqQ0g6wW.js +1 -0
- package/dist/web/assets/chunk-XPW4576I-CtcaMb09.js +32 -0
- package/dist/web/assets/chunk-XZSTWKYB-BYxFzZwS.js +94 -0
- package/dist/web/assets/chunk-YBOYWFTD-Dx_fX35n.js +1 -0
- package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +1 -0
- package/dist/web/assets/clone-BSi6cgDh.js +1 -0
- package/dist/web/assets/code-editor-os78eUN8.js +1 -0
- package/dist/web/assets/columns-2-ChOTgl3e.js +1 -0
- package/dist/web/assets/cose-bilkent-S5V4N54A-CHHjH2dV.js +1 -0
- package/dist/web/assets/cytoscape.esm-Ccan6xou.js +321 -0
- package/dist/web/assets/dagre-CNtSxiE_.js +1 -0
- package/dist/web/assets/dagre-KLK3FWXG-ChenfPp1.js +4 -0
- package/dist/web/assets/database-viewer-DTwe0h8F.js +1 -0
- package/dist/web/assets/defaultLocale-CRZydyG6.js +1 -0
- package/dist/web/assets/diagram-E7M64L7V-CzKYZM0Y.js +24 -0
- package/dist/web/assets/diagram-IFDJBPK2-ChB_paPo.js +43 -0
- package/dist/web/assets/diagram-P4PSJMXO-D1eW1dkL.js +24 -0
- package/dist/web/assets/diff-viewer-CSyOOmS2.js +4 -0
- package/dist/web/assets/dist-Cce3efmT.js +1 -0
- package/dist/web/assets/{dist-QgqOdSYG.js → dist-T0Vhi0Mh.js} +1 -1
- package/dist/web/assets/erDiagram-INFDFZHY-mCvUFSn6.js +70 -0
- package/dist/web/assets/flowDiagram-PKNHOUZH-14ohZ1M1.js +162 -0
- package/dist/web/assets/ganttDiagram-A5KZAMGK-DIX0pLbk.js +292 -0
- package/dist/web/assets/git-graph-CwYW3F4P.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +1 -0
- package/dist/web/assets/gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js +65 -0
- package/dist/web/assets/graphlib-DhOZxqsh.js +1 -0
- package/dist/web/assets/index-WKLuYsBY.css +2 -0
- package/dist/web/assets/index-yMR7OUDx.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +2 -0
- package/dist/web/assets/init-B8gtcn7T.js +1 -0
- package/dist/web/assets/input-Brjz2Vv-.js +41 -0
- package/dist/web/assets/isArrayLikeObject-B4pdpV8V.js +1 -0
- package/dist/web/assets/isEmpty-C0YYdhYj.js +1 -0
- package/dist/web/assets/ishikawaDiagram-PHBUUO56-olazD6dZ.js +70 -0
- package/dist/web/assets/journeyDiagram-4ABVD52K-CttDH9bb.js +139 -0
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +1 -0
- package/dist/web/assets/kanban-definition-K7BYSVSG-BBXbI37U.js +89 -0
- package/dist/web/assets/katex-Bbu770d9.js +265 -0
- package/dist/web/assets/keybindings-store-B-BLLKiZ.js +1 -0
- package/dist/web/assets/line-DBLLF7lH.js +1 -0
- package/dist/web/assets/linear-BLFWatDe.js +1 -0
- package/dist/web/assets/markdown-renderer-DQWY7QvX.js +69 -0
- package/dist/web/assets/math-DwgHI-Cu.js +1 -0
- package/dist/web/assets/mermaid-parser.core-BKiGOTjR.js +4 -0
- package/dist/web/assets/mindmap-definition-YRQLILUH-DoT7m4Sz.js +68 -0
- package/dist/web/assets/ordinal-CCj7PWgZ.js +1 -0
- package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +1 -0
- package/dist/web/assets/path-DZF-JdEe.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +1 -0
- package/dist/web/assets/pieDiagram-SKSYHLDU-Bkh2E4zE.js +30 -0
- package/dist/web/assets/postgres-viewer-Ctv7NTI_.js +1 -0
- package/dist/web/assets/preload-helper-qlgyTAkD.js +1 -0
- package/dist/web/assets/quadrantDiagram-337W2JSQ-B7zgALOL.js +7 -0
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +1 -0
- package/dist/web/assets/react-BGf7KNLk.js +1 -0
- package/dist/web/assets/react-nm2Ru1Pt.js +1 -0
- package/dist/web/assets/requirementDiagram-Z7DCOOCP-D_5GXNRo.js +73 -0
- package/dist/web/assets/rough.esm-VLpapkIG.js +1 -0
- package/dist/web/assets/sankeyDiagram-WA2Y5GQK-BA9EFAAe.js +10 -0
- package/dist/web/assets/sequenceDiagram-2WXFIKYE-fyWIrHiG.js +145 -0
- package/dist/web/assets/settings-store-Bbhg_ptG.js +2 -0
- package/dist/web/assets/settings-tab-Daap0c_B.js +1 -0
- package/dist/web/assets/sqlite-viewer-DtNk76CE.js +1 -0
- package/dist/web/assets/src-BoSBNdA_.js +1 -0
- package/dist/web/assets/stateDiagram-RAJIS63D-DfRBcaBu.js +1 -0
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +1 -0
- package/dist/web/assets/{tab-store-NOBndc0_.js → tab-store-dpsCvqhH.js} +1 -1
- package/dist/web/assets/{table-B6neW6Hr.js → table-Yo02WRH-.js} +1 -1
- package/dist/web/assets/{tag-DJUYe5BQ.js → tag-CaC1ng2E.js} +1 -1
- package/dist/web/assets/{terminal-tab-0Y48dynP.js → terminal-tab-JEpjt3RD.js} +2 -2
- package/dist/web/assets/timeline-definition-YZTLITO2-DYfwJ1jM.js +61 -0
- package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +1 -0
- package/dist/web/assets/{use-monaco-theme-DwP4EHdO.js → use-monaco-theme-DHbyUrzJ.js} +1 -1
- package/dist/web/assets/vennDiagram-LZ73GAT5-DqbKNRD9.js +34 -0
- package/dist/web/assets/xychartDiagram-JWTSCODW-DhUL86qT.js +7 -0
- package/dist/web/index.html +13 -11
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
- package/snapshot-state.md +1526 -0
- package/src/providers/claude-agent-sdk.ts +7 -0
- package/src/server/index.ts +25 -16
- package/src/server/routes/accounts.ts +3 -8
- package/src/services/account-selector.service.ts +52 -9
- package/src/services/fs-browse.service.ts +10 -7
- package/src/web/app.tsx +8 -0
- package/src/web/components/chat/account-dialogs.tsx +377 -0
- package/src/web/components/chat/message-list.tsx +196 -45
- package/src/web/components/chat/usage-badge.tsx +56 -20
- package/src/web/components/settings/settings-tab.tsx +2 -5
- package/src/web/components/shared/diagram-overlay.tsx +139 -0
- package/src/web/components/shared/image-overlay.tsx +45 -0
- package/src/web/components/shared/markdown-renderer.tsx +55 -2
- package/src/web/stores/diagram-overlay-store.ts +16 -0
- package/src/web/stores/image-overlay-store.ts +18 -0
- package/test-tokens.mjs +212 -0
- package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
- package/dist/web/assets/api-settings-D4bgXrLU.js +0 -1
- package/dist/web/assets/chat-tab-CgVh-OsO.js +0 -7
- package/dist/web/assets/code-editor-DgvZlpB7.js +0 -1
- package/dist/web/assets/columns-2-BZ5wv2wA.js +0 -1
- package/dist/web/assets/database-viewer-CRZksTo-.js +0 -1
- package/dist/web/assets/diff-viewer-CPNLuddT.js +0 -4
- package/dist/web/assets/git-graph-BCtMSQwB.js +0 -1
- package/dist/web/assets/index-CfSJP_Fv.css +0 -2
- package/dist/web/assets/index-DcJqqWbL.js +0 -37
- package/dist/web/assets/input-CE3bFwLk.js +0 -41
- package/dist/web/assets/jsx-runtime-wQxeESYQ.js +0 -1
- package/dist/web/assets/keybindings-store-C1HiSDRb.js +0 -1
- package/dist/web/assets/markdown-renderer-Ci7qz558.js +0 -59
- package/dist/web/assets/postgres-viewer-C8PRJ87B.js +0 -1
- package/dist/web/assets/react-CYzKIDNi.js +0 -1
- package/dist/web/assets/react-rgzL83kk.js +0 -1
- package/dist/web/assets/settings-store-DL2KEbtc.js +0 -2
- package/dist/web/assets/settings-tab-CqnP28Dq.js +0 -1
- package/dist/web/assets/sqlite-viewer-BSceyudC.js +0 -1
- /package/dist/web/assets/{utils-DC-bdPS3.js → utils-btZ8C8-R.js} +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useEffect, useCallback, useRef, useState } from "react";
|
|
2
|
+
import { X, ZoomIn, ZoomOut, RotateCcw } from "lucide-react";
|
|
3
|
+
import { useDiagramOverlay } from "@/stores/diagram-overlay-store";
|
|
4
|
+
|
|
5
|
+
const MIN_ZOOM = 0.25;
|
|
6
|
+
const MAX_ZOOM = 4;
|
|
7
|
+
const ZOOM_STEP = 0.25;
|
|
8
|
+
|
|
9
|
+
/** Global diagram lightbox overlay with zoom & pan — mount once in app root */
|
|
10
|
+
export function DiagramOverlay() {
|
|
11
|
+
const { svg, close } = useDiagramOverlay();
|
|
12
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
const [zoom, setZoom] = useState(1);
|
|
14
|
+
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
15
|
+
const dragging = useRef(false);
|
|
16
|
+
const lastPos = useRef({ x: 0, y: 0 });
|
|
17
|
+
|
|
18
|
+
// Reset zoom/pan when opening new diagram
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (svg) {
|
|
21
|
+
setZoom(1);
|
|
22
|
+
setPan({ x: 0, y: 0 });
|
|
23
|
+
}
|
|
24
|
+
}, [svg]);
|
|
25
|
+
|
|
26
|
+
const handleKeyDown = useCallback(
|
|
27
|
+
(e: KeyboardEvent) => {
|
|
28
|
+
if (e.key === "Escape") close();
|
|
29
|
+
if (e.key === "=" || e.key === "+") setZoom((z) => Math.min(MAX_ZOOM, z + ZOOM_STEP));
|
|
30
|
+
if (e.key === "-") setZoom((z) => Math.max(MIN_ZOOM, z - ZOOM_STEP));
|
|
31
|
+
if (e.key === "0") { setZoom(1); setPan({ x: 0, y: 0 }); }
|
|
32
|
+
},
|
|
33
|
+
[close],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!svg) return;
|
|
38
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
39
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
40
|
+
}, [svg, handleKeyDown]);
|
|
41
|
+
|
|
42
|
+
const handleWheel = useCallback((e: React.WheelEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
|
|
45
|
+
setZoom((z) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z + delta)));
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
|
49
|
+
if (e.button !== 0) return;
|
|
50
|
+
dragging.current = true;
|
|
51
|
+
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
52
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
|
56
|
+
if (!dragging.current) return;
|
|
57
|
+
const dx = e.clientX - lastPos.current.x;
|
|
58
|
+
const dy = e.clientY - lastPos.current.y;
|
|
59
|
+
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
60
|
+
setPan((p) => ({ x: p.x + dx, y: p.y + dy }));
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const handlePointerUp = useCallback(() => {
|
|
64
|
+
dragging.current = false;
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
if (!svg) return null;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className="fixed inset-0 z-[100] flex flex-col bg-black/85 backdrop-blur-sm animate-in fade-in duration-150"
|
|
72
|
+
onClick={close}
|
|
73
|
+
>
|
|
74
|
+
{/* Toolbar */}
|
|
75
|
+
<div
|
|
76
|
+
className="flex items-center justify-between px-4 py-2 bg-black/40"
|
|
77
|
+
onClick={(e) => e.stopPropagation()}
|
|
78
|
+
>
|
|
79
|
+
<span className="text-sm text-white/70">
|
|
80
|
+
{Math.round(zoom * 100)}% — Scroll to zoom, drag to pan, 0 to reset
|
|
81
|
+
</span>
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
<button
|
|
84
|
+
onClick={() => setZoom((z) => Math.max(MIN_ZOOM, z - ZOOM_STEP))}
|
|
85
|
+
className="flex items-center justify-center size-8 rounded bg-white/10 hover:bg-white/20 text-white transition-colors"
|
|
86
|
+
aria-label="Zoom out"
|
|
87
|
+
>
|
|
88
|
+
<ZoomOut className="size-4" />
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => setZoom((z) => Math.min(MAX_ZOOM, z + ZOOM_STEP))}
|
|
92
|
+
className="flex items-center justify-center size-8 rounded bg-white/10 hover:bg-white/20 text-white transition-colors"
|
|
93
|
+
aria-label="Zoom in"
|
|
94
|
+
>
|
|
95
|
+
<ZoomIn className="size-4" />
|
|
96
|
+
</button>
|
|
97
|
+
<button
|
|
98
|
+
onClick={() => { setZoom(1); setPan({ x: 0, y: 0 }); }}
|
|
99
|
+
className="flex items-center justify-center size-8 rounded bg-white/10 hover:bg-white/20 text-white transition-colors"
|
|
100
|
+
aria-label="Reset zoom"
|
|
101
|
+
>
|
|
102
|
+
<RotateCcw className="size-4" />
|
|
103
|
+
</button>
|
|
104
|
+
<button
|
|
105
|
+
onClick={close}
|
|
106
|
+
className="flex items-center justify-center size-8 rounded bg-white/10 hover:bg-white/20 text-white transition-colors ml-2"
|
|
107
|
+
aria-label="Close"
|
|
108
|
+
>
|
|
109
|
+
<X className="size-5" />
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Diagram area */}
|
|
115
|
+
<div
|
|
116
|
+
ref={containerRef}
|
|
117
|
+
className="flex-1 overflow-hidden cursor-grab active:cursor-grabbing"
|
|
118
|
+
onClick={(e) => e.stopPropagation()}
|
|
119
|
+
onWheel={handleWheel}
|
|
120
|
+
onPointerDown={handlePointerDown}
|
|
121
|
+
onPointerMove={handlePointerMove}
|
|
122
|
+
onPointerUp={handlePointerUp}
|
|
123
|
+
>
|
|
124
|
+
<div
|
|
125
|
+
className="w-full h-full flex items-center justify-center"
|
|
126
|
+
style={{
|
|
127
|
+
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
|
128
|
+
transformOrigin: "center center",
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<div
|
|
132
|
+
className="mermaid-overlay-content bg-white rounded-lg p-6 shadow-2xl [&_svg]:max-w-full [&_svg]:h-auto"
|
|
133
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect, useCallback } from "react";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { useImageOverlay } from "@/stores/image-overlay-store";
|
|
4
|
+
|
|
5
|
+
/** Global image lightbox overlay — mount once in app root */
|
|
6
|
+
export function ImageOverlay() {
|
|
7
|
+
const { src, alt, close } = useImageOverlay();
|
|
8
|
+
|
|
9
|
+
const handleKeyDown = useCallback(
|
|
10
|
+
(e: KeyboardEvent) => {
|
|
11
|
+
if (e.key === "Escape") close();
|
|
12
|
+
},
|
|
13
|
+
[close],
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!src) return;
|
|
18
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
19
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
20
|
+
}, [src, handleKeyDown]);
|
|
21
|
+
|
|
22
|
+
if (!src) return null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in duration-150"
|
|
27
|
+
onClick={close}
|
|
28
|
+
>
|
|
29
|
+
<button
|
|
30
|
+
onClick={close}
|
|
31
|
+
className="absolute top-4 right-4 z-10 flex items-center justify-center size-8 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors"
|
|
32
|
+
aria-label="Close"
|
|
33
|
+
>
|
|
34
|
+
<X className="size-5" />
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
<img
|
|
38
|
+
src={src}
|
|
39
|
+
alt={alt}
|
|
40
|
+
className="max-w-[90vw] max-h-[90vh] object-contain rounded-lg shadow-2xl"
|
|
41
|
+
onClick={(e) => e.stopPropagation()}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -2,9 +2,27 @@ import { useMemo, useRef, useEffect } from "react";
|
|
|
2
2
|
import { marked } from "marked";
|
|
3
3
|
import { useTabStore } from "@/stores/tab-store";
|
|
4
4
|
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
5
|
+
import { useImageOverlay } from "@/stores/image-overlay-store";
|
|
6
|
+
import { useDiagramOverlay } from "@/stores/diagram-overlay-store";
|
|
5
7
|
import { openCommandPalette } from "@/hooks/use-global-keybindings";
|
|
6
8
|
import { api, projectUrl, getAuthToken } from "@/lib/api-client";
|
|
7
9
|
import { basename } from "@/lib/utils";
|
|
10
|
+
import mermaid from "mermaid";
|
|
11
|
+
|
|
12
|
+
/** Mermaid keywords that start a diagram definition */
|
|
13
|
+
const MERMAID_KEYWORDS = /^(sequenceDiagram|flowchart|graph\s|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|quadrantChart|requirementDiagram|gitGraph|mindmap|timeline|sankey|xychart|block-beta|packet-beta|architecture-beta|kanban)\b/;
|
|
14
|
+
|
|
15
|
+
let mermaidInitialized = false;
|
|
16
|
+
function ensureMermaidInit() {
|
|
17
|
+
if (mermaidInitialized) return;
|
|
18
|
+
mermaid.initialize({
|
|
19
|
+
startOnLoad: false,
|
|
20
|
+
theme: "default",
|
|
21
|
+
securityLevel: "loose",
|
|
22
|
+
fontFamily: "ui-sans-serif, system-ui, sans-serif",
|
|
23
|
+
});
|
|
24
|
+
mermaidInitialized = true;
|
|
25
|
+
}
|
|
8
26
|
|
|
9
27
|
/** Detect local absolute file paths (Unix or Windows) */
|
|
10
28
|
const LOCAL_PATH_RE = /^(\/|[A-Za-z]:[/\\])/;
|
|
@@ -88,11 +106,46 @@ export function MarkdownRenderer({ content, projectName, className = "", codeAct
|
|
|
88
106
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
89
107
|
const openTab = useTabStore((s) => s.openTab);
|
|
90
108
|
const fileTree = useFileStore((s) => s.tree);
|
|
109
|
+
const openImageOverlay = useImageOverlay((s) => s.open);
|
|
110
|
+
const openDiagramOverlay = useDiagramOverlay((s) => s.open);
|
|
91
111
|
|
|
92
112
|
useEffect(() => {
|
|
93
113
|
const container = containerRef.current;
|
|
94
114
|
if (!container) return;
|
|
95
115
|
|
|
116
|
+
// --- Render mermaid diagrams ---
|
|
117
|
+
const renderMermaid = async () => {
|
|
118
|
+
ensureMermaidInit();
|
|
119
|
+
const pres = container.querySelectorAll("pre");
|
|
120
|
+
for (const pre of pres) {
|
|
121
|
+
const code = pre.querySelector("code");
|
|
122
|
+
if (!code) continue;
|
|
123
|
+
const langClass = code.className ?? "";
|
|
124
|
+
const text = (code.textContent ?? "").trim();
|
|
125
|
+
const isMermaid = langClass.includes("language-mermaid") || MERMAID_KEYWORDS.test(text);
|
|
126
|
+
if (!isMermaid) continue;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
|
|
130
|
+
const { svg } = await mermaid.render(id, text);
|
|
131
|
+
const wrapper = document.createElement("div");
|
|
132
|
+
wrapper.className = "mermaid-diagram group relative cursor-pointer rounded-lg border border-border bg-white dark:bg-zinc-50 p-3 overflow-x-auto my-2";
|
|
133
|
+
wrapper.innerHTML = svg;
|
|
134
|
+
// Expand icon hint
|
|
135
|
+
const hint = document.createElement("div");
|
|
136
|
+
hint.className = "absolute top-2 right-2 flex items-center gap-1 px-2 py-1 rounded bg-black/60 text-white text-xs opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none";
|
|
137
|
+
hint.textContent = "Click to expand";
|
|
138
|
+
wrapper.appendChild(hint);
|
|
139
|
+
// Click to open overlay
|
|
140
|
+
wrapper.addEventListener("click", () => openDiagramOverlay(svg));
|
|
141
|
+
pre.replaceWith(wrapper);
|
|
142
|
+
} catch {
|
|
143
|
+
// Render failed — leave as code block
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
renderMermaid();
|
|
148
|
+
|
|
96
149
|
// --- Click handler for file links and clickable code ---
|
|
97
150
|
const handleClick = (e: MouseEvent) => {
|
|
98
151
|
const target = e.target as HTMLElement;
|
|
@@ -206,7 +259,7 @@ export function MarkdownRenderer({ content, projectName, className = "", codeAct
|
|
|
206
259
|
img.style.borderRadius = "0.375rem";
|
|
207
260
|
img.style.border = "1px solid var(--color-border)";
|
|
208
261
|
img.style.cursor = "pointer";
|
|
209
|
-
img.onclick = () =>
|
|
262
|
+
img.onclick = () => openImageOverlay(url, img.alt || basename(src));
|
|
210
263
|
})
|
|
211
264
|
.catch(() => {
|
|
212
265
|
img.style.opacity = "0.5";
|
|
@@ -263,7 +316,7 @@ export function MarkdownRenderer({ content, projectName, className = "", codeAct
|
|
|
263
316
|
container.removeEventListener("click", handleClick);
|
|
264
317
|
blobUrls.forEach((u) => URL.revokeObjectURL(u));
|
|
265
318
|
};
|
|
266
|
-
}, [html, projectName, openTab, codeActions]);
|
|
319
|
+
}, [html, projectName, openTab, codeActions, openImageOverlay, openDiagramOverlay]);
|
|
267
320
|
|
|
268
321
|
return (
|
|
269
322
|
<div
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
interface DiagramOverlayState {
|
|
4
|
+
/** SVG markup string to display */
|
|
5
|
+
svg: string | null;
|
|
6
|
+
/** Open the overlay with rendered SVG */
|
|
7
|
+
open: (svg: string) => void;
|
|
8
|
+
/** Close the overlay */
|
|
9
|
+
close: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useDiagramOverlay = create<DiagramOverlayState>((set) => ({
|
|
13
|
+
svg: null,
|
|
14
|
+
open: (svg) => set({ svg }),
|
|
15
|
+
close: () => set({ svg: null }),
|
|
16
|
+
}));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
interface ImageOverlayState {
|
|
4
|
+
/** Blob URL or data URL of the image to display */
|
|
5
|
+
src: string | null;
|
|
6
|
+
alt: string;
|
|
7
|
+
/** Open the overlay with a given image source */
|
|
8
|
+
open: (src: string, alt?: string) => void;
|
|
9
|
+
/** Close the overlay */
|
|
10
|
+
close: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useImageOverlay = create<ImageOverlayState>((set) => ({
|
|
14
|
+
src: null,
|
|
15
|
+
alt: "",
|
|
16
|
+
open: (src, alt = "") => set({ src, alt }),
|
|
17
|
+
close: () => set({ src: null, alt: "" }),
|
|
18
|
+
}));
|
package/test-tokens.mjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Test script to check access token & refresh token expiry behavior.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun test-tokens.mjs # test all accounts in dev DB
|
|
7
|
+
* bun test-tokens.mjs <account-id> # test specific account
|
|
8
|
+
* bun test-tokens.mjs --refresh <id> # force refresh and show new expiry
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Database from "bun:sqlite";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { createDecipheriv } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
// --- Config ---
|
|
17
|
+
const DB_PATH = join(homedir(), ".ppm", "ppm.dev.db");
|
|
18
|
+
const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
19
|
+
const OAUTH_TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
|
|
20
|
+
const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
21
|
+
|
|
22
|
+
// --- Crypto (matches src/lib/account-crypto.ts) ---
|
|
23
|
+
function getEncryptionKey() {
|
|
24
|
+
const keyPath = join(homedir(), ".ppm", "account.key");
|
|
25
|
+
try {
|
|
26
|
+
const hex = require("node:fs").readFileSync(keyPath, "utf-8").trim();
|
|
27
|
+
return Buffer.from(hex, "hex");
|
|
28
|
+
} catch {
|
|
29
|
+
console.error(`Cannot find encryption key at ${keyPath}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decrypt(encryptedValue) {
|
|
35
|
+
if (!encryptedValue || encryptedValue === "") return "";
|
|
36
|
+
const parts = encryptedValue.split(":");
|
|
37
|
+
if (parts.length !== 3) return encryptedValue; // not encrypted
|
|
38
|
+
const [ivHex, authTagHex, cipherHex] = parts;
|
|
39
|
+
const key = getEncryptionKey();
|
|
40
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
|
|
41
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
42
|
+
let decrypted = decipher.update(Buffer.from(cipherHex, "hex"), undefined, "utf8");
|
|
43
|
+
decrypted += decipher.final("utf8");
|
|
44
|
+
return decrypted;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Helpers ---
|
|
48
|
+
function formatExpiry(expiresAt) {
|
|
49
|
+
if (!expiresAt) return "N/A (no expiry)";
|
|
50
|
+
const now = Math.floor(Date.now() / 1000);
|
|
51
|
+
const diff = expiresAt - now;
|
|
52
|
+
const absMin = Math.abs(Math.floor(diff / 60));
|
|
53
|
+
const absHr = Math.floor(absMin / 60);
|
|
54
|
+
const remMin = absMin % 60;
|
|
55
|
+
const date = new Date(expiresAt * 1000).toLocaleString("vi-VN", { timeZone: "Asia/Saigon" });
|
|
56
|
+
if (diff > 0) {
|
|
57
|
+
return `${date} (còn ${absHr}h${remMin}m)`;
|
|
58
|
+
} else {
|
|
59
|
+
return `${date} (HẾT HẠN ${absHr}h${remMin}m trước)`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function testAccessToken(token) {
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(PROFILE_URL, {
|
|
66
|
+
headers: {
|
|
67
|
+
Accept: "application/json",
|
|
68
|
+
Authorization: `Bearer ${token}`,
|
|
69
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
70
|
+
"User-Agent": "ppm-token-test/1.0",
|
|
71
|
+
},
|
|
72
|
+
signal: AbortSignal.timeout(10_000),
|
|
73
|
+
});
|
|
74
|
+
if (res.status === 200) {
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
return { status: "VALID", code: 200, email: data.account?.email, name: data.account?.display_name };
|
|
77
|
+
}
|
|
78
|
+
if (res.status === 429) return { status: "VALID (rate-limited)", code: 429 };
|
|
79
|
+
const body = await res.text().catch(() => "");
|
|
80
|
+
return { status: "INVALID", code: res.status, error: body.slice(0, 200) };
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return { status: "ERROR", error: e.message };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function testRefreshToken(refreshToken) {
|
|
87
|
+
if (!refreshToken || refreshToken === "") {
|
|
88
|
+
return { status: "NO_TOKEN", error: "Empty refresh token (temporary account)" };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(OAUTH_TOKEN_URL, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
grant_type: "refresh_token",
|
|
96
|
+
client_id: OAUTH_CLIENT_ID,
|
|
97
|
+
refresh_token: refreshToken,
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
if (res.ok) {
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
return {
|
|
103
|
+
status: "VALID",
|
|
104
|
+
code: 200,
|
|
105
|
+
newAccessToken: data.access_token?.slice(0, 20) + "...",
|
|
106
|
+
newRefreshToken: data.refresh_token ? "YES (rotated)" : "NO (same)",
|
|
107
|
+
expiresIn: data.expires_in,
|
|
108
|
+
expiresInReadable: `${Math.floor(data.expires_in / 3600)}h${Math.floor((data.expires_in % 3600) / 60)}m`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const body = await res.text().catch(() => "");
|
|
112
|
+
return { status: "INVALID", code: res.status, error: body.slice(0, 300) };
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { status: "ERROR", error: e.message };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Main ---
|
|
119
|
+
const args = process.argv.slice(2);
|
|
120
|
+
const doRefresh = args.includes("--refresh");
|
|
121
|
+
const targetId = args.find((a) => a !== "--refresh");
|
|
122
|
+
|
|
123
|
+
console.log("=".repeat(70));
|
|
124
|
+
console.log("PPM Token Expiry Test");
|
|
125
|
+
console.log(`DB: ${DB_PATH}`);
|
|
126
|
+
console.log(`Time: ${new Date().toLocaleString("vi-VN", { timeZone: "Asia/Saigon" })}`);
|
|
127
|
+
console.log("=".repeat(70));
|
|
128
|
+
|
|
129
|
+
let db;
|
|
130
|
+
try {
|
|
131
|
+
db = new Database(DB_PATH, { readonly: true });
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.error(`Cannot open DB: ${e.message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const query = targetId
|
|
138
|
+
? db.prepare("SELECT * FROM accounts WHERE id = ?").all(targetId)
|
|
139
|
+
: db.prepare("SELECT * FROM accounts ORDER BY priority ASC, created_at ASC").all();
|
|
140
|
+
|
|
141
|
+
if (query.length === 0) {
|
|
142
|
+
console.log("No accounts found.");
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const row of query) {
|
|
147
|
+
console.log("\n" + "-".repeat(70));
|
|
148
|
+
console.log(`Account: ${row.label ?? "N/A"}`);
|
|
149
|
+
console.log(` ID: ${row.id}`);
|
|
150
|
+
console.log(` Email: ${row.email ?? "N/A"}`);
|
|
151
|
+
console.log(` Status: ${row.status}`);
|
|
152
|
+
console.log(` Expires: ${formatExpiry(row.expires_at)}`);
|
|
153
|
+
|
|
154
|
+
let accessToken, refreshToken;
|
|
155
|
+
try {
|
|
156
|
+
accessToken = decrypt(row.access_token);
|
|
157
|
+
refreshToken = decrypt(row.refresh_token);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.log(` ⚠ Decrypt failed: ${e.message}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const isOAuth = accessToken.startsWith("sk-ant-oat");
|
|
164
|
+
console.log(` Type: ${isOAuth ? "OAuth token" : "API key"}`);
|
|
165
|
+
console.log(` Has refresh token: ${refreshToken && refreshToken !== "" ? "YES" : "NO"}`);
|
|
166
|
+
|
|
167
|
+
if (!isOAuth) {
|
|
168
|
+
console.log(` → API keys don't expire. Skipping token tests.`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Test 1: Is access token still valid?
|
|
173
|
+
console.log("\n [1] Testing access token against profile API...");
|
|
174
|
+
const accessResult = await testAccessToken(accessToken);
|
|
175
|
+
if (accessResult.status.startsWith("VALID")) {
|
|
176
|
+
console.log(` ✓ Access token: ${accessResult.status}`);
|
|
177
|
+
if (accessResult.email) console.log(` Email: ${accessResult.email}, Name: ${accessResult.name}`);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(` ✗ Access token: ${accessResult.status} (HTTP ${accessResult.code})`);
|
|
180
|
+
if (accessResult.error) console.log(` Error: ${accessResult.error}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Test 2: Is refresh token still valid?
|
|
184
|
+
if (doRefresh) {
|
|
185
|
+
console.log("\n [2] Testing refresh token (will actually refresh!)...");
|
|
186
|
+
const refreshResult = await testRefreshToken(refreshToken);
|
|
187
|
+
if (refreshResult.status === "VALID") {
|
|
188
|
+
console.log(` ✓ Refresh token: VALID`);
|
|
189
|
+
console.log(` New access token: ${refreshResult.newAccessToken}`);
|
|
190
|
+
console.log(` New refresh token: ${refreshResult.newRefreshToken}`);
|
|
191
|
+
console.log(` New expires_in: ${refreshResult.expiresIn}s (${refreshResult.expiresInReadable})`);
|
|
192
|
+
console.log(` ⚠ NOTE: Old access token may now be invalidated!`);
|
|
193
|
+
console.log(` ⚠ Run this script WITHOUT --refresh to avoid side effects.`);
|
|
194
|
+
} else {
|
|
195
|
+
console.log(` ✗ Refresh token: ${refreshResult.status}`);
|
|
196
|
+
if (refreshResult.error) console.log(` Error: ${refreshResult.error}`);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
console.log("\n [2] Refresh token: SKIPPED (use --refresh to test)");
|
|
200
|
+
console.log(` ⚠ --refresh sẽ tạo access token MỚI, token cũ có thể bị invalidate!`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log("\n" + "=".repeat(70));
|
|
205
|
+
console.log("Summary:");
|
|
206
|
+
console.log(" - Access token (sk-ant-oat*): thường hết hạn sau ~1h (expires_in từ OAuth)");
|
|
207
|
+
console.log(" - Refresh token: không có expiry rõ ràng, chết khi Anthropic trả invalid_grant");
|
|
208
|
+
console.log(" - Khi refresh: Anthropic CÓ THỂ rotate refresh token (trả token mới)");
|
|
209
|
+
console.log(" - PPM auto-refresh mỗi 5 phút cho token sắp hết hạn (<5 phút)");
|
|
210
|
+
console.log("=".repeat(70));
|
|
211
|
+
|
|
212
|
+
db.close();
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: PPM test conventions and gotchas
|
|
3
|
-
description: Key patterns, pitfalls, and setup details for writing tests in the PPM project
|
|
4
|
-
type: project
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Test runner: `bun test` (Jest-compatible API from `bun:test`)
|
|
8
|
-
|
|
9
|
-
## Test structure
|
|
10
|
-
- `tests/setup.ts` — shared helpers: `createTempDir`, `cleanupDir`, `createTempGitRepo`, `buildTestApp`
|
|
11
|
-
- `tests/unit/services/` — unit tests for ConfigService, ProjectService, FileService, GitService
|
|
12
|
-
- `tests/integration/api/` — integration tests using `app.request()` (no real server needed)
|
|
13
|
-
|
|
14
|
-
## Critical gotchas
|
|
15
|
-
|
|
16
|
-
### ppm.yaml in CWD
|
|
17
|
-
The project root has a real `ppm.yaml` with `port: 5555`. `ConfigService.load(missingPath)` falls through to `LOCAL_CONFIG = "ppm.yaml"` in CWD when the given path doesn't exist. Always write an actual file before calling `load()` to avoid picking up this real config.
|
|
18
|
-
|
|
19
|
-
### Global configService in git routes
|
|
20
|
-
`src/server/routes/git.ts` imports and uses the global `configService` singleton (not injected). Integration tests for git API must mutate `configService.config.projects` directly to register the test repo. Restore to `[]` in `afterEach`.
|
|
21
|
-
|
|
22
|
-
### ConfigService.load() fallback behavior
|
|
23
|
-
Candidates checked in order: explicit path → PPM_CONFIG env → LOCAL_CONFIG (ppm.yaml) → HOME_CONFIG (~/.ppm/config.yaml). A missing explicit path does NOT stop the fallback chain.
|
|
24
|
-
|
|
25
|
-
### buildTestApp in setup.ts
|
|
26
|
-
Overrides `configService.save = () => {}` (no-op) to prevent tests writing to disk. Injects config directly by mutating private fields via `as unknown as`.
|
|
27
|
-
|
|
28
|
-
### Real git repos for git tests
|
|
29
|
-
`createTempGitRepo()` uses `Bun.spawn` with git env vars (author name/email) to create a real repo with an initial commit. No mocks for git operations.
|
|
30
|
-
|
|
31
|
-
**Why:** Tests must use real implementations — no fakes/mocks that diverge from production behavior.
|
|
32
|
-
**How to apply:** Always use `createTempGitRepo` for anything touching GitService or git API routes.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as e}from"./react-CYzKIDNi.js";import{t}from"./api-client-TUmacMRS.js";var n=e({addAccount:()=>a,deleteAccount:()=>o,exchangeOAuthCode:()=>f,getAISettings:()=>y,getAccountSettings:()=>c,getAccounts:()=>r,getActiveAccount:()=>i,getAllAccountUsages:()=>p,getOAuthUrl:()=>d,getProxySettings:()=>x,importAccounts:()=>m,patchAccount:()=>s,testAccountToken:()=>h,testExport:()=>g,testRawToken:()=>_,updateAISettings:()=>b,updateAccountSettings:()=>l,updateDeviceName:()=>v,updateProxySettings:()=>S,verifyAccount:()=>u});function r(){return t.get(`/api/accounts`)}function i(){return t.get(`/api/accounts/active`)}function a(e){return t.post(`/api/accounts`,e)}function o(e){return t.del(`/api/accounts/${e}`)}function s(e,n){return t.patch(`/api/accounts/${e}`,n)}function c(){return t.get(`/api/accounts/settings`)}function l(e){return t.put(`/api/accounts/settings`,e)}function u(e){return t.post(`/api/accounts/${e}/verify`)}function d(){return t.get(`/api/accounts/oauth/url`)}function f(e,n){return t.post(`/api/accounts/oauth/exchange`,{code:e,state:n})}function p(){return t.get(`/api/accounts/usage`)}function m(e){return t.post(`/api/accounts/import`,e)}function h(e,n=!1){return t.post(`/api/accounts/${e}/test-token`,{testRefresh:n})}function g(e,n=!1){return t.post(`/api/accounts/test-export`,{accountIds:e,includeRefreshToken:n})}function _(e){return t.post(`/api/accounts/test-raw-token`,{token:e})}function v(e){return t.put(`/api/settings/device-name`,{device_name:e})}function y(){return t.get(`/api/settings/ai`)}function b(e){return t.put(`/api/settings/ai`,e)}function x(){return t.get(`/api/settings/proxy`)}function S(e){return t.put(`/api/settings/proxy`,e)}export{b as _,y as a,u as b,i as c,x as d,m as f,_ as g,g as h,f as i,p as l,h as m,n,c as o,s as p,o as r,r as s,a as t,d as u,l as v,S as y};
|