hop2 0.11.53 → 0.11.54

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.
@@ -28,7 +28,8 @@ import {
28
28
  type SessionOriginScope,
29
29
  type SwitcherFolder,
30
30
  type SwitcherSession,
31
- type SwitcherSortMode
31
+ type SwitcherSortMode,
32
+ insertAfter
32
33
  } from "../utils/switcherModel";
33
34
  import { scanKeyboardProtocol } from "../utils/keyboardProtocol";
34
35
  import { DigestCard } from "./DigestCard";
@@ -2452,11 +2453,16 @@ export const SessionSwitcher = ({
2452
2453
  headers: { "Content-Type": "application/json" },
2453
2454
  body: JSON.stringify({ internalName: sessionKey(s), ...(target ? { target } : {}) })
2454
2455
  });
2455
- const data = await res.json().catch(() => ({} as { error?: string; name?: string; kind?: string; extract?: { turns?: number; bytes?: number } }));
2456
+ const data = await res.json().catch(() => ({} as { error?: string; name?: string; internalName?: string; kind?: string; extract?: { turns?: number; bytes?: number } }));
2456
2457
  if (!res.ok) {
2457
2458
  onNotice(data.error || (target ? "Handoff failed" : "Fork failed"));
2458
2459
  return;
2459
2460
  }
2461
+ // The new session belongs beside its source: the daemon files it in
2462
+ // the same folder; on a hand-arranged wall it takes the next slot.
2463
+ if (typeof data.internalName === "string" && data.internalName) {
2464
+ persistManualOrder(insertAfter(manualOrder, sessionKey(s), data.internalName));
2465
+ }
2460
2466
  if (data.kind === "handoff") {
2461
2467
  const kb = Math.max(1, Math.round((data.extract?.bytes || 0) / 1024));
2462
2468
  onNotice(`${data.name} — ${data.extract?.turns ?? "?"} turns handed off (${kb} KB); it reads them first`);
@@ -266,6 +266,19 @@ const orderManually = (sessions: SwitcherSession[], manualOrder: string[]): Swit
266
266
  });
267
267
  };
268
268
 
269
+ /**
270
+ * Place `key` right after `afterKey` in a manual order. A source the user
271
+ * never placed leaves the order alone: both then sort by name at the tail,
272
+ * where a fork's name already follows its source's.
273
+ */
274
+ export const insertAfter = (order: string[], afterKey: string, key: string): string[] => {
275
+ const at = order.indexOf(afterKey);
276
+ if (at < 0 || afterKey === key) return order;
277
+ const without = order.filter((k) => k !== key);
278
+ const i = without.indexOf(afterKey);
279
+ return [...without.slice(0, i + 1), key, ...without.slice(i + 1)];
280
+ };
281
+
269
282
  /**
270
283
  * Split sessions into the user's folders plus everything still loose.
271
284
  * Folders belong to MANUAL mode only: Recent is ordered by the machine and
@@ -5,7 +5,8 @@ import {
5
5
  filterSessionsByOrigin,
6
6
  projectKey,
7
7
  relativeTime,
8
- type SwitcherSession
8
+ type SwitcherSession,
9
+ insertAfter
9
10
  } from "../src/utils/switcherModel";
10
11
 
11
12
  const mk = (over: Partial<SwitcherSession> & { name: string }): SwitcherSession => ({
@@ -523,3 +524,15 @@ describe("origin scope vs foldered sessions", () => {
523
524
  expect(filterSessionsByOrigin(sessions, "agent").map((s) => s.name)).toContain("mybot");
524
525
  });
525
526
  });
527
+
528
+ describe("insertAfter — a fork takes the slot after its source", () => {
529
+ it("inserts right after a placed source, moving an existing entry", () => {
530
+ expect(insertAfter(["a", "b", "c"], "a", "x")).toEqual(["a", "x", "b", "c"]);
531
+ expect(insertAfter(["a", "b", "x"], "a", "x")).toEqual(["a", "x", "b"]);
532
+ });
533
+ it("leaves the order alone when the source was never placed, or is itself", () => {
534
+ const order = ["a", "b"];
535
+ expect(insertAfter(order, "zzz", "x")).toBe(order);
536
+ expect(insertAfter(order, "a", "a")).toBe(order);
537
+ });
538
+ });
package/hop CHANGED
@@ -11769,18 +11769,24 @@ happens until someone clicks it.
11769
11769
  saveDefinition: false,
11770
11770
  command
11771
11771
  });
11772
+ // Filed where its source is. The effective config is the
11773
+ // wall's own truth for the folder (saved, runtime and
11774
+ // durable layers merged); the durable meta alone missed
11775
+ // sessions filed before folders were written there.
11776
+ const folderId = config.folderId || meta?.folderId || null;
11772
11777
  rememberRuntimeSessionMetadata(forkInternal, {
11773
11778
  type: 'terminal',
11774
11779
  displayName: forkName,
11775
11780
  cwd,
11776
11781
  createdBy,
11777
- folderId: meta?.folderId || undefined
11782
+ folderId: folderId || undefined
11778
11783
  });
11779
11784
  writeClaudeSessionMeta(forkInternal, {
11780
11785
  internalName: forkInternal,
11781
11786
  displayName: forkName,
11782
11787
  cwd,
11783
11788
  createdBy,
11789
+ folderId: folderId || undefined,
11784
11790
  forkedFrom: source,
11785
11791
  ...(handoff ? { handoffFrom: sourceKind, handoffTo: wantTarget } : {})
11786
11792
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hop2",
3
- "version": "0.11.53",
3
+ "version": "0.11.54",
4
4
  "description": "🐰 Hop into your terminal from anywhere. Secure remote shell access with 2FA via Cloudflare Tunnel. Next-gen rewrite with native terminal sharing.",
5
5
  "main": "hop",
6
6
  "bin": {