hop2 0.11.19 → 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.
- package/hay/apps/web/src/components/SessionSwitcher.tsx +35 -8
- package/hay/apps/web/src/utils/switcherModel.ts +7 -1
- package/hay/apps/web/test/SessionSwitcher.test.tsx +33 -0
- package/hay/apps/web/test/switcherModel.test.ts +25 -0
- package/hay-web/assets/index-CIfGRbUF.js +134 -0
- package/hay-web/assets/index-CIfGRbUF.js.map +1 -0
- package/hay-web/assets/index-DjSB7l7z.js +134 -0
- package/hay-web/assets/index-DjSB7l7z.js.map +1 -0
- package/hay-web/index.html +1 -1
- package/hop +193 -4
- package/hopa +2 -0
- package/mcp/hop-mcp.js +31 -0
- package/package.json +1 -1
- package/skills/hop-session/SKILL.md +25 -1
|
@@ -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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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
|
-
|
|
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
|
+
});
|