threadnote 0.7.12 → 1.1.0

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/docs/index.html CHANGED
@@ -1098,14 +1098,14 @@
1098
1098
  <div class="terminal-bar"><span></span><span></span><span></span></div>
1099
1099
  <pre><span class="cmd">$ curl -fsSL https://raw.githubusercontent.com/Kashkovsky/threadnote/main/scripts/install.sh | sh</span>
1100
1100
 
1101
- <span class="out">Installed threadnote@0.4.1</span>
1101
+ <span class="out">Installed threadnote@1.0.0</span>
1102
1102
  <span class="out">Already exists: ~/.local/bin/threadnote</span>
1103
1103
  <span class="out">OpenViking installs into Python; neither uv nor pipx is</span>
1104
1104
  <span class="out">on PATH so threadnote would fall back to `pip install</span>
1105
1105
  <span class="out">--user`, which fails on PEP 668 setups.</span>
1106
1106
  <span class="out">Install uv now? [Y/n] </span><span class="out-strong">Y</span>
1107
1107
  <span class="out">Installing uv via Homebrew…</span>
1108
- <span class="out-strong">OK openviking 0.3.12 ready · server healthy</span></pre>
1108
+ <span class="out-strong">OK openviking 0.3.24 ready · server healthy</span></pre>
1109
1109
  </div>
1110
1110
  <p class="muted" style="margin-top: 0.75rem; font-size: 0.9rem">
1111
1111
  Or manually: <code>npm install -g threadnote && threadnote install</code>. On a fresh macOS / modern
@@ -1219,8 +1219,8 @@ projects:
1219
1219
  <div class="terminal">
1220
1220
  <div class="terminal-bar"><span></span><span></span><span></span></div>
1221
1221
  <pre><span class="cmd">$ threadnote update</span>
1222
- <span class="out">Latest: threadnote@0.4.1 (installed: 0.4.0)</span>
1223
- <span class="out">Running: npm install -g threadnote@0.4.1</span>
1222
+ <span class="out">Latest: threadnote@1.0.0 (installed: 0.7.12)</span>
1223
+ <span class="out">Running: npm install -g threadnote@1.0.0</span>
1224
1224
  <span class="out">Running: threadnote repair</span>
1225
1225
  <span class="out-strong">All checks passed. Restart agents to reload MCP.</span></pre>
1226
1226
  </div>
@@ -1585,7 +1585,7 @@ threadnote doctor --dry-run</code></pre>
1585
1585
  </div>
1586
1586
 
1587
1587
  <p class="colophon">
1588
- threadnote · GPL-3.0-or-later · built on OpenViking 0.3.12 · use <span class="kbd">↑</span>
1588
+ threadnote · GPL-3.0-or-later · built on OpenViking 0.3.24 · use <span class="kbd">↑</span>
1589
1589
  <span class="kbd">↓</span> / <span class="kbd">j</span> <span class="kbd">k</span> to navigate
1590
1590
  </p>
1591
1591
  </div>
package/docs/migration.md CHANGED
@@ -239,7 +239,7 @@ canonical instructions, active commands, active skills, MCP config, or anything
239
239
 
240
240
  ## Cutover Checklist
241
241
 
242
- - `doctor --dry-run` reports a healthy OpenViking server.
242
+ - `doctor --dry-run` reports a healthy OpenViking server and no stale recall index warnings.
243
243
  - `threadnote` works from a different repo or subdirectory.
244
244
  - `mcp-install` has been applied for the agent the developer actually uses.
245
245
  - `recall` returns seeded guidance.
@@ -120,8 +120,13 @@ threadnote start
120
120
 
121
121
  ## Claude MCP Fails While Health Is OK
122
122
 
123
- OpenViking `0.3.12` can be healthy at `/health` while returning `404` at `/mcp`. That means the stable server does not
124
- expose the native HTTP MCP endpoint.
123
+ Threadnote uses its bundled stdio MCP adapter by default, even when the installed OpenViking server exposes native
124
+ `/mcp`. The adapter adds Threadnote-specific tools and behavior such as shared-memory sync, exact recall fallback,
125
+ seeded-resource recall augmentation, and recall-index repair.
126
+
127
+ The adapter also exposes raw OpenViking parity tools with `ov_*` names for native behaviors such as code symbol
128
+ navigation, watch management, raw search/read/list/store/remember, grep/glob, resource import, and forget. Prefer
129
+ Threadnote-named tools for memory workflows; use `ov_*` when you intentionally want native OpenViking behavior.
125
130
 
126
131
  Use the default stdio adapter:
127
132
 
@@ -133,7 +138,7 @@ claude mcp list
133
138
  `mcp-install claude` writes user-scoped Claude config by default. This is intentional: local-scoped config only applies
134
139
  to one repo/project, and the `threadnote` shim runs the implementation from the checkout that installed it.
135
140
 
136
- Only use `--native-http` with an OpenViking build that actually exposes `/mcp`.
141
+ Only use `--native-http` when you intentionally want the raw OpenViking HTTP endpoint instead of Threadnote's adapter.
137
142
 
138
143
  ## Worktree Was Deleted
139
144
 
@@ -146,8 +151,8 @@ From any fresh checkout, run:
146
151
  threadnote repair
147
152
  ```
148
153
 
149
- `repair` reinstalls the `threadnote` shim, repairs generated config files, starts OpenViking if needed, and rewrites
150
- Codex/Claude/Cursor/Copilot MCP configs to point at the current checkout.
154
+ `repair` reinstalls the `threadnote` shim, repairs generated config files, starts OpenViking if needed, repairs stale
155
+ recall indexes, and rewrites Codex/Claude/Cursor/Copilot MCP configs to point at the current checkout.
151
156
 
152
157
  ## MCP Install Is Only Printing Commands
153
158
 
package/manager/app.css CHANGED
@@ -113,11 +113,12 @@ textarea {
113
113
  .sidebar {
114
114
  background: rgba(20, 20, 26, 0.94);
115
115
  display: grid;
116
- grid-template-rows: auto auto auto minmax(0, 1fr);
116
+ grid-template-rows: auto auto auto auto minmax(0, 1fr);
117
117
  min-height: 0;
118
118
  min-width: 0;
119
119
  overflow: hidden;
120
120
  padding: 16px;
121
+ row-gap: 0;
121
122
  }
122
123
 
123
124
  .sidebar-resizer {
@@ -241,6 +242,32 @@ body.is-resizing-sidebar * {
241
242
  width: auto;
242
243
  }
243
244
 
245
+ .nav-tree-tabs {
246
+ display: grid;
247
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
248
+ margin: 0 0 10px;
249
+ }
250
+
251
+ .nav-tree-tabs button {
252
+ border-radius: 0;
253
+ min-width: 0;
254
+ }
255
+
256
+ .nav-tree-tabs button:first-child {
257
+ border-radius: 6px 0 0 6px;
258
+ }
259
+
260
+ .nav-tree-tabs button:last-child {
261
+ border-left: 0;
262
+ border-radius: 0 6px 6px 0;
263
+ }
264
+
265
+ .nav-tree-tabs button.is-active {
266
+ background: var(--accent-soft);
267
+ border-color: var(--accent-line);
268
+ color: var(--accent);
269
+ }
270
+
244
271
  .tree {
245
272
  min-height: 0;
246
273
  overflow: auto;
@@ -251,6 +278,12 @@ body.is-resizing-sidebar * {
251
278
  margin: 2px 0;
252
279
  }
253
280
 
281
+ .tree-empty {
282
+ color: var(--muted);
283
+ font-size: 13px;
284
+ margin: 12px 4px;
285
+ }
286
+
254
287
  .tree summary,
255
288
  .tree-row {
256
289
  align-items: center;
@@ -268,6 +301,14 @@ body.is-resizing-sidebar * {
268
301
  list-style: none;
269
302
  }
270
303
 
304
+ .tree summary.is-readonly {
305
+ grid-template-columns: 14px minmax(0, 1fr);
306
+ }
307
+
308
+ .tree-row.is-readonly {
309
+ grid-template-columns: minmax(0, 1fr);
310
+ }
311
+
271
312
  .tree summary::-webkit-details-marker {
272
313
  display: none;
273
314
  }
package/manager/app.js CHANGED
@@ -34731,6 +34731,7 @@
34731
34731
  var SIDEBAR_WIDTH_MIN = 260;
34732
34732
  var SIDEBAR_WIDTH_MAX = 560;
34733
34733
  var token = typeof window === "undefined" ? "" : new URLSearchParams(window.location.search).get("token") ?? "";
34734
+ var EMPTY_SELECTED_URIS = /* @__PURE__ */ new Set();
34734
34735
  function clampSidebarWidth(width) {
34735
34736
  return Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, Math.round(width)));
34736
34737
  }
@@ -34742,6 +34743,7 @@
34742
34743
  const [panel, setPanel] = (0, import_react2.useState)("doctor");
34743
34744
  const [state, setState] = (0, import_react2.useState)();
34744
34745
  const [tree, setTree] = (0, import_react2.useState)();
34746
+ const [resourceTree, setResourceTree] = (0, import_react2.useState)();
34745
34747
  const [shares, setShares] = (0, import_react2.useState)([]);
34746
34748
  const [doctor, setDoctor] = (0, import_react2.useState)([]);
34747
34749
  const [doctorOutput, setDoctorOutput] = (0, import_react2.useState)("");
@@ -34754,6 +34756,7 @@
34754
34756
  const [openSelect, setOpenSelect] = (0, import_react2.useState)();
34755
34757
  const [filter, setFilter] = (0, import_react2.useState)("");
34756
34758
  const [showSystem, setShowSystem] = (0, import_react2.useState)(false);
34759
+ const [navTreeTab, setNavTreeTab] = (0, import_react2.useState)("memories");
34757
34760
  const [toast, setToast] = (0, import_react2.useState)("");
34758
34761
  const [output, setOutput] = (0, import_react2.useState)("");
34759
34762
  const [recallQuery, setRecallQuery] = (0, import_react2.useState)("");
@@ -34801,7 +34804,7 @@
34801
34804
  setMemoryViewMode("edit");
34802
34805
  return;
34803
34806
  }
34804
- const node2 = tree ? findNode(tree, selectedUri) : void 0;
34807
+ const node2 = findNodeInTrees([tree, resourceTree], selectedUri);
34805
34808
  if (node2?.isDir) {
34806
34809
  setMemory(void 0);
34807
34810
  setContent("");
@@ -34809,8 +34812,12 @@
34809
34812
  setTarget({ kind: "durable", project: "", status: "active", team: node2.sharedTeam ?? "", topic: "" });
34810
34813
  return;
34811
34814
  }
34815
+ if (isResourceUri(selectedUri)) {
34816
+ void loadResource(selectedUri);
34817
+ return;
34818
+ }
34812
34819
  void loadMemory(selectedUri);
34813
- }, [selectedUri, tree]);
34820
+ }, [resourceTree, selectedUri, tree]);
34814
34821
  (0, import_react2.useEffect)(() => {
34815
34822
  const firstAvailable = state?.agents.find((item) => item.available && (item.id === "codex" || item.id === "claude"));
34816
34823
  if (firstAvailable) {
@@ -34818,12 +34825,12 @@
34818
34825
  }
34819
34826
  }, [state]);
34820
34827
  const selectedNode = (0, import_react2.useMemo)(
34821
- () => tree && selectedUri ? findNode(tree, selectedUri) : void 0,
34822
- [tree, selectedUri]
34828
+ () => selectedUri ? findNodeInTrees([tree, resourceTree], selectedUri) : void 0,
34829
+ [resourceTree, selectedUri, tree]
34823
34830
  );
34824
34831
  const visibleSelectedUris = (0, import_react2.useMemo)(
34825
- () => pruneSelectedMemoryUris(selectedUris, tree, { filter, showSystem }),
34826
- [filter, selectedUris, showSystem, tree]
34832
+ () => navTreeTab === "memories" ? pruneSelectedMemoryUris(selectedUris, tree, { filter, showSystem }) : EMPTY_SELECTED_URIS,
34833
+ [filter, navTreeTab, selectedUris, showSystem, tree]
34827
34834
  );
34828
34835
  const selectedList = (0, import_react2.useMemo)(() => [...visibleSelectedUris], [visibleSelectedUris]);
34829
34836
  const outputUris = (0, import_react2.useMemo)(() => vikingUrisFromText(output), [output]);
@@ -34835,6 +34842,7 @@
34835
34842
  ]);
34836
34843
  setState(nextState);
34837
34844
  setTree(nextTree.tree);
34845
+ setResourceTree(nextTree.resourcesTree);
34838
34846
  setShares(nextShares.shares);
34839
34847
  toastMessage("Refreshed");
34840
34848
  }
@@ -34854,6 +34862,15 @@
34854
34862
  topic: next.record?.metadata.topic ?? ""
34855
34863
  });
34856
34864
  }
34865
+ async function loadResource(uri) {
34866
+ const result = await api("/api/read", { uri });
34867
+ setMemory(void 0);
34868
+ setContent(result.content || result.output);
34869
+ setOutput(result.output || result.content);
34870
+ setReadUri(uri);
34871
+ setMemoryViewMode(isMarkdownUri(uri) ? "preview" : "edit");
34872
+ setTarget({ kind: "durable", project: "", status: "active", team: "", topic: "" });
34873
+ }
34857
34874
  async function readContext(uri) {
34858
34875
  const trimmed = uri.trim();
34859
34876
  if (!trimmed) {
@@ -34886,7 +34903,7 @@
34886
34903
  toastMessage(label);
34887
34904
  await refreshTreeOnly();
34888
34905
  if (selectedUri) {
34889
- await loadMemory(selectedUri).catch(() => void 0);
34906
+ await reloadSelected(selectedUri);
34890
34907
  }
34891
34908
  } catch (err) {
34892
34909
  toastMessage(errorMessage(err));
@@ -34911,6 +34928,14 @@
34911
34928
  async function refreshTreeOnly() {
34912
34929
  const next = await api("/api/tree");
34913
34930
  setTree(next.tree);
34931
+ setResourceTree(next.resourcesTree);
34932
+ }
34933
+ async function reloadSelected(uri) {
34934
+ if (isResourceUri(uri)) {
34935
+ await loadResource(uri).catch(() => void 0);
34936
+ } else {
34937
+ await loadMemory(uri).catch(() => void 0);
34938
+ }
34914
34939
  }
34915
34940
  async function saveCurrent() {
34916
34941
  await runAction(
@@ -35058,7 +35083,7 @@
35058
35083
  setSelectedUris(new Set(failedUris));
35059
35084
  if (currentSelectedUri && selectedList.includes(currentSelectedUri)) {
35060
35085
  if (failedUris.includes(currentSelectedUri)) {
35061
- await loadMemory(currentSelectedUri).catch(() => void 0);
35086
+ await reloadSelected(currentSelectedUri);
35062
35087
  } else {
35063
35088
  setSelectedUri(void 0);
35064
35089
  setMemory(void 0);
@@ -35066,7 +35091,7 @@
35066
35091
  setMemoryViewMode("edit");
35067
35092
  }
35068
35093
  } else if (currentSelectedUri) {
35069
- await loadMemory(currentSelectedUri).catch(() => void 0);
35094
+ await reloadSelected(currentSelectedUri);
35070
35095
  }
35071
35096
  await refreshTreeOnly();
35072
35097
  toastMessage(failedUris.length === 0 ? "Bulk action complete" : "Bulk action completed with failures");
@@ -35106,7 +35131,7 @@
35106
35131
  if (draftingConsolidation || applyingConsolidation) {
35107
35132
  return;
35108
35133
  }
35109
- const uris = selectedList.length > 0 ? selectedList : selectedUri ? [selectedUri] : [];
35134
+ const uris = selectedList.length > 0 ? selectedList : selectedUri && !isResourceUri(selectedUri) ? [selectedUri] : [];
35110
35135
  if (uris.length < 2) {
35111
35136
  toastMessage("Select at least two memories");
35112
35137
  return;
@@ -35172,7 +35197,7 @@
35172
35197
  setContent("");
35173
35198
  setMemoryViewMode("edit");
35174
35199
  } else if (currentSelectedUri) {
35175
- await loadMemory(currentSelectedUri).catch(() => void 0);
35200
+ await reloadSelected(currentSelectedUri);
35176
35201
  }
35177
35202
  await refreshTreeOnly();
35178
35203
  toastMessage("Applied consolidation");
@@ -35235,16 +35260,20 @@
35235
35260
  }
35236
35261
  }
35237
35262
  const selectedIsDir = selectedNode?.isDir === true;
35263
+ const selectedIsResource = selectedUri ? isResourceUri(selectedUri) : false;
35238
35264
  const selectedIsMarkdown = Boolean(selectedNode && isMarkdownNode(selectedNode));
35239
35265
  const markdownPreview = markdownBodyForPreview(content3);
35240
- const canMutate = Boolean(selectedUri && !selectedIsDir);
35241
- const canRemoveFolder = Boolean(selectedNode?.isDir && selectedNode.relativePath && !selectedNode.isShared);
35266
+ const canMutate = Boolean(selectedUri && !selectedIsDir && !selectedIsResource);
35267
+ const canRemoveFolder = Boolean(
35268
+ selectedNode?.isDir && selectedNode.relativePath && !selectedNode.isShared && !selectedIsResource
35269
+ );
35242
35270
  const consolidationBusy = draftingConsolidation || applyingConsolidation;
35271
+ const canDraftConsolidation = selectedList.length > 0 || !selectedIsResource;
35243
35272
  const doctorBusy = doctorAction !== void 0;
35244
35273
  const controlsBlocked = bulkAction !== void 0;
35245
35274
  const busyOverlayMessage = bulkAction ? `${actionProgressLabel(bulkAction)} ${selectedList.length} selected ${selectedList.length === 1 ? "memory" : "memories"}...` : "";
35246
35275
  const doctorBusyMessage = doctorAction ? `${doctorAction}...` : "";
35247
- const metadataFieldsDisabled = Boolean(memory || selectedIsDir);
35276
+ const metadataFieldsDisabled = Boolean(memory || selectedIsDir || selectedIsResource);
35248
35277
  const appStyle = { "--sidebar-width": `${sidebarWidth}px` };
35249
35278
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "app", style: appStyle, children: [
35250
35279
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("aside", { className: "sidebar", children: [
@@ -35274,7 +35303,7 @@
35274
35303
  disabled: controlsBlocked,
35275
35304
  value: filter,
35276
35305
  onChange: (event) => setFilter(event.target.value),
35277
- placeholder: "Filter memories",
35306
+ placeholder: "Filter memories and resources",
35278
35307
  type: "search"
35279
35308
  }
35280
35309
  ),
@@ -35290,7 +35319,39 @@
35290
35319
  ),
35291
35320
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Show system files" })
35292
35321
  ] }),
35293
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("nav", { className: "tree", "aria-label": "Memory tree", children: tree ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35322
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "nav-tree-tabs", "aria-label": "Navigation tree", children: [
35323
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35324
+ "button",
35325
+ {
35326
+ className: navTreeTab === "memories" ? "is-active" : void 0,
35327
+ disabled: controlsBlocked,
35328
+ onClick: () => setNavTreeTab("memories"),
35329
+ type: "button",
35330
+ children: "Memories"
35331
+ }
35332
+ ),
35333
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35334
+ "button",
35335
+ {
35336
+ className: navTreeTab === "resources" ? "is-active" : void 0,
35337
+ disabled: controlsBlocked,
35338
+ onClick: () => setNavTreeTab("resources"),
35339
+ type: "button",
35340
+ children: "Resources"
35341
+ }
35342
+ )
35343
+ ] }),
35344
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("nav", { className: "tree", "aria-label": "Context tree", children: navTreeTab === "resources" ? resourceTree ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35345
+ Tree,
35346
+ {
35347
+ filter,
35348
+ node: resourceTree,
35349
+ onSelect: selectTreeUri,
35350
+ selectable: false,
35351
+ selectedUri,
35352
+ showSystem
35353
+ }
35354
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "tree-empty", children: "No resources" }) : tree ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35294
35355
  Tree,
35295
35356
  {
35296
35357
  filter,
@@ -35312,7 +35373,7 @@
35312
35373
  selectionDisabled: bulkAction !== void 0,
35313
35374
  showSystem
35314
35375
  }
35315
- ) : null })
35376
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "tree-empty", children: "No memories" }) })
35316
35377
  ] }),
35317
35378
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35318
35379
  "div",
@@ -35374,7 +35435,7 @@
35374
35435
  "button",
35375
35436
  {
35376
35437
  className: memoryViewMode === "edit" ? "is-active" : void 0,
35377
- disabled: selectedIsDir || controlsBlocked,
35438
+ disabled: selectedIsDir || selectedIsResource || controlsBlocked,
35378
35439
  onClick: () => setMemoryViewMode("edit"),
35379
35440
  children: "Edit"
35380
35441
  }
@@ -35384,7 +35445,7 @@
35384
35445
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35385
35446
  "button",
35386
35447
  {
35387
- disabled: selectedIsDir || controlsBlocked,
35448
+ disabled: selectedIsDir || selectedIsResource || controlsBlocked,
35388
35449
  onClick: () => void (memory ? saveCurrent() : saveNew()),
35389
35450
  children: "Save"
35390
35451
  }
@@ -35431,9 +35492,9 @@
35431
35492
  memoryViewMode === "preview" && selectedIsMarkdown && !selectedIsDir ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MarkdownViewer, { markdown: markdownPreview }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35432
35493
  "textarea",
35433
35494
  {
35434
- disabled: selectedIsDir || controlsBlocked,
35495
+ disabled: selectedIsDir || selectedIsResource || controlsBlocked,
35435
35496
  onChange: (event) => setContent(event.target.value),
35436
- placeholder: selectedIsDir ? "Folder selected" : "Memory content",
35497
+ placeholder: selectedIsDir ? "Folder selected" : selectedIsResource ? "Resource content" : "Memory content",
35437
35498
  spellCheck: false,
35438
35499
  value: content3
35439
35500
  }
@@ -35471,7 +35532,14 @@
35471
35532
  value: agent
35472
35533
  }
35473
35534
  ),
35474
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { disabled: consolidationBusy || controlsBlocked, onClick: () => void draftConsolidation(), children: draftingConsolidation ? "Drafting..." : "Draft" })
35535
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35536
+ "button",
35537
+ {
35538
+ disabled: consolidationBusy || controlsBlocked || !canDraftConsolidation,
35539
+ onClick: () => void draftConsolidation(),
35540
+ children: draftingConsolidation ? "Drafting..." : "Draft"
35541
+ }
35542
+ )
35475
35543
  ] }),
35476
35544
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35477
35545
  "textarea",
@@ -35717,18 +35785,18 @@
35717
35785
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35718
35786
  "button",
35719
35787
  {
35720
- onClick: () => void runAction("Seed dry run complete", () => api("/api/seed", { confirm: true, dryRun: true })),
35721
- children: "Seed Dry Run"
35788
+ onClick: () => window.confirm("Run Threadnote seed and write resources?") ? void runAction("Seed complete", () => api("/api/seed", { confirm: true })) : void 0,
35789
+ children: "Seed"
35722
35790
  }
35723
35791
  ),
35724
35792
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35725
35793
  "button",
35726
35794
  {
35727
- onClick: () => void runAction(
35728
- "Seed skills dry run complete",
35729
- () => api("/api/seed", { confirm: true, dryRun: true, skills: true })
35730
- ),
35731
- children: "Seed Skills Dry Run"
35795
+ onClick: () => window.confirm("Run Threadnote seed-skills and write resources?") ? void runAction(
35796
+ "Seed skills complete",
35797
+ () => api("/api/seed", { confirm: true, skills: true })
35798
+ ) : void 0,
35799
+ children: "Seed Skills"
35732
35800
  }
35733
35801
  )
35734
35802
  ] })
@@ -35741,6 +35809,8 @@
35741
35809
  ] });
35742
35810
  }
35743
35811
  function Tree(props) {
35812
+ const selectable = props.selectable !== false;
35813
+ const selectedUris = props.selectedUris ?? EMPTY_SELECTED_URIS;
35744
35814
  if (!props.showSystem && props.node.isSystem) {
35745
35815
  return null;
35746
35816
  }
@@ -35748,45 +35818,39 @@
35748
35818
  return null;
35749
35819
  }
35750
35820
  if (props.node.isDir) {
35751
- const selectableUris = selectableMemoryUris(props.node, { filter: props.filter, showSystem: props.showSystem });
35752
- const selectedCount = selectableUris.filter((uri) => props.selectedUris.has(uri)).length;
35821
+ const selectableUris = selectable ? selectableMemoryUris(props.node, { filter: props.filter, showSystem: props.showSystem }) : [];
35822
+ const selectedCount = selectableUris.filter((uri) => selectedUris.has(uri)).length;
35753
35823
  const checked = selectableUris.length > 0 && selectedCount === selectableUris.length;
35754
35824
  const indeterminate = selectedCount > 0 && selectedCount < selectableUris.length;
35825
+ const summaryClass = treeItemClass(props.selectedUri === props.node.uri, !selectable);
35755
35826
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("details", { open: props.node.relativePath.split("/").length < 3, children: [
35756
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
35757
- "summary",
35758
- {
35759
- className: props.selectedUri === props.node.uri ? "is-active" : void 0,
35760
- onClick: () => props.onSelect(props.node.uri),
35761
- title: props.node.uri,
35762
- children: [
35763
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35764
- TreeSelectionCheckbox,
35765
- {
35766
- checked,
35767
- disabled: props.selectionDisabled || selectableUris.length === 0,
35768
- indeterminate,
35769
- onChange: (checked2) => props.onToggleSelection(props.node, checked2)
35770
- }
35771
- ),
35772
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": "true", className: "tree-caret" }),
35773
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "tree-name", children: props.node.name })
35774
- ]
35775
- }
35776
- ),
35827
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("summary", { className: summaryClass, onClick: () => props.onSelect(props.node.uri), title: props.node.uri, children: [
35828
+ selectable ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35829
+ TreeSelectionCheckbox,
35830
+ {
35831
+ checked,
35832
+ disabled: props.selectionDisabled === true || selectableUris.length === 0,
35833
+ indeterminate,
35834
+ onChange: (checked2) => props.onToggleSelection?.(props.node, checked2)
35835
+ }
35836
+ ) : null,
35837
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": "true", className: "tree-caret" }),
35838
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "tree-name", children: props.node.name })
35839
+ ] }),
35777
35840
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "tree-children", children: (props.node.children ?? []).map((child) => /* @__PURE__ */ (0, import_react3.createElement)(Tree, { ...props, key: child.uri, node: child })) })
35778
35841
  ] });
35779
35842
  }
35780
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `tree-row ${props.selectedUri === props.node.uri ? "is-active" : ""}`, children: [
35781
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35843
+ const rowClass = treeItemClass(props.selectedUri === props.node.uri, !selectable, "tree-row");
35844
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: rowClass, children: [
35845
+ selectable ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
35782
35846
  "input",
35783
35847
  {
35784
- checked: props.selectedUris.has(props.node.uri),
35785
- disabled: props.selectionDisabled,
35786
- onChange: (event) => props.onToggleSelection(props.node, event.target.checked),
35848
+ checked: selectedUris.has(props.node.uri),
35849
+ disabled: props.selectionDisabled === true,
35850
+ onChange: (event) => props.onToggleSelection?.(props.node, event.target.checked),
35787
35851
  type: "checkbox"
35788
35852
  }
35789
- ),
35853
+ ) : null,
35790
35854
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "tree-file", onClick: () => props.onSelect(props.node.uri), title: props.node.uri, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "tree-name", children: props.node.name }) })
35791
35855
  ] });
35792
35856
  }
@@ -36066,6 +36130,21 @@
36066
36130
  }
36067
36131
  return void 0;
36068
36132
  }
36133
+ function findNodeInTrees(trees, uri) {
36134
+ for (const tree of trees) {
36135
+ const node2 = tree ? findNode(tree, uri) : void 0;
36136
+ if (node2) {
36137
+ return node2;
36138
+ }
36139
+ }
36140
+ return void 0;
36141
+ }
36142
+ function treeItemClass(active, readOnly, base) {
36143
+ const classes = [base, active ? "is-active" : void 0, readOnly ? "is-readonly" : void 0].filter(
36144
+ (value) => typeof value === "string"
36145
+ );
36146
+ return classes.length > 0 ? classes.join(" ") : void 0;
36147
+ }
36069
36148
  function countFiles(node2) {
36070
36149
  if (!node2.isDir) {
36071
36150
  return 1;
@@ -36101,7 +36180,13 @@
36101
36180
  return changed ? next : selectedUris;
36102
36181
  }
36103
36182
  function isMarkdownNode(node2) {
36104
- return !node2.isDir && node2.name.toLowerCase().endsWith(".md");
36183
+ return !node2.isDir && isMarkdownUri(node2.name);
36184
+ }
36185
+ function isMarkdownUri(uri) {
36186
+ return uri.toLowerCase().endsWith(".md");
36187
+ }
36188
+ function isResourceUri(uri) {
36189
+ return uri === "viking://resources" || uri.startsWith("viking://resources/");
36105
36190
  }
36106
36191
  function markdownBodyForPreview(content3) {
36107
36192
  if (!content3.startsWith("MEMORY\n") && !content3.startsWith("HANDOFF\n")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.7.12",
3
+ "version": "1.1.0",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",