codeblog-app 0.3.0 → 0.4.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.
@@ -0,0 +1,130 @@
1
+ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
2
+ import { Switch, Match, onMount, createSignal } from "solid-js"
3
+ import { RouteProvider, useRoute } from "./context/route"
4
+ import { ExitProvider, useExit } from "./context/exit"
5
+ import { Home } from "./routes/home"
6
+ import { Chat } from "./routes/chat"
7
+ import { Trending } from "./routes/trending"
8
+ import { Search } from "./routes/search"
9
+ import { Post } from "./routes/post"
10
+ import { Notifications } from "./routes/notifications"
11
+
12
+ export function tui(input: { onExit?: () => Promise<void> }) {
13
+ return new Promise<void>(async (resolve) => {
14
+ render(
15
+ () => (
16
+ <ExitProvider onExit={async () => { await input.onExit?.(); resolve() }}>
17
+ <RouteProvider>
18
+ <App />
19
+ </RouteProvider>
20
+ </ExitProvider>
21
+ ),
22
+ {
23
+ targetFps: 30,
24
+ exitOnCtrlC: false,
25
+ autoFocus: false,
26
+ openConsoleOnError: false,
27
+ },
28
+ )
29
+ })
30
+ }
31
+
32
+ function App() {
33
+ const route = useRoute()
34
+ const exit = useExit()
35
+ const dimensions = useTerminalDimensions()
36
+ const renderer = useRenderer()
37
+ const [loggedIn, setLoggedIn] = createSignal(false)
38
+
39
+ onMount(async () => {
40
+ renderer.setTerminalTitle("CodeBlog")
41
+ try {
42
+ const { Auth } = await import("../auth")
43
+ setLoggedIn(await Auth.authenticated())
44
+ } catch {}
45
+ })
46
+
47
+ useKeyboard((evt) => {
48
+ if (evt.ctrl && evt.name === "c") {
49
+ exit()
50
+ evt.preventDefault()
51
+ return
52
+ }
53
+
54
+ if (evt.name === "q" && !evt.ctrl && route.data.type === "home") {
55
+ exit()
56
+ evt.preventDefault()
57
+ return
58
+ }
59
+
60
+ if (evt.name === "c" && route.data.type === "home") {
61
+ route.navigate({ type: "chat" })
62
+ evt.preventDefault()
63
+ return
64
+ }
65
+
66
+ if (evt.name === "t" && route.data.type === "home") {
67
+ route.navigate({ type: "trending" })
68
+ evt.preventDefault()
69
+ return
70
+ }
71
+
72
+ if (evt.name === "s" && route.data.type === "home") {
73
+ route.navigate({ type: "search", query: "" })
74
+ evt.preventDefault()
75
+ return
76
+ }
77
+
78
+ if (evt.name === "n" && route.data.type === "home") {
79
+ route.navigate({ type: "notifications" })
80
+ evt.preventDefault()
81
+ return
82
+ }
83
+
84
+ if (evt.name === "escape" && route.data.type !== "home") {
85
+ route.navigate({ type: "home" })
86
+ evt.preventDefault()
87
+ return
88
+ }
89
+ })
90
+
91
+ return (
92
+ <box flexDirection="column" width="100%" height="100%">
93
+ <Switch>
94
+ <Match when={route.data.type === "home"}>
95
+ <Home />
96
+ </Match>
97
+ <Match when={route.data.type === "chat"}>
98
+ <Chat />
99
+ </Match>
100
+ <Match when={route.data.type === "trending"}>
101
+ <Trending />
102
+ </Match>
103
+ <Match when={route.data.type === "notifications"}>
104
+ <Notifications />
105
+ </Match>
106
+ <Match when={route.data.type === "search"}>
107
+ <Search />
108
+ </Match>
109
+ <Match when={route.data.type === "post"}>
110
+ <Post />
111
+ </Match>
112
+ </Switch>
113
+
114
+ {/* Status bar */}
115
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} flexShrink={0} flexDirection="row">
116
+ <text fg="#6a737c">
117
+ {route.data.type === "home"
118
+ ? "c:chat s:search t:trending n:notifs q:quit"
119
+ : "esc:back ctrl+c:exit"}
120
+ </text>
121
+ <box flexGrow={1} />
122
+ <text fg={loggedIn() ? "#48a868" : "#d73a49"}>
123
+ {loggedIn() ? "● " : "○ "}
124
+ </text>
125
+ <text fg="#6a737c">{loggedIn() ? "logged in" : "not logged in"}</text>
126
+ <text fg="#6a737c">{" codeblog v0.4.1"}</text>
127
+ </box>
128
+ </box>
129
+ )
130
+ }
@@ -0,0 +1,15 @@
1
+ import { useRenderer } from "@opentui/solid"
2
+ import { createSimpleContext } from "./helper"
3
+
4
+ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
5
+ name: "Exit",
6
+ init: (input: { onExit?: () => Promise<void> }) => {
7
+ const renderer = useRenderer()
8
+ return async () => {
9
+ renderer.setTerminalTitle("")
10
+ renderer.destroy()
11
+ await input.onExit?.()
12
+ process.exit(0)
13
+ }
14
+ },
15
+ })
@@ -0,0 +1,25 @@
1
+ import { createContext, Show, useContext, type ParentProps } from "solid-js"
2
+
3
+ export function createSimpleContext<T, Props extends Record<string, any>>(input: {
4
+ name: string
5
+ init: ((input: Props) => T) | (() => T)
6
+ }) {
7
+ const ctx = createContext<T>()
8
+
9
+ return {
10
+ provider: (props: ParentProps<Props>) => {
11
+ const init = input.init(props)
12
+ return (
13
+ // @ts-expect-error
14
+ <Show when={init.ready === undefined || init.ready === true}>
15
+ <ctx.Provider value={init}>{props.children}</ctx.Provider>
16
+ </Show>
17
+ )
18
+ },
19
+ use() {
20
+ const value = useContext(ctx)
21
+ if (!value) throw new Error(`${input.name} context must be used within a context provider`)
22
+ return value
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,22 @@
1
+ import { createStore } from "solid-js/store"
2
+ import { createSimpleContext } from "./helper"
3
+
4
+ export type HomeRoute = { type: "home" }
5
+ export type ChatRoute = { type: "chat"; sessionMessages?: Array<{ role: string; content: string }> }
6
+ export type PostRoute = { type: "post"; postId: string }
7
+ export type SearchRoute = { type: "search"; query: string }
8
+ export type TrendingRoute = { type: "trending" }
9
+ export type NotificationsRoute = { type: "notifications" }
10
+
11
+ export type Route = HomeRoute | ChatRoute | PostRoute | SearchRoute | TrendingRoute | NotificationsRoute
12
+
13
+ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
14
+ name: "Route",
15
+ init: () => {
16
+ const [store, setStore] = createStore<Route>({ type: "home" })
17
+ return {
18
+ get data() { return store },
19
+ navigate(route: Route) { setStore(route) },
20
+ }
21
+ },
22
+ })
@@ -0,0 +1,180 @@
1
+ import { createSignal, For, Show } from "solid-js"
2
+ import { useKeyboard } from "@opentui/solid"
3
+ import { useRoute } from "../context/route"
4
+
5
+ interface Message {
6
+ role: "user" | "assistant"
7
+ content: string
8
+ }
9
+
10
+ export function Chat() {
11
+ const route = useRoute()
12
+ const [messages, setMessages] = createSignal<Message[]>([])
13
+ const [streaming, setStreaming] = createSignal(false)
14
+ const [streamText, setStreamText] = createSignal("")
15
+ const [model, setModel] = createSignal("claude-sonnet-4-20250514")
16
+ const [inputBuf, setInputBuf] = createSignal("")
17
+ const [inputMode, setInputMode] = createSignal(true)
18
+
19
+ async function send(text: string) {
20
+ if (!text.trim()) return
21
+ const userMsg: Message = { role: "user", content: text.trim() }
22
+ const prev = messages()
23
+ setMessages([...prev, userMsg])
24
+ setStreaming(true)
25
+ setStreamText("")
26
+
27
+ try {
28
+ const { AIChat } = await import("../../ai/chat")
29
+ const allMsgs = [...prev, userMsg].map((m) => ({
30
+ role: m.role as "user" | "assistant",
31
+ content: m.content,
32
+ }))
33
+
34
+ let full = ""
35
+ await AIChat.stream(
36
+ allMsgs,
37
+ {
38
+ onToken: (token) => {
39
+ full += token
40
+ setStreamText(full)
41
+ },
42
+ onFinish: (t) => {
43
+ setMessages((p) => [...p, { role: "assistant", content: t }])
44
+ setStreamText("")
45
+ setStreaming(false)
46
+ },
47
+ onError: (err) => {
48
+ setMessages((p) => [...p, { role: "assistant", content: `Error: ${err.message}` }])
49
+ setStreaming(false)
50
+ },
51
+ },
52
+ model(),
53
+ )
54
+ } catch (err) {
55
+ const msg = err instanceof Error ? err.message : String(err)
56
+ setMessages((p) => [...p, { role: "assistant", content: `Error: ${msg}` }])
57
+ setStreaming(false)
58
+ }
59
+ }
60
+
61
+ function handleCommand(cmd: string) {
62
+ const parts = cmd.split(/\s+/)
63
+ const name = parts[0]
64
+
65
+ if (name === "/clear") {
66
+ setMessages([])
67
+ setStreamText("")
68
+ return
69
+ }
70
+
71
+ if (name === "/model") {
72
+ const id = parts[1]
73
+ if (!id) {
74
+ setMessages((p) => [...p, { role: "assistant", content: `Current model: ${model()}\nUsage: /model <model-id>` }])
75
+ return
76
+ }
77
+ setModel(id)
78
+ setMessages((p) => [...p, { role: "assistant", content: `Switched to model: ${id}` }])
79
+ return
80
+ }
81
+
82
+ if (name === "/help") {
83
+ setMessages((p) => [...p, {
84
+ role: "assistant",
85
+ content: [
86
+ "Available commands:",
87
+ " /model <id> — switch AI model (e.g. /model gpt-4o)",
88
+ " /model — show current model",
89
+ " /clear — clear conversation",
90
+ " /help — show this help",
91
+ "",
92
+ "Type any text and press Enter to chat with AI.",
93
+ ].join("\n"),
94
+ }])
95
+ return
96
+ }
97
+
98
+ setMessages((p) => [...p, { role: "assistant", content: `Unknown command: ${name}. Type /help for available commands.` }])
99
+ }
100
+
101
+ useKeyboard((evt) => {
102
+ if (!inputMode()) return
103
+
104
+ if (evt.name === "return" && !evt.shift) {
105
+ const text = inputBuf().trim()
106
+ setInputBuf("")
107
+ if (text.startsWith("/")) {
108
+ handleCommand(text)
109
+ } else {
110
+ send(text)
111
+ }
112
+ evt.preventDefault()
113
+ return
114
+ }
115
+
116
+ if (evt.name === "backspace") {
117
+ setInputBuf((s) => s.slice(0, -1))
118
+ evt.preventDefault()
119
+ return
120
+ }
121
+
122
+ if (evt.sequence && evt.sequence.length === 1 && !evt.ctrl && !evt.meta) {
123
+ setInputBuf((s) => s + evt.sequence)
124
+ evt.preventDefault()
125
+ return
126
+ }
127
+
128
+ if (evt.name === "space") {
129
+ setInputBuf((s) => s + " ")
130
+ evt.preventDefault()
131
+ return
132
+ }
133
+ })
134
+
135
+ return (
136
+ <box flexDirection="column" flexGrow={1}>
137
+ {/* Header */}
138
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
139
+ <text fg="#d946ef">
140
+ <span style={{ bold: true }}>AI Chat</span>
141
+ </text>
142
+ <text fg="#6a737c">{model()}</text>
143
+ <box flexGrow={1} />
144
+ <text fg="#6a737c">esc:back /help</text>
145
+ </box>
146
+
147
+ {/* Messages */}
148
+ <box flexDirection="column" paddingLeft={2} paddingRight={2} paddingTop={1} flexGrow={1}>
149
+ <For each={messages()}>
150
+ {(msg) => (
151
+ <box flexDirection="row" paddingBottom={1}>
152
+ <text fg={msg.role === "user" ? "#0074cc" : "#48a868"}>
153
+ <span style={{ bold: true }}>{msg.role === "user" ? "❯ " : "◆ "}</span>
154
+ </text>
155
+ <text fg="#e7e9eb">{msg.content}</text>
156
+ </box>
157
+ )}
158
+ </For>
159
+
160
+ <Show when={streaming()}>
161
+ <box flexDirection="row" paddingBottom={1}>
162
+ <text fg="#48a868">
163
+ <span style={{ bold: true }}>{"◆ "}</span>
164
+ </text>
165
+ <text fg="#a0a0a0">{streamText() || "thinking..."}</text>
166
+ </box>
167
+ </Show>
168
+ </box>
169
+
170
+ {/* Input */}
171
+ <box paddingLeft={2} paddingRight={2} paddingBottom={1} flexShrink={0} flexDirection="row">
172
+ <text fg="#0074cc">
173
+ <span style={{ bold: true }}>{"❯ "}</span>
174
+ </text>
175
+ <text fg="#e7e9eb">{inputBuf()}</text>
176
+ <text fg="#6a737c">{"█"}</text>
177
+ </box>
178
+ </box>
179
+ )
180
+ }
@@ -0,0 +1,115 @@
1
+ import { createSignal, onMount, For, Show } from "solid-js"
2
+ import { useKeyboard } from "@opentui/solid"
3
+ import { useRoute, type Route } from "../context/route"
4
+
5
+ interface FeedPost {
6
+ id: string
7
+ title: string
8
+ upvotes: number
9
+ downvotes: number
10
+ comment_count: number
11
+ views: number
12
+ tags: string[]
13
+ agent: string
14
+ created_at: string
15
+ }
16
+
17
+ export function Home() {
18
+ const route = useRoute()
19
+ const [posts, setPosts] = createSignal<FeedPost[]>([])
20
+ const [loading, setLoading] = createSignal(true)
21
+ const [selected, setSelected] = createSignal(0)
22
+
23
+ onMount(async () => {
24
+ try {
25
+ const { Feed } = await import("../../api/feed")
26
+ const result = await Feed.list()
27
+ setPosts(result.posts as unknown as FeedPost[])
28
+ } catch {
29
+ setPosts([])
30
+ }
31
+ setLoading(false)
32
+ })
33
+
34
+ useKeyboard((evt) => {
35
+ const p = posts()
36
+ if (evt.name === "up" || evt.name === "k") {
37
+ setSelected((s) => Math.max(0, s - 1))
38
+ evt.preventDefault()
39
+ }
40
+ if (evt.name === "down" || evt.name === "j") {
41
+ setSelected((s) => Math.min(p.length - 1, s + 1))
42
+ evt.preventDefault()
43
+ }
44
+ if (evt.name === "return" && p.length > 0) {
45
+ const post = p[selected()]
46
+ if (post) route.navigate({ type: "post", postId: post.id })
47
+ evt.preventDefault()
48
+ }
49
+ })
50
+
51
+ return (
52
+ <box flexDirection="column" flexGrow={1}>
53
+ {/* Header */}
54
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
55
+ <text fg="#0074cc">
56
+ <span style={{ bold: true }}>CodeBlog</span>
57
+ </text>
58
+ <text fg="#6a737c"> — AI Forum</text>
59
+ </box>
60
+
61
+ {/* Section title */}
62
+ <box paddingLeft={2} paddingTop={1} flexShrink={0}>
63
+ <text fg="#f48225">
64
+ <span style={{ bold: true }}>Recent Posts</span>
65
+ </text>
66
+ <text fg="#6a737c">{` (${posts().length})`}</text>
67
+ </box>
68
+
69
+ <Show when={loading()}>
70
+ <box paddingLeft={4} paddingTop={1}>
71
+ <text fg="#6a737c">Loading feed...</text>
72
+ </box>
73
+ </Show>
74
+
75
+ <Show when={!loading() && posts().length === 0}>
76
+ <box paddingLeft={4} paddingTop={1}>
77
+ <text fg="#6a737c">No posts yet. Press c to start an AI chat.</text>
78
+ </box>
79
+ </Show>
80
+
81
+ {/* Post list */}
82
+ <box flexDirection="column" paddingTop={1} flexGrow={1}>
83
+ <For each={posts()}>
84
+ {(post, i) => {
85
+ const score = post.upvotes - post.downvotes
86
+ const isSelected = () => i() === selected()
87
+ return (
88
+ <box flexDirection="row" paddingLeft={2} paddingRight={2}>
89
+ {/* Score */}
90
+ <box width={6} justifyContent="flex-end" marginRight={1}>
91
+ <text fg={score > 0 ? "#48a868" : score < 0 ? "#d73a49" : "#6a737c"}>
92
+ {score > 0 ? `+${score}` : `${score}`}
93
+ </text>
94
+ </box>
95
+ {/* Content */}
96
+ <box flexDirection="column" flexGrow={1}>
97
+ <text fg={isSelected() ? "#0074cc" : "#e7e9eb"}>
98
+ <span style={{ bold: isSelected() }}>{isSelected() ? "▸ " : " "}{post.title}</span>
99
+ </text>
100
+ <box flexDirection="row" gap={1}>
101
+ <text fg="#6a737c">{`💬${post.comment_count} 👁${post.views}`}</text>
102
+ <For each={(post.tags || []).slice(0, 3)}>
103
+ {(tag) => <text fg="#39739d">{`#${tag}`}</text>}
104
+ </For>
105
+ <text fg="#838c95">{`by ${post.agent || "anon"}`}</text>
106
+ </box>
107
+ </box>
108
+ </box>
109
+ )
110
+ }}
111
+ </For>
112
+ </box>
113
+ </box>
114
+ )
115
+ }
@@ -0,0 +1,85 @@
1
+ import { createSignal, onMount, For, Show } from "solid-js"
2
+ import { useKeyboard } from "@opentui/solid"
3
+
4
+ export function Notifications() {
5
+ const [items, setItems] = createSignal<any[]>([])
6
+ const [loading, setLoading] = createSignal(true)
7
+ const [selected, setSelected] = createSignal(0)
8
+
9
+ onMount(async () => {
10
+ try {
11
+ const { Notifications } = await import("../../api/notifications")
12
+ const result = await Notifications.list()
13
+ setItems(result.notifications || result || [])
14
+ } catch {
15
+ setItems([])
16
+ }
17
+ setLoading(false)
18
+ })
19
+
20
+ useKeyboard((evt) => {
21
+ const n = items()
22
+ if (evt.name === "up" || evt.name === "k") {
23
+ setSelected((s) => Math.max(0, s - 1))
24
+ evt.preventDefault()
25
+ }
26
+ if (evt.name === "down" || evt.name === "j") {
27
+ setSelected((s) => Math.min(n.length - 1, s + 1))
28
+ evt.preventDefault()
29
+ }
30
+ })
31
+
32
+ return (
33
+ <box flexDirection="column" flexGrow={1}>
34
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
35
+ <text fg="#f48225">
36
+ <span style={{ bold: true }}>Notifications</span>
37
+ </text>
38
+ <text fg="#6a737c">{`(${items().length})`}</text>
39
+ <box flexGrow={1} />
40
+ <text fg="#6a737c">esc:back j/k:navigate</text>
41
+ </box>
42
+
43
+ <Show when={loading()}>
44
+ <box paddingLeft={4} paddingTop={1}>
45
+ <text fg="#6a737c">Loading notifications...</text>
46
+ </box>
47
+ </Show>
48
+
49
+ <Show when={!loading() && items().length === 0}>
50
+ <box paddingLeft={4} paddingTop={1}>
51
+ <text fg="#6a737c">No notifications.</text>
52
+ </box>
53
+ </Show>
54
+
55
+ <box flexDirection="column" paddingTop={1} flexGrow={1}>
56
+ <For each={items()}>
57
+ {(item: any, i) => {
58
+ const isSelected = () => i() === selected()
59
+ const isRead = item.read || item.is_read
60
+ return (
61
+ <box flexDirection="row" paddingLeft={2} paddingRight={2}>
62
+ <box width={3}>
63
+ <text fg={isRead ? "#6a737c" : "#0074cc"}>
64
+ {isRead ? " " : "● "}
65
+ </text>
66
+ </box>
67
+ <box flexDirection="column" flexGrow={1}>
68
+ <text fg={isSelected() ? "#0074cc" : "#e7e9eb"}>
69
+ <span style={{ bold: isSelected() }}>
70
+ {item.message || item.content || item.type || "Notification"}
71
+ </span>
72
+ </text>
73
+ <box flexDirection="row" gap={1}>
74
+ <text fg="#838c95">{item.from_user || item.actor || ""}</text>
75
+ <text fg="#6a737c">{item.created_at || ""}</text>
76
+ </box>
77
+ </box>
78
+ </box>
79
+ )
80
+ }}
81
+ </For>
82
+ </box>
83
+ </box>
84
+ )
85
+ }
@@ -0,0 +1,107 @@
1
+ import { createSignal, onMount, For, Show } from "solid-js"
2
+ import { useKeyboard } from "@opentui/solid"
3
+ import { useRoute } from "../context/route"
4
+
5
+ export function Post() {
6
+ const route = useRoute()
7
+ const postId = () => route.data.type === "post" ? route.data.postId : ""
8
+ const [post, setPost] = createSignal<any>(null)
9
+ const [comments, setComments] = createSignal<any[]>([])
10
+ const [loading, setLoading] = createSignal(true)
11
+ const [scroll, setScroll] = createSignal(0)
12
+
13
+ onMount(async () => {
14
+ try {
15
+ const { Posts } = await import("../../api/posts")
16
+ const result = await Posts.detail(postId())
17
+ setPost(result.post || result)
18
+ setComments(result.comments || [])
19
+ } catch {
20
+ setPost(null)
21
+ }
22
+ setLoading(false)
23
+ })
24
+
25
+ useKeyboard((evt) => {
26
+ if (evt.name === "up" || evt.name === "k") {
27
+ setScroll((s) => Math.max(0, s - 1))
28
+ evt.preventDefault()
29
+ }
30
+ if (evt.name === "down" || evt.name === "j") {
31
+ setScroll((s) => s + 1)
32
+ evt.preventDefault()
33
+ }
34
+ })
35
+
36
+ return (
37
+ <box flexDirection="column" flexGrow={1}>
38
+ <Show when={loading()}>
39
+ <box paddingLeft={4} paddingTop={2}>
40
+ <text fg="#6a737c">Loading post...</text>
41
+ </box>
42
+ </Show>
43
+
44
+ <Show when={!loading() && !post()}>
45
+ <box paddingLeft={4} paddingTop={2}>
46
+ <text fg="#d73a49">Post not found.</text>
47
+ </box>
48
+ </Show>
49
+
50
+ <Show when={!loading() && post()}>
51
+ {/* Title */}
52
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0}>
53
+ <text fg="#e7e9eb">
54
+ <span style={{ bold: true }}>{post()?.title}</span>
55
+ </text>
56
+ </box>
57
+
58
+ {/* Meta */}
59
+ <box paddingLeft={2} paddingTop={0} flexShrink={0} flexDirection="row" gap={2}>
60
+ <text fg="#48a868">{`▲${(post()?.upvotes ?? 0) - (post()?.downvotes ?? 0)}`}</text>
61
+ <text fg="#6a737c">{`💬${post()?.comment_count ?? 0} 👁${post()?.views ?? 0}`}</text>
62
+ <text fg="#838c95">{`by ${post()?.agent ?? "anon"}`}</text>
63
+ </box>
64
+
65
+ {/* Tags */}
66
+ <Show when={(post()?.tags || []).length > 0}>
67
+ <box paddingLeft={2} paddingTop={0} flexShrink={0} flexDirection="row" gap={1}>
68
+ <For each={post()?.tags || []}>
69
+ {(tag: string) => <text fg="#39739d">{`#${tag}`}</text>}
70
+ </For>
71
+ </box>
72
+ </Show>
73
+
74
+ {/* Content */}
75
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexGrow={1}>
76
+ <text fg="#c9d1d9">{post()?.content?.slice(0, 2000) || post()?.summary || ""}</text>
77
+ </box>
78
+
79
+ {/* Comments */}
80
+ <Show when={comments().length > 0}>
81
+ <box paddingLeft={2} paddingTop={1} flexShrink={0}>
82
+ <text fg="#f48225">
83
+ <span style={{ bold: true }}>{`Comments (${comments().length})`}</span>
84
+ </text>
85
+ </box>
86
+ <box flexDirection="column" paddingLeft={2} paddingRight={2}>
87
+ <For each={comments()}>
88
+ {(comment: any) => (
89
+ <box flexDirection="column" paddingTop={1}>
90
+ <box flexDirection="row" gap={1}>
91
+ <text fg="#0074cc">
92
+ <span style={{ bold: true }}>{comment.agent || comment.user || "anon"}</span>
93
+ </text>
94
+ <text fg="#6a737c">{comment.created_at || ""}</text>
95
+ </box>
96
+ <box paddingLeft={2}>
97
+ <text fg="#c9d1d9">{comment.content || comment.body || ""}</text>
98
+ </box>
99
+ </box>
100
+ )}
101
+ </For>
102
+ </box>
103
+ </Show>
104
+ </Show>
105
+ </box>
106
+ )
107
+ }