mindoodb-app-sdk 0.0.49 → 0.0.53

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/README.md CHANGED
@@ -229,6 +229,35 @@ Important constraints:
229
229
  - Call `session.menus.hide()` if your app needs to explicitly dismiss a pending host menu.
230
230
  - Host-rendered menus are intended for `runtime === "iframe"`. Window-mode apps can usually show local menus instead.
231
231
 
232
+ ### Host focus and notifications
233
+
234
+ A click inside the iframe already asks Haven to focus this launch. Call `requestHostFocus()` when the app should take focus without a click.
235
+
236
+ `hasHostFocus()` is true only when this launch is the surface the user is looking at: the focused tile on the active workspace page, or this launch open as a standalone runner. A background tile, another page, or another app is false. `onHostFocusChange()` fires when that answer changes, so a long task can decide whether to notify without polling.
237
+
238
+ ```ts
239
+ const stop = session.onHostFocusChange((focused) => {
240
+ if (!focused) {
241
+ void session.notify({
242
+ id: "sync",
243
+ severity: "info",
244
+ text: "Sync finished",
245
+ });
246
+ }
247
+ });
248
+
249
+ await session.notify({
250
+ id: "progress",
251
+ severity: "info",
252
+ text: "10%",
253
+ durationMs: 1500,
254
+ });
255
+ ```
256
+
257
+ Show a notice when the app does not have host focus, or when a long task finishes and the user may have looked away. Do not notify while `hasHostFocus()` is true and the user is already looking at the app.
258
+
259
+ Haven uses the app's registration label as the headline and ignores any title from the app. Pass the same `id` to update that notice in place. Omit `id` to add another notice. An id only matches notices from this launch. Omit `durationMs` and the notice stays until the user dismisses it or clicks it. A duration is clamped to about 1–8 seconds, which fits a short progress step such as "10%". Clicking the notice brings that launch back. The close button only dismisses it.
260
+
232
261
  ### Host-owned drag
233
262
 
234
263
  When two apps run as visible workspace chicklets, native HTML5 drag dies at the iframe boundary. The SDK therefore hands the gesture to Haven: the source starts it, Haven draws a PNG ghost above every frame, and the target hit-tests the drop.
@@ -458,7 +487,7 @@ A snapshot also carries `decryptionKeyId?: string` — the named shared key its
458
487
 
459
488
  - `set` -- assign top-level fields (shallow JSON merge).
460
489
  - `unset` -- remove top-level fields entirely.
461
- - `json` -- a **granular JSON patch** of `set` / `unset` / `listInsert` / `listDelete` operations applied at specific paths inside `document.data`, against a `baseHeads` version. See [Granular JSON edits](#granular-json-edits) below.
490
+ - `json` -- a **granular JSON patch** of `set` / `unset` / `listInsert` / `listDelete` operations (plus text splices/marks and `counterIncrement`) applied at specific paths inside `document.data`, against a `baseHeads` version. See [Granular JSON edits](#granular-json-edits) and [Typed values](#typed-values-atomic-strings-counters-timestamps) below.
462
491
  - `text` -- one or more **granular text patches** applied to specific string paths. Each patch carries the `baseHeads` version it was authored against and is merged on the Haven side with any concurrent changes that arrived since. This is the recommended way to edit collaborative text fields like a markdown body. See [Collaborative text editing](#collaborative-text-editing) below.
463
492
  - `richText` -- one or more **rich-text span snapshots** for formatted-document fields (Word/.docx style). See [Collaborative rich-text editing](#collaborative-rich-text-editing) below.
464
493
 
@@ -614,6 +643,109 @@ Haven applies the patch causally at `baseHeads` and merges it with any concurren
614
643
 
615
644
  Patch flavors compose. `set`/`unset` (top-level fields), `json`, `text`, and `richText` can all appear in the same `documents.update()` call and are applied atomically to one new document revision. Prefer small, intentional operations over whole-document rewrites so MindooDB change tracking stays meaningful and Automerge produces cleaner merges.
616
645
 
646
+ ### Typed values: atomic strings, counters, timestamps
647
+
648
+ `document.data` is JSON, and JSON has one kind of string and one kind of number. For collaborative data that is not enough, so the SDK adds three **typed values**. You create them with `MindooDBAppValue` and use them anywhere you write a value: `documents.create({ set })`, `documents.update({ set })`, and `set` / `listInsert` values of a JSON patch, also nested inside objects and arrays.
649
+
650
+ ```ts
651
+ import { MindooDBAppValue } from "mindoodb-app-sdk";
652
+
653
+ const task = await db.documents.create({
654
+ set: {
655
+ title: "Write release notes", // collaborative text
656
+ status: MindooDBAppValue.atomic("open"), // atomic string
657
+ votes: MindooDBAppValue.counter(0), // counter
658
+ dueAt: MindooDBAppValue.timestamp("2026-10-01T12:00:00Z"),
659
+ assignee: { userId: MindooDBAppValue.atomic("u-42"), name: "Ada" },
660
+ },
661
+ });
662
+
663
+ task.data.status; // "open"
664
+ task.data.votes; // 0
665
+ task.data.dueAt; // "2026-10-01T12:00:00.000Z"
666
+ ```
667
+
668
+ | Helper | Use it for | Reads back as |
669
+ |---|---|---|
670
+ | `MindooDBAppValue.atomic(string)` | ids, status and enum values, URLs, hashes, stored text cursors | `string` |
671
+ | `MindooDBAppValue.counter(initial = 0)` | tallies several people change: votes, likes, stock, seats | `number` |
672
+ | `MindooDBAppValue.timestamp(Date \| ms \| ISO string)` | dates and times | ISO 8601 string (UTC, with ms) |
673
+
674
+ Why they matter:
675
+
676
+ - **Plain strings are collaborative text.** Concurrent edits are merged character by character, which is right for titles, notes and bodies. For a status that one user changes from `"open"` to `"closed"` while another changes it to `"blocked"`, text merging can produce a mix of both words. An **atomic** string is replaced as a whole: one of the two values wins. It is also cheaper to store.
677
+ - **Plain numbers are last-writer-wins.** If two users both read `votes: 4` and each save `5`, one vote is lost. A **counter** sums concurrent increments, so the result is `6`.
678
+ - **An ISO string written as a plain string is text**, not a date. A **timestamp** stays a date for everyone who reads the document.
679
+
680
+ The helpers produce plain JSON (`{ "$mindoo": "atomic", "value": "open" }`), so the values pass the bridge unchanged, and Haven converts them into the matching collaborative type. The key `$mindoo` is reserved: an object carrying it must be one of these values, otherwise the write is rejected.
681
+
682
+ Reads return plain JSON, so **a value keeps its type only if you write it again with the helper.** Writing `status: "done"` stores text; write `status: MindooDBAppValue.atomic("done")`. Haven's database browser keeps the type when a user edits such a field in the JSON view.
683
+
684
+ #### Counters
685
+
686
+ Create a counter once, then change it only with `counterIncrement`:
687
+
688
+ ```ts
689
+ interface MindooDBAppJsonCounterIncrementPatch {
690
+ path: Array<string | number>;
691
+ delta: number; // safe integer, negative to decrement
692
+ }
693
+
694
+ await db.documents.update(docId, {
695
+ json: { counterIncrement: [{ path: ["votes"], delta: 1 }] },
696
+ });
697
+ ```
698
+
699
+ - Writing `MindooDBAppValue.counter(n)` or a plain number over an existing counter resets it. That is an assignment and does not merge with concurrent increments, so use it for creation and deliberate resets only.
700
+ - `counterIncrement` on a missing field creates the counter with value `delta`. If two devices do that concurrently for the same field, only one creation survives, so create shared counters up front, ideally in `documents.create`.
701
+ - `counterIncrement` on a plain number is rejected: the field is not a counter.
702
+ - `counterIncrement` runs after the other operations of the same JSON patch, so a patch may create an object with `set` and increment a counter inside it.
703
+ - In Haven's database browser a counter can be edited in the JSON view: the new value is saved as the difference to the loaded value, so increments made by others in the meantime are kept.
704
+
705
+ ### Text cursors (anchors for comments and highlights)
706
+
707
+ An index like "character 120" breaks as soon as another user inserts text before it. A **text cursor** identifies a character instead of a position, so it keeps pointing at the same spot while the text around it changes. Use cursors for comment and review anchors, highlights, bookmarks, "continue reading here" markers or links into a long text.
708
+
709
+ ```ts
710
+ // Anchor a comment to characters 120..134 of the body (end exclusive: 135).
711
+ // The end cursor names the LAST character of the range, see below.
712
+ const doc = await db.documents.get(docId);
713
+ const { cursors } = await db.documents.getTextCursors(docId, ["body"], [120, 134], {
714
+ heads: doc!.heads, // the version the user selected in
715
+ });
716
+ await db.documents.update(docId, {
717
+ json: {
718
+ listInsert: [{
719
+ path: ["comments"],
720
+ index: 0,
721
+ values: [{
722
+ text: "Source?",
723
+ from: MindooDBAppValue.atomic(cursors[0]!),
724
+ to: MindooDBAppValue.atomic(cursors[1]!),
725
+ }],
726
+ }],
727
+ },
728
+ });
729
+
730
+ // Later, after other users edited the body
731
+ const current = (await db.documents.get(docId))!;
732
+ const comment = current.data.comments[0];
733
+ const { positions } = await db.documents.resolveTextCursors(docId, ["body"], [comment.from, comment.to], {
734
+ heads: current.heads, // resolve in the version you are about to render
735
+ });
736
+ const [start, last] = positions;
737
+ const quoted = current.data.body.slice(start, last + 1);
738
+ ```
739
+
740
+ Details:
741
+
742
+ - A cursor names the character at its index. Text inserted exactly at that index lands in front of the character, so the cursor moves right. For a range, anchor the start on its first character and the end on its **last** character, then add 1 after resolving. An end cursor on the character after the range would grow the range whenever someone types at its end.
743
+ - `positions` accepts character indexes (clamped to the text length) and `"start"` / `"end"`. An `"end"` cursor stays at the end when text is appended.
744
+ - A cursor whose character was deleted resolves to where the character used to be. `move: "before"` makes it resolve towards the start of the text instead of the end.
745
+ - `heads` (or `revisionId`) creates or resolves cursors against that document version. Pass the heads your editor rendered from so positions the user selected are interpreted in the text they saw.
746
+ - Cursors only work on collaborative text: plain strings and rich text. Atomic strings, numbers and missing fields are rejected. Store the cursor strings themselves as atomic strings.
747
+ - Both calls need the `read` capability. The mock bridge from `mindoodb-app-sdk/testing` returns cursors that do not follow later edits; test anchoring behavior against Haven.
748
+
617
749
  ### Collaborative text editing
618
750
 
619
751
  MindooDB stores collaborative text fields as Automerge text CRDTs internally, but apps never touch Automerge directly -- the SDK bridge is JSON-only. Instead, your app describes text changes as **granular splice operations** against a known document version, and Haven applies and merges them on its side.
@@ -1060,6 +1192,18 @@ const setup = await db.getFulltextSetup(); // null when never configured
1060
1192
  - `attachments: true` indexes attachment text under the synthetic `_attachments` field using the host's extractors (Haven ships anydoc for Office/PDF/… plus plain-text extractors). Restrict a query to attachment content with `text: { query, fields: ["_attachments"] }`.
1061
1193
  - Changing the configuration (including `language`) rebuilds the index in the background; queries during the rebuild report `coverage: "rebuilding"`.
1062
1194
 
1195
+ **Making nested fields queryable** — `query()` only sees fields in the database's summary buffer. By default that is every top-level scalar or array of scalars up to 1 KB; nested objects and arrays of objects are skipped. Include what your list UIs need so they don't have to `documents.get` each document:
1196
+
1197
+ ```ts
1198
+ // Idempotent, synced via the `dbsetup` document. Requires the `update` capability.
1199
+ const current = await db.getSummarySetup(); // null = default auto-include
1200
+ const include = [...new Set([...(current?.include ?? []), "phases"])];
1201
+ await db.setSummarySetup({ ...current, include });
1202
+ ```
1203
+
1204
+ - `include` paths may be nested (`"meta.author"`) or non-scalar and bypass the size cap; `exclude` wins over both.
1205
+ - A change re-extracts all documents in the background; queries report `coverage: "rebuilding"` until the backfill finishes, so keep a fallback for missing fields.
1206
+
1063
1207
  **Extracting text from bytes you already have** (requires `attachments`):
1064
1208
 
1065
1209
  ```ts
@@ -1535,6 +1679,40 @@ wrangler deploy
1535
1679
 
1536
1680
  Other static hosting options work just as well: Netlify, Vercel, any web server serving your `dist/` folder.
1537
1681
 
1682
+ ### Sharing an app by its URL
1683
+
1684
+ Anyone who opens your app's address directly — not from inside Haven — has no host to talk to. Instead of letting the bridge handshake time out into an error, branch on `isLaunchedByHaven()` before mounting and show a landing page:
1685
+
1686
+ ```ts
1687
+ import { isLaunchedByHaven, renderHavenAppLandingPage } from "mindoodb-app-sdk";
1688
+
1689
+ if (isLaunchedByHaven()) {
1690
+ createApp(App).mount("#app");
1691
+ } else {
1692
+ void renderHavenAppLandingPage();
1693
+ }
1694
+ ```
1695
+
1696
+ Haven puts `mindoodbAppLaunchId` into every launch URL, so the check is synchronous. `renderHavenAppLandingPage()` reads `haven-app.json` from the same origin and shows the app's name, summary, description and screenshots, plus a button to `https://haven.mindoodb.com/?app=<your app URL>` (pass `havenUrl` for another Haven instance). Haven then walks a new user through setup with your app installed at the end, or offers an existing user to add it.
1697
+
1698
+ What the page shows comes from the optional `listing` in `haven-app.json`. Haven's setup wizard shows the same fields:
1699
+
1700
+ ```json
1701
+ {
1702
+ "listing": {
1703
+ "summary": { "en": "Plan trips together.", "de": "Reisen gemeinsam planen." },
1704
+ "description": { "en": "First paragraph.\n\nSecond paragraph." },
1705
+ "icon": "icon.svg",
1706
+ "screenshots": [{ "file": "screenshots/board.webp", "caption": { "en": "The trip board" } }],
1707
+ "publisher": { "name": "Mindoo GmbH", "url": "https://mindoo.de" }
1708
+ }
1709
+ }
1710
+ ```
1711
+
1712
+ Texts are a string or a per-locale map with an `en` fallback. Image paths are relative to the app origin or absolute `https:` URLs. The listing is display-only and grants nothing.
1713
+
1714
+ For automated and manual tests use a separate test URL that frames the app with a mock host instead — see "Browser test host" in [`TESTING.md`](./TESTING.md). End users never see that page, and they never see mock data.
1715
+
1538
1716
  ### Haven-hosted bundles
1539
1717
 
1540
1718
  Instead of Haven loading your app from your server on every launch, you can hand Haven the build itself. Haven stores it in its own cache and serves it from a service worker on an opaque origin, so the app loads offline and runs in a stricter sandbox than an external URL gets.
@@ -1636,6 +1814,10 @@ Connect options: `launchId?`, `targetOrigin?`, `connectTimeoutMs?`.
1636
1814
  | `closeSealedChannel(channelId)` | `Promise<void>` |
1637
1815
  | `menus.show(input)` | `Promise<MindooDBAppShowMenuResult>` |
1638
1816
  | `menus.hide()` | `Promise<void>` |
1817
+ | `requestHostFocus()` | `Promise<void>` |
1818
+ | `hasHostFocus()` | `Promise<boolean>` |
1819
+ | `onHostFocusChange(listener)` | `() => void` (unsubscribe) |
1820
+ | `notify(input)` | `Promise<{ id: string }>` |
1639
1821
  | `drag.setProfile(profile)` | `Promise<void>` |
1640
1822
  | `drag.start(input)` | `Promise<MindooDBAppDragStartResult>` |
1641
1823
  | `drag.cancel()` | `Promise<void>` |
@@ -1670,6 +1852,8 @@ Host-kept key/value state, the only durable storage a hosted app has. See [Stora
1670
1852
  | `attachments` | `MindooDBAppAttachmentApi` |
1671
1853
  | `getFulltextSetup()` | `Promise<MindooDBAppFulltextSetup \| null>` |
1672
1854
  | `setFulltextSetup(config)` | `Promise<void>` |
1855
+ | `getSummarySetup()` | `Promise<MindooDBAppSummarySetup \| null>` |
1856
+ | `setSummarySetup(config)` | `Promise<void>` |
1673
1857
 
1674
1858
  ### MindooDBAppDocumentApi
1675
1859
 
package/TESTING.md CHANGED
@@ -107,6 +107,7 @@ Each database entry can provide:
107
107
  - `methods.views` for session-level `createView()` and `openView()` calls (overrides the default evaluating navigator)
108
108
  - `methods.attachments`
109
109
  - `fulltextSetup` — the initial config returned by `db.getFulltextSetup()`; `db.setFulltextSetup()` overwrites it for the lifetime of the handle. Use this to test your app's full-text bootstrap logic. Note the mock evaluates `text` query clauses regardless of this config.
110
+ - `summarySetup` — the initial config returned by `db.getSummarySetup()`; `db.setSummarySetup()` overwrites it for the lifetime of the handle. The mock's `query()` reads seeded documents directly and ignores this config; use it to test your app's summary bootstrap logic.
110
111
 
111
112
  ### Evaluating VirtualViews
112
113
 
@@ -218,6 +219,30 @@ expect(result.rows[0].includes?.customer?.fields.name).toBe("Acme");
218
219
  An include slot naming a database that was not seeded fails, the same way Haven
219
220
  rejects a database the app is not mapped to.
220
221
 
222
+ ### Host focus and notifications
223
+
224
+ A mock launch starts without host focus. `session.requestHostFocus()` sets it
225
+ and fires `onHostFocusChange` listeners; `mock.emitHostFocusChange(focused)`
226
+ simulates Haven moving focus to or away from the launch, so you can test code
227
+ that only notifies while the user is looking elsewhere. `session.notify()` shows
228
+ nothing and resolves with the passed `id`, or a fresh one when `id` is omitted:
229
+
230
+ ```ts
231
+ const mock = createMockMindooDBAppSession();
232
+
233
+ const seen: boolean[] = [];
234
+ const stop = mock.session.onHostFocusChange((focused) => seen.push(focused));
235
+
236
+ await mock.session.requestHostFocus();
237
+ mock.emitHostFocusChange(false);
238
+ expect(await mock.session.hasHostFocus()).toBe(false);
239
+ expect(seen).toEqual([true, false]);
240
+ stop();
241
+
242
+ const { id } = await mock.session.notify({ id: "sync", severity: "info", text: "Sync finished" });
243
+ expect(id).toBe("sync");
244
+ ```
245
+
221
246
  ## Level 2 example
222
247
 
223
248
  This pattern keeps the real `createMindooDBAppBridge()` code path and replaces only the host side.
@@ -287,12 +312,45 @@ Useful Level 2 methods:
287
312
  - `emitViewportChange()`
288
313
  - `emitQueryResult()` — push a `query-result` message to live query subscribers
289
314
  - `emitViewChanged()` — push a `view-changed` message to navigator `onDidUpdate` listeners
315
+ - `emitHostFocusChange()` — set host focus and push a `host-focus-changed` message to `session.onHostFocusChange()` listeners
290
316
  - `setRequestHandler()`
291
317
  - `clearRequestHandler()`
292
318
  - `postPortMessage()`
293
319
 
294
320
  The built-in request handling also covers `documents.query` and the `documents.liveQuery.*` RPCs against the seeded documents, so `db.documents.query()` / `db.documents.liveQuery()` and `navigator.onDidUpdate()` work end-to-end over the real bridge transport in Level 2 tests.
295
321
 
322
+ ## Browser test host
323
+
324
+ Level 1 and 2 run in Node/jsdom. To run the real app in a real browser — for Playwright, or to click through it yourself — mount the browser test host on a separate page. It frames the app exactly as Haven does (an iframe with `?mindoodbAppLaunchId=…`) and answers the bridge with the same mock state as the Vitest helpers:
325
+
326
+ ```ts
327
+ // src/testHost/main.ts, loaded by /__haven-test/index.html
328
+ import { mockDatabasesFromDefinition, mountHavenTestHost } from "mindoodb-app-sdk/testing";
329
+ import definition from "../../public/haven-app.json";
330
+
331
+ mountHavenTestHost({
332
+ appUrl: "/",
333
+ title: definition.label,
334
+ databases: mockDatabasesFromDefinition(definition, {
335
+ main: [{ id: "welcome", data: { type: "note", title: "Seeded note" } }],
336
+ }),
337
+ });
338
+ ```
339
+
340
+ The page shows the app next to a control panel: theme and host-focus toggles, a file picker that decides what the app's next `attachments.scan()` returns (none means the user cancelled), and a live log of notifications, attachment previews, scans and every RPC request. The same controls are scriptable as `window.__havenTestHost`:
341
+
342
+ ```ts
343
+ await page.goto("/__haven-test/");
344
+ const app = page.frameLocator('[data-testid="haven-test-app-frame"]');
345
+ await page.evaluate(() => window.__havenTestHost!.setNextScan({ fileName: "receipt.pdf", mimeType: "application/pdf", size: 1024 }));
346
+ await app.getByRole("button", { name: "Scan" }).click();
347
+ expect(await page.evaluate(() => window.__havenTestHost!.log.scans)).toHaveLength(1);
348
+ ```
349
+
350
+ Lower-level pieces, if you build your own page: `createBrowserTestHost({ frame, appUrl, ...mockOptions })` wires one iframe to a fake host, and `host.acceptConnection(message, ports)` answers a handshake you received yourself.
351
+
352
+ Keep the test page out of production builds. The starter template builds it only for `vite dev` and when `HAVEN_TEST_HOST=1` is set (for preview deployments), so the app's public URL keeps showing its landing page.
353
+
296
354
  ## When to use which level
297
355
 
298
356
  Choose Level 1 when:
@@ -1,3 +1,45 @@
1
+ // src/values.ts
2
+ var MINDOODB_APP_VALUE_TAG = "$mindoo";
3
+ var MindooDBAppValue = {
4
+ /**
5
+ * An atomic string for identifiers, status and enum values, URLs, hashes
6
+ * and stored text cursors. Plain strings are collaborative text, where two
7
+ * concurrent changes of `"open"` to `"closed"` and `"blocked"` can
8
+ * interleave into a mix of both words.
9
+ */
10
+ atomic(value) {
11
+ if (typeof value !== "string") {
12
+ throw new Error("MindooDBAppValue.atomic value must be a string");
13
+ }
14
+ return { $mindoo: "atomic", value };
15
+ },
16
+ /**
17
+ * A collaborative counter with an initial value (a safe integer). Create it
18
+ * once, then change it only with `json.counterIncrement`: writing a new
19
+ * counter is an assignment that does not merge with concurrent increments.
20
+ */
21
+ counter(value = 0) {
22
+ if (!Number.isSafeInteger(value)) {
23
+ throw new Error("MindooDBAppValue.counter value must be a safe integer");
24
+ }
25
+ return { $mindoo: "counter", value };
26
+ },
27
+ /** A timestamp from a `Date`, epoch milliseconds or an ISO 8601 string. */
28
+ timestamp(value) {
29
+ const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
30
+ if (typeof value === "string" && value.trim() === "" || Number.isNaN(date.getTime())) {
31
+ throw new Error(
32
+ "MindooDBAppValue.timestamp value must be a Date, epoch milliseconds or an ISO 8601 date-time string"
33
+ );
34
+ }
35
+ return { $mindoo: "timestamp", value: date.toISOString() };
36
+ },
37
+ /** Whether `value` is a tagged typed value (any kind). */
38
+ isTyped(value) {
39
+ return value !== null && typeof value === "object" && !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, MINDOODB_APP_VALUE_TAG);
40
+ }
41
+ };
42
+
1
43
  // src/drag.ts
2
44
  var MINDOODB_APP_MAX_DRAG_TYPES = 8;
3
45
  var MINDOODB_APP_MAX_DRAG_PAYLOAD_BYTES = 32 * 1024;
@@ -392,11 +434,13 @@ function bindDragSource(element, options, start, follow) {
392
434
  // src/hostShortcuts.ts
393
435
  var DEFAULT_HAVEN_HOST_SHORTCUTS = [
394
436
  { action: "open-workspace", code: "Enter", shiftKey: true, metaOrCtrl: true },
395
- { action: "toggle-app-drawer", code: "Space", shiftKey: true, metaOrCtrl: true }
437
+ { action: "toggle-app-drawer", code: "Space", shiftKey: true, metaOrCtrl: true },
438
+ { action: "toggle-workspace-maximize", code: "KeyM", shiftKey: true, metaOrCtrl: true }
396
439
  ];
397
440
  var HOST_SHORTCUT_ACTIONS = /* @__PURE__ */ new Set([
398
441
  "open-workspace",
399
- "toggle-app-drawer"
442
+ "toggle-app-drawer",
443
+ "toggle-workspace-maximize"
400
444
  ]);
401
445
  function normalizeShortcutCode(code) {
402
446
  return code === "NumpadEnter" ? "Enter" : code;
@@ -450,7 +494,7 @@ function applyHostedDocumentOverscrollContain(doc = globalThis.document) {
450
494
  if (!doc?.documentElement) {
451
495
  return;
452
496
  }
453
- doc.documentElement.style.overscrollBehavior = "contain";
497
+ doc.documentElement.style.overscrollBehavior = "none";
454
498
  if (doc.body) {
455
499
  doc.body.style.overscrollBehavior = "contain";
456
500
  }
@@ -627,6 +671,21 @@ var PortRpcClient = class {
627
671
  }
628
672
  };
629
673
 
674
+ // src/hostFocus.ts
675
+ function installHostFocusCapture(options) {
676
+ const target = options.target ?? (typeof document === "undefined" ? void 0 : document);
677
+ if (!target) {
678
+ return () => void 0;
679
+ }
680
+ const onPointerDown = () => {
681
+ options.onPointerDown();
682
+ };
683
+ target.addEventListener("pointerdown", onPointerDown, true);
684
+ return () => {
685
+ target.removeEventListener("pointerdown", onPointerDown, true);
686
+ };
687
+ }
688
+
630
689
  // src/client/createMindooDBAppBridge.ts
631
690
  var PROTOCOL2 = "mindoodb-app-bridge";
632
691
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
@@ -1226,6 +1285,23 @@ var MindooDBAppDatabaseImpl = class {
1226
1285
  path,
1227
1286
  revisionId: options?.revisionId
1228
1287
  }),
1288
+ getTextCursors: async (docId, path, positions, options) => await this.rpc.call("documents.textCursors.get", {
1289
+ databaseId: this.databaseId,
1290
+ docId,
1291
+ path,
1292
+ positions,
1293
+ heads: options?.heads,
1294
+ move: options?.move,
1295
+ revisionId: options?.revisionId
1296
+ }),
1297
+ resolveTextCursors: async (docId, path, cursors, options) => await this.rpc.call("documents.textCursors.resolve", {
1298
+ databaseId: this.databaseId,
1299
+ docId,
1300
+ path,
1301
+ cursors,
1302
+ heads: options?.heads,
1303
+ revisionId: options?.revisionId
1304
+ }),
1229
1305
  getAutomergeSnapshot: async (docId, options) => await this.rpc.call("documents.automerge.getSnapshot", {
1230
1306
  databaseId: this.databaseId,
1231
1307
  docId,
@@ -1236,6 +1312,26 @@ var MindooDBAppDatabaseImpl = class {
1236
1312
  docId,
1237
1313
  patch
1238
1314
  }),
1315
+ applyAutomergeChangesBatch: async (items) => {
1316
+ if (items.length === 0) return [];
1317
+ try {
1318
+ return await this.rpc.call("documents.automerge.applyChangesBatch", {
1319
+ databaseId: this.databaseId,
1320
+ items
1321
+ });
1322
+ } catch (error) {
1323
+ if (!isMethodNotFoundError(error)) throw error;
1324
+ const results = [];
1325
+ for (const item of items) {
1326
+ results.push(await this.rpc.call("documents.automerge.applyChanges", {
1327
+ databaseId: this.databaseId,
1328
+ docId: item.docId,
1329
+ patch: item.patch
1330
+ }));
1331
+ }
1332
+ return results;
1333
+ }
1334
+ },
1239
1335
  create: async (input) => await this.rpc.call("documents.create", {
1240
1336
  databaseId: this.databaseId,
1241
1337
  input
@@ -1576,6 +1672,21 @@ var MindooDBAppDatabaseImpl = class {
1576
1672
  config
1577
1673
  });
1578
1674
  }
1675
+ /** Reads the summary buffer configuration from the synced `dbsetup` document. */
1676
+ async getSummarySetup() {
1677
+ const result = await this.rpc.call(
1678
+ "database.getSummarySetup",
1679
+ { databaseId: this.databaseId }
1680
+ );
1681
+ return result.config;
1682
+ }
1683
+ /** Writes (or removes, with `null`) the summary buffer configuration. */
1684
+ async setSummarySetup(config) {
1685
+ await this.rpc.call("database.setSummarySetup", {
1686
+ databaseId: this.databaseId,
1687
+ config
1688
+ });
1689
+ }
1579
1690
  };
1580
1691
  var MindooDBAppDragApiImpl = class {
1581
1692
  constructor(rpc) {
@@ -1718,6 +1829,18 @@ var MindooDBAppSessionImpl = class {
1718
1829
  }
1719
1830
  }
1720
1831
  }) : () => void 0;
1832
+ this.stopHostFocus = installHostFocusCapture({
1833
+ onPointerDown: () => {
1834
+ try {
1835
+ this.rpc.postMessage({
1836
+ protocol: PROTOCOL2,
1837
+ kind: "workspace-focus-requested"
1838
+ });
1839
+ } catch (error) {
1840
+ console.warn("[mindoodb-app-sdk] Could not request host focus.", error);
1841
+ }
1842
+ }
1843
+ });
1721
1844
  }
1722
1845
  rpc;
1723
1846
  onDisconnect;
@@ -1727,6 +1850,7 @@ var MindooDBAppSessionImpl = class {
1727
1850
  beforeCloseListeners = /* @__PURE__ */ new Set();
1728
1851
  hostShortcuts = DEFAULT_HAVEN_HOST_SHORTCUTS;
1729
1852
  stopHostShortcuts;
1853
+ stopHostFocus;
1730
1854
  /**
1731
1855
  * Lets every listener finish, then tells the host it may proceed.
1732
1856
  *
@@ -1862,6 +1986,27 @@ var MindooDBAppSessionImpl = class {
1862
1986
  }
1863
1987
  });
1864
1988
  }
1989
+ /** Ask Haven to treat this launch as the focused surface. */
1990
+ async requestHostFocus() {
1991
+ await this.rpc.call("session.requestHostFocus", {});
1992
+ }
1993
+ /** Whether this launch is the surface the user is looking at. */
1994
+ async hasHostFocus() {
1995
+ const result = await this.rpc.call("session.hasHostFocus", {});
1996
+ return result?.focused === true;
1997
+ }
1998
+ /** Fires when the host-focus answer for this launch changes. */
1999
+ onHostFocusChange(listener) {
2000
+ return this.rpc.addMessageListener((message) => {
2001
+ if (message.kind === "host-focus-changed") {
2002
+ listener(message.focused);
2003
+ }
2004
+ });
2005
+ }
2006
+ /** Ask Haven to show or update a notice for this launch. */
2007
+ async notify(input) {
2008
+ return await this.rpc.call("notifications.show", input);
2009
+ }
1865
2010
  /** Register work to run before the host tears this launch down. */
1866
2011
  onBeforeClose(listener) {
1867
2012
  this.beforeCloseListeners.add(listener);
@@ -1872,6 +2017,7 @@ var MindooDBAppSessionImpl = class {
1872
2017
  /** Disconnects from the host and always disposes the underlying port client. */
1873
2018
  async disconnect() {
1874
2019
  this.stopHostShortcuts();
2020
+ this.stopHostFocus();
1875
2021
  this.drag.dispose();
1876
2022
  try {
1877
2023
  await this.rpc.call("session.disconnect", {});
@@ -1925,6 +2071,8 @@ function createMindooDBAppBridge() {
1925
2071
  }
1926
2072
 
1927
2073
  export {
2074
+ MINDOODB_APP_VALUE_TAG,
2075
+ MindooDBAppValue,
1928
2076
  MINDOODB_APP_MAX_DRAG_TYPES,
1929
2077
  MINDOODB_APP_MAX_DRAG_PAYLOAD_BYTES,
1930
2078
  MINDOODB_APP_MAX_DRAG_PREVIEW_BYTES,