@vibecook/ghosttea-react 0.11.1 → 0.12.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.
Files changed (57) hide show
  1. package/README.md +27 -0
  2. package/dist/TerminalSurface.d.ts.map +1 -1
  3. package/dist/TerminalSurface.js +31 -4
  4. package/dist/TerminalSurface.js.map +1 -1
  5. package/dist/appearance/AppearanceSettings.d.ts +3 -2
  6. package/dist/appearance/AppearanceSettings.d.ts.map +1 -1
  7. package/dist/appearance/AppearanceSettings.js +3 -2
  8. package/dist/appearance/AppearanceSettings.js.map +1 -1
  9. package/dist/appearance/BackdropBlurControl.d.ts +5 -0
  10. package/dist/appearance/BackdropBlurControl.d.ts.map +1 -0
  11. package/dist/appearance/BackdropBlurControl.js +42 -0
  12. package/dist/appearance/BackdropBlurControl.js.map +1 -0
  13. package/dist/appearance/types.d.ts +6 -0
  14. package/dist/appearance/types.d.ts.map +1 -1
  15. package/dist/link-input.d.ts +18 -0
  16. package/dist/link-input.d.ts.map +1 -0
  17. package/dist/link-input.js +27 -0
  18. package/dist/link-input.js.map +1 -0
  19. package/dist/renderers/background-geometry.d.ts +16 -0
  20. package/dist/renderers/background-geometry.d.ts.map +1 -0
  21. package/dist/renderers/background-geometry.js +20 -0
  22. package/dist/renderers/background-geometry.js.map +1 -0
  23. package/dist/renderers/types.d.ts +2 -0
  24. package/dist/renderers/types.d.ts.map +1 -1
  25. package/dist/renderers/types.js.map +1 -1
  26. package/dist/renderers/webgpu-renderer.d.ts.map +1 -1
  27. package/dist/renderers/webgpu-renderer.js +10 -2
  28. package/dist/renderers/webgpu-renderer.js.map +1 -1
  29. package/dist/runtime.d.ts +5 -0
  30. package/dist/runtime.d.ts.map +1 -1
  31. package/dist/runtime.js +50 -6
  32. package/dist/runtime.js.map +1 -1
  33. package/dist/styles.css +13 -0
  34. package/dist/terminal-render.worker.js +71 -9
  35. package/dist/terminal-render.worker.js.map +3 -3
  36. package/dist/use-terminal-links.d.ts +17 -0
  37. package/dist/use-terminal-links.d.ts.map +1 -0
  38. package/dist/use-terminal-links.js +101 -0
  39. package/dist/use-terminal-links.js.map +1 -0
  40. package/dist/worker-messages.d.ts +5 -0
  41. package/dist/worker-messages.d.ts.map +1 -1
  42. package/dist/workspace/Workspace.d.ts +6 -3
  43. package/dist/workspace/Workspace.d.ts.map +1 -1
  44. package/dist/workspace/Workspace.js +18 -9
  45. package/dist/workspace/Workspace.js.map +1 -1
  46. package/dist/workspace/index.d.ts +2 -2
  47. package/dist/workspace/index.d.ts.map +1 -1
  48. package/dist/workspace/index.js.map +1 -1
  49. package/dist/workspace/pane-focus.d.ts +8 -0
  50. package/dist/workspace/pane-focus.d.ts.map +1 -0
  51. package/dist/workspace/pane-focus.js +24 -0
  52. package/dist/workspace/pane-focus.js.map +1 -0
  53. package/dist/workspace/pane-layout.d.ts +2 -1
  54. package/dist/workspace/pane-layout.d.ts.map +1 -1
  55. package/dist/workspace/pane-layout.js +29 -2
  56. package/dist/workspace/pane-layout.js.map +1 -1
  57. package/package.json +4 -4
package/dist/runtime.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { terminalLinkUrl } from "@vibecook/ghosttea-protocol";
1
2
  import { ControlClient } from "@vibecook/ghosttea";
2
3
  import { DEFAULT_ROUTED_PROTOCOL_LIMITS, PROTOCOL_MAJOR, PROTOCOL_MINOR, SESSION_SCROLLBACK_PROTOCOL_MINOR, STRUCTURED_ERROR_PROTOCOL_MINOR, isRoutedSessionAttachGrant, isRoutedTerminalOpenTicket, isValidScrollbackBytes, } from "@vibecook/ghosttea-protocol";
3
4
  import { FRAME_MAGIC, FrameFlag } from "@vibecook/ghosttea-frame";
@@ -110,6 +111,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
110
111
  #performanceRequestId = 1;
111
112
  #performanceRequests = new Map();
112
113
  #counterRequests = new Map();
114
+ #linksByHandle = new Map();
113
115
  #disposed = false;
114
116
  constructor(options) {
115
117
  super();
@@ -155,6 +157,10 @@ export class GhostteaTerminalRuntime extends EventTarget {
155
157
  else if (data.type === "clipboard-write") {
156
158
  this.#platform.writeClipboard(data.text);
157
159
  }
160
+ else if (data.type === "link-targets") {
161
+ this.#linksByHandle.set(data.sessionHandle, data.links.filter((link) => terminalLinkUrl(link.uri) !== null));
162
+ this.dispatchEvent(new CustomEvent("link-targets", { detail: { sessionHandle: data.sessionHandle } }));
163
+ }
158
164
  else if (data.type === "scrollbar-state") {
159
165
  this.#scrollbarByHandle.set(data.sessionHandle, data.scrollbar);
160
166
  this.dispatchEvent(new CustomEvent("scrollbar-state", {
@@ -1804,6 +1810,29 @@ export class GhostteaTerminalRuntime extends EventTarget {
1804
1810
  this.#routedByActivation.delete(entry.state.activationId);
1805
1811
  this.#routedGeometry.delete(sessionId);
1806
1812
  }
1813
+ /**
1814
+ * Detach one of our views and let the seat go with it.
1815
+ *
1816
+ * The daemon clears the resize controller when its holder detaches, but a
1817
+ * local session announces control only through the legacy `control-changed`
1818
+ * frame, which cannot say "no controller": the clear never reaches this
1819
+ * client, and a pane that remounts (a split re-parents its surface) would
1820
+ * see its own previous incarnation on the record forever and never claim.
1821
+ * Our own detach is the one clear this client can predict, so the record is
1822
+ * cleared here at the same revision and the session's remaining views get
1823
+ * their one look at the empty seat (§4.2.3).
1824
+ */
1825
+ #detachView(sessionId, viewId) {
1826
+ this.#control?.notify({ type: "detach-session", sessionId, viewId });
1827
+ const control = this.#controlBySession.get(sessionId);
1828
+ if (!control?.controller || control.controller.viewId !== viewId)
1829
+ return;
1830
+ this.#controlBySession.set(sessionId, { controller: null, revision: control.revision });
1831
+ for (const other of this.#viewIdsForSession(sessionId)) {
1832
+ if (other !== viewId)
1833
+ this.#maybeReclaim(other);
1834
+ }
1835
+ }
1807
1836
  #createMountLease(mounted) {
1808
1837
  let disposed = false;
1809
1838
  return {
@@ -1829,7 +1858,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
1829
1858
  if (this.#routedHost)
1830
1859
  this.#releaseRoutedView(mounted.sessionId, mounted.viewId);
1831
1860
  else
1832
- this.#control?.notify({ type: "detach-session", sessionId: mounted.sessionId, viewId: mounted.viewId });
1861
+ this.#detachView(mounted.sessionId, mounted.viewId);
1833
1862
  this.#views.delete(mounted.viewId);
1834
1863
  this.#focusByView.delete(mounted.viewId);
1835
1864
  }
@@ -2269,6 +2298,16 @@ export class GhostteaTerminalRuntime extends EventTarget {
2269
2298
  setEffects(sessionHandle, effects, surfaceId) {
2270
2299
  this.#postWorker({ type: "effects", sessionHandle, ...(surfaceId ? { surfaceId } : {}), effects });
2271
2300
  }
2301
+ links(sessionHandle) {
2302
+ return this.#platform.openExternal
2303
+ ? (this.#linksByHandle.get(sessionHandle) ?? []).filter((link) => link.explicit || this.#configSnapshot?.renderer.linkUrl !== false)
2304
+ : [];
2305
+ }
2306
+ async openLink(uri) {
2307
+ const url = terminalLinkUrl(uri);
2308
+ if (url)
2309
+ await this.#platform.openExternal?.(url);
2310
+ }
2272
2311
  setSelection(sessionHandle, selection, surfaceId) {
2273
2312
  this.#postWorker({ type: "selection", sessionHandle, ...(surfaceId ? { surfaceId } : {}), selection });
2274
2313
  }
@@ -2369,9 +2408,12 @@ export class GhostteaTerminalRuntime extends EventTarget {
2369
2408
  }
2370
2409
  return;
2371
2410
  }
2372
- // The legacy protocol has no release verb. Clearing the local epoch still
2373
- // closes every resize path immediately; a later explicit claim can renew it.
2374
- view.controlEpoch = undefined;
2411
+ // The legacy protocol has no release verb, so the daemon keeps this view
2412
+ // seated. Dropping the request is what closes every resize path; the epoch
2413
+ // stays, because it is still the seat's truth and a later explicit claim
2414
+ // resumes on it. Clearing it here would leave the view with the record in
2415
+ // its own name and no epoch: the funnel's one-claim-per-attachment guard
2416
+ // refuses that claim, and a pane hidden by a zoom never resizes again.
2375
2417
  }
2376
2418
  setViewInputPolicy(viewId, readWrite) {
2377
2419
  const view = this.#views.get(viewId);
@@ -2480,6 +2522,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
2480
2522
  this.#sessionMountReferences.delete(handle);
2481
2523
  const subscriptionChanged = this.#subscribedSessionHandles.delete(handle);
2482
2524
  this.#cancelMetadataRefresh(handle);
2525
+ this.#linksByHandle.delete(handle);
2483
2526
  this.#mouseTrackingByHandle.delete(handle);
2484
2527
  this.#scrollbarByHandle.delete(handle);
2485
2528
  for (const mounted of [...this.#mountedEntries]) {
@@ -2494,7 +2537,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
2494
2537
  this.#postWorker({ type: "unmount", surfaceId: mounted.viewId });
2495
2538
  this.#mountGenerationBySurface.delete(mounted.viewId);
2496
2539
  if (detachViews)
2497
- this.#control?.notify({ type: "detach-session", sessionId: mounted.sessionId, viewId: mounted.viewId });
2540
+ this.#detachView(mounted.sessionId, mounted.viewId);
2498
2541
  this.#views.delete(mounted.viewId);
2499
2542
  this.#focusByView.delete(mounted.viewId);
2500
2543
  }
@@ -2505,7 +2548,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
2505
2548
  if (view.sessionId === sessionId) {
2506
2549
  view.pendingInput.length = 0;
2507
2550
  if (detachViews)
2508
- this.#control?.notify({ type: "detach-session", sessionId, viewId });
2551
+ this.#detachView(sessionId, viewId);
2509
2552
  this.#views.delete(viewId);
2510
2553
  this.#focusByView.delete(viewId);
2511
2554
  }
@@ -2615,6 +2658,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
2615
2658
  this.#recoveredSessions.clear();
2616
2659
  this.#focusByView.clear();
2617
2660
  this.#mountGenerationBySurface.clear();
2661
+ this.#linksByHandle.clear();
2618
2662
  this.#mouseTrackingByHandle.clear();
2619
2663
  this.#scrollbarByHandle.clear();
2620
2664
  this.#subscribedSessionHandles.clear();