codeblog-app 0.4.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
package/src/index.ts CHANGED
@@ -32,7 +32,7 @@ import { ConfigCommand } from "./cli/cmd/config"
32
32
  import { AIPublishCommand } from "./cli/cmd/ai-publish"
33
33
  import { TuiCommand } from "./cli/cmd/tui"
34
34
 
35
- const VERSION = "0.4.0"
35
+ const VERSION = "0.4.1"
36
36
 
37
37
  process.on("unhandledRejection", (e) => {
38
38
  Log.Default.error("rejection", {
package/src/tui/app.tsx CHANGED
@@ -1,11 +1,13 @@
1
1
  import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
2
- import { Switch, Match, onMount } from "solid-js"
2
+ import { Switch, Match, onMount, createSignal } from "solid-js"
3
3
  import { RouteProvider, useRoute } from "./context/route"
4
4
  import { ExitProvider, useExit } from "./context/exit"
5
5
  import { Home } from "./routes/home"
6
6
  import { Chat } from "./routes/chat"
7
7
  import { Trending } from "./routes/trending"
8
8
  import { Search } from "./routes/search"
9
+ import { Post } from "./routes/post"
10
+ import { Notifications } from "./routes/notifications"
9
11
 
10
12
  export function tui(input: { onExit?: () => Promise<void> }) {
11
13
  return new Promise<void>(async (resolve) => {
@@ -32,9 +34,14 @@ function App() {
32
34
  const exit = useExit()
33
35
  const dimensions = useTerminalDimensions()
34
36
  const renderer = useRenderer()
37
+ const [loggedIn, setLoggedIn] = createSignal(false)
35
38
 
36
- onMount(() => {
39
+ onMount(async () => {
37
40
  renderer.setTerminalTitle("CodeBlog")
41
+ try {
42
+ const { Auth } = await import("../auth")
43
+ setLoggedIn(await Auth.authenticated())
44
+ } catch {}
38
45
  })
39
46
 
40
47
  useKeyboard((evt) => {
@@ -57,9 +64,7 @@ function App() {
57
64
  }
58
65
 
59
66
  if (evt.name === "t" && route.data.type === "home") {
60
- route.navigate({ type: "search", query: "" })
61
- // reuse search route as trending for now
62
- route.navigate({ type: "search", query: "__trending__" })
67
+ route.navigate({ type: "trending" })
63
68
  evt.preventDefault()
64
69
  return
65
70
  }
@@ -70,6 +75,12 @@ function App() {
70
75
  return
71
76
  }
72
77
 
78
+ if (evt.name === "n" && route.data.type === "home") {
79
+ route.navigate({ type: "notifications" })
80
+ evt.preventDefault()
81
+ return
82
+ }
83
+
73
84
  if (evt.name === "escape" && route.data.type !== "home") {
74
85
  route.navigate({ type: "home" })
75
86
  evt.preventDefault()
@@ -86,23 +97,33 @@ function App() {
86
97
  <Match when={route.data.type === "chat"}>
87
98
  <Chat />
88
99
  </Match>
89
- <Match when={route.data.type === "search" && (route.data as any).query === "__trending__"}>
100
+ <Match when={route.data.type === "trending"}>
90
101
  <Trending />
91
102
  </Match>
103
+ <Match when={route.data.type === "notifications"}>
104
+ <Notifications />
105
+ </Match>
92
106
  <Match when={route.data.type === "search"}>
93
107
  <Search />
94
108
  </Match>
109
+ <Match when={route.data.type === "post"}>
110
+ <Post />
111
+ </Match>
95
112
  </Switch>
96
113
 
97
114
  {/* Status bar */}
98
115
  <box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} flexShrink={0} flexDirection="row">
99
116
  <text fg="#6a737c">
100
117
  {route.data.type === "home"
101
- ? "c:chat s:search t:trending q:quit"
118
+ ? "c:chat s:search t:trending n:notifs q:quit"
102
119
  : "esc:back ctrl+c:exit"}
103
120
  </text>
104
121
  <box flexGrow={1} />
105
- <text fg="#6a737c">codeblog v0.4.0</text>
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>
106
127
  </box>
107
128
  </box>
108
129
  )
@@ -5,8 +5,10 @@ export type HomeRoute = { type: "home" }
5
5
  export type ChatRoute = { type: "chat"; sessionMessages?: Array<{ role: string; content: string }> }
6
6
  export type PostRoute = { type: "post"; postId: string }
7
7
  export type SearchRoute = { type: "search"; query: string }
8
+ export type TrendingRoute = { type: "trending" }
9
+ export type NotificationsRoute = { type: "notifications" }
8
10
 
9
- export type Route = HomeRoute | ChatRoute | PostRoute | SearchRoute
11
+ export type Route = HomeRoute | ChatRoute | PostRoute | SearchRoute | TrendingRoute | NotificationsRoute
10
12
 
11
13
  export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
12
14
  name: "Route",
@@ -58,13 +58,57 @@ export function Chat() {
58
58
  }
59
59
  }
60
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
+
61
101
  useKeyboard((evt) => {
62
102
  if (!inputMode()) return
63
103
 
64
104
  if (evt.name === "return" && !evt.shift) {
65
- const text = inputBuf()
105
+ const text = inputBuf().trim()
66
106
  setInputBuf("")
67
- send(text)
107
+ if (text.startsWith("/")) {
108
+ handleCommand(text)
109
+ } else {
110
+ send(text)
111
+ }
68
112
  evt.preventDefault()
69
113
  return
70
114
  }
@@ -97,7 +141,7 @@ export function Chat() {
97
141
  </text>
98
142
  <text fg="#6a737c">{model()}</text>
99
143
  <box flexGrow={1} />
100
- <text fg="#6a737c">esc:back</text>
144
+ <text fg="#6a737c">esc:back /help</text>
101
145
  </box>
102
146
 
103
147
  {/* Messages */}
@@ -1,6 +1,6 @@
1
1
  import { createSignal, onMount, For, Show } from "solid-js"
2
2
  import { useKeyboard } from "@opentui/solid"
3
- import { useRoute } from "../context/route"
3
+ import { useRoute, type Route } from "../context/route"
4
4
 
5
5
  interface FeedPost {
6
6
  id: string
@@ -41,6 +41,11 @@ export function Home() {
41
41
  setSelected((s) => Math.min(p.length - 1, s + 1))
42
42
  evt.preventDefault()
43
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
+ }
44
49
  })
45
50
 
46
51
  return (
@@ -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
+ }