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.
- package/CHANGELOG.md +22 -0
- package/package.json +5 -8
- package/scripts/browser-perf.mjs +27 -0
- package/src/components/ConversationThread.tsx +9 -4
- package/src/components/DmWorkspace.tsx +26 -9
- package/src/components/EmbeddedTweetCard.tsx +9 -4
- package/src/components/FloatingPreview.tsx +382 -0
- package/src/components/InboxCard.tsx +6 -4
- package/src/components/MarkdownViewer.tsx +59 -100
- package/src/components/ProfilePreview.tsx +39 -81
- package/src/components/SmartTimestamp.tsx +72 -0
- package/src/components/TimelineCard.tsx +24 -6
- package/src/components/TweetArticleCard.tsx +66 -0
- package/src/components/TweetRichText.tsx +33 -2
- package/src/components/useDebouncedValue.ts +12 -0
- package/src/components/useTimelineRouteData.ts +149 -47
- package/src/lib/api-client.ts +117 -2
- package/src/lib/archive-finder.ts +38 -0
- package/src/lib/authored-live.ts +2 -62
- package/src/lib/bird.ts +32 -1
- package/src/lib/client-cache.ts +109 -0
- package/src/lib/db.ts +57 -4
- package/src/lib/mention-threads-live.ts +2 -1
- package/src/lib/mentions-live.ts +2 -46
- package/src/lib/ndjson-stream.ts +106 -0
- package/src/lib/period-digest.ts +184 -24
- package/src/lib/present.ts +99 -1
- package/src/lib/profile-analysis.ts +1 -1
- package/src/lib/queries.ts +68 -6
- package/src/lib/sqlite.ts +5 -2
- package/src/lib/timeline-collections-live.ts +2 -1
- package/src/lib/timeline-live.ts +2 -1
- package/src/lib/tweet-render.ts +78 -0
- package/src/lib/tweet-search-live.ts +2 -1
- package/src/lib/types.ts +8 -0
- package/src/lib/ui.ts +1 -1
- package/src/router.tsx +1 -1
- package/src/routes/__root.tsx +0 -13
- package/src/routes/api/period-digest.tsx +21 -69
- package/src/routes/api/profile-analysis.tsx +22 -77
- package/src/routes/api/search-discussion.tsx +19 -75
- package/src/routes/api/status.tsx +3 -1
- package/src/routes/dms.tsx +40 -22
- package/src/routes/links.tsx +75 -9
- package/src/routes/today.tsx +67 -11
- package/vite.config.ts +0 -2
package/src/routes/today.tsx
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
231
|
-
event.detail
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
-
:
|
|
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
|
|
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: {
|