@cortexkit/aft-opencode 0.36.1 → 0.37.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.
@@ -259,6 +259,49 @@ export function shouldSuppressUninitializedDowngrade(
259
259
  return incomingCacheRole === "not_initialized" && haveInitializedForContext;
260
260
  }
261
261
 
262
+ /**
263
+ * Cross-project contamination belt. The RPC layer can hand back a snapshot
264
+ * describing a DIFFERENT project than the one this sidebar asked about — a
265
+ * multi-project host (Desktop / `opencode serve`) whose status handler
266
+ * resolved another project's warm bridge, including long-lived processes
267
+ * still running pre-fix plugin code. Rendering it shows another repo's
268
+ * indexes/health in this window.
269
+ *
270
+ * A mismatched project_root is acceptable ONLY when the serving handler says
271
+ * it resolved that directory DELIBERATELY: new servers attach
272
+ * `served_directory` (their own cwd, or the SDK-verified `opencode -s` resume
273
+ * directory) to every status response. That marker is handler-attached
274
+ * provenance — it cannot be faked by snapshot contents. We explicitly do NOT
275
+ * use `snapshot.session.id` here: Rust echoes the REQUESTED session id into
276
+ * the snapshot, so it matches even when the data came from another project's
277
+ * bridge (the hole that let contamination through this belt's first version).
278
+ *
279
+ * Rules:
280
+ * - placeholder/synthetic snapshots (no project_root) → accept (not data)
281
+ * - project_root (or canonical_root) matches the sidebar directory → accept
282
+ * - mismatched root AND served_directory matches a snapshot root → accept
283
+ * (deliberate, SDK-verified resume serve from a new server)
284
+ * - otherwise → reject (stray; includes everything old servers cross-serve)
285
+ */
286
+ export function isSnapshotForContext(
287
+ snapshot: AftStatusSnapshot,
288
+ directory: string,
289
+ servedDirectory: string | undefined,
290
+ ): boolean {
291
+ const stripSlash = (p: string) => p.replace(/\/+$/, "");
292
+ const roots = [snapshot.project_root, snapshot.canonical_root].filter(
293
+ (r): r is string => typeof r === "string" && r.length > 0,
294
+ );
295
+ if (roots.length === 0) return true; // placeholder / synthetic
296
+ const dir = stripSlash(directory);
297
+ if (roots.some((r) => stripSlash(r) === dir)) return true;
298
+ if (typeof servedDirectory === "string" && servedDirectory.length > 0) {
299
+ const served = stripSlash(servedDirectory);
300
+ return roots.some((r) => stripSlash(r) === served);
301
+ }
302
+ return false;
303
+ }
304
+
262
305
  const SidebarContent = (props: {
263
306
  api: TuiPluginApi;
264
307
  sessionID: () => string;
@@ -330,12 +373,32 @@ const SidebarContent = (props: {
330
373
  const response = await client.call(
331
374
  "status",
332
375
  { sessionID: sid },
333
- { signal: controller.signal },
376
+ {
377
+ signal: controller.signal,
378
+ // With several RPC servers alive for this project hash, a stray
379
+ // warm response (another project's bridge) must not beat the right
380
+ // server or the placeholder — skip it at the port-scan level.
381
+ accept: (result) => {
382
+ const rec = result as Record<string, unknown>;
383
+ if (rec?.success === false) return true; // errors handled below
384
+ return isSnapshotForContext(
385
+ coerceAftStatus(rec),
386
+ directory,
387
+ rec?.served_directory as string | undefined,
388
+ );
389
+ },
390
+ },
334
391
  );
335
392
  if (controller.signal.aborted || requestGeneration !== generation) return;
336
393
  if (currentDirectory() !== directory || props.sessionID() !== sid) return;
337
394
  if (response && (response as Record<string, unknown>).success !== false) {
338
395
  const snapshot = coerceAftStatus(response as Record<string, unknown>);
396
+ // Belt: never render a snapshot describing another project (see
397
+ // isSnapshotForContext). Keep whatever we currently show instead.
398
+ const servedDirectory = (response as Record<string, unknown>).served_directory as
399
+ | string
400
+ | undefined;
401
+ if (!isSnapshotForContext(snapshot, directory, servedDirectory)) return;
339
402
  // Stale-while-revalidate: keep the last-good snapshot instead of
340
403
  // flickering to the lazy-bridge placeholder on a transient
341
404
  // not_initialized. See shouldSuppressUninitializedDowngrade.
@@ -1,8 +0,0 @@
1
- import type { ToolDefinition } from "@opencode-ai/plugin";
2
- import type { PluginContext } from "../types.js";
3
- /**
4
- * Tool definitions for scope-aware structure commands:
5
- * add_member, add_derive, wrap_try_catch, add_decorator, add_struct_tags.
6
- */
7
- export declare function structureTools(ctx: PluginContext): Record<string, ToolDefinition>;
8
- //# sourceMappingURL=structure.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["../../src/tools/structure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAWjD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAyJjF"}