codeblog-app 2.3.2 → 2.3.3
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/drizzle/0000_init.sql +34 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/package.json +73 -8
- package/src/ai/__tests__/chat.test.ts +188 -0
- package/src/ai/__tests__/compat.test.ts +46 -0
- package/src/ai/__tests__/home.ai-stream.integration.test.ts +77 -0
- package/src/ai/__tests__/provider-registry.test.ts +98 -0
- package/src/ai/__tests__/provider.test.ts +239 -0
- package/src/ai/__tests__/stream-events.test.ts +152 -0
- package/src/ai/__tests__/tools.test.ts +93 -0
- package/src/ai/chat.ts +336 -0
- package/src/ai/configure.ts +144 -0
- package/src/ai/models.ts +67 -0
- package/src/ai/provider-registry.ts +150 -0
- package/src/ai/provider.ts +264 -0
- package/src/ai/stream-events.ts +64 -0
- package/src/ai/tools.ts +118 -0
- package/src/ai/types.ts +105 -0
- package/src/auth/index.ts +49 -0
- package/src/auth/oauth.ts +141 -0
- package/src/cli/__tests__/commands.test.ts +229 -0
- package/src/cli/cmd/agent.ts +97 -0
- package/src/cli/cmd/ai.ts +10 -0
- package/src/cli/cmd/chat.ts +190 -0
- package/src/cli/cmd/comment.ts +67 -0
- package/src/cli/cmd/config.ts +154 -0
- package/src/cli/cmd/feed.ts +53 -0
- package/src/cli/cmd/forum.ts +106 -0
- package/src/cli/cmd/login.ts +45 -0
- package/src/cli/cmd/logout.ts +14 -0
- package/src/cli/cmd/me.ts +188 -0
- package/src/cli/cmd/post.ts +25 -0
- package/src/cli/cmd/publish.ts +64 -0
- package/src/cli/cmd/scan.ts +78 -0
- package/src/cli/cmd/search.ts +35 -0
- package/src/cli/cmd/setup.ts +632 -0
- package/src/cli/cmd/tui.ts +20 -0
- package/src/cli/cmd/uninstall.ts +281 -0
- package/src/cli/cmd/update.ts +139 -0
- package/src/cli/cmd/vote.ts +50 -0
- package/src/cli/cmd/whoami.ts +18 -0
- package/src/cli/mcp-print.ts +6 -0
- package/src/cli/ui.ts +357 -0
- package/src/config/index.ts +125 -0
- package/src/flag/index.ts +23 -0
- package/src/global/index.ts +38 -0
- package/src/id/index.ts +20 -0
- package/src/index.ts +212 -0
- package/src/mcp/__tests__/client.test.ts +149 -0
- package/src/mcp/__tests__/e2e.ts +331 -0
- package/src/mcp/__tests__/integration.ts +148 -0
- package/src/mcp/client.ts +118 -0
- package/src/server/index.ts +48 -0
- package/src/storage/chat.ts +73 -0
- package/src/storage/db.ts +85 -0
- package/src/storage/schema.sql.ts +39 -0
- package/src/storage/schema.ts +1 -0
- package/src/tui/__tests__/input-intent.test.ts +27 -0
- package/src/tui/__tests__/stream-assembler.test.ts +33 -0
- package/src/tui/ai-stream.ts +28 -0
- package/src/tui/app.tsx +224 -0
- package/src/tui/commands.ts +224 -0
- package/src/tui/context/exit.tsx +15 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/route.tsx +24 -0
- package/src/tui/context/theme.tsx +471 -0
- package/src/tui/input-intent.ts +26 -0
- package/src/tui/routes/home.tsx +1053 -0
- package/src/tui/routes/model.tsx +213 -0
- package/src/tui/routes/notifications.tsx +87 -0
- package/src/tui/routes/post.tsx +102 -0
- package/src/tui/routes/search.tsx +105 -0
- package/src/tui/routes/setup.tsx +267 -0
- package/src/tui/routes/trending.tsx +107 -0
- package/src/tui/stream-assembler.ts +49 -0
- package/src/util/__tests__/context.test.ts +31 -0
- package/src/util/__tests__/lazy.test.ts +37 -0
- package/src/util/context.ts +23 -0
- package/src/util/error.ts +46 -0
- package/src/util/lazy.ts +18 -0
- package/src/util/log.ts +144 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { createSignal } from "solid-js"
|
|
2
|
+
import { useKeyboard } from "@opentui/solid"
|
|
3
|
+
import { useTheme, THEME_NAMES, THEMES } from "../context/theme"
|
|
4
|
+
|
|
5
|
+
// High-contrast colors that are visible on ANY terminal background
|
|
6
|
+
const HC = {
|
|
7
|
+
title: "#ff6600",
|
|
8
|
+
text: "#888888",
|
|
9
|
+
selected: "#00cc00",
|
|
10
|
+
dim: "#999999",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveThemeDef(name: string) {
|
|
14
|
+
const fallback = THEMES.codeblog ?? Object.values(THEMES).find(Boolean)
|
|
15
|
+
if (!fallback) {
|
|
16
|
+
throw new Error("No themes available")
|
|
17
|
+
}
|
|
18
|
+
return THEMES[name] ?? fallback
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ThemeSetup() {
|
|
22
|
+
const theme = useTheme()
|
|
23
|
+
const modes = ["dark", "light"] as const
|
|
24
|
+
const [step, setStep] = createSignal<"mode" | "theme">("mode")
|
|
25
|
+
const [modeIdx, setModeIdx] = createSignal(0)
|
|
26
|
+
const [themeIdx, setThemeIdx] = createSignal(0)
|
|
27
|
+
const getThemeName = (index: number) => THEME_NAMES[index] ?? "codeblog"
|
|
28
|
+
const getThemeColors = (name: string) => resolveThemeDef(name)[theme.mode]
|
|
29
|
+
|
|
30
|
+
useKeyboard((evt) => {
|
|
31
|
+
if (step() === "mode") {
|
|
32
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
33
|
+
setModeIdx((i) => (i - 1 + modes.length) % modes.length)
|
|
34
|
+
evt.preventDefault()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
38
|
+
setModeIdx((i) => (i + 1) % modes.length)
|
|
39
|
+
evt.preventDefault()
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
if (evt.name === "return") {
|
|
43
|
+
theme.setMode(modes[modeIdx()] ?? "dark")
|
|
44
|
+
setStep("theme")
|
|
45
|
+
evt.preventDefault()
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (step() === "theme") {
|
|
51
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
52
|
+
const next = (themeIdx() - 1 + THEME_NAMES.length) % THEME_NAMES.length
|
|
53
|
+
setThemeIdx(next)
|
|
54
|
+
theme.set(getThemeName(next))
|
|
55
|
+
evt.preventDefault()
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
59
|
+
const next = (themeIdx() + 1) % THEME_NAMES.length
|
|
60
|
+
setThemeIdx(next)
|
|
61
|
+
theme.set(getThemeName(next))
|
|
62
|
+
evt.preventDefault()
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (evt.name === "return") {
|
|
66
|
+
theme.finishSetup()
|
|
67
|
+
evt.preventDefault()
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (evt.name === "escape") {
|
|
71
|
+
setStep("mode")
|
|
72
|
+
evt.preventDefault()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<box flexDirection="column" flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
|
|
80
|
+
<box flexGrow={1} minHeight={0} />
|
|
81
|
+
|
|
82
|
+
<box flexShrink={0} flexDirection="column" alignItems="center">
|
|
83
|
+
<text fg={HC.title}>
|
|
84
|
+
<span style={{ bold: true }}>{"★ Welcome to CodeBlog ★"}</span>
|
|
85
|
+
</text>
|
|
86
|
+
<box height={1} />
|
|
87
|
+
</box>
|
|
88
|
+
|
|
89
|
+
{step() === "mode" ? (
|
|
90
|
+
<box flexShrink={0} flexDirection="column" width="100%" maxWidth={50}>
|
|
91
|
+
<text fg={HC.title}>
|
|
92
|
+
<span style={{ bold: true }}>{"What is your terminal background color?"}</span>
|
|
93
|
+
</text>
|
|
94
|
+
<box height={1} />
|
|
95
|
+
{modes.map((m, i) => (
|
|
96
|
+
<box flexDirection="row" paddingLeft={2}>
|
|
97
|
+
<text fg={modeIdx() === i ? HC.selected : HC.dim}>
|
|
98
|
+
{modeIdx() === i ? "❯ " : " "}
|
|
99
|
+
</text>
|
|
100
|
+
<text fg={modeIdx() === i ? HC.selected : HC.dim}>
|
|
101
|
+
<span style={{ bold: modeIdx() === i }}>
|
|
102
|
+
{m === "dark" ? "Dark background (black/dark terminal)" : "Light background (white/light terminal)"}
|
|
103
|
+
</span>
|
|
104
|
+
</text>
|
|
105
|
+
</box>
|
|
106
|
+
))}
|
|
107
|
+
<box height={1} />
|
|
108
|
+
<text fg={HC.text}>{"↑↓ select · Enter confirm"}</text>
|
|
109
|
+
</box>
|
|
110
|
+
) : (
|
|
111
|
+
<box flexShrink={0} flexDirection="column" width="100%" maxWidth={60}>
|
|
112
|
+
<text fg={theme.colors.text}>
|
|
113
|
+
<span style={{ bold: true }}>{"Choose a color theme:"}</span>
|
|
114
|
+
</text>
|
|
115
|
+
<box height={1} />
|
|
116
|
+
{THEME_NAMES.map((name, i) => {
|
|
117
|
+
const c = getThemeColors(name)
|
|
118
|
+
return (
|
|
119
|
+
<box flexDirection="row" paddingLeft={2}>
|
|
120
|
+
<text fg={themeIdx() === i ? c.primary : theme.colors.textMuted}>
|
|
121
|
+
{themeIdx() === i ? "❯ " : " "}
|
|
122
|
+
</text>
|
|
123
|
+
<text fg={themeIdx() === i ? c.text : theme.colors.textMuted}>
|
|
124
|
+
<span style={{ bold: themeIdx() === i }}>
|
|
125
|
+
{name.padEnd(14)}
|
|
126
|
+
</span>
|
|
127
|
+
</text>
|
|
128
|
+
<text fg={c.logo1}>{" ●"}</text>
|
|
129
|
+
<text fg={c.logo2}>{"●"}</text>
|
|
130
|
+
<text fg={c.primary}>{"●"}</text>
|
|
131
|
+
<text fg={c.accent}>{"●"}</text>
|
|
132
|
+
<text fg={c.success}>{"●"}</text>
|
|
133
|
+
<text fg={c.error}>{"●"}</text>
|
|
134
|
+
</box>
|
|
135
|
+
)
|
|
136
|
+
})}
|
|
137
|
+
<box height={1} />
|
|
138
|
+
<text fg={theme.colors.textMuted}>{"↑↓ select · Enter confirm · Esc back"}</text>
|
|
139
|
+
</box>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<box flexGrow={1} minHeight={0} />
|
|
143
|
+
</box>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function ThemePicker(props: { onDone: () => void }) {
|
|
148
|
+
const theme = useTheme()
|
|
149
|
+
const [idx, setIdx] = createSignal(Math.max(0, THEME_NAMES.indexOf(theme.name)))
|
|
150
|
+
const [tab, setTab] = createSignal<"theme" | "mode">("theme")
|
|
151
|
+
const getThemeName = (index: number) => THEME_NAMES[index] ?? "codeblog"
|
|
152
|
+
const getThemeColors = (name: string) => resolveThemeDef(name)[theme.mode]
|
|
153
|
+
|
|
154
|
+
useKeyboard((evt) => {
|
|
155
|
+
if (tab() === "theme") {
|
|
156
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
157
|
+
const next = (idx() - 1 + THEME_NAMES.length) % THEME_NAMES.length
|
|
158
|
+
setIdx(next)
|
|
159
|
+
theme.set(getThemeName(next))
|
|
160
|
+
evt.preventDefault()
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
164
|
+
const next = (idx() + 1) % THEME_NAMES.length
|
|
165
|
+
setIdx(next)
|
|
166
|
+
theme.set(getThemeName(next))
|
|
167
|
+
evt.preventDefault()
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (evt.name === "tab") {
|
|
171
|
+
setTab("mode")
|
|
172
|
+
evt.preventDefault()
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (evt.name === "return" || evt.name === "escape") {
|
|
176
|
+
props.onDone()
|
|
177
|
+
evt.preventDefault()
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (tab() === "mode") {
|
|
183
|
+
if (evt.name === "up" || evt.name === "down" || evt.name === "k" || evt.name === "j") {
|
|
184
|
+
theme.setMode(theme.mode === "dark" ? "light" : "dark")
|
|
185
|
+
evt.preventDefault()
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (evt.name === "tab") {
|
|
189
|
+
setTab("theme")
|
|
190
|
+
evt.preventDefault()
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
if (evt.name === "return" || evt.name === "escape") {
|
|
194
|
+
props.onDone()
|
|
195
|
+
evt.preventDefault()
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<box flexDirection="column" flexGrow={1} paddingLeft={2} paddingRight={2} paddingTop={1}>
|
|
203
|
+
<box flexDirection="row" gap={4} flexShrink={0}>
|
|
204
|
+
<text fg={theme.colors.primary}>
|
|
205
|
+
<span style={{ bold: true }}>Theme Settings</span>
|
|
206
|
+
</text>
|
|
207
|
+
<box flexGrow={1} />
|
|
208
|
+
<text fg={theme.colors.textMuted}>{"Tab: switch section · Enter/Esc: done"}</text>
|
|
209
|
+
</box>
|
|
210
|
+
|
|
211
|
+
<box flexDirection="row" flexGrow={1} paddingTop={1} gap={4}>
|
|
212
|
+
{/* Theme list */}
|
|
213
|
+
<box flexDirection="column" width={40}>
|
|
214
|
+
<text fg={tab() === "theme" ? theme.colors.text : theme.colors.textMuted}>
|
|
215
|
+
<span style={{ bold: true }}>{"Color Theme"}</span>
|
|
216
|
+
</text>
|
|
217
|
+
<box height={1} />
|
|
218
|
+
{THEME_NAMES.map((name, i) => {
|
|
219
|
+
const c = getThemeColors(name)
|
|
220
|
+
return (
|
|
221
|
+
<box flexDirection="row">
|
|
222
|
+
<text fg={idx() === i ? c.primary : theme.colors.textMuted}>
|
|
223
|
+
{idx() === i && tab() === "theme" ? "❯ " : " "}
|
|
224
|
+
</text>
|
|
225
|
+
<text fg={idx() === i ? c.text : theme.colors.textMuted}>
|
|
226
|
+
<span style={{ bold: idx() === i }}>
|
|
227
|
+
{name.padEnd(14)}
|
|
228
|
+
</span>
|
|
229
|
+
</text>
|
|
230
|
+
<text fg={c.logo1}>{" ●"}</text>
|
|
231
|
+
<text fg={c.logo2}>{"●"}</text>
|
|
232
|
+
<text fg={c.primary}>{"●"}</text>
|
|
233
|
+
<text fg={c.accent}>{"●"}</text>
|
|
234
|
+
<text fg={c.success}>{"●"}</text>
|
|
235
|
+
<text fg={c.error}>{"●"}</text>
|
|
236
|
+
</box>
|
|
237
|
+
)
|
|
238
|
+
})}
|
|
239
|
+
</box>
|
|
240
|
+
|
|
241
|
+
{/* Mode toggle */}
|
|
242
|
+
<box flexDirection="column" width={30}>
|
|
243
|
+
<text fg={tab() === "mode" ? theme.colors.text : theme.colors.textMuted}>
|
|
244
|
+
<span style={{ bold: true }}>{"Background Mode"}</span>
|
|
245
|
+
</text>
|
|
246
|
+
<box height={1} />
|
|
247
|
+
<box flexDirection="row">
|
|
248
|
+
<text fg={theme.mode === "dark" && tab() === "mode" ? theme.colors.primary : theme.colors.textMuted}>
|
|
249
|
+
{theme.mode === "dark" && tab() === "mode" ? "❯ " : " "}
|
|
250
|
+
</text>
|
|
251
|
+
<text fg={theme.mode === "dark" ? theme.colors.text : theme.colors.textMuted}>
|
|
252
|
+
<span style={{ bold: theme.mode === "dark" }}>{"Dark"}</span>
|
|
253
|
+
</text>
|
|
254
|
+
</box>
|
|
255
|
+
<box flexDirection="row">
|
|
256
|
+
<text fg={theme.mode === "light" && tab() === "mode" ? theme.colors.primary : theme.colors.textMuted}>
|
|
257
|
+
{theme.mode === "light" && tab() === "mode" ? "❯ " : " "}
|
|
258
|
+
</text>
|
|
259
|
+
<text fg={theme.mode === "light" ? theme.colors.text : theme.colors.textMuted}>
|
|
260
|
+
<span style={{ bold: theme.mode === "light" }}>{"Light"}</span>
|
|
261
|
+
</text>
|
|
262
|
+
</box>
|
|
263
|
+
</box>
|
|
264
|
+
</box>
|
|
265
|
+
</box>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createSignal, onMount, For, Show } from "solid-js"
|
|
2
|
+
import { useKeyboard } from "@opentui/solid"
|
|
3
|
+
import { useTheme } from "../context/theme"
|
|
4
|
+
import { McpBridge } from "../../mcp/client"
|
|
5
|
+
|
|
6
|
+
export function Trending() {
|
|
7
|
+
const theme = useTheme()
|
|
8
|
+
const [data, setData] = createSignal<any>(null)
|
|
9
|
+
const [loading, setLoading] = createSignal(true)
|
|
10
|
+
const [tab, setTab] = createSignal<"posts" | "tags" | "agents">("posts")
|
|
11
|
+
|
|
12
|
+
onMount(async () => {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await McpBridge.callToolJSON<any>("trending_topics", {})
|
|
15
|
+
setData(raw.trending || raw)
|
|
16
|
+
} catch {
|
|
17
|
+
setData(null)
|
|
18
|
+
}
|
|
19
|
+
setLoading(false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
useKeyboard((evt) => {
|
|
23
|
+
if (evt.name === "1") { setTab("posts"); evt.preventDefault() }
|
|
24
|
+
if (evt.name === "2") { setTab("tags"); evt.preventDefault() }
|
|
25
|
+
if (evt.name === "3") { setTab("agents"); evt.preventDefault() }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<box flexDirection="column" flexGrow={1}>
|
|
30
|
+
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
|
|
31
|
+
<text fg={theme.colors.accent}>
|
|
32
|
+
<span style={{ bold: true }}>Trending</span>
|
|
33
|
+
</text>
|
|
34
|
+
<box flexGrow={1} />
|
|
35
|
+
<text fg={theme.colors.textMuted}>esc:back</text>
|
|
36
|
+
</box>
|
|
37
|
+
|
|
38
|
+
<box paddingLeft={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={2}>
|
|
39
|
+
<text fg={tab() === "posts" ? theme.colors.primary : theme.colors.textMuted}>
|
|
40
|
+
<span style={{ bold: tab() === "posts" }}>[1] Posts</span>
|
|
41
|
+
</text>
|
|
42
|
+
<text fg={tab() === "tags" ? theme.colors.primary : theme.colors.textMuted}>
|
|
43
|
+
<span style={{ bold: tab() === "tags" }}>[2] Tags</span>
|
|
44
|
+
</text>
|
|
45
|
+
<text fg={tab() === "agents" ? theme.colors.primary : theme.colors.textMuted}>
|
|
46
|
+
<span style={{ bold: tab() === "agents" }}>[3] Agents</span>
|
|
47
|
+
</text>
|
|
48
|
+
</box>
|
|
49
|
+
|
|
50
|
+
<Show when={loading()}>
|
|
51
|
+
<box paddingLeft={4} paddingTop={1}>
|
|
52
|
+
<text fg={theme.colors.textMuted}>Loading trending...</text>
|
|
53
|
+
</box>
|
|
54
|
+
</Show>
|
|
55
|
+
|
|
56
|
+
<Show when={!loading() && data()}>
|
|
57
|
+
<Show when={tab() === "posts"}>
|
|
58
|
+
<box flexDirection="column" paddingTop={1}>
|
|
59
|
+
<For each={data()?.top_upvoted || data()?.posts || []}>
|
|
60
|
+
{(post: any) => (
|
|
61
|
+
<box flexDirection="row" paddingLeft={2} paddingRight={2}>
|
|
62
|
+
<box width={6} justifyContent="flex-end" marginRight={1}>
|
|
63
|
+
<text fg={theme.colors.success}>{`▲${post.score ?? post.upvotes ?? 0}`}</text>
|
|
64
|
+
</box>
|
|
65
|
+
<box flexDirection="column" flexGrow={1}>
|
|
66
|
+
<text fg={theme.colors.text}>
|
|
67
|
+
<span style={{ bold: true }}>{post.title}</span>
|
|
68
|
+
</text>
|
|
69
|
+
<text fg={theme.colors.textMuted}>{`👁${post.views ?? 0} 💬${post.comments ?? post.comment_count ?? 0} by ${post.agent ?? "anon"}`}</text>
|
|
70
|
+
</box>
|
|
71
|
+
</box>
|
|
72
|
+
)}
|
|
73
|
+
</For>
|
|
74
|
+
</box>
|
|
75
|
+
</Show>
|
|
76
|
+
|
|
77
|
+
<Show when={tab() === "tags"}>
|
|
78
|
+
<box flexDirection="column" paddingTop={1} paddingLeft={2}>
|
|
79
|
+
<For each={data()?.trending_tags || data()?.tags || []}>
|
|
80
|
+
{(tag: any) => (
|
|
81
|
+
<box flexDirection="row" gap={2}>
|
|
82
|
+
<text fg={theme.colors.primary}>{`#${tag.tag || tag.name || tag}`}</text>
|
|
83
|
+
<text fg={theme.colors.textMuted}>{`${tag.count ?? ""} posts`}</text>
|
|
84
|
+
</box>
|
|
85
|
+
)}
|
|
86
|
+
</For>
|
|
87
|
+
</box>
|
|
88
|
+
</Show>
|
|
89
|
+
|
|
90
|
+
<Show when={tab() === "agents"}>
|
|
91
|
+
<box flexDirection="column" paddingTop={1} paddingLeft={2}>
|
|
92
|
+
<For each={data()?.top_agents || data()?.agents || []}>
|
|
93
|
+
{(agent: any) => (
|
|
94
|
+
<box flexDirection="row" gap={2}>
|
|
95
|
+
<text fg={theme.colors.primary}>
|
|
96
|
+
<span style={{ bold: true }}>{agent.name || agent.username || agent}</span>
|
|
97
|
+
</text>
|
|
98
|
+
<text fg={theme.colors.textMuted}>{`${agent.posts ?? agent.post_count ?? ""} posts`}</text>
|
|
99
|
+
</box>
|
|
100
|
+
)}
|
|
101
|
+
</For>
|
|
102
|
+
</box>
|
|
103
|
+
</Show>
|
|
104
|
+
</Show>
|
|
105
|
+
</box>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class TuiStreamAssembler {
|
|
2
|
+
private text = ""
|
|
3
|
+
private finished = false
|
|
4
|
+
private lastSeq = 0
|
|
5
|
+
|
|
6
|
+
reset() {
|
|
7
|
+
this.text = ""
|
|
8
|
+
this.finished = false
|
|
9
|
+
this.lastSeq = 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getText() {
|
|
13
|
+
return this.text
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pushDelta(delta: string, seq?: number) {
|
|
17
|
+
if (!delta || this.finished) return this.text
|
|
18
|
+
if (seq !== undefined && seq <= this.lastSeq) return this.text
|
|
19
|
+
if (seq !== undefined) this.lastSeq = seq
|
|
20
|
+
|
|
21
|
+
this.text += delta
|
|
22
|
+
return this.text
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pushFinal(finalText: string) {
|
|
26
|
+
if (!finalText) {
|
|
27
|
+
this.finished = true
|
|
28
|
+
return this.text
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!this.text) {
|
|
32
|
+
this.text = finalText
|
|
33
|
+
this.finished = true
|
|
34
|
+
return this.text
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (finalText.length >= this.text.length && finalText.includes(this.text)) {
|
|
38
|
+
this.text = finalText
|
|
39
|
+
this.finished = true
|
|
40
|
+
return this.text
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (finalText.length > this.text.length) {
|
|
44
|
+
this.text = finalText
|
|
45
|
+
}
|
|
46
|
+
this.finished = true
|
|
47
|
+
return this.text
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test"
|
|
2
|
+
import { Context } from "../context"
|
|
3
|
+
|
|
4
|
+
describe("Context", () => {
|
|
5
|
+
test("create and provide context", () => {
|
|
6
|
+
const ctx = Context.create<string>("test")
|
|
7
|
+
|
|
8
|
+
ctx.provide("hello", () => {
|
|
9
|
+
expect(ctx.use()).toBe("hello")
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test("throws outside provider", () => {
|
|
14
|
+
const ctx = Context.create<number>("num")
|
|
15
|
+
expect(() => ctx.use()).toThrow(Context.NotFound)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("nested contexts work correctly", () => {
|
|
19
|
+
const ctx = Context.create<string>("nested")
|
|
20
|
+
|
|
21
|
+
ctx.provide("outer", () => {
|
|
22
|
+
expect(ctx.use()).toBe("outer")
|
|
23
|
+
|
|
24
|
+
ctx.provide("inner", () => {
|
|
25
|
+
expect(ctx.use()).toBe("inner")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(ctx.use()).toBe("outer")
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test"
|
|
2
|
+
import { lazy } from "../lazy"
|
|
3
|
+
|
|
4
|
+
describe("lazy", () => {
|
|
5
|
+
test("initializes value on first call", () => {
|
|
6
|
+
let count = 0
|
|
7
|
+
const val = lazy(() => {
|
|
8
|
+
count++
|
|
9
|
+
return 42
|
|
10
|
+
})
|
|
11
|
+
expect(val()).toBe(42)
|
|
12
|
+
expect(count).toBe(1)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("caches value on subsequent calls", () => {
|
|
16
|
+
let count = 0
|
|
17
|
+
const val = lazy(() => {
|
|
18
|
+
count++
|
|
19
|
+
return "hello"
|
|
20
|
+
})
|
|
21
|
+
val()
|
|
22
|
+
val()
|
|
23
|
+
val()
|
|
24
|
+
expect(count).toBe(1)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("reset clears cached value", () => {
|
|
28
|
+
let count = 0
|
|
29
|
+
const val = lazy(() => {
|
|
30
|
+
count++
|
|
31
|
+
return count
|
|
32
|
+
})
|
|
33
|
+
expect(val()).toBe(1)
|
|
34
|
+
val.reset()
|
|
35
|
+
expect(val()).toBe(2)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks"
|
|
2
|
+
|
|
3
|
+
export namespace Context {
|
|
4
|
+
export class NotFound extends Error {
|
|
5
|
+
constructor(public override readonly name: string) {
|
|
6
|
+
super(`No context found for ${name}`)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function create<T>(name: string) {
|
|
11
|
+
const storage = new AsyncLocalStorage<T>()
|
|
12
|
+
return {
|
|
13
|
+
use() {
|
|
14
|
+
const result = storage.getStore()
|
|
15
|
+
if (!result) throw new NotFound(name)
|
|
16
|
+
return result
|
|
17
|
+
},
|
|
18
|
+
provide<R>(value: T, fn: () => R) {
|
|
19
|
+
return storage.run(value, fn)
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
|
|
3
|
+
export abstract class NamedError extends Error {
|
|
4
|
+
abstract schema(): z.core.$ZodType
|
|
5
|
+
abstract toObject(): { name: string; data: any }
|
|
6
|
+
|
|
7
|
+
static create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data) {
|
|
8
|
+
const schema = z
|
|
9
|
+
.object({
|
|
10
|
+
name: z.literal(name),
|
|
11
|
+
data,
|
|
12
|
+
})
|
|
13
|
+
.meta({ ref: name })
|
|
14
|
+
const result = class extends NamedError {
|
|
15
|
+
public static readonly Schema = schema
|
|
16
|
+
public override readonly name = name as Name
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
public readonly data: z.input<Data>,
|
|
20
|
+
options?: ErrorOptions,
|
|
21
|
+
) {
|
|
22
|
+
super(name, options)
|
|
23
|
+
this.name = name
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static isInstance(input: any): input is InstanceType<typeof result> {
|
|
27
|
+
return typeof input === "object" && "name" in input && input.name === name
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
schema() {
|
|
31
|
+
return schema
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toObject() {
|
|
35
|
+
return { name, data: this.data }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
Object.defineProperty(result, "name", { value: name })
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public static readonly Unknown = NamedError.create(
|
|
43
|
+
"UnknownError",
|
|
44
|
+
z.object({ message: z.string() }),
|
|
45
|
+
)
|
|
46
|
+
}
|
package/src/util/lazy.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function lazy<T>(fn: () => T) {
|
|
2
|
+
let value: T | undefined
|
|
3
|
+
let loaded = false
|
|
4
|
+
|
|
5
|
+
const result = (): T => {
|
|
6
|
+
if (loaded) return value as T
|
|
7
|
+
value = fn()
|
|
8
|
+
loaded = true
|
|
9
|
+
return value as T
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
result.reset = () => {
|
|
13
|
+
loaded = false
|
|
14
|
+
value = undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return result
|
|
18
|
+
}
|