@hienlh/ppm 0.17.53 → 0.17.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +17 -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-D12van6q.js → ai-resource-editor-2h4gRHil.js} +1 -1
  5. package/dist/web/assets/{audio-preview-C3YJNKjD.js → audio-preview-Duhsn8R0.js} +1 -1
  6. package/dist/web/assets/{chat-tab-B6Elb3uK.js → chat-tab-CdS8pzow.js} +3 -3
  7. package/dist/web/assets/{code-editor-DM-ay_BF.js → code-editor-Bq5OC1R8.js} +2 -2
  8. package/dist/web/assets/{conflict-editor-4rb0oS2i.js → conflict-editor-DdX0vWbq.js} +1 -1
  9. package/dist/web/assets/{csv-preview-Bqfk0OeJ.js → csv-preview-DzCzp1eI.js} +1 -1
  10. package/dist/web/assets/{database-viewer-CrBOZBE4.js → database-viewer-Bc0SxzHp.js} +1 -1
  11. package/dist/web/assets/{diff-viewer-BX02Fg5_.js → diff-viewer-D1_p6x3B.js} +1 -1
  12. package/dist/web/assets/{docx-preview-rR_dLI4P.js → docx-preview-DwMr29B9.js} +1 -1
  13. package/dist/web/assets/{extension-webview-D4rHB1h7.js → extension-webview-DekZ27i8.js} +1 -1
  14. package/dist/web/assets/{git-log-panel-BryqAkz2.js → git-log-panel-BfND10PZ.js} +1 -1
  15. package/dist/web/assets/{glide-data-grid-CcGfgB3d.js → glide-data-grid-CR7uGT3d.js} +1 -1
  16. package/dist/web/assets/{group-chat-tab-rB8JuZIe.js → group-chat-tab-DtnJcJnm.js} +1 -1
  17. package/dist/web/assets/{image-preview-i-jTDsjR.js → image-preview-IL6Io7vM.js} +1 -1
  18. package/dist/web/assets/index-CVJmlKhp.css +2 -0
  19. package/dist/web/assets/index-oAxj66VZ.js +50 -0
  20. package/dist/web/assets/{keybindings-store-C3dkxGGX.js → keybindings-store-Dq7MhLxP.js} +1 -1
  21. package/dist/web/assets/{markdown-renderer-DEXzjjQ8.js → markdown-renderer-BWY-49fv.js} +1 -1
  22. package/dist/web/assets/{markdown-renderer-PbB8SlkU.js → markdown-renderer-SKo8Onun.js} +1 -1
  23. package/dist/web/assets/{notification-store-6-9K7U26.js → notification-store-U1Rbcui7.js} +1 -1
  24. package/dist/web/assets/{pdf-preview-k7xI6wlU.js → pdf-preview-Dl5cTIGt.js} +1 -1
  25. package/dist/web/assets/{postgres-viewer-5zQgJlQz.js → postgres-viewer-DR_shYSN.js} +1 -1
  26. package/dist/web/assets/settings-store-B3LfpCPa.js +2 -0
  27. package/dist/web/assets/{settings-tab-DDNHT-XP.js → settings-tab-DI2q_Qj3.js} +1 -1
  28. package/dist/web/assets/{sql-query-editor-CYLWqNpT.js → sql-query-editor-BFnHUYW2.js} +1 -1
  29. package/dist/web/assets/{sqlite-viewer-DANvopw7.js → sqlite-viewer-DZAF9v9K.js} +1 -1
  30. package/dist/web/assets/{system-monitor-tab-Cfs1hq4q.js → system-monitor-tab-BIn4ZRWr.js} +1 -1
  31. package/dist/web/assets/{terminal-tab-CTB-qo9d.js → terminal-tab-BI4jMhtL.js} +1 -1
  32. package/dist/web/assets/{tool-cards-BBr3Pix3.js → tool-cards-DooXE9m_.js} +2 -2
  33. package/dist/web/assets/{use-monaco-theme-BJHYbrpI.js → use-monaco-theme-rmEGn72e.js} +1 -1
  34. package/dist/web/assets/{video-preview-Dv1PD_Rj.js → video-preview-DDrh4ENk.js} +1 -1
  35. package/dist/web/index.html +4 -4
  36. package/dist/web/sw.js +1 -1
  37. package/package.json +1 -1
  38. package/src/server/routes/settings.ts +1 -1
  39. package/src/server/routes/upgrade.ts +23 -5
  40. package/src/services/upgrade.service.ts +13 -3
  41. package/src/types/config.ts +5 -4
  42. package/src/web/components/auth/login-screen.tsx +22 -4
  43. package/src/web/components/layout/upgrade-button.tsx +70 -33
  44. package/src/web/components/settings/settings-tab.tsx +3 -8
  45. package/src/web/index.html +1 -1
  46. package/src/web/lib/changelog.ts +13 -0
  47. package/src/web/stores/settings-store.ts +29 -6
  48. package/.rollout-mark +0 -2
  49. package/dist/web/assets/index-6_hleyhK.css +0 -2
  50. package/dist/web/assets/index-C4m82SWg.js +0 -50
  51. package/dist/web/assets/settings-store-BbFtHCGd.js +0 -2
@@ -1,9 +1,9 @@
1
- import { useState, useEffect, useCallback } from "react";
1
+ import { useState, useEffect, useCallback, useMemo } from "react";
2
2
  import { api } from "@/lib/api-client";
3
3
  import { toast } from "sonner";
4
4
  import { Loader2, ArrowUpCircle, RefreshCw, Download, ExternalLink, History } from "lucide-react";
5
5
  import { useSettingsStore } from "@/stores/settings-store";
6
- import { fetchChangelogSince, fetchRecentChangelog, compareSemver, type ChangelogSection } from "@/lib/changelog";
6
+ import { fetchRecentChangelog, newestSectionVersion, compareSemver, type ChangelogSection } from "@/lib/changelog";
7
7
  import { cn } from "@/lib/utils";
8
8
 
9
9
  const POLL_INTERVAL_MS = 60_000;
@@ -113,26 +113,36 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
113
113
  const [notes, setNotes] = useState<ChangelogSection[] | null>(null);
114
114
  const [notesLoading, setNotesLoading] = useState(false);
115
115
 
116
- useEffect(() => {
117
- let timer: ReturnType<typeof setInterval>;
118
- async function check() {
119
- try {
120
- const data = await api.get<UpgradeStatus>("/api/upgrade");
121
- setCurrentVersion(data.currentVersion);
122
- if (data.availableVersion) {
123
- setAvailableVersion(data.availableVersion);
124
- setDismissed(!!sessionStorage.getItem(DISMISS_KEY_PREFIX + data.availableVersion));
125
- } else {
126
- setAvailableVersion(null);
127
- }
128
- } catch {
129
- // ignore — chip just shows the known version
116
+ // `force` asks the server to bypass its registry cache. The background poll
117
+ // stays cached (one request per client per minute must not hit the registry),
118
+ // but a user opening the popover is asking "is there one *now*" and must not
119
+ // be answered from an answer taken up to five minutes ago.
120
+ const check = useCallback(async (force = false) => {
121
+ try {
122
+ const data = await api.get<UpgradeStatus>(force ? "/api/upgrade?refresh=1" : "/api/upgrade");
123
+ setCurrentVersion(data.currentVersion);
124
+ if (data.availableVersion) {
125
+ setAvailableVersion(data.availableVersion);
126
+ setDismissed(!!sessionStorage.getItem(DISMISS_KEY_PREFIX + data.availableVersion));
127
+ } else {
128
+ setAvailableVersion(null);
130
129
  }
130
+ } catch {
131
+ // ignore — chip just shows the known version
131
132
  }
133
+ }, []);
134
+
135
+ useEffect(() => {
132
136
  check();
133
- timer = setInterval(check, POLL_INTERVAL_MS);
137
+ const timer = setInterval(() => check(), POLL_INTERVAL_MS);
134
138
  return () => clearInterval(timer);
135
- }, []);
139
+ }, [check]);
140
+
141
+ // Opening the popover re-checks immediately, so the panel never disagrees
142
+ // with itself: release notes and the Update button are resolved together.
143
+ useEffect(() => {
144
+ if (open) check(true);
145
+ }, [open, check]);
136
146
 
137
147
  const current = currentVersion ?? version;
138
148
  // QA hook (latched in sessionStorage — see getTestTarget). Forces the update
@@ -145,25 +155,44 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
145
155
  const realUpdate = !!availableVersion && !!current && compareSemver(availableVersion, current) > 0
146
156
  ? availableVersion
147
157
  : null;
148
- const effectiveAvailable = realUpdate ?? (isTest ? testTarget : null);
149
- const hasUpdate = !!effectiveAvailable && !dismissed;
150
158
 
151
159
  // Load release notes each time the popover opens. Refetching (not caching for
152
160
  // the session) avoids latching a stale/empty result from a transient fetch:
153
161
  // the changelog source can briefly lag the version signal after a release.
162
+ // Always the recent list, unfiltered — the sections themselves are what tell
163
+ // us whether a newer release exists, so filtering here would hide the signal.
154
164
  useEffect(() => {
155
165
  if (!open || !current) return;
156
166
  setNotesLoading(true);
157
- // Update mode → sections newer than installed (test hook shows all).
158
- // No update → recent releases so clicking the version chip still shows notes.
159
- const load = hasUpdate
160
- ? fetchChangelogSince(isTest ? "0.0.0" : current)
161
- : fetchRecentChangelog();
162
- load
167
+ fetchRecentChangelog()
163
168
  .then(setNotes)
164
169
  .catch(() => setNotes([]))
165
170
  .finally(() => setNotesLoading(false));
166
- }, [open, current, hasUpdate, isTest]);
171
+ }, [open, current]);
172
+
173
+ // The published CHANGELOG is the earliest signal of a release: it updates the
174
+ // moment the release is pushed, while the registry only flips once the
175
+ // package is published. Whenever the notes name a version newer than the
176
+ // installed one, offer the upgrade — seeing a release in this panel and not
177
+ // being able to take it is never the right outcome, so the panel's own
178
+ // content is a first-class update signal, not decoration.
179
+ const changelogUpdate = useMemo(() => {
180
+ if (!notes || !current) return null;
181
+ const newest = newestSectionVersion(notes);
182
+ return newest && compareSemver(newest, current) > 0 ? newest : null;
183
+ }, [notes, current]);
184
+
185
+ const effectiveAvailable = realUpdate ?? changelogUpdate ?? (isTest ? testTarget : null);
186
+ const hasUpdate = !!effectiveAvailable && !dismissed;
187
+
188
+ // In update mode the list answers "what am I about to get", so drop anything
189
+ // already installed. If that leaves nothing (registry ahead of the changelog)
190
+ // keep the full list rather than an empty panel.
191
+ const visibleNotes = useMemo(() => {
192
+ if (!notes || !hasUpdate || isTest || !current) return notes;
193
+ const newer = notes.filter((s) => compareSemver(s.version, current) > 0);
194
+ return newer.length > 0 ? newer : notes;
195
+ }, [notes, hasUpdate, isTest, current]);
167
196
 
168
197
  // The server is restarting into the new version — stop showing "Updating…",
169
198
  // switch to the reconnect state, and auto-reload once it's listening again
@@ -185,7 +214,7 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
185
214
  } else {
186
215
  toast.info(data.message || "Upgrade installed. Restart PPM manually.");
187
216
  setUpgrading(false);
188
- if (availableVersion) sessionStorage.setItem(DISMISS_KEY_PREFIX + availableVersion, "1");
217
+ if (effectiveAvailable) sessionStorage.setItem(DISMISS_KEY_PREFIX + effectiveAvailable, "1");
189
218
  setDismissed(true);
190
219
  }
191
220
  } catch (e) {
@@ -193,18 +222,26 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
193
222
  // Expected: self-replace killed the server before the response flushed.
194
223
  // The upgrade is in progress — wait for it, don't report a failure.
195
224
  enterRestartWait();
225
+ } else if (/already on latest/i.test((e as Error).message)) {
226
+ // Offered from the changelog, but the package registry has not caught up
227
+ // yet — the release is announced and not yet installable. Say so, and
228
+ // leave the offer standing so the next attempt can succeed.
229
+ toast.info(`v${effectiveAvailable} is announced but not published yet — try again shortly.`);
230
+ setUpgrading(false);
196
231
  } else {
197
232
  toast.error(`Upgrade failed: ${(e as Error).message}`);
198
233
  setUpgrading(false);
199
234
  }
200
235
  }
201
- }, [availableVersion, enterRestartWait]);
236
+ }, [effectiveAvailable, enterRestartWait]);
202
237
 
203
238
  const handleDismiss = useCallback(() => {
204
- if (availableVersion) sessionStorage.setItem(DISMISS_KEY_PREFIX + availableVersion, "1");
239
+ // Key on the offered version, not just the server-reported one: an update
240
+ // known only from the changelog must be dismissable too.
241
+ if (effectiveAvailable) sessionStorage.setItem(DISMISS_KEY_PREFIX + effectiveAvailable, "1");
205
242
  setDismissed(true);
206
243
  setOpen(false);
207
- }, [availableVersion]);
244
+ }, [effectiveAvailable]);
208
245
 
209
246
  if (!current && !hasUpdate) return null;
210
247
 
@@ -277,9 +314,9 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
277
314
  <span className="flex items-center gap-2 text-text-3">
278
315
  <Loader2 className="size-3.5 animate-spin" /> Loading release notes…
279
316
  </span>
280
- ) : notes && notes.length > 0 ? (
317
+ ) : visibleNotes && visibleNotes.length > 0 ? (
281
318
  <div className="space-y-2.5">
282
- {notes.map((s) => (
319
+ {visibleNotes.map((s) => (
283
320
  <ChangelogEntry key={s.version} version={s.version} body={s.body} />
284
321
  ))}
285
322
  </div>
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, useRef } from "react";
2
2
  import {
3
- Moon, Sun, Monitor, Bell, Check, ChevronRight, ArrowLeft,
3
+ Bell, Check, ChevronRight, ArrowLeft,
4
4
  Bot, BellRing, Keyboard, Globe, Puzzle, Bug, FolderSearch, WrapText, CalendarClock, DatabaseZap,
5
5
  } from "lucide-react";
6
6
  import { Button } from "@/components/ui/button";
@@ -10,7 +10,6 @@ import { ScrollArea } from "@/components/ui/scroll-area";
10
10
  import { Separator } from "@/components/ui/separator";
11
11
  import { useShallow } from "zustand/react/shallow";
12
12
  import { useSettingsStore, type EditorTabStyle } from "@/stores/settings-store";
13
- import type { PpmThemeMode } from "@/theme/types";
14
13
  import { cn } from "@/lib/utils";
15
14
  import { AISettingsSection } from "./ai-settings-section";
16
15
  import { KeyboardShortcutsSection } from "./keyboard-shortcuts-section";
@@ -23,11 +22,7 @@ import { SchedulesSettingsSection } from "./schedules/schedules-settings-section
23
22
  import { ThemeManagerSection } from "./theme-manager-section";
24
23
  import { QueryAuditSection } from "./query-audit-section";
25
24
  import { ThemeGrid } from "./theme-grid";
26
- const THEME_OPTIONS: { value: PpmThemeMode; label: string; icon: React.ElementType }[] = [
27
- { value: "light", label: "Light", icon: Sun },
28
- { value: "dark", label: "Dark", icon: Moon },
29
- { value: "system", label: "System", icon: Monitor },
30
- ];
25
+ import { THEME_MODE_OPTIONS } from "@/theme/theme-mode-options";
31
26
 
32
27
  const TAB_STYLE_OPTIONS: { value: EditorTabStyle; label: string }[] = [
33
28
  { value: "default", label: "Default" },
@@ -155,7 +150,7 @@ export function SettingsTab() {
155
150
  <ThemeGrid />
156
151
  {/* Mode override (applies within the selected style; System follows OS) */}
157
152
  <div className="flex gap-1.5 pt-1">
158
- {THEME_OPTIONS.map((opt) => {
153
+ {THEME_MODE_OPTIONS.map((opt) => {
159
154
  const Icon = opt.icon;
160
155
  return (
161
156
  <Button
@@ -18,7 +18,7 @@
18
18
  try {
19
19
  var raw = localStorage.getItem("ppm-settings");
20
20
  var s = raw ? JSON.parse(raw) : {};
21
- var mode = s.themeMode || s.theme || "dark";
21
+ var mode = s.themeMode || s.theme || "system";
22
22
  if (mode === "system") {
23
23
  mode = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
24
24
  }
@@ -51,6 +51,19 @@ export function parseChangelog(md: string): ChangelogSection[] {
51
51
  return sections;
52
52
  }
53
53
 
54
+ /**
55
+ * Newest version among the given sections, or null when there are none.
56
+ * Order-independent on purpose: the file is authored newest-first by
57
+ * convention, and a release that lands out of order must not read as "no newer
58
+ * version" to the upgrade check that consumes this.
59
+ */
60
+ export function newestSectionVersion(sections: ChangelogSection[]): string | null {
61
+ return sections.reduce<string | null>(
62
+ (best, s) => (best && compareSemver(s.version, best) <= 0 ? best : s.version),
63
+ null,
64
+ );
65
+ }
66
+
54
67
  /** Parse CHANGELOG markdown into version sections strictly newer than `since`. */
55
68
  export function parseChangelogSince(md: string, since: string): ChangelogSection[] {
56
69
  return parseChangelog(md).filter((s) => compareSemver(s.version, since) > 0);
@@ -60,6 +60,8 @@ interface SettingsState {
60
60
  setDockPosition: (position: DockPosition) => void;
61
61
  setDbSidebarExpanded: (next: DbSidebarExpanded) => void;
62
62
  fetchServerInfo: () => Promise<void>;
63
+ /** Re-push the in-memory theme selection to the server (see the action for why). */
64
+ syncThemeToServer: () => Promise<void>;
63
65
  }
64
66
 
65
67
  interface PersistedSettings {
@@ -103,7 +105,8 @@ function initialTheme(p: PersistedSettings): { style: PpmThemeStyle; mode: PpmTh
103
105
  if (p.theme && VALID_MODES.includes(p.theme as PpmThemeMode)) {
104
106
  return { style: "aurora", mode: p.theme as PpmThemeMode };
105
107
  }
106
- return { style: "aurora", mode: "dark" };
108
+ // No stored selection: follow the OS rather than forcing dark.
109
+ return { style: "aurora", mode: "system" };
107
110
  }
108
111
 
109
112
  function isValidSidebarTab(tab: unknown): tab is SidebarActiveTab {
@@ -185,14 +188,25 @@ function persistUiPref(update: Partial<PersistedSettings>) {
185
188
  pushUiPrefsToServer(update);
186
189
  }
187
190
 
188
- /** Push the current theme selection to the dedicated server endpoint (fire-and-forget). */
189
- function pushThemeToServer(style: PpmThemeStyle, mode: PpmThemeMode, customThemeId?: string) {
191
+ /**
192
+ * Push the current theme selection to the dedicated server endpoint.
193
+ * Errors are swallowed, so callers may ignore the promise; awaiting it only
194
+ * matters when a following request must observe the write (see
195
+ * `syncThemeToServer`).
196
+ */
197
+ function pushThemeToServer(
198
+ style: PpmThemeStyle,
199
+ mode: PpmThemeMode,
200
+ customThemeId?: string,
201
+ ): Promise<void> {
190
202
  const token = getAuthToken();
191
- fetch("/api/settings/theme", {
203
+ return fetch("/api/settings/theme", {
192
204
  method: "PUT",
193
205
  headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
194
206
  body: JSON.stringify({ style, mode, ...(customThemeId ? { customThemeId } : {}) }),
195
- }).catch(() => {});
207
+ })
208
+ .then(() => {})
209
+ .catch(() => {});
196
210
  }
197
211
 
198
212
  /** Apply server-stored UI prefs to the store + localStorage (no re-push). */
@@ -268,7 +282,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
268
282
 
269
283
  setThemeFromPayload: ({ style, mode, customThemeId }) => {
270
284
  const resolvedStyle = VALID_STYLES.includes(style as PpmThemeStyle) ? (style as PpmThemeStyle) : "aurora";
271
- const resolvedMode = VALID_MODES.includes(mode) ? mode : "dark";
285
+ const resolvedMode = VALID_MODES.includes(mode) ? mode : "system";
272
286
  persistSettings({ themeStyle: resolvedStyle, themeMode: resolvedMode, customThemeId });
273
287
  set({ themeStyle: resolvedStyle, themeMode: resolvedMode, customThemeId });
274
288
  },
@@ -417,4 +431,13 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
417
431
  void get().fetchThemes();
418
432
  } catch {}
419
433
  },
434
+
435
+ // A theme picked before authenticating cannot reach the server: the endpoint
436
+ // is auth-gated, so that PUT 401s and is dropped. `fetchServerInfo` then lets
437
+ // the *server* theme win and re-runs the moment auth state flips, which would
438
+ // discard the pre-auth choice. Callers that have just obtained a valid token
439
+ // await this first so the PUT lands before that GET reads it back.
440
+ syncThemeToServer: async () => {
441
+ await pushThemeToServer(get().themeStyle, get().themeMode, get().customThemeId);
442
+ },
420
443
  }));
package/.rollout-mark DELETED
@@ -1,2 +0,0 @@
1
- logmark=10732315
2
- {"v":"0.17.50","port":3214,"url":"https://promo-laughing-postposted-personals.trycloudflare.com","tunnelPid":53404,"edgePid":56920,"serverPort":51159}