backpack-viewer 0.4.0 → 0.5.1

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/dist/config.js CHANGED
@@ -18,6 +18,7 @@ export function loadViewerConfig() {
18
18
  const raw = fs.readFileSync(filePath, "utf-8");
19
19
  const user = JSON.parse(raw);
20
20
  return {
21
+ server: { ...defaultConfig.server, ...(user.server ?? {}) },
21
22
  keybindings: { ...defaultConfig.keybindings, ...(user.keybindings ?? {}) },
22
23
  display: { ...defaultConfig.display, ...(user.display ?? {}) },
23
24
  layout: { ...defaultConfig.layout, ...(user.layout ?? {}) },
@@ -33,6 +33,10 @@
33
33
  "walkMode": "w",
34
34
  "walkIsolate": "i"
35
35
  },
36
+ "server": {
37
+ "host": "127.0.0.1",
38
+ "port": 5173
39
+ },
36
40
  "display": {
37
41
  "edges": true,
38
42
  "edgeLabels": true,
package/dist/dialog.d.ts CHANGED
@@ -3,6 +3,19 @@
3
3
  export declare function showConfirm(title: string, message: string): Promise<boolean>;
4
4
  /** Show a prompt dialog with an input field. Returns null if cancelled. */
5
5
  export declare function showPrompt(title: string, placeholder?: string, defaultValue?: string): Promise<string | null>;
6
+ export interface BackpackAddResult {
7
+ path: string;
8
+ activate: boolean;
9
+ }
10
+ /**
11
+ * Show the Add Backpack dialog. Single path field with an optional
12
+ * native folder picker (Chromium only) and drag-and-drop hint. No
13
+ * name field — the display name is derived from the path tail by
14
+ * the backend. No suggestion chips — user pastes any path.
15
+ *
16
+ * Returns null if the user cancels.
17
+ */
18
+ export declare function showBackpackAddDialog(): Promise<BackpackAddResult | null>;
6
19
  /** Show a danger confirmation (for destructive actions). */
7
20
  export declare function showDangerConfirm(title: string, message: string): Promise<boolean>;
8
21
  /** Show a brief toast notification. */
package/dist/dialog.js CHANGED
@@ -87,6 +87,122 @@ export function showPrompt(title, placeholder, defaultValue) {
87
87
  input.select();
88
88
  });
89
89
  }
90
+ /**
91
+ * Show the Add Backpack dialog. Single path field with an optional
92
+ * native folder picker (Chromium only) and drag-and-drop hint. No
93
+ * name field — the display name is derived from the path tail by
94
+ * the backend. No suggestion chips — user pastes any path.
95
+ *
96
+ * Returns null if the user cancels.
97
+ */
98
+ export function showBackpackAddDialog() {
99
+ return new Promise((resolve) => {
100
+ const overlay = createOverlay();
101
+ const modal = createModal(overlay, "Add Backpack");
102
+ const description = document.createElement("p");
103
+ description.className = "bp-dialog-message";
104
+ description.textContent =
105
+ "Enter the absolute path to a directory that should become a backpack. It will be shown in the sidebar using the last segment of the path as its display name.";
106
+ modal.appendChild(description);
107
+ const pathLabel = document.createElement("label");
108
+ pathLabel.className = "bp-dialog-label";
109
+ pathLabel.textContent = "Path";
110
+ modal.appendChild(pathLabel);
111
+ const pathRow = document.createElement("div");
112
+ pathRow.className = "bp-dialog-path-row";
113
+ modal.appendChild(pathRow);
114
+ const pathInput = document.createElement("input");
115
+ pathInput.type = "text";
116
+ pathInput.className = "bp-dialog-input bp-dialog-path-input";
117
+ pathInput.placeholder = "/Users/you/OneDrive/work";
118
+ pathRow.appendChild(pathInput);
119
+ const browseBtn = document.createElement("button");
120
+ browseBtn.type = "button";
121
+ browseBtn.className = "bp-dialog-btn bp-dialog-browse-btn";
122
+ browseBtn.textContent = "Browse...";
123
+ pathRow.appendChild(browseBtn);
124
+ // File System Access API is Chromium-only. On Safari/Firefox the
125
+ // picker doesn't exist; even on Chromium it returns a handle, not
126
+ // a filesystem path, so the user still types or pastes the
127
+ // absolute path. The picker is only a hint.
128
+ const hasNativePicker = typeof window.showDirectoryPicker === "function";
129
+ if (!hasNativePicker) {
130
+ browseBtn.disabled = true;
131
+ browseBtn.title =
132
+ "Browser doesn't support native folder picker — paste the path manually";
133
+ }
134
+ browseBtn.addEventListener("click", async (e) => {
135
+ e.preventDefault();
136
+ try {
137
+ const handle = await window.showDirectoryPicker({
138
+ mode: "read",
139
+ });
140
+ pickerHint.textContent = `Picked "${handle.name}" — paste the absolute path to it below.`;
141
+ pathInput.focus();
142
+ }
143
+ catch {
144
+ // User cancelled
145
+ }
146
+ });
147
+ const pickerHint = document.createElement("div");
148
+ pickerHint.className = "bp-dialog-picker-hint";
149
+ modal.appendChild(pickerHint);
150
+ // Activate checkbox
151
+ const activateRow = document.createElement("div");
152
+ activateRow.className = "bp-dialog-activate-row";
153
+ const activateCheckbox = document.createElement("input");
154
+ activateCheckbox.type = "checkbox";
155
+ activateCheckbox.id = "bp-dialog-activate";
156
+ activateCheckbox.checked = true;
157
+ const activateLabel = document.createElement("label");
158
+ activateLabel.htmlFor = "bp-dialog-activate";
159
+ activateLabel.textContent = "Switch to this backpack after registering";
160
+ activateRow.appendChild(activateCheckbox);
161
+ activateRow.appendChild(activateLabel);
162
+ modal.appendChild(activateRow);
163
+ // Drag-and-drop: dropping a folder gives us the folder name via
164
+ // webkitGetAsEntry but not the full OS path (sandboxed). Use it as
165
+ // a visual hint only.
166
+ pathInput.addEventListener("dragover", (e) => {
167
+ e.preventDefault();
168
+ pathInput.classList.add("bp-dialog-drag-over");
169
+ });
170
+ pathInput.addEventListener("dragleave", () => {
171
+ pathInput.classList.remove("bp-dialog-drag-over");
172
+ });
173
+ pathInput.addEventListener("drop", (e) => {
174
+ e.preventDefault();
175
+ pathInput.classList.remove("bp-dialog-drag-over");
176
+ const items = e.dataTransfer?.items;
177
+ if (!items || items.length === 0)
178
+ return;
179
+ const entry = items[0].webkitGetAsEntry?.();
180
+ if (entry?.isDirectory) {
181
+ pickerHint.textContent = `Dropped "${entry.name}" — paste the absolute path to it below.`;
182
+ }
183
+ });
184
+ const submit = () => {
185
+ const p = pathInput.value.trim();
186
+ if (!p)
187
+ return;
188
+ overlay.remove();
189
+ resolve({ path: p, activate: activateCheckbox.checked });
190
+ };
191
+ pathInput.addEventListener("keydown", (e) => {
192
+ if (e.key === "Enter")
193
+ submit();
194
+ if (e.key === "Escape") {
195
+ overlay.remove();
196
+ resolve(null);
197
+ }
198
+ });
199
+ addButtons(modal, [
200
+ { label: "Cancel", onClick: () => { overlay.remove(); resolve(null); } },
201
+ { label: "Register", accent: true, onClick: submit },
202
+ ]);
203
+ pathInput.focus();
204
+ });
205
+ }
90
206
  /** Show a danger confirmation (for destructive actions). */
91
207
  export function showDangerConfirm(title, message) {
92
208
  return new Promise((resolve) => {
package/dist/main.js CHANGED
@@ -528,11 +528,11 @@ async function main() {
528
528
  // live-reload channel, so we refresh immediately as fallback.
529
529
  await refreshBackpacksAndGraphs();
530
530
  },
531
- onBackpackRegister: async (name, p, activate) => {
531
+ onBackpackRegister: async (p, activate) => {
532
532
  await fetch("/api/backpacks", {
533
533
  method: "POST",
534
534
  headers: { "Content-Type": "application/json" },
535
- body: JSON.stringify({ name, path: p, activate }),
535
+ body: JSON.stringify({ path: p, activate }),
536
536
  });
537
537
  await refreshBackpacksAndGraphs();
538
538
  },
@@ -771,6 +771,17 @@ async function main() {
771
771
  sidebar.setBackpacks(list);
772
772
  }
773
773
  catch { }
774
+ // Fire-and-forget stale-version check. If we're running an out-of-date
775
+ // viewer (classic npx cache trap), show a banner in the sidebar with
776
+ // the exact command to unblock.
777
+ fetch("/api/version-check")
778
+ .then((r) => r.json())
779
+ .then((info) => {
780
+ if (info.stale && info.latest) {
781
+ sidebar.setStaleVersionBanner(info.current, info.latest);
782
+ }
783
+ })
784
+ .catch(() => { });
774
785
  // Load ontology list (local + remote in parallel)
775
786
  const [summaries, remotes] = await Promise.all([
776
787
  listOntologies(),
package/dist/sidebar.d.ts CHANGED
@@ -14,10 +14,11 @@ export interface SidebarCallbacks {
14
14
  onBranchDelete?: (graphName: string, branchName: string) => void;
15
15
  onSnippetLoad?: (graphName: string, snippetId: string) => void;
16
16
  onSnippetDelete?: (graphName: string, snippetId: string) => void;
17
- onBackpackSwitch?: (name: string) => void;
18
- onBackpackRegister?: (name: string, path: string, activate: boolean) => void;
17
+ onBackpackSwitch?: (pathOrName: string) => void;
18
+ onBackpackRegister?: (path: string, activate: boolean) => void;
19
19
  }
20
20
  export declare function initSidebar(container: HTMLElement, onSelectOrCallbacks: ((name: string) => void) | SidebarCallbacks): {
21
+ setStaleVersionBanner(current: string, latest: string): void;
21
22
  setBackpacks(list: BackpackSummary[]): void;
22
23
  setActiveBackpack(entry: BackpackSummary): void;
23
24
  getActiveBackpack(): BackpackSummary | null;
package/dist/sidebar.js CHANGED
@@ -1,4 +1,4 @@
1
- import { showConfirm, showPrompt } from "./dialog";
1
+ import { showConfirm, showPrompt, showBackpackAddDialog } from "./dialog";
2
2
  function formatTokenCount(n) {
3
3
  if (n >= 1000)
4
4
  return `${(n / 1000).toFixed(1)}k tokens`;
@@ -51,6 +51,14 @@ export function initSidebar(container, onSelectOrCallbacks) {
51
51
  headingRow.appendChild(heading);
52
52
  headingRow.appendChild(collapseBtn);
53
53
  container.appendChild(headingRow);
54
+ // Stale-version banner — hidden by default. setStaleVersionBanner()
55
+ // populates and reveals it when the startup check detects that the
56
+ // running viewer is older than the latest published npm version
57
+ // (classic npx cache trap).
58
+ const staleBanner = document.createElement("div");
59
+ staleBanner.className = "sidebar-stale-banner";
60
+ staleBanner.hidden = true;
61
+ container.appendChild(staleBanner);
54
62
  // Backpack picker pill — discrete indicator of the active backpack with
55
63
  // a dropdown to switch between registered ones.
56
64
  const backpackPicker = document.createElement("button");
@@ -146,14 +154,10 @@ export function initSidebar(container, onSelectOrCallbacks) {
146
154
  closePicker();
147
155
  if (!cbs.onBackpackRegister)
148
156
  return;
149
- const name = await showPrompt("Backpack name", "Short kebab-case name (e.g. work, family, project-alpha)", "");
150
- if (!name)
157
+ const result = await showBackpackAddDialog();
158
+ if (!result)
151
159
  return;
152
- const p = await showPrompt("Backpack path", "Absolute or tilde-expanded path to the graphs directory", "");
153
- if (!p)
154
- return;
155
- const activate = await showConfirm("Switch to new backpack?", `Make "${name}" the active backpack immediately?`);
156
- cbs.onBackpackRegister(name, p, activate);
160
+ cbs.onBackpackRegister(result.path, result.activate);
157
161
  });
158
162
  pickerDropdown.appendChild(addItem);
159
163
  }
@@ -185,6 +189,22 @@ export function initSidebar(container, onSelectOrCallbacks) {
185
189
  }
186
190
  });
187
191
  return {
192
+ setStaleVersionBanner(current, latest) {
193
+ staleBanner.replaceChildren();
194
+ const title = document.createElement("div");
195
+ title.className = "sidebar-stale-banner-title";
196
+ title.textContent = `Viewer ${current} is out of date`;
197
+ const subtitle = document.createElement("div");
198
+ subtitle.className = "sidebar-stale-banner-subtitle";
199
+ subtitle.textContent = `Latest is ${latest}. Your version is stuck because of an npx cache.`;
200
+ const hint = document.createElement("pre");
201
+ hint.className = "sidebar-stale-banner-hint";
202
+ hint.textContent = "npm cache clean --force\nnpx backpack-viewer@latest";
203
+ staleBanner.appendChild(title);
204
+ staleBanner.appendChild(subtitle);
205
+ staleBanner.appendChild(hint);
206
+ staleBanner.hidden = false;
207
+ },
188
208
  setBackpacks(list) {
189
209
  currentBackpacks = list.slice();
190
210
  const active = list.find((b) => b.active) ?? null;
package/dist/style.css CHANGED
@@ -318,6 +318,40 @@ body {
318
318
  opacity: 1;
319
319
  }
320
320
 
321
+ /* --- Stale viewer version banner --- */
322
+
323
+ .sidebar-stale-banner {
324
+ background: #fff3cd;
325
+ color: #5c3a00;
326
+ border: 1px solid #e6c263;
327
+ border-radius: 6px;
328
+ padding: 10px 12px;
329
+ margin-bottom: 10px;
330
+ font-size: 11px;
331
+ }
332
+
333
+ .sidebar-stale-banner-title {
334
+ font-weight: 600;
335
+ margin-bottom: 2px;
336
+ }
337
+
338
+ .sidebar-stale-banner-subtitle {
339
+ opacity: 0.85;
340
+ margin-bottom: 6px;
341
+ }
342
+
343
+ .sidebar-stale-banner-hint {
344
+ background: rgba(0, 0, 0, 0.08);
345
+ border-radius: 4px;
346
+ padding: 6px 8px;
347
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
348
+ font-size: 10px;
349
+ line-height: 1.4;
350
+ margin: 0;
351
+ white-space: pre-wrap;
352
+ word-break: break-all;
353
+ }
354
+
321
355
  /* --- Backpack picker (active backpack indicator + switcher) --- */
322
356
 
323
357
  .backpack-picker-container {
@@ -330,10 +364,10 @@ body {
330
364
  align-items: center;
331
365
  gap: 6px;
332
366
  width: 100%;
333
- padding: 4px 8px;
367
+ padding: 6px 10px;
334
368
  background: var(--bg-base);
335
369
  border: 1px solid var(--border);
336
- border-radius: 999px;
370
+ border-radius: 6px;
337
371
  color: var(--fg);
338
372
  font-size: 11px;
339
373
  font-family: inherit;
@@ -2135,6 +2169,68 @@ body {
2135
2169
  border-color: var(--accent);
2136
2170
  }
2137
2171
 
2172
+ .bp-dialog-label {
2173
+ display: block;
2174
+ font-size: 11px;
2175
+ color: var(--text-muted);
2176
+ margin-bottom: 4px;
2177
+ margin-top: 8px;
2178
+ text-transform: uppercase;
2179
+ letter-spacing: 0.04em;
2180
+ }
2181
+
2182
+ .bp-dialog-path-row {
2183
+ display: flex;
2184
+ gap: 8px;
2185
+ margin-bottom: 4px;
2186
+ }
2187
+
2188
+ .bp-dialog-path-input {
2189
+ flex: 1;
2190
+ margin-bottom: 0;
2191
+ }
2192
+
2193
+ .bp-dialog-browse-btn {
2194
+ flex-shrink: 0;
2195
+ padding: 8px 14px;
2196
+ font-size: 12px;
2197
+ }
2198
+
2199
+ .bp-dialog-browse-btn:disabled {
2200
+ opacity: 0.4;
2201
+ cursor: not-allowed;
2202
+ }
2203
+
2204
+ .bp-dialog-path-input.bp-dialog-drag-over {
2205
+ border-color: var(--accent);
2206
+ background: var(--bg-hover);
2207
+ }
2208
+
2209
+ .bp-dialog-picker-hint {
2210
+ font-size: 11px;
2211
+ color: var(--text-muted);
2212
+ min-height: 1em;
2213
+ margin-bottom: 8px;
2214
+ }
2215
+
2216
+ .bp-dialog-activate-row {
2217
+ display: flex;
2218
+ align-items: center;
2219
+ gap: 8px;
2220
+ margin-top: 8px;
2221
+ margin-bottom: 12px;
2222
+ font-size: 12px;
2223
+ color: var(--text-muted);
2224
+ }
2225
+
2226
+ .bp-dialog-activate-row input[type="checkbox"] {
2227
+ margin: 0;
2228
+ }
2229
+
2230
+ .bp-dialog-activate-row label {
2231
+ cursor: pointer;
2232
+ }
2233
+
2138
2234
  .bp-dialog-buttons {
2139
2235
  display: flex;
2140
2236
  justify-content: flex-end;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backpack-viewer",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Web-based graph visualizer for backpack-ontology — Canvas 2D, force-directed layout, live reload",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Noah Irzinger",
@@ -23,7 +23,7 @@
23
23
  "release:major": "npm version major && git push && git push --tags"
24
24
  },
25
25
  "dependencies": {
26
- "backpack-ontology": "^0.4.0"
26
+ "backpack-ontology": "^0.5.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^25.5.0",