birdclaw 0.8.0 → 0.8.2

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 (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +5 -8
  3. package/scripts/browser-perf.mjs +27 -0
  4. package/src/components/ConversationThread.tsx +9 -4
  5. package/src/components/DmWorkspace.tsx +26 -9
  6. package/src/components/EmbeddedTweetCard.tsx +9 -4
  7. package/src/components/FloatingPreview.tsx +382 -0
  8. package/src/components/InboxCard.tsx +6 -4
  9. package/src/components/MarkdownViewer.tsx +59 -100
  10. package/src/components/ProfilePreview.tsx +39 -81
  11. package/src/components/SmartTimestamp.tsx +72 -0
  12. package/src/components/TimelineCard.tsx +24 -6
  13. package/src/components/TweetArticleCard.tsx +66 -0
  14. package/src/components/TweetRichText.tsx +33 -2
  15. package/src/components/useDebouncedValue.ts +12 -0
  16. package/src/components/useTimelineRouteData.ts +149 -47
  17. package/src/lib/api-client.ts +117 -2
  18. package/src/lib/archive-finder.ts +38 -0
  19. package/src/lib/authored-live.ts +2 -62
  20. package/src/lib/bird.ts +32 -1
  21. package/src/lib/client-cache.ts +109 -0
  22. package/src/lib/db.ts +57 -4
  23. package/src/lib/mention-threads-live.ts +2 -1
  24. package/src/lib/mentions-live.ts +2 -46
  25. package/src/lib/ndjson-stream.ts +106 -0
  26. package/src/lib/period-digest.ts +184 -24
  27. package/src/lib/present.ts +99 -1
  28. package/src/lib/profile-analysis.ts +1 -1
  29. package/src/lib/queries.ts +68 -6
  30. package/src/lib/sqlite.ts +5 -2
  31. package/src/lib/timeline-collections-live.ts +2 -1
  32. package/src/lib/timeline-live.ts +2 -1
  33. package/src/lib/tweet-render.ts +78 -0
  34. package/src/lib/tweet-search-live.ts +2 -1
  35. package/src/lib/types.ts +8 -0
  36. package/src/lib/ui.ts +1 -1
  37. package/src/router.tsx +1 -1
  38. package/src/routes/__root.tsx +0 -13
  39. package/src/routes/api/period-digest.tsx +21 -69
  40. package/src/routes/api/profile-analysis.tsx +22 -77
  41. package/src/routes/api/search-discussion.tsx +19 -75
  42. package/src/routes/api/status.tsx +3 -1
  43. package/src/routes/dms.tsx +40 -22
  44. package/src/routes/links.tsx +75 -9
  45. package/src/routes/today.tsx +67 -11
  46. package/vite.config.ts +0 -2
@@ -53,6 +53,8 @@ function digestUrl(
53
53
  url.searchParams.set("includeDms", String(includeDms));
54
54
  url.searchParams.set("maxTweets", "5000");
55
55
  url.searchParams.set("maxLinks", "20");
56
+ // Cloudflare caps proxied requests; live timeline sync remains a separate job/UI action.
57
+ url.searchParams.set("liveSync", "false");
56
58
  if (refresh) {
57
59
  url.searchParams.set("refresh", "true");
58
60
  }
@@ -77,6 +79,11 @@ async function digestRequestError(response: Response) {
77
79
  } catch {
78
80
  detail = "";
79
81
  }
82
+ if (response.status === 524) {
83
+ return new Error(
84
+ "Digest startup timed out at Cloudflare (524). Retry to open a new stream.",
85
+ );
86
+ }
80
87
  return new Error(
81
88
  detail
82
89
  ? `Digest request failed (${status}): ${detail}`
@@ -84,6 +91,20 @@ async function digestRequestError(response: Response) {
84
91
  );
85
92
  }
86
93
 
94
+ function digestStreamError(cause: unknown, phase: string) {
95
+ const message = cause instanceof Error ? cause.message : String(cause);
96
+ if (
97
+ cause instanceof TypeError &&
98
+ /network error|failed to fetch|load failed/i.test(message)
99
+ ) {
100
+ return `Digest connection was interrupted while ${phase.toLowerCase()}. Retry to continue.`;
101
+ }
102
+ if (cause instanceof SyntaxError) {
103
+ return `Digest stream returned invalid data while ${phase.toLowerCase()}. Retry to continue.`;
104
+ }
105
+ return message || "Digest failed";
106
+ }
107
+
87
108
  function formatCounts(context: PeriodDigestContext | null) {
88
109
  if (!context) return "Local Twitter memory, summarized as it streams.";
89
110
  const counts = context.counts;
@@ -200,8 +221,11 @@ function useDigestStream(period: PeriodOption, includeDms: boolean) {
200
221
  setError(null);
201
222
  setStatus("Starting digest");
202
223
  setLoading(true);
224
+ let latestStatus = "Starting digest";
225
+ let completed = false;
203
226
 
204
227
  fetch(digestUrl(period, includeDms, refresh), {
228
+ cache: "no-store",
205
229
  signal: controller.signal,
206
230
  })
207
231
  .then(async (response) => {
@@ -217,7 +241,14 @@ function useDigestStream(period: PeriodOption, includeDms: boolean) {
217
241
  const pump = (): Promise<void> =>
218
242
  reader.read().then(({ done, value }) => {
219
243
  if (!isActiveRequest()) return;
220
- if (done) return;
244
+ if (done) {
245
+ if (!completed) {
246
+ throw new Error(
247
+ `Digest connection closed while ${latestStatus.toLowerCase()}. Retry to continue.`,
248
+ );
249
+ }
250
+ return;
251
+ }
221
252
  buffer += decoder.decode(value, { stream: true });
222
253
  let newline = buffer.indexOf("\n");
223
254
  while (newline >= 0) {
@@ -227,17 +258,18 @@ function useDigestStream(period: PeriodOption, includeDms: boolean) {
227
258
  const event = JSON.parse(line) as PeriodDigestStreamEvent;
228
259
  if (!isActiveRequest()) return;
229
260
  if (event.type === "status") {
230
- setStatus(
231
- event.detail
232
- ? `${event.label} · ${event.detail}`
233
- : event.label,
234
- );
261
+ latestStatus = event.detail
262
+ ? `${event.label} · ${event.detail}`
263
+ : event.label;
264
+ setStatus(latestStatus);
235
265
  } else if (event.type === "start") {
236
266
  setContext(event.context);
237
267
  } else if (event.type === "delta") {
238
- setStatus("Streaming AI summary");
268
+ latestStatus = "Streaming AI summary";
269
+ setStatus(latestStatus);
239
270
  setMarkdown((current) => current + event.delta);
240
271
  } else if (event.type === "done") {
272
+ completed = true;
241
273
  setResult(event.result);
242
274
  setContext(event.result.context);
243
275
  setMarkdown(event.result.markdown);
@@ -245,6 +277,7 @@ function useDigestStream(period: PeriodOption, includeDms: boolean) {
245
277
  event.result.cached ? "Loaded cached report" : "Ready",
246
278
  );
247
279
  } else if (event.type === "error") {
280
+ completed = true;
248
281
  setError(event.error);
249
282
  setStatus("Digest failed");
250
283
  }
@@ -257,7 +290,7 @@ function useDigestStream(period: PeriodOption, includeDms: boolean) {
257
290
  })
258
291
  .catch((cause: unknown) => {
259
292
  if (!isActiveRequest()) return;
260
- setError(cause instanceof Error ? cause.message : "Digest failed");
293
+ setError(digestStreamError(cause, latestStatus));
261
294
  setStatus("Digest failed");
262
295
  })
263
296
  .finally(() => {
@@ -421,7 +454,24 @@ function TodayRoute() {
421
454
  </div>
422
455
  </header>
423
456
 
424
- {error ? <div className={errorCopyClass}>{error}</div> : null}
457
+ {error ? (
458
+ <div
459
+ className={cx(
460
+ errorCopyClass,
461
+ "flex items-center justify-between gap-3",
462
+ )}
463
+ role="alert"
464
+ >
465
+ <span>{error}</span>
466
+ <button
467
+ className="shrink-0 font-semibold underline underline-offset-2"
468
+ onClick={() => run(true)}
469
+ type="button"
470
+ >
471
+ Retry
472
+ </button>
473
+ </div>
474
+ ) : null}
425
475
 
426
476
  <div className="border-b border-[var(--line)] px-4 py-2 text-[13px] text-[var(--ink-soft)]">
427
477
  <span className="inline-flex items-center gap-1">
@@ -436,7 +486,9 @@ function TodayRoute() {
436
486
  ? status
437
487
  : result
438
488
  ? `${result.cached ? "Cached" : "Ready"} · ${result.context.window.label}`
439
- : "Ready"}
489
+ : error
490
+ ? "Digest failed"
491
+ : "Ready"}
440
492
  </span>
441
493
  </div>
442
494
 
@@ -447,7 +499,11 @@ function TodayRoute() {
447
499
  />
448
500
  ) : (
449
501
  <div className="px-4 py-5 text-[14px] text-[var(--ink-soft)]">
450
- {loading ? status : "Waiting for the first tokens..."}
502
+ {loading
503
+ ? status
504
+ : error
505
+ ? "No digest was generated. Retry to start a new run."
506
+ : "Waiting for the first tokens..."}
451
507
  </div>
452
508
  )}
453
509
  </div>
package/vite.config.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import tailwindcss from "@tailwindcss/vite";
2
- import { devtools } from "@tanstack/devtools-vite";
3
2
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
4
3
  import viteReact from "@vitejs/plugin-react";
5
4
  import { defineConfig } from "vite";
@@ -11,7 +10,6 @@ const extraAllowedHosts =
11
10
 
12
11
  const config = defineConfig({
13
12
  plugins: [
14
- devtools(),
15
13
  tailwindcss(),
16
14
  tanstackStart({
17
15
  router: {