hop2 0.11.20 → 0.11.21

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.
@@ -2368,23 +2368,49 @@ export const SessionSwitcher = ({
2368
2368
  // or be an agent-created session they never opened. Name it, say
2369
2369
  // where it lives, and put it on screen — the answer to "it doesn't
2370
2370
  // work" should be the session that was in the way.
2371
+ // Search the WHOLE fleet, not the visible wall: the holder may be
2372
+ // hidden — parked, or an agent-created session on the other tab
2373
+ // (the mybot incident: create "failed", the explanation was a
2374
+ // session the user could not see). A silent 409 with an invisible
2375
+ // cause reads as "it just doesn't work"; the answer must be the
2376
+ // session that was in the way, revealed.
2371
2377
  const taken = res.status === 409
2372
- ? wallSessions.find((s) => (s.displayName || s.name).toLowerCase() === next.toLowerCase())
2378
+ ? sessions.find((s) => (s.displayName || s.name).toLowerCase() === next.toLowerCase())
2373
2379
  : null;
2374
2380
  if (taken) {
2375
2381
  const home = folders.find((f) => f.id === taken.folderId);
2376
- onNotice(`"${taken.displayName || taken.name}" already exists${home ? ` in ${home.name}` : ""} — showing it`);
2382
+ const visible = wallSessions.some((s) => sessionKey(s) === sessionKey(taken));
2383
+ const hiddenByOrigin = !visibleSessions.some((s) => sessionKey(s) === sessionKey(taken));
2384
+ const where = taken.parked
2385
+ ? " — it was hidden among the parked sessions"
2386
+ : hiddenByOrigin
2387
+ ? " — it was hidden on the Agent sessions tab"
2388
+ : home ? ` in ${home.name}` : "";
2389
+ onNotice(`"${taken.displayName || taken.name}" already exists${where} — showing it`);
2377
2390
  setCreating(false);
2378
2391
  setCreateDraft("");
2379
2392
  setCreateInFolder(null);
2393
+ // Reveal before focusing: flip whatever filter is hiding it.
2394
+ if (hiddenByOrigin) setOriginScope("all");
2395
+ if (taken.parked) setParkedOpen(true);
2380
2396
  const key = sessionKey(taken);
2381
2397
  setFocusedKey(key);
2382
- const idx = navIndexByKey.get(key);
2383
- if (typeof idx === "number") {
2384
- setKbdIndex(idx);
2385
- requestAnimationFrame(() => {
2386
- document.querySelector(`[data-nav-index="${idx}"]`)?.scrollIntoView({ block: "center" });
2387
- });
2398
+ if (visible) {
2399
+ const idx = navIndexByKey.get(key);
2400
+ if (typeof idx === "number") {
2401
+ setKbdIndex(idx);
2402
+ requestAnimationFrame(() => {
2403
+ document.querySelector(`[data-nav-index="${idx}"]`)?.scrollIntoView({ block: "center" });
2404
+ });
2405
+ }
2406
+ } else {
2407
+ // It enters the wall on the NEXT render (scope/park state just
2408
+ // changed); scroll once the card exists.
2409
+ window.setTimeout(() => {
2410
+ const el = document.querySelector(`[data-session-key="${CSS.escape(key)}"]`)
2411
+ || document.querySelector(`[data-nav-index]`);
2412
+ el?.scrollIntoView({ block: "center" });
2413
+ }, 120);
2388
2414
  }
2389
2415
  return;
2390
2416
  }
@@ -2491,6 +2517,7 @@ export const SessionSwitcher = ({
2491
2517
  role="button"
2492
2518
  tabIndex={0}
2493
2519
  data-nav-index={navIndexByKey.get(key)}
2520
+ data-session-key={key}
2494
2521
  className={`switcher-card${current ? " current" : ""}${activeNow ? " active-now" : ""}${kbdSelected ? " kbd-selected" : ""}${focusedKey === key ? " focused" : ""}${dragKey === key ? " dragging" : ""}${draggable ? " draggable" : ""}`}
2495
2522
  draggable={draggable}
2496
2523
  onDragStart={draggable ? (e) => { setDragKey(key); e.dataTransfer.effectAllowed = "move"; } : undefined}
@@ -182,7 +182,13 @@ export const filterSessionsByOrigin = (
182
182
  ) => {
183
183
  if (scope === "all") return sessions;
184
184
  if (scope === "agent") return sessions.filter((session) => session.createdBy === "agent");
185
- return sessions.filter((session) => session.createdBy !== "agent");
185
+ // The user wall: everything not agent-made — PLUS anything filed in a
186
+ // folder. Folders are the user's own structure, and an agent only files a
187
+ // session into one because the user asked for it there ("create mybot
188
+ // under Softwares", relayed through an agent, landed createdBy=agent and
189
+ // VANISHED from the default wall — created, filed, invisible). Placement
190
+ // in user space outranks provenance.
191
+ return sessions.filter((session) => session.createdBy !== "agent" || !!session.folderId);
186
192
  };
187
193
 
188
194
  /**
@@ -357,3 +357,36 @@ describe("manual folders in the wall", () => {
357
357
  expect(screen.getByRole("button", { name: /Search full history for/ })).toBeTruthy();
358
358
  });
359
359
  });
360
+
361
+ describe("create colliding with a hidden session", () => {
362
+ afterEach(() => { vi.unstubAllGlobals(); });
363
+
364
+ it("explains the invisible holder and reveals it instead of failing mutely", async () => {
365
+ // The mybot incident, second act: the name is taken by a session the
366
+ // user cannot SEE (agent-created, loose, other tab). A bare "already in
367
+ // use" against an empty wall reads as "create silently did nothing".
368
+ const holder: SwitcherSession = {
369
+ name: "mybot", displayName: "mybot", internalName: "mybot",
370
+ active: true, starting: false, createdBy: "agent"
371
+ };
372
+ const onNotice = vi.fn();
373
+ vi.stubGlobal("fetch", vi.fn(async () => ({
374
+ ok: false,
375
+ status: 409,
376
+ json: async () => ({ error: "Session name already in use" })
377
+ })));
378
+
379
+ render(<SessionSwitcher {...props} onNotice={onNotice} sessions={[...sessions, holder]} open />);
380
+ // The holder is genuinely hidden on the default (user) tab.
381
+ expect(screen.queryByText("mybot")).toBeNull();
382
+
383
+ fireEvent.click(screen.getByRole("button", { name: "New session" }));
384
+ fireEvent.change(screen.getByLabelText("New session name"), { target: { value: "mybot" } });
385
+ fireEvent.submit(screen.getByLabelText("New session name").closest("form")!);
386
+ await new Promise((r) => setTimeout(r, 50));
387
+
388
+ expect(onNotice).toHaveBeenCalledWith(expect.stringMatching(/already exists.*hidden.*Agent/i));
389
+ // And the wall now SHOWS it — scope flipped so the answer is on screen.
390
+ expect(screen.getByText("mybot")).toBeTruthy();
391
+ });
392
+ });
@@ -473,3 +473,28 @@ describe("manual order never moves on its own", () => {
473
473
  expect(m.mode === "manual" && m.rows.map((r) => r.name)).toEqual(["c", "a", "b"]);
474
474
  });
475
475
  });
476
+
477
+ describe("origin scope vs foldered sessions", () => {
478
+ const mk = (name: string, extra: Partial<SwitcherSession> = {}): SwitcherSession => ({
479
+ name, displayName: name, internalName: name, active: true, starting: false,
480
+ createdBy: "user", ...extra
481
+ });
482
+
483
+ it("a session an agent filed into the user's folder shows on the user wall", () => {
484
+ // The mybot incident: "create mybot under Softwares", relayed through an
485
+ // agent, landed createdBy=agent — created, filed, and invisible on the
486
+ // default tab. Placement in the user's own structure outranks provenance.
487
+ const sessions = [
488
+ mk("mine"),
489
+ mk("mybot", { createdBy: "agent", folderId: "f_soft" }),
490
+ mk("worker", { createdBy: "agent" })
491
+ ];
492
+ const user = filterSessionsByOrigin(sessions, "user").map((s) => s.name);
493
+ expect(user).toContain("mybot");
494
+ expect(user).toContain("mine");
495
+ expect(user).not.toContain("worker"); // loose agent workers stay on their tab
496
+
497
+ // The agent tab still lists it — it IS agent-created; no tab lies.
498
+ expect(filterSessionsByOrigin(sessions, "agent").map((s) => s.name)).toContain("mybot");
499
+ });
500
+ });