@hienlh/ppm 0.17.34 → 0.17.37
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 +17 -0
- package/CLAUDE.md +1 -0
- package/Dockerfile.test +5 -1
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/bun.lock +5 -0
- package/dist/web/assets/{ai-resource-editor-QNfSU0Fi.js → ai-resource-editor-DAIaB4mX.js} +1 -1
- package/dist/web/assets/{api-client-DhXpjLdi.js → api-client-BgM59GxL.js} +1 -1
- package/dist/web/assets/api-client-BwMdPtp6.js +1 -0
- package/dist/web/assets/{api-settings-BUYvGBdi.js → api-settings-D108koFG.js} +1 -1
- package/dist/web/assets/api-settings-D2fY0GUc.js +1 -0
- package/dist/web/assets/{api-themes-BpoKUPkQ.js → api-themes-cPGCYGB_.js} +1 -1
- package/dist/web/assets/{audio-preview-IokB77Cj.js → audio-preview-Df6_blMs.js} +1 -1
- package/dist/web/assets/chat-tab-yPXWmX01.js +13 -0
- package/dist/web/assets/{code-editor-B8YiBPOX.js → code-editor-CSDB2W8N.js} +2 -2
- package/dist/web/assets/{conflict-editor-CY1o5kBO.js → conflict-editor-BqMdj782.js} +1 -1
- package/dist/web/assets/{csv-preview-AEg0GB-V.js → csv-preview-BC_BuOzK.js} +1 -1
- package/dist/web/assets/{database-viewer-C5Meg0s_.js → database-viewer-DZQDQOye.js} +1 -1
- package/dist/web/assets/diff-viewer-CaKD5pR5.js +4 -0
- package/dist/web/assets/docx-preview-MBY4op8y.js +12 -0
- package/dist/web/assets/{edit-diff-preview-_CXUdwsF.js → edit-diff-preview-D1TEK7Jz.js} +1 -1
- package/dist/web/assets/{extension-webview-CQceYb7O.js → extension-webview-CMtucoeg.js} +1 -1
- package/dist/web/assets/git-log-panel-DWZUIupS.js +1 -0
- package/dist/web/assets/{glide-data-grid-cGxkgluR.js → glide-data-grid-SLs8Xp6G.js} +1 -1
- package/dist/web/assets/{group-chat-tab-B-eA5ytm.js → group-chat-tab-Celp6ihW.js} +1 -1
- package/dist/web/assets/image-gallery-CObzTisL.js +1 -0
- package/dist/web/assets/{image-preview-DRUzYptI.js → image-preview-lRcpEoEk.js} +1 -1
- package/dist/web/assets/index-BkH2b_6W.js +50 -0
- package/dist/web/assets/index-eW0VEaRF.css +2 -0
- package/dist/web/assets/keybindings-store-CtfEQL3U.js +1 -0
- package/dist/web/assets/markdown-renderer-D9r_4-qe.js +3 -0
- package/dist/web/assets/markdown-renderer-DkYo6o9f.js +1 -0
- package/dist/web/assets/notification-store-OF6L8lWu.js +1 -0
- package/dist/web/assets/panel-store-CpKvXNnm.js +1 -0
- package/dist/web/assets/pdf-preview-D17y9l8U.js +1 -0
- package/dist/web/assets/{postgres-viewer-BeantJZe.js → postgres-viewer-DgpX_Kyl.js} +1 -1
- package/dist/web/assets/project-store-8_AdCduJ.js +1 -0
- package/dist/web/assets/{settings-store-DFMz_Oer.js → settings-store-BhZ7VYJD.js} +2 -2
- package/dist/web/assets/settings-tab-Cou9P1gO.js +1 -0
- package/dist/web/assets/{sql-query-editor-DqU9zfVy.js → sql-query-editor-BIeAQego.js} +1 -1
- package/dist/web/assets/sqlite-viewer-SipxlqWM.js +1 -0
- package/dist/web/assets/{system-monitor-tab-Bue4p4Vb.js → system-monitor-tab-Cp-rP5HZ.js} +1 -1
- package/dist/web/assets/{tab-store-CNxCwdXw.js → tab-store-BQmDVh9c.js} +1 -1
- package/dist/web/assets/{terminal-tab-BfNnKU71.js → terminal-tab-C9T_X4oo.js} +1 -1
- package/dist/web/assets/tool-cards-BFVmoF_j.js +7 -0
- package/dist/web/assets/use-blob-url-BYDGMRMU.js +1 -0
- package/dist/web/assets/{use-monaco-theme-5XjAzOzE.js → use-monaco-theme-Bd1GBgB3.js} +1 -1
- package/dist/web/assets/{video-preview-DMqLOqmc.js → video-preview-DT8B2-re.js} +1 -1
- package/dist/web/index.html +8 -8
- package/dist/web/sw.js +1 -1
- package/docker-compose.test.yml +5 -1
- package/package.json +4 -3
- package/src/server/routes/chat.ts +66 -1
- package/src/services/image-dimensions.ts +67 -0
- package/src/services/transcript-images.ts +174 -0
- package/src/shared/tool-result-content.ts +14 -3
- package/src/web/components/chat/chat-history-bar.tsx +3 -104
- package/src/web/components/chat/message-input.tsx +4 -2
- package/src/web/components/chat/message-list.tsx +10 -3
- package/src/web/components/chat/session-debug-dialog.tsx +240 -0
- package/src/web/components/chat/tool-image-preview.tsx +4 -2
- package/src/web/components/group-chat/group-create-dialog.tsx +2 -1
- package/src/web/components/layout/editor-panel.tsx +31 -5
- package/src/web/components/layout/notification-bell-popover.tsx +5 -1
- package/src/web/components/layout/tab-bar.tsx +2 -2
- package/src/web/components/shared/image-overlay.tsx +188 -24
- package/src/web/components/shared/markdown-context.ts +1 -1
- package/src/web/components/shared/markdown-renderer.tsx +3 -1
- package/src/web/hooks/use-draft.ts +17 -2
- package/src/web/hooks/use-image-transform.ts +276 -0
- package/src/web/lib/api-client.ts +50 -7
- package/src/web/lib/clipboard.ts +41 -0
- package/src/web/lib/file-download.ts +1 -1
- package/src/web/lib/image-gallery.ts +25 -0
- package/src/web/stores/image-overlay-store.ts +35 -7
- package/src/web/stores/panel-store.ts +40 -6
- package/src/web/stores/panel-utils.ts +19 -0
- package/dist/web/assets/api-client-DNTkryX1.js +0 -1
- package/dist/web/assets/api-settings-Bp5HXQCW.js +0 -1
- package/dist/web/assets/chat-tab-BZNEvS88.js +0 -13
- package/dist/web/assets/diff-viewer-BRRGvZnp.js +0 -4
- package/dist/web/assets/docx-preview-DqTX7NDp.js +0 -12
- package/dist/web/assets/git-log-panel-D7QlAJqu.js +0 -1
- package/dist/web/assets/index-BuX7JkCt.js +0 -50
- package/dist/web/assets/index-PX2pZ3bH.css +0 -2
- package/dist/web/assets/keybindings-store-B3plqyxF.js +0 -1
- package/dist/web/assets/markdown-renderer-BKDbjc3g.js +0 -3
- package/dist/web/assets/markdown-renderer-C9Gy8tM2.js +0 -1
- package/dist/web/assets/notification-store-jFDauzse.js +0 -1
- package/dist/web/assets/panel-store-D-RxcUgH.js +0 -1
- package/dist/web/assets/pdf-preview-CzHUqXJB.js +0 -1
- package/dist/web/assets/project-store-C1jInVih.js +0 -1
- package/dist/web/assets/settings-tab-CqQHhqV1.js +0 -1
- package/dist/web/assets/sqlite-viewer-BH891DC0.js +0 -1
- package/dist/web/assets/tool-cards-CSOwe7_i.js +0 -7
- package/dist/web/assets/use-blob-url-jSRmyXVg.js +0 -1
- package/skills-lock.json +0 -11
- package/spike-memory-region-dump.ps1 +0 -69
- /package/dist/web/assets/{csv-parser-BPS94T2h.js → csv-parser-DVHBcvuE.js} +0 -0
- /package/dist/web/assets/{lib-DA3QSc5_.js → lib-DTaN9riD.js} +0 -0
- /package/dist/web/assets/{libesm-BS7D9de3.js → libesm-DdtEjhIi.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-CsgYwG03.js → vendor-xterm-QDr0jvpJ.js} +0 -0
package/src/web/lib/clipboard.ts
CHANGED
|
@@ -20,6 +20,47 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
|
|
20
20
|
return legacyCopy(text);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Whether an image can be placed on the clipboard at all on this origin.
|
|
25
|
+
*
|
|
26
|
+
* Unlike text there is no `execCommand` fallback for binary data, so on the plain-HTTP
|
|
27
|
+
* origins PPM is often reached from there is nothing to try — callers should disable the
|
|
28
|
+
* action and say why rather than offer a button that always fails.
|
|
29
|
+
*/
|
|
30
|
+
export function canCopyImage(): boolean {
|
|
31
|
+
return typeof ClipboardItem !== "undefined" && !!navigator.clipboard?.write;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Copy an image to the clipboard, converting to PNG first.
|
|
36
|
+
*
|
|
37
|
+
* Browsers accept a very short list of image types on the clipboard — PNG is the only one
|
|
38
|
+
* supported everywhere — so a JPEG or WebP has to be repainted through a canvas.
|
|
39
|
+
*/
|
|
40
|
+
export async function copyImageToClipboard(src: string): Promise<boolean> {
|
|
41
|
+
if (!canCopyImage()) return false;
|
|
42
|
+
try {
|
|
43
|
+
const blob = await fetch(src).then((r) => r.blob());
|
|
44
|
+
const png = blob.type === "image/png" ? blob : await toPng(blob);
|
|
45
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function toPng(blob: Blob): Promise<Blob> {
|
|
53
|
+
const bitmap = await createImageBitmap(blob);
|
|
54
|
+
const canvas = document.createElement("canvas");
|
|
55
|
+
canvas.width = bitmap.width;
|
|
56
|
+
canvas.height = bitmap.height;
|
|
57
|
+
canvas.getContext("2d")?.drawImage(bitmap, 0, 0);
|
|
58
|
+
bitmap.close();
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("canvas produced no blob"))), "image/png");
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
23
64
|
function legacyCopy(text: string): boolean {
|
|
24
65
|
const textarea = document.createElement("textarea");
|
|
25
66
|
textarea.value = text;
|
|
@@ -24,7 +24,7 @@ export async function downloadFolder(projectName: string, folderPath: string): P
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** Hidden <a> tag download trigger — avoids popup blockers */
|
|
27
|
-
function triggerDownload(url: string, filename: string): void {
|
|
27
|
+
export function triggerDownload(url: string, filename: string): void {
|
|
28
28
|
const a = document.createElement("a");
|
|
29
29
|
a.href = url;
|
|
30
30
|
a.download = filename;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GalleryImage } from "@/stores/image-overlay-store";
|
|
2
|
+
|
|
3
|
+
/** Marks a container whose tagged images form one navigable set. */
|
|
4
|
+
export const GALLERY_ROOT_ATTR = "data-image-gallery";
|
|
5
|
+
/** Marks an `<img>` that belongs to the enclosing gallery. */
|
|
6
|
+
export const GALLERY_ITEM_ATTR = "data-gallery-item";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collect the images a viewer may move between, in the order they appear on screen.
|
|
10
|
+
*
|
|
11
|
+
* Read from the DOM rather than a registry because every image already owns a blob URL that
|
|
12
|
+
* is revoked when its component unmounts — so the mounted elements are exactly the set whose
|
|
13
|
+
* sources are still valid, and the DOM already holds them in the order the user sees.
|
|
14
|
+
*
|
|
15
|
+
* The reach is therefore whatever is mounted. The chat transcript is deliberately not
|
|
16
|
+
* virtualised (`message-list.tsx`), so that is the whole conversation today; if it ever gains
|
|
17
|
+
* data windowing, navigation narrows to the loaded window along with everything else.
|
|
18
|
+
*/
|
|
19
|
+
export function collectGallery(from: Element | null): GalleryImage[] {
|
|
20
|
+
const root = from?.closest(`[${GALLERY_ROOT_ATTR}]`);
|
|
21
|
+
if (!root) return [];
|
|
22
|
+
return [...root.querySelectorAll<HTMLImageElement>(`img[${GALLERY_ITEM_ATTR}]`)]
|
|
23
|
+
.filter((img) => !!img.src)
|
|
24
|
+
.map((img) => ({ src: img.src, alt: img.alt }));
|
|
25
|
+
}
|
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
2
|
|
|
3
|
+
export interface GalleryImage {
|
|
4
|
+
/** Blob URL or data URL of the image. */
|
|
5
|
+
src: string;
|
|
6
|
+
alt: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
interface ImageOverlayState {
|
|
4
|
-
/**
|
|
10
|
+
/** Images the overlay can move between; a single entry when opened without a gallery. */
|
|
11
|
+
images: GalleryImage[];
|
|
12
|
+
index: number;
|
|
13
|
+
/** Convenience mirrors of the current entry — every existing consumer reads these. */
|
|
5
14
|
src: string | null;
|
|
6
15
|
alt: string;
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Open the overlay. Passing a gallery lets the arrows walk its siblings; the clicked
|
|
18
|
+
* source decides where in that list the viewer starts.
|
|
19
|
+
*/
|
|
20
|
+
open: (src: string, alt?: string, gallery?: GalleryImage[]) => void;
|
|
10
21
|
close: () => void;
|
|
22
|
+
/** Move by a signed offset, stopping at the ends rather than wrapping. */
|
|
23
|
+
go: (delta: number) => void;
|
|
11
24
|
}
|
|
12
25
|
|
|
13
|
-
export const useImageOverlay = create<ImageOverlayState>((set) => ({
|
|
26
|
+
export const useImageOverlay = create<ImageOverlayState>((set, get) => ({
|
|
27
|
+
images: [],
|
|
28
|
+
index: 0,
|
|
14
29
|
src: null,
|
|
15
30
|
alt: "",
|
|
16
|
-
open: (src, alt = "") =>
|
|
17
|
-
|
|
31
|
+
open: (src, alt = "", gallery) => {
|
|
32
|
+
const images = gallery?.length ? gallery : [{ src, alt }];
|
|
33
|
+
const found = images.findIndex((i) => i.src === src);
|
|
34
|
+
const index = found >= 0 ? found : 0;
|
|
35
|
+
const current = images[index]!;
|
|
36
|
+
set({ images, index, src: current.src, alt: current.alt || alt });
|
|
37
|
+
},
|
|
38
|
+
close: () => set({ images: [], index: 0, src: null, alt: "" }),
|
|
39
|
+
go: (delta) => {
|
|
40
|
+
const { images, index } = get();
|
|
41
|
+
const next = index + delta;
|
|
42
|
+
if (next < 0 || next >= images.length) return;
|
|
43
|
+
const current = images[next]!;
|
|
44
|
+
set({ index: next, src: current.src, alt: current.alt });
|
|
45
|
+
},
|
|
18
46
|
}));
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
savePanelLayout,
|
|
16
16
|
loadPanelLayout,
|
|
17
17
|
deriveTabId,
|
|
18
|
+
visibleTabs,
|
|
18
19
|
DOCK_PANEL_ID,
|
|
19
20
|
} from "./panel-utils";
|
|
20
21
|
import {
|
|
@@ -248,10 +249,16 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
248
249
|
// Load from localStorage
|
|
249
250
|
const loaded = loadPanelLayout(projectName);
|
|
250
251
|
if (loaded && Object.keys(loaded.panels).length > 0) {
|
|
251
|
-
// Migrate:
|
|
252
|
+
// Migrate: drop obsolete tab types, and tabs belonging to another project.
|
|
253
|
+
// A foreign tab is filtered out of the tab bar, so it can never be closed —
|
|
254
|
+
// and closeTab is the only thing that removes an emptied panel, so leaving
|
|
255
|
+
// one behind wedges its panel permanently (it renders as a blank slot).
|
|
256
|
+
let healed = false;
|
|
252
257
|
const migratedPanels: typeof loaded.panels = {};
|
|
253
258
|
for (const [pid, panel] of Object.entries(loaded.panels)) {
|
|
254
|
-
const filteredTabs = panel.tabs
|
|
259
|
+
const filteredTabs = visibleTabs(panel.tabs, projectName)
|
|
260
|
+
.filter((t) => !OBSOLETE_TAB_TYPES.has(t.type));
|
|
261
|
+
if (filteredTabs.length !== panel.tabs.length) healed = true;
|
|
255
262
|
const filteredHistory = panel.tabHistory.filter(
|
|
256
263
|
(id) => filteredTabs.some((t) => t.id === id),
|
|
257
264
|
);
|
|
@@ -261,6 +268,22 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
261
268
|
migratedPanels[pid] = { ...panel, tabs: filteredTabs, tabHistory: filteredHistory, activeTabId };
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
// Apply the "no empty panel" invariant on restore, not just in closeTab:
|
|
272
|
+
// the filtering above can empty a panel, and a grid id with no panel behind
|
|
273
|
+
// it renders nothing either. The last panel always stays — the grid needs
|
|
274
|
+
// something to render into.
|
|
275
|
+
let migratedGrid = loaded.grid;
|
|
276
|
+
for (const pid of migratedGrid.flat()) {
|
|
277
|
+
if ((migratedPanels[pid]?.tabs.length ?? 0) > 0) continue;
|
|
278
|
+
if (migratedGrid.flat().length <= 1) break;
|
|
279
|
+
migratedGrid = gridRemovePanel(migratedGrid, pid);
|
|
280
|
+
delete migratedPanels[pid];
|
|
281
|
+
healed = true;
|
|
282
|
+
}
|
|
283
|
+
const migratedFocused = migratedGrid.flat().includes(loaded.focusedPanelId)
|
|
284
|
+
? loaded.focusedPanelId
|
|
285
|
+
: (migratedGrid.flat()[0] ?? loaded.focusedPanelId);
|
|
286
|
+
|
|
264
287
|
// Merge into flat panels map (keep-alive: old panels stay).
|
|
265
288
|
// The shared __dock__ panel holds live tabs from ALL projects — union the
|
|
266
289
|
// loaded project's persisted dock tabs into it rather than replacing, so a
|
|
@@ -283,8 +306,8 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
283
306
|
// Focus the switched-to project's own dock tab (not another project's).
|
|
284
307
|
mergedDockPanel.activeTabId = pickDockActiveTab(mergedDockPanel, projectName);
|
|
285
308
|
const mergedPanels = { ...panels, ...migratedPanels, [DOCK_PANEL_ID]: mergedDockPanel };
|
|
286
|
-
newProjectGrids[projectName] =
|
|
287
|
-
newProjectFocused[projectName] =
|
|
309
|
+
newProjectGrids[projectName] = migratedGrid;
|
|
310
|
+
newProjectFocused[projectName] = migratedFocused;
|
|
288
311
|
const restoredDock = collapseRestoredDockOnMobile(
|
|
289
312
|
loaded.dock ?? restoreDockForProject(projectName, newProjectDock),
|
|
290
313
|
get().isMobile(),
|
|
@@ -292,13 +315,24 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
292
315
|
set({
|
|
293
316
|
currentProject: projectName,
|
|
294
317
|
panels: mergedPanels,
|
|
295
|
-
grid:
|
|
296
|
-
focusedPanelId:
|
|
318
|
+
grid: migratedGrid,
|
|
319
|
+
focusedPanelId: migratedFocused,
|
|
297
320
|
projectGrids: newProjectGrids,
|
|
298
321
|
projectFocused: newProjectFocused,
|
|
299
322
|
dock: restoredDock,
|
|
300
323
|
projectDock: newProjectDock,
|
|
301
324
|
});
|
|
325
|
+
// Write the healed layout back so the repair sticks (and reaches the server
|
|
326
|
+
// blob), instead of being redone on every load.
|
|
327
|
+
if (healed) {
|
|
328
|
+
savePanelLayout(projectName, {
|
|
329
|
+
panels: migratedPanels,
|
|
330
|
+
grid: migratedGrid,
|
|
331
|
+
focusedPanelId: migratedFocused,
|
|
332
|
+
dock: restoredDock,
|
|
333
|
+
dockPanel: mergedDockPanel,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
302
336
|
} else {
|
|
303
337
|
// Create empty layout — EmptyPanel will show quick-open buttons
|
|
304
338
|
const p = createPanel();
|
|
@@ -103,6 +103,25 @@ export function gridRemovePanel(grid: string[][], panelId: string): string[][] {
|
|
|
103
103
|
.filter((col) => col.length > 0);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The subset of `tabs` that the given project is allowed to see.
|
|
108
|
+
*
|
|
109
|
+
* A grid panel can end up holding another project's tab (a race in openTab during
|
|
110
|
+
* a project switch, or a tab stamped with a project name that does not exist). The
|
|
111
|
+
* tab bar hides those, so every other "is this panel empty?" check must agree —
|
|
112
|
+
* otherwise the panel renders neither tabs nor the empty state, and since it has no
|
|
113
|
+
* closable tab, the auto-close in closeTab can never fire and the panel is stuck.
|
|
114
|
+
*
|
|
115
|
+
* Callers spell "no active project" two ways: TabBar passes null, while the
|
|
116
|
+
* per-project layout key is the "__global__" sentinel. Both mean unfiltered, so
|
|
117
|
+
* they are normalized here — a split between them would resurrect the exact
|
|
118
|
+
* disagreement this helper exists to prevent.
|
|
119
|
+
*/
|
|
120
|
+
export function visibleTabs(tabs: Tab[], projectName: string | null): Tab[] {
|
|
121
|
+
if (!projectName || projectName === "__global__") return tabs;
|
|
122
|
+
return tabs.filter((t) => !t.projectId || t.projectId === projectName);
|
|
123
|
+
}
|
|
124
|
+
|
|
106
125
|
export function findPanelPosition(grid: string[][], panelId: string): { row: number; col: number } | null {
|
|
107
126
|
for (let r = 0; r < grid.length; r++) {
|
|
108
127
|
const c = grid[r]!.indexOf(panelId);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./vendor-markdown-CKTlmXbz.js";import"./vendor-ui-DMRGWRuc.js";import{r as e,t}from"./api-client-DhXpjLdi.js";export{t as api,e as projectUrl};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./vendor-markdown-CKTlmXbz.js";import"./vendor-ui-DMRGWRuc.js";import"./api-client-DhXpjLdi.js";import{v as e}from"./api-settings-BUYvGBdi.js";export{e as updateDeviceName};
|