@hyperdrive.bot/paseo-server 0.2.8 → 0.3.1

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 (79) hide show
  1. package/dist/server/server/file-explorer/service.js +9 -0
  2. package/dist/server/server/search/extract.d.ts +53 -0
  3. package/dist/server/server/search/extract.js +222 -0
  4. package/dist/server/server/search/index-store.d.ts +83 -0
  5. package/dist/server/server/search/index-store.js +241 -0
  6. package/dist/server/server/search/indexer.d.ts +69 -0
  7. package/dist/server/server/search/indexer.js +362 -0
  8. package/dist/server/server/search/resolve-cwd.d.ts +40 -0
  9. package/dist/server/server/search/resolve-cwd.js +109 -0
  10. package/dist/server/server/search/session-search-service.d.ts +59 -0
  11. package/dist/server/server/search/session-search-service.js +171 -0
  12. package/dist/server/server/session.js +10 -7
  13. package/dist/server/server/workspace/recall-search-feature.js +25 -16
  14. package/dist/server/server/workspace/sessions-search-route.d.ts +18 -46
  15. package/dist/server/server/workspace/sessions-search-route.js +27 -163
  16. package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js → desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js} +5 -5
  17. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.br +0 -0
  18. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.gz +0 -0
  19. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.map.br +0 -0
  20. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.map.gz +0 -0
  21. package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js → desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js} +3 -3
  22. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.br +0 -0
  23. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.gz +0 -0
  24. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.map.br +0 -0
  25. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.map.gz +0 -0
  26. package/dist/server/web-ui/_expo/static/js/web/{index-e2ffd4d4419cad7f5153525e95f645b8.js → index-fad30dd59837afbdf33b7d9190c754e9.js} +246 -245
  27. package/dist/server/web-ui/_expo/static/js/web/index-fad30dd59837afbdf33b7d9190c754e9.js.br +0 -0
  28. package/dist/server/web-ui/_expo/static/js/web/index-fad30dd59837afbdf33b7d9190c754e9.js.gz +0 -0
  29. package/dist/server/web-ui/_expo/static/js/web/index-fad30dd59837afbdf33b7d9190c754e9.js.map.br +0 -0
  30. package/dist/server/web-ui/_expo/static/js/web/{index-e2ffd4d4419cad7f5153525e95f645b8.js.map.gz → index-fad30dd59837afbdf33b7d9190c754e9.js.map.gz} +0 -0
  31. package/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js → indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js} +3 -3
  32. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js.br +0 -0
  33. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js.gz +0 -0
  34. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js.map.br +0 -0
  35. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js.map.gz +0 -0
  36. package/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js → native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js} +5 -5
  37. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js.br +0 -0
  38. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js.gz +0 -0
  39. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js.map.br +0 -0
  40. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js.map.gz +0 -0
  41. package/dist/server/web-ui/_expo/static/js/web/{profiler-8ca15c1659e234673816bc7bad0f88b6.js → profiler-24af32fdd64b094ad2dc78cf1625bd07.js} +11 -11
  42. package/dist/server/web-ui/_expo/static/js/web/profiler-24af32fdd64b094ad2dc78cf1625bd07.js.br +0 -0
  43. package/dist/server/web-ui/_expo/static/js/web/profiler-24af32fdd64b094ad2dc78cf1625bd07.js.gz +0 -0
  44. package/dist/server/web-ui/_expo/static/js/web/profiler-24af32fdd64b094ad2dc78cf1625bd07.js.map.br +0 -0
  45. package/dist/server/web-ui/_expo/static/js/web/{profiler-8ca15c1659e234673816bc7bad0f88b6.js.map.gz → profiler-24af32fdd64b094ad2dc78cf1625bd07.js.map.gz} +0 -0
  46. package/dist/server/web-ui/_expo/static/js/web/{startRecording-1a240d0212111d3eb294a9407e136e6a.js → startRecording-d113d3bf7d809773ff5c95198366c905.js} +60 -60
  47. package/dist/server/web-ui/_expo/static/js/web/startRecording-d113d3bf7d809773ff5c95198366c905.js.br +0 -0
  48. package/dist/server/web-ui/_expo/static/js/web/startRecording-d113d3bf7d809773ff5c95198366c905.js.gz +0 -0
  49. package/dist/server/web-ui/_expo/static/js/web/startRecording-d113d3bf7d809773ff5c95198366c905.js.map.br +0 -0
  50. package/dist/server/web-ui/_expo/static/js/web/{startRecording-1a240d0212111d3eb294a9407e136e6a.js.map.gz → startRecording-d113d3bf7d809773ff5c95198366c905.js.map.gz} +0 -0
  51. package/dist/server/web-ui/index.html +1 -1
  52. package/dist/server/web-ui/index.html.br +0 -0
  53. package/dist/server/web-ui/index.html.gz +0 -0
  54. package/package.json +2 -2
  55. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.br +0 -0
  56. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.gz +0 -0
  57. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.map.br +0 -0
  58. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.map.gz +0 -0
  59. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.br +0 -0
  60. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.gz +0 -0
  61. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.map.br +0 -0
  62. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.map.gz +0 -0
  63. package/dist/server/web-ui/_expo/static/js/web/index-e2ffd4d4419cad7f5153525e95f645b8.js.br +0 -0
  64. package/dist/server/web-ui/_expo/static/js/web/index-e2ffd4d4419cad7f5153525e95f645b8.js.gz +0 -0
  65. package/dist/server/web-ui/_expo/static/js/web/index-e2ffd4d4419cad7f5153525e95f645b8.js.map.br +0 -0
  66. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js.br +0 -0
  67. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js.gz +0 -0
  68. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js.map.br +0 -0
  69. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js.map.gz +0 -0
  70. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js.br +0 -0
  71. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js.gz +0 -0
  72. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js.map.br +0 -0
  73. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js.map.gz +0 -0
  74. package/dist/server/web-ui/_expo/static/js/web/profiler-8ca15c1659e234673816bc7bad0f88b6.js.br +0 -0
  75. package/dist/server/web-ui/_expo/static/js/web/profiler-8ca15c1659e234673816bc7bad0f88b6.js.gz +0 -0
  76. package/dist/server/web-ui/_expo/static/js/web/profiler-8ca15c1659e234673816bc7bad0f88b6.js.map.br +0 -0
  77. package/dist/server/web-ui/_expo/static/js/web/startRecording-1a240d0212111d3eb294a9407e136e6a.js.br +0 -0
  78. package/dist/server/web-ui/_expo/static/js/web/startRecording-1a240d0212111d3eb294a9407e136e6a.js.gz +0 -0
  79. package/dist/server/web-ui/_expo/static/js/web/startRecording-1a240d0212111d3eb294a9407e136e6a.js.map.br +0 -0
@@ -0,0 +1,171 @@
1
+ // MARRA CUSTOMIZATION — the daemon-side session search service.
2
+ //
3
+ // Owns the FTS5 index and the background indexing loop, and exposes a single
4
+ // `search()` the three transports share (HTTP `GET /sessions/search`, the WS
5
+ // `search_sessions_request` handler, and `watch.sessionSearch`).
6
+ //
7
+ // WHY THIS EXISTS — the recall CLI it replaces failed structurally:
8
+ // • a subprocess spawn per keystroke,
9
+ // • a full Tantivy re-index inside every spawn,
10
+ // • a global writer lock serialising those spawns against each other.
11
+ // On a loaded host (39-agent fleet) every cmd+K keystroke hit the subprocess
12
+ // timeout, so search was disabled by default in 73ae98f3.
13
+ //
14
+ // Here indexing and querying are decoupled: writes happen only on a timer in
15
+ // the background, queries are pure reads against a warm SQLite file. Measured
16
+ // on the real 4.6 GB / 8,117-file corpus:
17
+ // • steady-state pass: 8,117 files stat'd, ~0 bytes read, 0.9 s
18
+ // • query p95: 0.5 ms – 32.7 ms (vs recall's 30 s timeout)
19
+ //
20
+ // The indexing loop is intentionally NOT a worker_thread. Measured cost of a
21
+ // steady-state pass is ~0.9 s of mostly-async stat() calls; the parse work
22
+ // that would justify a thread only happens on first build or after a large
23
+ // append. Spawning a thread would add ~30 MB RSS and a serialisation boundary
24
+ // around the SQLite handle to save single-digit milliseconds of event-loop
25
+ // time per minute. If a future corpus makes passes expensive, `runIndexPass`
26
+ // is already a pure function of (store, adapters) and can move to a worker
27
+ // with no change to callers.
28
+ import { SessionIndexStore } from "./index-store.js";
29
+ import { defaultAdapters, defaultIndexPath, runIndexPass } from "./indexer.js";
30
+ import { createCwdResolver } from "./resolve-cwd.js";
31
+ /**
32
+ * Under a test runner, never auto-start indexing.
33
+ *
34
+ * Every current call site injects a fake, so the real service is not
35
+ * constructed in the suite today. But a future test that calls
36
+ * `runSessionsSearch` without a dep would otherwise construct the singleton
37
+ * and immediately begin walking the DEVELOPER'S real ~/.claude/projects —
38
+ * gigabytes of disk I/O in the middle of a unit test, competing with the
39
+ * timing-sensitive pty tests that share the pool. Cheap guard, removes a whole
40
+ * class of "why is the suite flaky on my machine" before it can start.
41
+ */
42
+ function isTestRunner() {
43
+ return process.env.VITEST === "true" || process.env.NODE_ENV === "test";
44
+ }
45
+ const DEFAULT_INTERVAL_MS = 60000;
46
+ export class SessionSearchService {
47
+ constructor(options = {}) {
48
+ this.timer = null;
49
+ this.abort = new AbortController();
50
+ this.passInFlight = null;
51
+ /** False until the first pass finishes, so callers can be honest about warm-up. */
52
+ this.warm = false;
53
+ this.store = new SessionIndexStore(options.indexPath ?? defaultIndexPath());
54
+ this.adapters = options.adapters ?? defaultAdapters();
55
+ this.logger = options.logger;
56
+ this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
57
+ if (!options.manual && !isTestRunner())
58
+ this.start();
59
+ }
60
+ start() {
61
+ // Kick a pass immediately so a fresh daemon warms without waiting a full
62
+ // interval, then settle into the timer.
63
+ void this.runPass();
64
+ this.timer = setInterval(() => void this.runPass(), this.intervalMs);
65
+ // Never hold the process open for an index refresh.
66
+ this.timer.unref?.();
67
+ }
68
+ /** One indexing pass. Overlapping calls join the in-flight pass. */
69
+ async runPass() {
70
+ if (this.passInFlight) {
71
+ await this.passInFlight;
72
+ return;
73
+ }
74
+ const pending = runIndexPass(this.store, {
75
+ adapters: this.adapters,
76
+ ...(this.logger ? { logger: this.logger } : {}),
77
+ signal: this.abort.signal,
78
+ })
79
+ .then(() => {
80
+ this.warm = true;
81
+ return undefined;
82
+ })
83
+ .catch((err) => {
84
+ // A failed pass must never take the daemon down or break querying —
85
+ // the previous index stays queryable and the next pass retries.
86
+ this.logger?.warn({ err }, "session-search: index pass failed");
87
+ })
88
+ .finally(() => {
89
+ this.passInFlight = null;
90
+ });
91
+ this.passInFlight = pending;
92
+ await pending;
93
+ }
94
+ /** True once at least one indexing pass has completed. */
95
+ isWarm() {
96
+ return this.warm;
97
+ }
98
+ stats() {
99
+ return this.store.stats();
100
+ }
101
+ /**
102
+ * Query the index and return hits in the transport's existing shape.
103
+ *
104
+ * Every hit's cwd is resolved to a directory that exists on THIS machine
105
+ * before it is returned, because the palette resumes a session into its cwd
106
+ * — a hit pointing at another machine's path is a result you cannot click.
107
+ */
108
+ async search(params) {
109
+ const query = (params.query ?? "").trim();
110
+ const sources = params.sources
111
+ ? params.sources
112
+ .split(",")
113
+ .map((s) => s.trim())
114
+ .filter((s) => s.length > 0)
115
+ : undefined;
116
+ const hits = this.store.search({
117
+ query,
118
+ ...(params.limit !== undefined ? { limit: params.limit } : {}),
119
+ ...(sources ? { sources } : {}),
120
+ });
121
+ const resolveCwd = createCwdResolver();
122
+ const results = [];
123
+ for (const hit of hits) {
124
+ const resolved = await resolveCwd(hit.cwd);
125
+ results.push({
126
+ sid: hit.sid,
127
+ source: hit.source,
128
+ cwd: resolved.cwd,
129
+ timestamp: hit.timestamp,
130
+ snippet: hit.snippet,
131
+ resumeCommand: buildResumeCommand(hit.source, hit.sid),
132
+ });
133
+ }
134
+ return { query, results };
135
+ }
136
+ close() {
137
+ this.abort.abort();
138
+ if (this.timer)
139
+ clearInterval(this.timer);
140
+ this.timer = null;
141
+ this.store.close();
142
+ }
143
+ }
144
+ function buildResumeCommand(source, sid) {
145
+ switch (source.toLowerCase()) {
146
+ case "claude":
147
+ return `claude --resume ${sid}`;
148
+ case "codex":
149
+ return `codex resume ${sid}`;
150
+ default:
151
+ return "";
152
+ }
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // Process-wide singleton
156
+ // ---------------------------------------------------------------------------
157
+ //
158
+ // One index per daemon. The three transports resolve the same instance, so
159
+ // there is exactly one writer and no lock contention — the failure that made
160
+ // the recall implementation return "LockBusy" and look broken.
161
+ let singleton = null;
162
+ export function getSessionSearchService(options = {}) {
163
+ singleton ?? (singleton = new SessionSearchService(options));
164
+ return singleton;
165
+ }
166
+ /** Test seam: drop the singleton so a fixture instance can take its place. */
167
+ export function resetSessionSearchServiceForTests() {
168
+ singleton?.close();
169
+ singleton = null;
170
+ }
171
+ //# sourceMappingURL=session-search-service.js.map
@@ -3397,15 +3397,18 @@ export class Session {
3397
3397
  }
3398
3398
  }
3399
3399
  // COMPAT(sessionsListRpc): WS equivalent of HTTP `GET /sessions/search`.
3400
- // Spawns the recall CLI via the shared `runSessionsSearch` core (default
3401
- // `recall` on PATH, 8s timeout — same as the HTTP route). Errors are
3402
- // returned in the payload; the client turns a payload error into a thrown
3403
- // rejection so its useQuery falls back to an empty list.
3400
+ // Queries the SQLite FTS5 session index via the shared `runSessionsSearch`
3401
+ // core (same core as the HTTP route). This is the path the command palette
3402
+ // actually uses the relay forbids HTTP search so it is the one that has
3403
+ // to stay fast and never throw. Errors are returned in the payload; the
3404
+ // client turns a payload error into a thrown rejection so its useQuery falls
3405
+ // back to an empty list.
3404
3406
  async handleSearchSessionsRequest(msg) {
3405
3407
  const { requestId, query, limit, sources } = msg;
3406
- // COMPAT(recallSearch): recall disabled → never spawn the CLI (defends against a
3407
- // stale client that saw the capability before this daemon disabled it). Return an
3408
- // empty result set so the palette shows its default listAllSessions browse.
3408
+ // Kill switch (PASEO_DISABLE_SESSION_SEARCH=1) → never touch the index.
3409
+ // Also defends against a stale client that saw the capability before this
3410
+ // daemon disabled it. Returns an empty result set so the palette falls back
3411
+ // to its default listAllSessions browse.
3409
3412
  if (!recallSearchEnabled()) {
3410
3413
  this.emit({
3411
3414
  type: "search_sessions_response",
@@ -1,22 +1,31 @@
1
- // COMPAT(recallSearch): added in v0.1.X, remove gate after 2026-12-03.
1
+ // Single source of truth for the session-search capability.
2
2
  //
3
- // Single source of truth for the `recallSearch` capability. The `server_info`
4
- // handshake flag (`buildServerInfoStatusPayload()` in websocket-server.ts) AND the
5
- // two daemon-side entry points that spawn the `recall` CLI (the
6
- // `search_sessions_request` and `watch.sessionSearch` handlers in session.ts) all
7
- // read this, so the advertised capability and the code that actually runs `recall`
8
- // can never drift.
3
+ // The `server_info` handshake flag (`buildServerInfoStatusPayload()` in
4
+ // websocket-server.ts) AND the daemon-side entry points that serve search (the
5
+ // `search_sessions_request` and `watch.sessionSearch` handlers in session.ts)
6
+ // all read this, so the advertised capability and the code that actually
7
+ // answers a query can never drift.
9
8
  //
10
- // DISABLED BY DEFAULT (2026-07-28): `recall` re-indexes its Tantivy store on every
11
- // invocation and, under a loaded host, blows past its subprocess timeout failing
12
- // cmd+K search and piling CPU load onto an already-busy daemon. With recall off, the
13
- // command palette falls back to Paseo's default title/metadata session browse
14
- // (`listAllSessions`) and the watch session-search serves recent sessions.
9
+ // HISTORY — this was DISABLED BY DEFAULT on 2026-07-28 (73ae98f3) because the
10
+ // engine was the `recall` CLI: it re-indexed its Tantivy store on every
11
+ // invocation and, under a loaded host, blew past its subprocess timeout,
12
+ // failing cmd+K and piling CPU onto an already-busy daemon.
15
13
  //
16
- // Re-enable at runtime with NO rebuild by exporting `PASEO_ENABLE_RECALL_SEARCH=1`
17
- // in the daemon's environment. Read at call time (not a module-load constant) so the
18
- // env var is honored without a process restart and can be toggled per-test.
14
+ // RE-ENABLED BY DEFAULT on 2026-07-29: the engine is now an in-process SQLite
15
+ // FTS5 index maintained incrementally in the background (`../search/`). The
16
+ // three causes of the original failure are structurally gone no subprocess,
17
+ // no re-index on read, no writer lock on the query path. Measured on the real
18
+ // 8,117-file / 4.6 GB corpus: query p95 0.5–32.7 ms against a 150 ms debounce,
19
+ // and a steady-state index pass reads ~0 bytes in 0.9 s.
20
+ //
21
+ // Escape hatch, read at call time so it needs no rebuild or restart: export
22
+ // `PASEO_DISABLE_SESSION_SEARCH=1` to turn search off, and the palette falls
23
+ // back to the title/metadata browse (`listAllSessions`).
24
+ //
25
+ // `PASEO_ENABLE_RECALL_SEARCH` is intentionally no longer consulted: it is
26
+ // redundant now that search is on by default, and keeping it would mean a
27
+ // host that never set it stays broken for no reason.
19
28
  export function recallSearchEnabled() {
20
- return process.env.PASEO_ENABLE_RECALL_SEARCH === "1";
29
+ return process.env.PASEO_DISABLE_SESSION_SEARCH !== "1";
21
30
  }
22
31
  //# sourceMappingURL=recall-search-feature.js.map
@@ -1,27 +1,21 @@
1
1
  import type express from "express";
2
2
  import type pino from "pino";
3
- export interface SearchResultEntry {
4
- sid: string;
5
- source: string;
6
- cwd: string;
7
- timestamp: string;
8
- snippet: string;
9
- resumeCommand: string;
10
- }
11
- export interface SearchResultsResponse {
3
+ import { type SearchResultEntry, type SearchResultsResponse } from "../search/session-search-service.js";
4
+ export type { SearchResultEntry, SearchResultsResponse };
5
+ export interface RunSessionsSearchParams {
12
6
  query: string;
13
- results: SearchResultEntry[];
7
+ /** Max results. Clamped to [1, 100]; defaults to 20. */
8
+ limit?: number;
9
+ /** Comma-separated source list (e.g. "paseo,claude"). Defaults to all. */
10
+ sources?: string;
11
+ }
12
+ export interface RunSessionsSearchDeps {
13
+ /** Override the search implementation (tests inject a fixture). */
14
+ runSearch?: (params: RunSessionsSearchParams) => Promise<SearchResultsResponse>;
14
15
  }
15
16
  export interface SessionsSearchRouteOptions {
16
- /** Override the recall binary path. Defaults to `recall` on PATH. Tests inject a fixture. */
17
- recallBinaryPath?: string;
18
- /** Override the spawn factory for tests. Returns stdout JSON synchronously. */
19
- spawnRecall?: (args: string[]) => Promise<string>;
20
- /** Subprocess timeout in ms. Defaults to 30s (recall re-indexes on every
21
- * invocation; a large corpus can push cold reads past 8s). */
22
- timeoutMs?: number;
23
- /** Override the openable check (tests inject a fixture). Defaults to on-disk existence. */
24
- isOpenable?: (entry: SearchResultEntry) => Promise<boolean>;
17
+ /** Override the search implementation (tests inject a fixture). */
18
+ runSearch?: (params: RunSessionsSearchParams) => Promise<SearchResultsResponse>;
25
19
  }
26
20
  export declare class RecallNotInstalledError extends Error {
27
21
  constructor();
@@ -30,38 +24,16 @@ export declare class RecallTimedOutError extends Error {
30
24
  readonly waitedMs: number;
31
25
  constructor(waitedMs: number);
32
26
  }
33
- /** Thrown when the recall subprocess emits stdout that isn't valid JSON. */
34
27
  export declare class RecallParseError extends Error {
35
28
  constructor();
36
29
  }
37
- export interface RunSessionsSearchParams {
38
- query: string;
39
- /** Max results. Clamped to [1, 100]; defaults to 20. */
40
- limit?: number;
41
- /** Comma-separated source list (e.g. "paseo,claude"). Defaults to "paseo,claude". */
42
- sources?: string;
43
- }
44
- export interface RunSessionsSearchDeps {
45
- /** Override the spawn factory (tests inject a fixture). */
46
- runRecall?: (args: string[]) => Promise<string>;
47
- recallBinaryPath?: string;
48
- timeoutMs?: number;
49
- /**
50
- * Override the openable check (tests inject a fixture). Defaults to verifying
51
- * the hit's transcript/agent record still exists on disk — recall's index
52
- * goes stale and returns deleted sessions.
53
- */
54
- isOpenable?: (entry: SearchResultEntry) => Promise<boolean>;
55
- }
56
30
  /**
57
- * Core recall FTS spawns the `recall` CLI and returns parsed results. Shared
58
- * by the HTTP `GET /sessions/search` route AND the `search_sessions_request`
59
- * WS handler (so the command palette works over the relay).
31
+ * Core session FTS, shared by every transport.
60
32
  *
61
- * Sub-`MIN_QUERY_LEN` queries return `{ results: [] }` WITHOUT spawning.
62
- * Throws `RecallNotInstalledError` / `RecallTimedOutError` / `RecallParseError`
63
- * (or a generic Error) on failure callers map these to transport-specific
64
- * error codes.
33
+ * Sub-`MIN_QUERY_LEN` queries return `{ results: [] }` without touching the
34
+ * index. Hits reference a file that existed when it was indexed, and every
35
+ * returned `cwd` is resolved to a directory that exists on THIS machine — a
36
+ * hit you cannot resume is not a useful hit.
65
37
  */
66
38
  export declare function runSessionsSearch(params: RunSessionsSearchParams, logger: pino.Logger, deps?: RunSessionsSearchDeps): Promise<SearchResultsResponse>;
67
39
  export declare function registerSessionsSearchRoute(app: express.Express, logger: pino.Logger, options?: SessionsSearchRouteOptions): void;
@@ -1,11 +1,10 @@
1
- import { spawn } from "node:child_process";
2
- import { isSessionOpenable } from "./session-openable.js";
3
- import { recallSearchEnabled } from "./recall-search-feature.js";
4
- const DEFAULT_TIMEOUT_MS = 30000;
5
- const MAX_LIMIT = 100;
6
- const DEFAULT_LIMIT = 20;
1
+ import { getSessionSearchService, } from "../search/session-search-service.js";
7
2
  const MIN_QUERY_LEN = 2;
8
- const DEFAULT_SOURCES = "paseo,claude";
3
+ // COMPAT: these error types were thrown by the recall subprocess engine and
4
+ // are still imported by session.ts. The FTS engine cannot produce them —
5
+ // there is no subprocess to be missing, to time out, or to emit unparseable
6
+ // stdout — but they stay exported so existing catch-sites keep compiling.
7
+ // Remove once session.ts drops the imports.
9
8
  export class RecallNotInstalledError extends Error {
10
9
  constructor() {
11
10
  super("recall binary not found on PATH");
@@ -19,192 +18,57 @@ export class RecallTimedOutError extends Error {
19
18
  this.name = "RecallTimedOutError";
20
19
  }
21
20
  }
22
- /** Thrown when the recall subprocess emits stdout that isn't valid JSON. */
23
21
  export class RecallParseError extends Error {
24
22
  constructor() {
25
23
  super("recall_parse_failed");
26
24
  this.name = "RecallParseError";
27
25
  }
28
26
  }
29
- function spawnRecallDefault(binaryPath, args, timeoutMs) {
30
- return new Promise((resolve, reject) => {
31
- const child = spawn(binaryPath, args, { stdio: ["ignore", "pipe", "pipe"] });
32
- let stdout = "";
33
- let stderr = "";
34
- const timer = setTimeout(() => {
35
- child.kill("SIGTERM");
36
- reject(new RecallTimedOutError(timeoutMs));
37
- }, timeoutMs);
38
- child.stdout.on("data", (chunk) => {
39
- stdout += chunk.toString("utf-8");
40
- });
41
- child.stderr.on("data", (chunk) => {
42
- stderr += chunk.toString("utf-8");
43
- });
44
- child.on("error", (err) => {
45
- clearTimeout(timer);
46
- if (err.code === "ENOENT") {
47
- reject(new RecallNotInstalledError());
48
- }
49
- else {
50
- reject(err);
51
- }
52
- });
53
- child.on("close", (code) => {
54
- clearTimeout(timer);
55
- if (code === 0) {
56
- resolve(stdout);
57
- }
58
- else {
59
- reject(new Error(`recall exited with code ${code}: ${stderr.trim()}`));
60
- }
61
- });
62
- });
63
- }
64
- // recall (re)builds its Tantivy index on each invocation and takes a writer
65
- // lock. The command palette debounce fires a search per keystroke, so without
66
- // serialization concurrent recall processes collide on the lock:
67
- // "Failed to acquire Lockfile: LockBusy — Failed to acquire index lock"
68
- // → the search returns nothing and the palette looks broken. Serialize all
69
- // recall spawns within this daemon process to one-at-a-time, and retry once on
70
- // a lock-busy error (covers a stale lock from a killed process too).
71
- const serializeRecall = (() => {
72
- let chain = Promise.resolve();
73
- return (fn) => {
74
- const run = chain.then(fn, fn);
75
- chain = run.then(() => undefined, () => undefined);
76
- return run;
77
- };
78
- })();
79
- function isRecallLockBusy(error) {
80
- const message = error instanceof Error ? error.message : String(error);
81
- return (message.includes("LockBusy") ||
82
- message.includes("acquire index lock") ||
83
- message.includes("Failed to acquire"));
84
- }
85
- function spawnRecallSerialized(binaryPath, args, timeoutMs) {
86
- return serializeRecall(async () => {
87
- try {
88
- return await spawnRecallDefault(binaryPath, args, timeoutMs);
89
- }
90
- catch (error) {
91
- if (isRecallLockBusy(error)) {
92
- await new Promise((resolve) => setTimeout(resolve, 200));
93
- return await spawnRecallDefault(binaryPath, args, timeoutMs);
94
- }
95
- throw error;
96
- }
97
- });
98
- }
99
- function buildSnippet(messages) {
100
- const first = messages[0];
101
- if (!first)
102
- return "";
103
- const collapsed = first.content.replace(/\s+/g, " ").trim();
104
- return collapsed.length > 220 ? `${collapsed.slice(0, 220)}…` : collapsed;
105
- }
106
- function toEntry(raw) {
107
- return {
108
- sid: raw.session_id,
109
- source: raw.source,
110
- cwd: raw.cwd,
111
- timestamp: raw.timestamp,
112
- snippet: buildSnippet(raw.relevant_messages ?? []),
113
- resumeCommand: raw.resume_command,
114
- };
115
- }
116
27
  /**
117
- * Core recall FTS spawns the `recall` CLI and returns parsed results. Shared
118
- * by the HTTP `GET /sessions/search` route AND the `search_sessions_request`
119
- * WS handler (so the command palette works over the relay).
28
+ * Core session FTS, shared by every transport.
120
29
  *
121
- * Sub-`MIN_QUERY_LEN` queries return `{ results: [] }` WITHOUT spawning.
122
- * Throws `RecallNotInstalledError` / `RecallTimedOutError` / `RecallParseError`
123
- * (or a generic Error) on failure callers map these to transport-specific
124
- * error codes.
30
+ * Sub-`MIN_QUERY_LEN` queries return `{ results: [] }` without touching the
31
+ * index. Hits reference a file that existed when it was indexed, and every
32
+ * returned `cwd` is resolved to a directory that exists on THIS machine — a
33
+ * hit you cannot resume is not a useful hit.
125
34
  */
126
35
  export async function runSessionsSearch(params, logger, deps = {}) {
127
36
  const query = (params.query ?? "").trim();
128
37
  if (query.length < MIN_QUERY_LEN) {
129
38
  return { query, results: [] };
130
39
  }
131
- const rawLimit = params.limit;
132
- const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0
133
- ? Math.min(rawLimit, MAX_LIMIT)
134
- : DEFAULT_LIMIT;
135
- const sources = params.sources && params.sources.length > 0 ? params.sources : DEFAULT_SOURCES;
136
- const binaryPath = deps.recallBinaryPath ?? "recall";
137
- const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
138
- const runRecall = deps.runRecall ?? ((args) => spawnRecallSerialized(binaryPath, args, timeoutMs));
139
- const args = ["search", query, "--source", sources, "--limit", String(limit)];
140
- const stdout = await runRecall(args);
141
- let parsed;
142
- try {
143
- parsed = JSON.parse(stdout);
144
- }
145
- catch (err) {
146
- logger.error({ err, stdout: stdout.slice(0, 200) }, "failed to parse recall stdout");
147
- throw new RecallParseError();
148
- }
149
- const mapped = Array.isArray(parsed.results)
150
- ? parsed.results.map(toEntry)
151
- : [];
152
- // Drop hits whose underlying source no longer exists on disk (recall's index
153
- // goes stale). Without this the palette offers sessions that fail to open.
154
- const isOpenable = deps.isOpenable ?? ((entry) => isSessionOpenable(entry));
155
- const openableFlags = await Promise.all(mapped.map((entry) => isOpenable(entry)));
156
- const results = mapped.filter((_, index) => openableFlags[index]);
157
- const dropped = mapped.length - results.length;
158
- if (dropped > 0) {
159
- logger.debug({ dropped, kept: results.length }, "filtered unopenable recall hits");
160
- }
161
- return { query: parsed.query ?? query, results };
40
+ if (deps.runSearch)
41
+ return deps.runSearch({ ...params, query });
42
+ const service = getSessionSearchService({ logger });
43
+ return service.search({
44
+ query,
45
+ ...(params.limit !== undefined ? { limit: params.limit } : {}),
46
+ ...(params.sources !== undefined ? { sources: params.sources } : {}),
47
+ });
162
48
  }
163
49
  export function registerSessionsSearchRoute(app, logger, options = {}) {
164
50
  const routeLogger = logger.child({ component: "sessions-search-route" });
165
- const binaryPath = options.recallBinaryPath ?? "recall";
166
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
167
- const runRecall = options.spawnRecall ?? ((args) => spawnRecallSerialized(binaryPath, args, timeoutMs));
168
51
  app.get("/sessions/search", (req, res) => {
169
52
  void (async () => {
170
53
  const query = typeof req.query.q === "string" ? req.query.q.trim() : "";
171
- // COMPAT(recallSearch): recall disabled → never spawn the CLI; return empty so
172
- // a direct (desktop/web) palette falls back to its default listAllSessions browse.
173
- if (!recallSearchEnabled()) {
174
- res.status(200).json({ query, results: [] });
175
- return;
176
- }
177
54
  const rawLimit = typeof req.query.limit === "string" ? Number.parseInt(req.query.limit, 10) : undefined;
178
55
  const sources = typeof req.query.source === "string" && req.query.source.length > 0
179
56
  ? req.query.source
180
57
  : undefined;
181
58
  try {
182
- const out = await runSessionsSearch({ query, limit: rawLimit, sources }, routeLogger, {
183
- runRecall,
184
- ...(options.isOpenable ? { isOpenable: options.isOpenable } : {}),
185
- });
59
+ const out = await runSessionsSearch({
60
+ query,
61
+ ...(rawLimit !== undefined && Number.isFinite(rawLimit) ? { limit: rawLimit } : {}),
62
+ ...(sources !== undefined ? { sources } : {}),
63
+ }, routeLogger, options.runSearch ? { runSearch: options.runSearch } : {});
186
64
  res.status(200).json(out);
187
65
  }
188
66
  catch (err) {
189
- if (err instanceof RecallNotInstalledError) {
190
- routeLogger.warn({ binaryPath }, "recall binary not installed on PATH");
191
- res.status(503).json({ error: "recall_not_installed" });
192
- return;
193
- }
194
- if (err instanceof RecallTimedOutError) {
195
- routeLogger.warn({ waitedMs: err.waitedMs, query }, "recall subprocess timed out");
196
- res.status(504).json({ error: "recall_timeout" });
197
- return;
198
- }
199
- if (err instanceof RecallParseError) {
200
- res.status(500).json({ error: "recall_parse_failed" });
201
- return;
202
- }
203
- routeLogger.error({ err, query }, "recall subprocess failed");
204
- res.status(500).json({ error: "recall_failed" });
67
+ routeLogger.error({ err, query }, "session search failed");
68
+ res.status(500).json({ error: "search_failed" });
205
69
  }
206
70
  })();
207
71
  });
208
- routeLogger.info({ binaryPath, timeoutMs }, "Registered GET /sessions/search");
72
+ routeLogger.info("Registered GET /sessions/search (sqlite fts5)");
209
73
  }
210
74
  //# sourceMappingURL=sessions-search-route.js.map
@@ -1,5 +1,5 @@
1
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentBridge=function(){const l=(0,s.createDesktopPreviewUrlResolver)({reader:(0,s.createDesktopFileReader)(),objectUrls:(0,s.createBrowserObjectUrlMinter)()});return{copyFile:t.copyDesktopAttachmentFile,writeBase64:t.writeDesktopAttachmentBase64,writeBytes:t.writeDesktopAttachmentBytes,deleteFile:t.deleteDesktopAttachmentFile,garbageCollect:t.garbageCollectDesktopAttachmentFiles,readFileBase64:s.readDesktopFileBase64,resolvePreviewUrl:t=>l.resolve(t),releasePreviewUrl:t=>l.release(t)}};var t=r(d[0]),s=r(d[1])},4775,[4780,4781]);
2
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.writeDesktopAttachmentBase64=async function(n){return await(0,t.invokeDesktopCommand)("write_attachment_base64",{attachmentId:n.attachmentId,base64:n.base64,extension:n.extension??null})},e.writeDesktopAttachmentBytes=async function(n){return await(0,t.invokeDesktopCommand)("write_attachment_bytes",{attachmentId:n.attachmentId,bytes:n.bytes,extension:n.extension??null})},e.copyDesktopAttachmentFile=async function(n){return await(0,t.invokeDesktopCommand)("copy_attachment_file",{attachmentId:n.attachmentId,sourcePath:n.sourcePath,extension:n.extension??null})},e.deleteDesktopAttachmentFile=async function(n){return await(0,t.invokeDesktopCommand)("delete_attachment_file",{path:n.path})},e.garbageCollectDesktopAttachmentFiles=async function(n){return await(0,t.invokeDesktopCommand)("garbage_collect_attachment_files",{referencedIds:[...n.referencedIds]})};var t=r(d[0])},4780,[3406]);
3
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.readDesktopFileBase64=s,e.createDesktopFileReader=function(){return{readFileBase64:s}},e.createBrowserObjectUrlMinter=function(){return{tryCreate({mimeType:t,base64:n}){if("undefined"==typeof URL||"function"!=typeof URL.createObjectURL)return null;const o=c(n),s=new ArrayBuffer(o.byteLength);new Uint8Array(s).set(o);const u=new Blob([s],{type:t});return URL.createObjectURL(u)},revoke(t){"undefined"!=typeof URL&&"function"==typeof URL.revokeObjectURL&&URL.revokeObjectURL(t)}}},e.createDesktopPreviewUrlResolver=function(t){const n=new Set;return{async resolve(o){const s=await t.reader.readFileBase64(o.storageKey),c=t.objectUrls.tryCreate({mimeType:o.mimeType,base64:s});return null===c?`data:${o.mimeType};base64,${s}`:(n.add(c),c)},async release({url:o}){n.has(o)&&(n.delete(o),t.objectUrls.revoke(o))}}};var t=r(d[0]),n=r(d[1]),o=r(d[2]);async function s(o){return await(0,n.invokeDesktopCommand)("read_file_base64",{path:(0,t.fileUriToPath)(o)})}function c(t){return new Uint8Array(o.Buffer.from(t,"base64"))}},4781,[4478,3406,52]);
4
- //# sourceMappingURL=/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.map
5
- //# debugId=747c960c-b789-4565-8ddf-04a127947565
1
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentBridge=function(){const l=(0,s.createDesktopPreviewUrlResolver)({reader:(0,s.createDesktopFileReader)(),objectUrls:(0,s.createBrowserObjectUrlMinter)()});return{copyFile:t.copyDesktopAttachmentFile,writeBase64:t.writeDesktopAttachmentBase64,writeBytes:t.writeDesktopAttachmentBytes,deleteFile:t.deleteDesktopAttachmentFile,garbageCollect:t.garbageCollectDesktopAttachmentFiles,readFileBase64:s.readDesktopFileBase64,resolvePreviewUrl:t=>l.resolve(t),releasePreviewUrl:t=>l.release(t)}};var t=r(d[0]),s=r(d[1])},4776,[4781,4782]);
2
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.writeDesktopAttachmentBase64=async function(n){return await(0,t.invokeDesktopCommand)("write_attachment_base64",{attachmentId:n.attachmentId,base64:n.base64,extension:n.extension??null})},e.writeDesktopAttachmentBytes=async function(n){return await(0,t.invokeDesktopCommand)("write_attachment_bytes",{attachmentId:n.attachmentId,bytes:n.bytes,extension:n.extension??null})},e.copyDesktopAttachmentFile=async function(n){return await(0,t.invokeDesktopCommand)("copy_attachment_file",{attachmentId:n.attachmentId,sourcePath:n.sourcePath,extension:n.extension??null})},e.deleteDesktopAttachmentFile=async function(n){return await(0,t.invokeDesktopCommand)("delete_attachment_file",{path:n.path})},e.garbageCollectDesktopAttachmentFiles=async function(n){return await(0,t.invokeDesktopCommand)("garbage_collect_attachment_files",{referencedIds:[...n.referencedIds]})};var t=r(d[0])},4781,[3406]);
3
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.readDesktopFileBase64=s,e.createDesktopFileReader=function(){return{readFileBase64:s}},e.createBrowserObjectUrlMinter=function(){return{tryCreate({mimeType:t,base64:n}){if("undefined"==typeof URL||"function"!=typeof URL.createObjectURL)return null;const o=c(n),s=new ArrayBuffer(o.byteLength);new Uint8Array(s).set(o);const u=new Blob([s],{type:t});return URL.createObjectURL(u)},revoke(t){"undefined"!=typeof URL&&"function"==typeof URL.revokeObjectURL&&URL.revokeObjectURL(t)}}},e.createDesktopPreviewUrlResolver=function(t){const n=new Set;return{async resolve(o){const s=await t.reader.readFileBase64(o.storageKey),c=t.objectUrls.tryCreate({mimeType:o.mimeType,base64:s});return null===c?`data:${o.mimeType};base64,${s}`:(n.add(c),c)},async release({url:o}){n.has(o)&&(n.delete(o),t.objectUrls.revoke(o))}}};var t=r(d[0]),n=r(d[1]),o=r(d[2]);async function s(o){return await(0,n.invokeDesktopCommand)("read_file_base64",{path:(0,t.fileUriToPath)(o)})}function c(t){return new Uint8Array(o.Buffer.from(t,"base64"))}},4782,[4478,3406,52]);
4
+ //# sourceMappingURL=/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.map
5
+ //# debugId=9957cc72-3a9f-4def-9949-c3b1006a22a8
@@ -1,3 +1,3 @@
1
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentStore=function(n){return{storageType:"desktop-file",async save(s){const o=s.id??(0,t.generateAttachmentId)(),l=s.fileName??null;if("file_uri"===s.source.kind)return await c(n,{id:o,uri:s.source.uri,mimeType:s.mimeType,fileName:l});if("data_url"===s.source.kind){const c=(0,t.parseDataUrl)(s.source.dataUrl),p=(0,t.normalizeMimeType)(s.mimeType??c.mimeType);return await y(n,{id:o,base64:c.base64,mimeType:p,fileName:l})}if("bytes"===s.source.kind){const c=(0,t.normalizeMimeType)(s.mimeType);return await p(n,{id:o,bytes:s.source.bytes,mimeType:c,fileName:l})}const f=(0,t.normalizeMimeType)(s.mimeType??s.source.blob.type),u=await(0,t.blobToBase64)(s.source.blob);return await y(n,{id:o,base64:u,mimeType:f,fileName:l})},encodeBase64:async({attachment:t})=>(f(t),await n.readFileBase64(t.storageKey)),resolvePreviewUrl:async({attachment:t})=>(f(t),await n.resolvePreviewUrl(t)),async releasePreviewUrl({attachment:t,url:s}){f(t),await n.releasePreviewUrl({url:s})},async delete({attachment:t}){f(t),await n.deleteFile({path:t.storageKey})},async garbageCollect({referencedIds:t}){await n.garbageCollect({referencedIds:Array.from(t)})}}};var t=r(d[0]);const n={"image/png":".png","image/jpeg":".jpg","image/jpg":".jpg","image/gif":".gif","image/webp":".webp","image/avif":".avif","image/heic":".heic","image/heif":".heif","image/tiff":".tiff","image/bmp":".bmp","image/svg+xml":".svg"};function s(t){const n=t.replace(/\\/g,"/").split("/"),s=n[n.length-1];return s&&s.length>0?s:null}function o(s){const o=(0,t.getFileExtensionFromName)(s.fileName);if(o)return o;const l=(0,t.getFileExtensionFromName)(s.sourcePath);return l||(n[s.mimeType]??".img")}function l(t){return{id:t.id,mimeType:t.mimeType,storageType:"desktop-file",storageKey:t.storagePath,fileName:t.fileName??null,byteSize:t.byteSize,createdAt:Date.now()}}async function c(n,c){const y=(0,t.fileUriToPath)(c.uri),p=c.fileName??s(y),f=(0,t.normalizeMimeType)(c.mimeType),u=o({fileName:p,sourcePath:y,mimeType:f}),T=await n.copyFile({attachmentId:c.id,sourcePath:y,extension:u});return l({id:c.id,mimeType:f,storagePath:T.path,byteSize:T.byteSize,fileName:p})}async function y(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBase64({attachmentId:n.id,base64:n.base64,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}async function p(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBytes({attachmentId:n.id,bytes:n.bytes,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}function f(t){if("desktop-file"!==t.storageType)throw new Error(`Unsupported desktop attachment storage type '${t.storageType}'.`)}},4774,[4478]);
2
- //# sourceMappingURL=/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.map
3
- //# debugId=2a4fa395-43e2-4546-9402-21da8e005abc
1
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentStore=function(n){return{storageType:"desktop-file",async save(s){const o=s.id??(0,t.generateAttachmentId)(),l=s.fileName??null;if("file_uri"===s.source.kind)return await c(n,{id:o,uri:s.source.uri,mimeType:s.mimeType,fileName:l});if("data_url"===s.source.kind){const c=(0,t.parseDataUrl)(s.source.dataUrl),p=(0,t.normalizeMimeType)(s.mimeType??c.mimeType);return await y(n,{id:o,base64:c.base64,mimeType:p,fileName:l})}if("bytes"===s.source.kind){const c=(0,t.normalizeMimeType)(s.mimeType);return await p(n,{id:o,bytes:s.source.bytes,mimeType:c,fileName:l})}const f=(0,t.normalizeMimeType)(s.mimeType??s.source.blob.type),u=await(0,t.blobToBase64)(s.source.blob);return await y(n,{id:o,base64:u,mimeType:f,fileName:l})},encodeBase64:async({attachment:t})=>(f(t),await n.readFileBase64(t.storageKey)),resolvePreviewUrl:async({attachment:t})=>(f(t),await n.resolvePreviewUrl(t)),async releasePreviewUrl({attachment:t,url:s}){f(t),await n.releasePreviewUrl({url:s})},async delete({attachment:t}){f(t),await n.deleteFile({path:t.storageKey})},async garbageCollect({referencedIds:t}){await n.garbageCollect({referencedIds:Array.from(t)})}}};var t=r(d[0]);const n={"image/png":".png","image/jpeg":".jpg","image/jpg":".jpg","image/gif":".gif","image/webp":".webp","image/avif":".avif","image/heic":".heic","image/heif":".heif","image/tiff":".tiff","image/bmp":".bmp","image/svg+xml":".svg"};function s(t){const n=t.replace(/\\/g,"/").split("/"),s=n[n.length-1];return s&&s.length>0?s:null}function o(s){const o=(0,t.getFileExtensionFromName)(s.fileName);if(o)return o;const l=(0,t.getFileExtensionFromName)(s.sourcePath);return l||(n[s.mimeType]??".img")}function l(t){return{id:t.id,mimeType:t.mimeType,storageType:"desktop-file",storageKey:t.storagePath,fileName:t.fileName??null,byteSize:t.byteSize,createdAt:Date.now()}}async function c(n,c){const y=(0,t.fileUriToPath)(c.uri),p=c.fileName??s(y),f=(0,t.normalizeMimeType)(c.mimeType),u=o({fileName:p,sourcePath:y,mimeType:f}),T=await n.copyFile({attachmentId:c.id,sourcePath:y,extension:u});return l({id:c.id,mimeType:f,storagePath:T.path,byteSize:T.byteSize,fileName:p})}async function y(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBase64({attachmentId:n.id,base64:n.base64,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}async function p(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBytes({attachmentId:n.id,bytes:n.bytes,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}function f(t){if("desktop-file"!==t.storageType)throw new Error(`Unsupported desktop attachment storage type '${t.storageType}'.`)}},4775,[4478]);
2
+ //# sourceMappingURL=/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.map
3
+ //# debugId=7c4e6bb3-27e3-443f-a7f2-85de05d44e99