opencode-agora 0.4.0 → 0.4.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/README.md +89 -415
- package/dist/atomic-write.d.ts +10 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +23 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth/refresh.d.ts +17 -0
- package/dist/auth/refresh.d.ts.map +1 -0
- package/dist/auth/refresh.js +50 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/cli/app.d.ts +11 -19
- package/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +159 -2028
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands/browse.d.ts +4 -0
- package/dist/cli/commands/browse.d.ts.map +1 -0
- package/dist/cli/commands/browse.js +80 -0
- package/dist/cli/commands/browse.js.map +1 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +125 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/community.d.ts +12 -0
- package/dist/cli/commands/community.d.ts.map +1 -0
- package/dist/cli/commands/community.js +453 -0
- package/dist/cli/commands/community.js.map +1 -0
- package/dist/cli/commands/export.d.ts +3 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +108 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +299 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/learn.d.ts +4 -0
- package/dist/cli/commands/learn.d.ts.map +1 -0
- package/dist/cli/commands/learn.js +62 -0
- package/dist/cli/commands/learn.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts +9 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -0
- package/dist/cli/commands/marketplace.js +321 -0
- package/dist/cli/commands/marketplace.js.map +1 -0
- package/dist/cli/commands/notify.d.ts +3 -0
- package/dist/cli/commands/notify.d.ts.map +1 -0
- package/dist/cli/commands/notify.js +59 -0
- package/dist/cli/commands/notify.js.map +1 -0
- package/dist/cli/commands/operations.d.ts +16 -0
- package/dist/cli/commands/operations.d.ts.map +1 -0
- package/dist/cli/commands/operations.js +1041 -0
- package/dist/cli/commands/operations.js.map +1 -0
- package/dist/cli/commands/outdated.d.ts +3 -0
- package/dist/cli/commands/outdated.d.ts.map +1 -0
- package/dist/cli/commands/outdated.js +48 -0
- package/dist/cli/commands/outdated.js.map +1 -0
- package/dist/cli/commands/ping.d.ts +3 -0
- package/dist/cli/commands/ping.d.ts.map +1 -0
- package/dist/cli/commands/ping.js +56 -0
- package/dist/cli/commands/ping.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +35 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/commands/today.d.ts +3 -0
- package/dist/cli/commands/today.d.ts.map +1 -0
- package/dist/cli/commands/today.js +142 -0
- package/dist/cli/commands/today.js.map +1 -0
- package/dist/cli/commands/types.d.ts +5 -0
- package/dist/cli/commands/types.d.ts.map +1 -0
- package/dist/cli/commands/types.js +2 -0
- package/dist/cli/commands/types.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +41 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/commands/welcome.d.ts +3 -0
- package/dist/cli/commands/welcome.d.ts.map +1 -0
- package/dist/cli/commands/welcome.js +97 -0
- package/dist/cli/commands/welcome.js.map +1 -0
- package/dist/cli/commands-meta.d.ts.map +1 -1
- package/dist/cli/commands-meta.js +286 -29
- package/dist/cli/commands-meta.js.map +1 -1
- package/dist/cli/completions-gen.d.ts +2 -0
- package/dist/cli/completions-gen.d.ts.map +1 -0
- package/dist/cli/completions-gen.js +195 -0
- package/dist/cli/completions-gen.js.map +1 -0
- package/dist/cli/completions.d.ts.map +1 -1
- package/dist/cli/completions.js +42 -5
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/flags.d.ts +19 -0
- package/dist/cli/flags.d.ts.map +1 -0
- package/dist/cli/flags.js +91 -0
- package/dist/cli/flags.js.map +1 -0
- package/dist/cli/format.d.ts +19 -0
- package/dist/cli/format.d.ts.map +1 -0
- package/dist/cli/format.js +249 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/helpers.d.ts +95 -0
- package/dist/cli/helpers.d.ts.map +1 -0
- package/dist/cli/helpers.js +301 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/mcp-server.d.ts +7 -1
- package/dist/cli/mcp-server.d.ts.map +1 -1
- package/dist/cli/mcp-server.js +70 -2
- package/dist/cli/mcp-server.js.map +1 -1
- package/dist/cli/menu.d.ts.map +1 -1
- package/dist/cli/menu.js +11 -3
- package/dist/cli/menu.js.map +1 -1
- package/dist/cli/pages/community.d.ts +6 -0
- package/dist/cli/pages/community.d.ts.map +1 -1
- package/dist/cli/pages/community.js +882 -64
- package/dist/cli/pages/community.js.map +1 -1
- package/dist/cli/pages/helpers.d.ts +14 -9
- package/dist/cli/pages/helpers.d.ts.map +1 -1
- package/dist/cli/pages/helpers.js +37 -6
- package/dist/cli/pages/helpers.js.map +1 -1
- package/dist/cli/pages/home.d.ts +1 -0
- package/dist/cli/pages/home.d.ts.map +1 -1
- package/dist/cli/pages/home.js +203 -120
- package/dist/cli/pages/home.js.map +1 -1
- package/dist/cli/pages/marketplace.d.ts +2 -0
- package/dist/cli/pages/marketplace.d.ts.map +1 -1
- package/dist/cli/pages/marketplace.js +524 -62
- package/dist/cli/pages/marketplace.js.map +1 -1
- package/dist/cli/pages/news.d.ts +28 -0
- package/dist/cli/pages/news.d.ts.map +1 -1
- package/dist/cli/pages/news.js +209 -82
- package/dist/cli/pages/news.js.map +1 -1
- package/dist/cli/pages/settings.d.ts.map +1 -1
- package/dist/cli/pages/settings.js +163 -33
- package/dist/cli/pages/settings.js.map +1 -1
- package/dist/cli/pages/types.d.ts +1 -1
- package/dist/cli/pages/types.d.ts.map +1 -1
- package/dist/cli/prompter.d.ts.map +1 -1
- package/dist/cli/prompter.js +43 -8
- package/dist/cli/prompter.js.map +1 -1
- package/dist/cli/shell.d.ts +2 -2
- package/dist/cli/shell.d.ts.map +1 -1
- package/dist/cli/shell.js +321 -18
- package/dist/cli/shell.js.map +1 -1
- package/dist/cli/tui.d.ts +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +69 -23
- package/dist/cli/tui.js.map +1 -1
- package/dist/community/client.d.ts +45 -8
- package/dist/community/client.d.ts.map +1 -1
- package/dist/community/client.js +118 -23
- package/dist/community/client.js.map +1 -1
- package/dist/community/search.d.ts +25 -0
- package/dist/community/search.d.ts.map +1 -0
- package/dist/community/search.js +62 -0
- package/dist/community/search.js.map +1 -0
- package/dist/community/types.d.ts +21 -0
- package/dist/community/types.d.ts.map +1 -1
- package/dist/community/types.js +1 -1
- package/dist/config.d.ts +0 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -15
- package/dist/config.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +142 -68
- package/dist/data.js.map +1 -1
- package/dist/format.d.ts +0 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +0 -2
- package/dist/format.js.map +1 -1
- package/dist/hubs/cache.d.ts +6 -0
- package/dist/hubs/cache.d.ts.map +1 -0
- package/dist/hubs/cache.js +46 -0
- package/dist/hubs/cache.js.map +1 -0
- package/dist/hubs/enrichment.d.ts +43 -0
- package/dist/hubs/enrichment.d.ts.map +1 -0
- package/dist/hubs/enrichment.js +239 -0
- package/dist/hubs/enrichment.js.map +1 -0
- package/dist/hubs/github.d.ts +12 -0
- package/dist/hubs/github.d.ts.map +1 -0
- package/dist/hubs/github.js +54 -0
- package/dist/hubs/github.js.map +1 -0
- package/dist/hubs/huggingface.d.ts +27 -0
- package/dist/hubs/huggingface.d.ts.map +1 -0
- package/dist/hubs/huggingface.js +88 -0
- package/dist/hubs/huggingface.js.map +1 -0
- package/dist/hubs/quality.d.ts +26 -0
- package/dist/hubs/quality.d.ts.map +1 -0
- package/dist/hubs/quality.js +57 -0
- package/dist/hubs/quality.js.map +1 -0
- package/dist/hubs/types.d.ts +30 -0
- package/dist/hubs/types.d.ts.map +1 -0
- package/dist/hubs/types.js +2 -0
- package/dist/hubs/types.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -1
- package/dist/init.js +2 -2
- package/dist/init.js.map +1 -1
- package/dist/live.d.ts +10 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +31 -1
- package/dist/live.js.map +1 -1
- package/dist/marketplace.d.ts +16 -3
- package/dist/marketplace.d.ts.map +1 -1
- package/dist/marketplace.js +174 -7
- package/dist/marketplace.js.map +1 -1
- package/dist/news/cache.d.ts.map +1 -1
- package/dist/news/cache.js +4 -3
- package/dist/news/cache.js.map +1 -1
- package/dist/news/score.js +1 -1
- package/dist/news/sources/arxiv.d.ts.map +1 -1
- package/dist/news/sources/arxiv.js +10 -6
- package/dist/news/sources/arxiv.js.map +1 -1
- package/dist/news/sources/github-trending.d.ts.map +1 -1
- package/dist/news/sources/github-trending.js +9 -5
- package/dist/news/sources/github-trending.js.map +1 -1
- package/dist/news/sources/hn.d.ts.map +1 -1
- package/dist/news/sources/hn.js +8 -4
- package/dist/news/sources/hn.js.map +1 -1
- package/dist/news/sources/reddit.d.ts.map +1 -1
- package/dist/news/sources/reddit.js +5 -4
- package/dist/news/sources/reddit.js.map +1 -1
- package/dist/news/sources/rss.d.ts +10 -11
- package/dist/news/sources/rss.d.ts.map +1 -1
- package/dist/news/sources/rss.js +11 -99
- package/dist/news/sources/rss.js.map +1 -1
- package/dist/news/types.d.ts +3 -0
- package/dist/news/types.d.ts.map +1 -1
- package/dist/news/types.js +15 -6
- package/dist/news/types.js.map +1 -1
- package/dist/outdated.d.ts +24 -0
- package/dist/outdated.d.ts.map +1 -0
- package/dist/outdated.js +53 -0
- package/dist/outdated.js.map +1 -0
- package/dist/preferences.d.ts.map +1 -1
- package/dist/preferences.js +4 -4
- package/dist/preferences.js.map +1 -1
- package/dist/scan.d.ts +26 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +179 -0
- package/dist/scan.js.map +1 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +14 -20
- package/dist/settings.js.map +1 -1
- package/dist/state.d.ts +9 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +41 -19
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/package.json +4 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { communityBoardsSource, communityThreadsSource, communityThreadSource } from '../../community/client.js';
|
|
1
|
+
import { communityBoardsSource, communityThreadsSource, communityThreadSource, communitySearchSource, createThreadSource, createReplySource, voteSource, flagSource } from '../../community/client.js';
|
|
2
2
|
import { BOARD_LABELS } from '../../community/types.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { vlen, rail, noRail, sep, frame, pageSourceOptions } from './helpers.js';
|
|
4
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
5
5
|
function hoursAgo(iso) {
|
|
6
6
|
return Math.max(0, (Date.now() - new Date(iso).getTime()) / 3600000);
|
|
7
7
|
}
|
|
@@ -12,17 +12,44 @@ function fmtAge(hours) {
|
|
|
12
12
|
return Math.round(hours) + 'h';
|
|
13
13
|
return Math.round(hours / 24) + 'd';
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
return process.env.AGORA_DATA_DIR || (process.env.HOME ? process.env.HOME + '/.config/agora' : '.agora');
|
|
17
|
-
}
|
|
15
|
+
// ── Module state ─────────────────────────────────────────────────────────────
|
|
18
16
|
let cachedBoards = [];
|
|
19
17
|
let cachedThreads = [];
|
|
20
18
|
let cachedReplies = [];
|
|
21
19
|
let boardsLoading = true;
|
|
22
20
|
const state = {
|
|
23
|
-
view: 'boards',
|
|
24
|
-
|
|
21
|
+
view: 'boards',
|
|
22
|
+
boardCur: 0,
|
|
23
|
+
threadCur: 0,
|
|
24
|
+
replyCur: 0,
|
|
25
|
+
filter: '',
|
|
26
|
+
filtering: false,
|
|
27
|
+
threadSort: 'top',
|
|
28
|
+
composer: null,
|
|
29
|
+
flagModal: null,
|
|
30
|
+
expandedItems: new Set(),
|
|
31
|
+
userVotes: new Map(),
|
|
32
|
+
statusMessage: '',
|
|
33
|
+
statusTimer: null,
|
|
34
|
+
search: null
|
|
25
35
|
};
|
|
36
|
+
// Debounce timer for search API calls
|
|
37
|
+
let searchDebounceTimer = null;
|
|
38
|
+
// ── Pure helpers (exported for tests) ────────────────────────────────────────
|
|
39
|
+
export function isCollapsed(id, flagCount, expandedItems) {
|
|
40
|
+
return flagCount >= 3 && !expandedItems.has(id);
|
|
41
|
+
}
|
|
42
|
+
export function renderCollapsed(flagCount) {
|
|
43
|
+
return `[flagged: ${flagCount} · press X to expand]`;
|
|
44
|
+
}
|
|
45
|
+
export function voteGlyph(yourVote, score, style) {
|
|
46
|
+
if (yourVote === 1)
|
|
47
|
+
return style.accent('▲') + style.accent(String(score));
|
|
48
|
+
if (yourVote === -1)
|
|
49
|
+
return style.accent('▼') + style.accent(String(score));
|
|
50
|
+
return style.dim('↑') + String(score);
|
|
51
|
+
}
|
|
52
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
26
53
|
function flattenReplies(replies, depth = 0) {
|
|
27
54
|
const flat = [];
|
|
28
55
|
for (const r of replies) {
|
|
@@ -34,33 +61,318 @@ function flattenReplies(replies, depth = 0) {
|
|
|
34
61
|
return flat;
|
|
35
62
|
}
|
|
36
63
|
function buildSourceOptions(ctx) {
|
|
37
|
-
|
|
38
|
-
let apiUrl = process.env.AGORA_API_URL || '';
|
|
39
|
-
let token = process.env.AGORA_TOKEN || process.env.AGORA_API_TOKEN || '';
|
|
40
|
-
if (!apiUrl || !token) {
|
|
41
|
-
try {
|
|
42
|
-
const agoraState = loadAgoraState(dir);
|
|
43
|
-
const auth = getAuthState(agoraState);
|
|
44
|
-
if (auth) {
|
|
45
|
-
if (!apiUrl)
|
|
46
|
-
apiUrl = auth.apiUrl || '';
|
|
47
|
-
if (!token)
|
|
48
|
-
token = auth.token || '';
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch { /* ignore */ }
|
|
52
|
-
}
|
|
53
|
-
return { useApi: Boolean(apiUrl), apiUrl, token, fetcher: ctx.io.fetcher, timeoutMs: 10000 };
|
|
64
|
+
return pageSourceOptions(ctx);
|
|
54
65
|
}
|
|
66
|
+
function setStatus(msg) {
|
|
67
|
+
if (state.statusTimer)
|
|
68
|
+
clearTimeout(state.statusTimer);
|
|
69
|
+
state.statusMessage = msg;
|
|
70
|
+
state.statusTimer = setTimeout(() => {
|
|
71
|
+
state.statusMessage = '';
|
|
72
|
+
state.statusTimer = null;
|
|
73
|
+
}, 3000);
|
|
74
|
+
}
|
|
75
|
+
// ── Data loading ──────────────────────────────────────────────────────────────
|
|
55
76
|
async function loadBoards(ctx) {
|
|
56
77
|
try {
|
|
57
78
|
const opts = buildSourceOptions(ctx);
|
|
58
79
|
const result = await communityBoardsSource(opts);
|
|
59
80
|
cachedBoards = result.data.boards;
|
|
60
81
|
}
|
|
61
|
-
catch {
|
|
82
|
+
catch {
|
|
83
|
+
/* keep empty */
|
|
84
|
+
}
|
|
62
85
|
boardsLoading = false;
|
|
86
|
+
ctx.repaint();
|
|
87
|
+
}
|
|
88
|
+
async function loadThreads(boardId, ctx) {
|
|
89
|
+
const opts = buildSourceOptions(ctx);
|
|
90
|
+
const result = await communityThreadsSource(opts, boardId, state.threadSort);
|
|
91
|
+
cachedThreads = result.data.threads;
|
|
92
|
+
}
|
|
93
|
+
// ── Composer state-machine ─────────────────────────────────────────────────
|
|
94
|
+
function composerInsertChar(ch) {
|
|
95
|
+
if (!state.composer)
|
|
96
|
+
return;
|
|
97
|
+
const c = state.composer;
|
|
98
|
+
const line = c.lines[c.cursorLine] ?? '';
|
|
99
|
+
c.lines[c.cursorLine] = line.slice(0, c.cursorCol) + ch + line.slice(c.cursorCol);
|
|
100
|
+
c.cursorCol++;
|
|
101
|
+
}
|
|
102
|
+
function composerBackspace() {
|
|
103
|
+
if (!state.composer)
|
|
104
|
+
return;
|
|
105
|
+
const c = state.composer;
|
|
106
|
+
const line = c.lines[c.cursorLine] ?? '';
|
|
107
|
+
if (c.cursorCol > 0) {
|
|
108
|
+
c.lines[c.cursorLine] = line.slice(0, c.cursorCol - 1) + line.slice(c.cursorCol);
|
|
109
|
+
c.cursorCol--;
|
|
110
|
+
}
|
|
111
|
+
else if (c.cursorLine > 0) {
|
|
112
|
+
const prev = c.lines[c.cursorLine - 1] ?? '';
|
|
113
|
+
c.lines.splice(c.cursorLine, 1);
|
|
114
|
+
c.cursorLine--;
|
|
115
|
+
c.cursorCol = prev.length;
|
|
116
|
+
c.lines[c.cursorLine] = prev + line;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function composerNewline() {
|
|
120
|
+
if (!state.composer)
|
|
121
|
+
return;
|
|
122
|
+
const c = state.composer;
|
|
123
|
+
const line = c.lines[c.cursorLine] ?? '';
|
|
124
|
+
const rest = line.slice(c.cursorCol);
|
|
125
|
+
c.lines[c.cursorLine] = line.slice(0, c.cursorCol);
|
|
126
|
+
c.lines.splice(c.cursorLine + 1, 0, rest);
|
|
127
|
+
c.cursorLine++;
|
|
128
|
+
c.cursorCol = 0;
|
|
129
|
+
}
|
|
130
|
+
function openComposer(mode, replyTo) {
|
|
131
|
+
state.composer = {
|
|
132
|
+
active: true,
|
|
133
|
+
mode,
|
|
134
|
+
lines: [''],
|
|
135
|
+
cursorLine: 0,
|
|
136
|
+
cursorCol: 0,
|
|
137
|
+
title: mode === 'new-thread' ? '' : undefined,
|
|
138
|
+
board: mode === 'new-thread' ? (state.board ?? 'meta') : undefined,
|
|
139
|
+
replyTo,
|
|
140
|
+
status: 'editing'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function closeComposer() {
|
|
144
|
+
state.composer = null;
|
|
145
|
+
}
|
|
146
|
+
async function sendComposer(ctx) {
|
|
147
|
+
if (!state.composer)
|
|
148
|
+
return;
|
|
149
|
+
const c = state.composer;
|
|
150
|
+
const content = c.lines.join('\n').trim();
|
|
151
|
+
if (!content)
|
|
152
|
+
return;
|
|
153
|
+
c.status = 'sending';
|
|
154
|
+
const opts = buildSourceOptions(ctx);
|
|
155
|
+
try {
|
|
156
|
+
if (c.mode === 'reply' && c.replyTo) {
|
|
157
|
+
await createReplySource(opts, c.replyTo, { content });
|
|
158
|
+
// Refresh thread
|
|
159
|
+
const tid = state.thread ?? '';
|
|
160
|
+
if (tid) {
|
|
161
|
+
const result = await communityThreadSource(opts, tid);
|
|
162
|
+
cachedReplies = result.data.replies;
|
|
163
|
+
}
|
|
164
|
+
closeComposer();
|
|
165
|
+
setStatus('Reply posted.');
|
|
166
|
+
}
|
|
167
|
+
else if (c.mode === 'new-thread') {
|
|
168
|
+
const title = (c.title ?? '').trim();
|
|
169
|
+
if (!title) {
|
|
170
|
+
c.status = 'error';
|
|
171
|
+
c.errorMessage = 'Title is required.';
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const board = c.board ?? 'meta';
|
|
175
|
+
await createThreadSource(opts, { board, title, content });
|
|
176
|
+
// Refresh threads
|
|
177
|
+
const result = await communityThreadsSource(opts, board);
|
|
178
|
+
cachedThreads = result.data.threads;
|
|
179
|
+
state.view = 'threads';
|
|
180
|
+
closeComposer();
|
|
181
|
+
setStatus('Thread created.');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
c.status = 'error';
|
|
186
|
+
c.errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ── Voting ────────────────────────────────────────────────────────────────────
|
|
190
|
+
async function doVote(targetId, targetType, dir, ctx) {
|
|
191
|
+
const current = state.userVotes.get(targetId) ?? 0;
|
|
192
|
+
const newVal = current === dir ? 0 : dir;
|
|
193
|
+
// Optimistic update
|
|
194
|
+
state.userVotes.set(targetId, newVal);
|
|
195
|
+
const delta = newVal - current;
|
|
196
|
+
if (targetType === 'discussion') {
|
|
197
|
+
const t = cachedThreads.find((x) => x.id === targetId);
|
|
198
|
+
if (t)
|
|
199
|
+
t.score += delta;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const flat = flattenReplies(cachedReplies);
|
|
203
|
+
const rn = flat.find((f) => f.reply.id === targetId);
|
|
204
|
+
if (rn)
|
|
205
|
+
rn.reply.score += delta;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const opts = buildSourceOptions(ctx);
|
|
209
|
+
const result = await voteSource(opts, targetId, { value: newVal, targetType });
|
|
210
|
+
// Reconcile with actual score
|
|
211
|
+
if (targetType === 'discussion') {
|
|
212
|
+
const t = cachedThreads.find((x) => x.id === targetId);
|
|
213
|
+
if (t)
|
|
214
|
+
t.score = result.data.score;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const flat = flattenReplies(cachedReplies);
|
|
218
|
+
const rn = flat.find((f) => f.reply.id === targetId);
|
|
219
|
+
if (rn)
|
|
220
|
+
rn.reply.score = result.data.score;
|
|
221
|
+
}
|
|
222
|
+
state.userVotes.set(targetId, result.data.userVote);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
setStatus('Vote failed.');
|
|
226
|
+
// Revert optimistic
|
|
227
|
+
state.userVotes.set(targetId, current);
|
|
228
|
+
if (targetType === 'discussion') {
|
|
229
|
+
const t = cachedThreads.find((x) => x.id === targetId);
|
|
230
|
+
if (t)
|
|
231
|
+
t.score -= delta;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const flat = flattenReplies(cachedReplies);
|
|
235
|
+
const rn = flat.find((f) => f.reply.id === targetId);
|
|
236
|
+
if (rn)
|
|
237
|
+
rn.reply.score -= delta;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ── Flagging ──────────────────────────────────────────────────────────────────
|
|
242
|
+
async function submitFlag(reason, ctx) {
|
|
243
|
+
if (!state.flagModal)
|
|
244
|
+
return;
|
|
245
|
+
const { targetId, targetType, notes } = state.flagModal;
|
|
246
|
+
const opts = buildSourceOptions(ctx);
|
|
247
|
+
try {
|
|
248
|
+
await flagSource(opts, targetId, { reason, notes: notes.trim() || undefined, targetType });
|
|
249
|
+
setStatus('Flagged.');
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
setStatus('Flag failed.');
|
|
253
|
+
}
|
|
254
|
+
state.flagModal = null;
|
|
63
255
|
}
|
|
256
|
+
// ── Search ────────────────────────────────────────────────────────────────────
|
|
257
|
+
function openSearch(scope) {
|
|
258
|
+
state.search = {
|
|
259
|
+
active: true,
|
|
260
|
+
query: '',
|
|
261
|
+
cursorCol: 0,
|
|
262
|
+
results: null,
|
|
263
|
+
loading: false,
|
|
264
|
+
selectedIndex: 0,
|
|
265
|
+
scope
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function closeSearch() {
|
|
269
|
+
if (searchDebounceTimer) {
|
|
270
|
+
clearTimeout(searchDebounceTimer);
|
|
271
|
+
searchDebounceTimer = null;
|
|
272
|
+
}
|
|
273
|
+
state.search = null;
|
|
274
|
+
}
|
|
275
|
+
function scheduleSearch(ctx) {
|
|
276
|
+
if (!state.search)
|
|
277
|
+
return;
|
|
278
|
+
if (searchDebounceTimer)
|
|
279
|
+
clearTimeout(searchDebounceTimer);
|
|
280
|
+
const q = state.search.query;
|
|
281
|
+
if (q.length < 2) {
|
|
282
|
+
state.search.results = null;
|
|
283
|
+
state.search.loading = false;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
state.search.loading = true;
|
|
287
|
+
// Debounce: fire the API call only when the user pauses typing for 400ms
|
|
288
|
+
searchDebounceTimer = setTimeout(async () => {
|
|
289
|
+
searchDebounceTimer = null;
|
|
290
|
+
if (!state.search || state.search.query !== q)
|
|
291
|
+
return;
|
|
292
|
+
try {
|
|
293
|
+
const opts = buildSourceOptions(ctx);
|
|
294
|
+
const board = state.search.scope === 'all' ? undefined : state.search.scope;
|
|
295
|
+
const result = await communitySearchSource(opts, q, board);
|
|
296
|
+
if (state.search && state.search.query === q) {
|
|
297
|
+
state.search.results = result.data;
|
|
298
|
+
state.search.loading = false;
|
|
299
|
+
state.search.selectedIndex = 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
if (state.search)
|
|
304
|
+
state.search.loading = false;
|
|
305
|
+
}
|
|
306
|
+
}, 400);
|
|
307
|
+
}
|
|
308
|
+
function searchFlatList(results) {
|
|
309
|
+
return [...results.results.threads, ...results.results.replies];
|
|
310
|
+
}
|
|
311
|
+
function renderSearchView(ctx, width, height) {
|
|
312
|
+
const { style } = ctx;
|
|
313
|
+
const s = state.search;
|
|
314
|
+
if (!s)
|
|
315
|
+
return frame([], width, height);
|
|
316
|
+
const lines = [];
|
|
317
|
+
const rule = ' ' + style.dim('─'.repeat(Math.max(0, width - 2)));
|
|
318
|
+
// Header
|
|
319
|
+
const scopeLabel = 'scope: ' + s.scope;
|
|
320
|
+
const headerLeft = style.bold('─ SEARCH ─');
|
|
321
|
+
const headerRight = style.dim(scopeLabel);
|
|
322
|
+
const gap = Math.max(2, width - vlen(' ' + headerLeft) - vlen(headerRight) - 2);
|
|
323
|
+
lines.push(' ' + headerLeft + ' '.repeat(gap) + headerRight);
|
|
324
|
+
// Query input line
|
|
325
|
+
const cursor = style.dim('▏');
|
|
326
|
+
const queryBefore = s.query.slice(0, s.cursorCol);
|
|
327
|
+
const queryAfter = s.query.slice(s.cursorCol);
|
|
328
|
+
lines.push(' ' + style.accent('> ') + queryBefore + cursor + queryAfter);
|
|
329
|
+
lines.push(rule);
|
|
330
|
+
if (!s.results && !s.loading) {
|
|
331
|
+
lines.push(' ' + style.dim('type to search (min 2 chars) · Tab to change scope'));
|
|
332
|
+
}
|
|
333
|
+
else if (s.loading) {
|
|
334
|
+
lines.push(' ' + style.dim('(loading…)'));
|
|
335
|
+
}
|
|
336
|
+
else if (s.results) {
|
|
337
|
+
const flat = searchFlatList(s.results);
|
|
338
|
+
if (flat.length === 0) {
|
|
339
|
+
lines.push(' ' + style.dim('(no results)'));
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
if (s.results.truncated) {
|
|
343
|
+
lines.push(' ' + style.dim('(truncated — refine query)'));
|
|
344
|
+
}
|
|
345
|
+
const threads = s.results.results.threads;
|
|
346
|
+
const replies = s.results.results.replies;
|
|
347
|
+
if (threads.length > 0) {
|
|
348
|
+
lines.push(' ' + style.bold('THREADS (' + threads.length + ')'));
|
|
349
|
+
threads.forEach((hit, i) => {
|
|
350
|
+
const absIdx = i;
|
|
351
|
+
const sel = absIdx === s.selectedIndex;
|
|
352
|
+
const lead = sel ? style.accent('> ') : ' ';
|
|
353
|
+
lines.push(lead + style.dim('/' + hit.board + ' · ') + (sel ? style.bold(hit.title) : hit.title));
|
|
354
|
+
lines.push(' ' + style.dim(' ') + renderSnippet(hit.snippet, style));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (replies.length > 0) {
|
|
358
|
+
lines.push(' ' + style.bold('REPLIES (' + replies.length + ')'));
|
|
359
|
+
replies.forEach((hit, i) => {
|
|
360
|
+
const absIdx = threads.length + i;
|
|
361
|
+
const sel = absIdx === s.selectedIndex;
|
|
362
|
+
const lead = sel ? style.accent('> ') : ' ';
|
|
363
|
+
lines.push(lead + style.dim('/' + hit.board + ' · in "' + hit.title + '"'));
|
|
364
|
+
lines.push(' ' + style.dim(' ') + renderSnippet(hit.snippet, style));
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return frame(lines, width, height);
|
|
370
|
+
}
|
|
371
|
+
function renderSnippet(snippet, style) {
|
|
372
|
+
// Highlight [matched] text with accent style; keep surrounding text dim
|
|
373
|
+
return snippet.replace(/\[([^\]]*)\]/g, (_match, inner) => style.accent(inner));
|
|
374
|
+
}
|
|
375
|
+
// ── Render ────────────────────────────────────────────────────────────────────
|
|
64
376
|
export const communityPage = {
|
|
65
377
|
id: 'community',
|
|
66
378
|
title: 'COMMUNITY',
|
|
@@ -68,30 +380,56 @@ export const communityPage = {
|
|
|
68
380
|
navIcon: 'C',
|
|
69
381
|
hotkeys: [
|
|
70
382
|
{ key: 'j/k', label: 'nav' },
|
|
383
|
+
{ key: 'g/G', label: 'top/end' },
|
|
71
384
|
{ key: 'Enter', label: 'open' },
|
|
72
385
|
{ key: 'n', label: 'new' },
|
|
73
|
-
{ key: '
|
|
386
|
+
{ key: 'o', label: 'sort' },
|
|
387
|
+
{ key: '/', label: 'search' },
|
|
74
388
|
{ key: 'r', label: 'reply' },
|
|
75
|
-
{ key: '
|
|
389
|
+
{ key: '+/-', label: 'vote' },
|
|
76
390
|
{ key: 'f', label: 'flag' },
|
|
77
|
-
{ key: '
|
|
391
|
+
{ key: 'X', label: 'expand' },
|
|
392
|
+
{ key: 'Esc', label: 'back' }
|
|
78
393
|
],
|
|
79
394
|
mount(_ctx) {
|
|
80
395
|
boardsLoading = true;
|
|
81
396
|
loadBoards(_ctx);
|
|
82
397
|
},
|
|
398
|
+
unmount() {
|
|
399
|
+
if (state.statusTimer) {
|
|
400
|
+
clearTimeout(state.statusTimer);
|
|
401
|
+
state.statusTimer = null;
|
|
402
|
+
}
|
|
403
|
+
if (searchDebounceTimer) {
|
|
404
|
+
clearTimeout(searchDebounceTimer);
|
|
405
|
+
searchDebounceTimer = null;
|
|
406
|
+
}
|
|
407
|
+
state.search = null;
|
|
408
|
+
state.composer = null;
|
|
409
|
+
state.flagModal = null;
|
|
410
|
+
state.filtering = false;
|
|
411
|
+
state.filter = '';
|
|
412
|
+
state.view = 'boards';
|
|
413
|
+
state.expandedItems.clear();
|
|
414
|
+
state.userVotes.clear();
|
|
415
|
+
},
|
|
83
416
|
render(ctx) {
|
|
84
417
|
const { style, width, height } = ctx;
|
|
85
418
|
const lines = [];
|
|
86
|
-
const rule = ' ' + style.dim('
|
|
419
|
+
const rule = ' ' + style.dim('─'.repeat(Math.max(0, width - 2)));
|
|
420
|
+
// Search overlay takes priority over all other views
|
|
421
|
+
if (state.search?.active) {
|
|
422
|
+
return renderSearchView(ctx, width, height);
|
|
423
|
+
}
|
|
87
424
|
if (boardsLoading) {
|
|
88
|
-
lines.push(' ' + style.dim('Loading community
|
|
425
|
+
lines.push(' ' + style.dim('Loading community…'));
|
|
89
426
|
return frame(lines, width, height);
|
|
90
427
|
}
|
|
91
428
|
if (state.view === 'boards') {
|
|
92
429
|
const newTotal = cachedBoards.reduce((s, b) => s + b.newToday, 0);
|
|
93
|
-
lines.push(' ' +
|
|
94
|
-
|
|
430
|
+
lines.push(' ' +
|
|
431
|
+
style.bold(style.accent('COMMUNITY')) +
|
|
432
|
+
style.dim(' ' + cachedBoards.length + ' boards · ' + newTotal + ' new today'));
|
|
95
433
|
lines.push(rule);
|
|
96
434
|
const list = cachedBoards.filter((b) => !state.filter || b.id.includes(state.filter));
|
|
97
435
|
if (list.length === 0) {
|
|
@@ -103,8 +441,10 @@ export const communityPage = {
|
|
|
103
441
|
const lead = sel ? rail(style) : noRail();
|
|
104
442
|
const displayName = '/' + b.id;
|
|
105
443
|
const name = sel ? style.bold(displayName) : displayName;
|
|
106
|
-
const stats = style.dim(b.threadCount.toString().padStart(4) +
|
|
107
|
-
|
|
444
|
+
const stats = style.dim(b.threadCount.toString().padStart(4) +
|
|
445
|
+
' th ' +
|
|
446
|
+
b.newToday.toString().padStart(2) +
|
|
447
|
+
' new');
|
|
108
448
|
const gap = Math.max(2, width - vlen(' ' + lead + displayName) - vlen(stats) - 1);
|
|
109
449
|
lines.push(' ' + lead + name + ' '.repeat(gap) + stats);
|
|
110
450
|
const label = BOARD_LABELS[b.id];
|
|
@@ -116,22 +456,39 @@ export const communityPage = {
|
|
|
116
456
|
else if (state.view === 'threads') {
|
|
117
457
|
const board = state.board ?? cachedBoards[state.boardCur]?.id ?? 'mcp';
|
|
118
458
|
const list = cachedThreads.filter((t) => !state.filter || t.title.toLowerCase().includes(state.filter.toLowerCase()));
|
|
119
|
-
lines.push(' ' +
|
|
120
|
-
|
|
459
|
+
lines.push(' ' +
|
|
460
|
+
style.bold(style.accent('/' + board)) +
|
|
461
|
+
style.dim(' ' + list.length + ' threads') +
|
|
462
|
+
' ' +
|
|
463
|
+
style.dim('sort: ') +
|
|
464
|
+
style.accent(state.threadSort));
|
|
121
465
|
lines.push(rule);
|
|
122
466
|
if (list.length === 0) {
|
|
123
|
-
lines.push(' ' + style.dim('Empty board. ')
|
|
124
|
-
+ style.accent('n') + style.dim(' to start.'));
|
|
467
|
+
lines.push(' ' + style.dim('Empty board. ') + style.accent('n') + style.dim(' to start.'));
|
|
125
468
|
}
|
|
126
469
|
else {
|
|
127
470
|
list.forEach((t, i) => {
|
|
128
471
|
const sel = i === state.threadCur;
|
|
129
472
|
const lead = sel ? rail(style) : noRail();
|
|
130
|
-
const title = sel ? style.bold(t.title) : t.title;
|
|
131
473
|
const age = fmtAge(hoursAgo(t.createdAt));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
474
|
+
if (isCollapsed(t.id, t.flagCount, state.expandedItems)) {
|
|
475
|
+
const collapsed = renderCollapsed(t.flagCount);
|
|
476
|
+
const title = sel ? style.bold(collapsed) : style.dim(collapsed);
|
|
477
|
+
lines.push(' ' + lead + title);
|
|
478
|
+
lines.push(rule);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const title = sel ? style.bold(t.title) : t.title;
|
|
482
|
+
let authorMeta = t.author;
|
|
483
|
+
if (t.authorIsLLM)
|
|
484
|
+
authorMeta += style.dim(' [bot · ' + (t.authorModel ?? 'llm') + ']');
|
|
485
|
+
const meta = style.dim(authorMeta + ' · ' + age);
|
|
486
|
+
const myVote = state.userVotes.get(t.id) ?? 0;
|
|
487
|
+
const voteStr = voteGlyph(myVote, t.score, style);
|
|
488
|
+
const counts = voteStr +
|
|
489
|
+
style.dim(' ') +
|
|
490
|
+
style.accent(t.replyCount.toString().padStart(2)) +
|
|
491
|
+
style.dim(' replies');
|
|
135
492
|
lines.push(' ' + lead + title);
|
|
136
493
|
lines.push(' ' + meta + ' ' + counts);
|
|
137
494
|
lines.push(rule);
|
|
@@ -146,9 +503,13 @@ export const communityPage = {
|
|
|
146
503
|
}
|
|
147
504
|
else {
|
|
148
505
|
const age = fmtAge(hoursAgo(t.createdAt));
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
506
|
+
let authorMeta = t.author;
|
|
507
|
+
if (t.authorIsLLM)
|
|
508
|
+
authorMeta += style.dim(' [bot · ' + (t.authorModel ?? 'llm') + ']');
|
|
509
|
+
const myVoteThread = state.userVotes.get(t.id) ?? 0;
|
|
510
|
+
const threadVoteStr = voteGlyph(myVoteThread, t.score, style);
|
|
511
|
+
lines.push(' ' + style.bold(t.title) + ' ' + threadVoteStr);
|
|
512
|
+
lines.push(' ' + style.dim(authorMeta + ' · ' + age + ' · ' + t.replyCount + ' replies'));
|
|
152
513
|
lines.push(' ' + sep('body', width - 2, style));
|
|
153
514
|
lines.push(' ' + t.content);
|
|
154
515
|
lines.push(' ' + sep('replies', width - 2, style));
|
|
@@ -156,21 +517,311 @@ export const communityPage = {
|
|
|
156
517
|
flatReplies.forEach((rn, i) => {
|
|
157
518
|
const sel = i === state.replyCur;
|
|
158
519
|
const lead = sel ? rail(style) : noRail();
|
|
159
|
-
const indent = '
|
|
520
|
+
const indent = '│ '.repeat(rn.depth);
|
|
160
521
|
const ra = fmtAge(hoursAgo(rn.reply.createdAt));
|
|
161
|
-
|
|
162
|
-
|
|
522
|
+
if (isCollapsed(rn.reply.id, rn.reply.flagCount, state.expandedItems)) {
|
|
523
|
+
lines.push(' ' + indent + lead + style.dim(renderCollapsed(rn.reply.flagCount)));
|
|
524
|
+
lines.push(rule);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
let replyAuthor = rn.reply.author;
|
|
528
|
+
if (rn.reply.authorIsLLM)
|
|
529
|
+
replyAuthor += style.dim(' [bot · ' + (rn.reply.authorModel ?? 'llm') + ']');
|
|
530
|
+
const myVoteReply = state.userVotes.get(rn.reply.id) ?? 0;
|
|
531
|
+
const replyVoteStr = voteGlyph(myVoteReply, rn.reply.score, style);
|
|
532
|
+
lines.push(' ' +
|
|
533
|
+
indent +
|
|
534
|
+
lead +
|
|
535
|
+
style.dim(replyAuthor + ' · ' + ra) +
|
|
536
|
+
' ' +
|
|
537
|
+
replyVoteStr);
|
|
163
538
|
lines.push(' ' + indent + rn.reply.content);
|
|
164
539
|
lines.push(rule);
|
|
165
540
|
});
|
|
166
541
|
}
|
|
167
542
|
}
|
|
543
|
+
// Status message
|
|
544
|
+
if (state.statusMessage) {
|
|
545
|
+
lines.push(' ' + style.dim(state.statusMessage));
|
|
546
|
+
}
|
|
168
547
|
if (state.filtering) {
|
|
169
|
-
lines.push(' ' + style.accent('/') + ' ' + state.filter + style.dim('
|
|
548
|
+
lines.push(' ' + style.accent('/') + ' ' + state.filter + style.dim('▏'));
|
|
549
|
+
}
|
|
550
|
+
// Flag modal overlay (last — overwrites bottom lines in frame)
|
|
551
|
+
if (state.flagModal?.active) {
|
|
552
|
+
const fm = state.flagModal;
|
|
553
|
+
const rendered = frame(lines, width, height).split('\n');
|
|
554
|
+
const modalLines = [
|
|
555
|
+
' ' + style.bold('Flag reason:'),
|
|
556
|
+
' ' + style.accent('1') + style.dim('. spam'),
|
|
557
|
+
' ' + style.accent('2') + style.dim('. harassment'),
|
|
558
|
+
' ' + style.accent('3') + style.dim('. undisclosed-llm'),
|
|
559
|
+
' ' + style.accent('4') + style.dim('. malicious'),
|
|
560
|
+
' ' + style.accent('5') + style.dim('. other'),
|
|
561
|
+
' ' + style.dim('Esc to cancel')
|
|
562
|
+
];
|
|
563
|
+
if (fm.awaitingNotes) {
|
|
564
|
+
modalLines.push(' Notes: ' + fm.notes + style.dim('▏'));
|
|
565
|
+
}
|
|
566
|
+
const startLine = Math.max(0, height - modalLines.length - 1);
|
|
567
|
+
rendered[startLine] = ' ' + style.dim('─'.repeat(Math.max(0, width - 2)));
|
|
568
|
+
for (let i = 0; i < modalLines.length; i++) {
|
|
569
|
+
if (startLine + 1 + i < height) {
|
|
570
|
+
rendered[startLine + 1 + i] = modalLines[i].padEnd(width).slice(0, width);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return rendered.join('\n');
|
|
574
|
+
}
|
|
575
|
+
// Composer overlay
|
|
576
|
+
if (state.composer?.active) {
|
|
577
|
+
const comp = state.composer;
|
|
578
|
+
const rendered = frame(lines, width, height).split('\n');
|
|
579
|
+
const label = comp.mode === 'reply'
|
|
580
|
+
? '[REPLY to ' + (comp.replyTo ?? '') + ']'
|
|
581
|
+
: '[NEW THREAD in /' + (comp.board ?? 'meta') + ']';
|
|
582
|
+
const compLines = [];
|
|
583
|
+
compLines.push(' ' + style.dim('─'.repeat(Math.max(0, width - 2))));
|
|
584
|
+
compLines.push(' ' + style.accent(label) + style.dim(' ' + comp.lines.length + ' lines'));
|
|
585
|
+
if (comp.mode === 'new-thread') {
|
|
586
|
+
compLines.push(' Title: ' + (comp.title ?? '') + (comp.status === 'editing' ? style.dim('▏') : ''));
|
|
587
|
+
}
|
|
588
|
+
const visibleContentLines = Math.max(3, height - rendered.length + compLines.length - 2);
|
|
589
|
+
const contentStart = Math.max(0, comp.cursorLine - visibleContentLines + 1);
|
|
590
|
+
for (let i = contentStart; i < Math.min(comp.lines.length, contentStart + visibleContentLines); i++) {
|
|
591
|
+
const lineText = comp.lines[i] ?? '';
|
|
592
|
+
if (i === comp.cursorLine) {
|
|
593
|
+
compLines.push(' ' +
|
|
594
|
+
lineText.slice(0, comp.cursorCol) +
|
|
595
|
+
style.dim('▏') +
|
|
596
|
+
lineText.slice(comp.cursorCol));
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
compLines.push(' ' + lineText);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (comp.status === 'sending') {
|
|
603
|
+
compLines.push(' ' + style.dim('Sending…'));
|
|
604
|
+
}
|
|
605
|
+
else if (comp.status === 'error') {
|
|
606
|
+
compLines.push(' ' + style.dim('Error: ' + (comp.errorMessage ?? 'unknown')));
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
compLines.push(' ' + style.dim('Ctrl+S send · Esc cancel'));
|
|
610
|
+
}
|
|
611
|
+
const startLine = Math.max(0, height - compLines.length);
|
|
612
|
+
for (let i = 0; i < compLines.length; i++) {
|
|
613
|
+
if (startLine + i < height) {
|
|
614
|
+
rendered[startLine + i] = (compLines[i] ?? '').padEnd(width).slice(0, width);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return rendered.join('\n');
|
|
170
618
|
}
|
|
171
619
|
return frame(lines, width, height);
|
|
172
620
|
},
|
|
173
621
|
async handleKey(event, ctx) {
|
|
622
|
+
// ── Search ──────────────────────────────────────────────────────────────
|
|
623
|
+
if (state.search?.active) {
|
|
624
|
+
const s = state.search;
|
|
625
|
+
const flat = s.results ? searchFlatList(s.results) : [];
|
|
626
|
+
if (event.key === 'esc') {
|
|
627
|
+
closeSearch();
|
|
628
|
+
return { kind: 'none' };
|
|
629
|
+
}
|
|
630
|
+
if (event.key === 'enter') {
|
|
631
|
+
const hit = flat[s.selectedIndex];
|
|
632
|
+
if (hit) {
|
|
633
|
+
closeSearch();
|
|
634
|
+
// Fetch thread if not in cache
|
|
635
|
+
const opts = buildSourceOptions(ctx);
|
|
636
|
+
let thread = cachedThreads.find((t) => t.id === hit.threadId);
|
|
637
|
+
if (!thread) {
|
|
638
|
+
const result = await communityThreadSource(opts, hit.threadId);
|
|
639
|
+
if (result.data.thread) {
|
|
640
|
+
cachedThreads = [...cachedThreads, result.data.thread];
|
|
641
|
+
cachedReplies = result.data.replies;
|
|
642
|
+
thread = result.data.thread;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
const result = await communityThreadSource(opts, hit.threadId);
|
|
647
|
+
cachedReplies = result.data.replies;
|
|
648
|
+
}
|
|
649
|
+
if (thread) {
|
|
650
|
+
state.board = thread.board;
|
|
651
|
+
state.thread = hit.threadId;
|
|
652
|
+
state.view = 'reader';
|
|
653
|
+
const flatR = flattenReplies(cachedReplies);
|
|
654
|
+
if (hit.kind === 'reply') {
|
|
655
|
+
const idx = flatR.findIndex((r) => r.reply.id === hit.id);
|
|
656
|
+
state.replyCur = idx >= 0 ? idx : 0;
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
state.replyCur = 0;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return { kind: 'none' };
|
|
664
|
+
}
|
|
665
|
+
if (event.key === 'j' || event.key === 'down') {
|
|
666
|
+
s.selectedIndex = Math.min(flat.length - 1, s.selectedIndex + 1);
|
|
667
|
+
return { kind: 'none' };
|
|
668
|
+
}
|
|
669
|
+
if (event.key === 'k' || event.key === 'up') {
|
|
670
|
+
s.selectedIndex = Math.max(0, s.selectedIndex - 1);
|
|
671
|
+
return { kind: 'none' };
|
|
672
|
+
}
|
|
673
|
+
if (event.key === 'tab') {
|
|
674
|
+
// Cycle scope: 'all' ↔ current board
|
|
675
|
+
const currentBoard = state.board ?? undefined;
|
|
676
|
+
if (s.scope === 'all' && currentBoard) {
|
|
677
|
+
s.scope = currentBoard;
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
s.scope = 'all';
|
|
681
|
+
}
|
|
682
|
+
scheduleSearch(ctx);
|
|
683
|
+
return { kind: 'none' };
|
|
684
|
+
}
|
|
685
|
+
if (event.key === 'backspace') {
|
|
686
|
+
if (s.cursorCol > 0) {
|
|
687
|
+
s.query = s.query.slice(0, s.cursorCol - 1) + s.query.slice(s.cursorCol);
|
|
688
|
+
s.cursorCol--;
|
|
689
|
+
scheduleSearch(ctx);
|
|
690
|
+
}
|
|
691
|
+
return { kind: 'none' };
|
|
692
|
+
}
|
|
693
|
+
if (event.key.length === 1 && !event.ctrl && !event.meta) {
|
|
694
|
+
s.query = s.query.slice(0, s.cursorCol) + event.key + s.query.slice(s.cursorCol);
|
|
695
|
+
s.cursorCol++;
|
|
696
|
+
scheduleSearch(ctx);
|
|
697
|
+
return { kind: 'none' };
|
|
698
|
+
}
|
|
699
|
+
if (event.key === 'space') {
|
|
700
|
+
s.query = s.query.slice(0, s.cursorCol) + ' ' + s.query.slice(s.cursorCol);
|
|
701
|
+
s.cursorCol++;
|
|
702
|
+
scheduleSearch(ctx);
|
|
703
|
+
return { kind: 'none' };
|
|
704
|
+
}
|
|
705
|
+
return { kind: 'none' };
|
|
706
|
+
}
|
|
707
|
+
// ── Flag modal ──────────────────────────────────────────────────────────
|
|
708
|
+
if (state.flagModal?.active) {
|
|
709
|
+
const fm = state.flagModal;
|
|
710
|
+
if (fm.awaitingNotes) {
|
|
711
|
+
if (event.key === 'esc') {
|
|
712
|
+
state.flagModal = null;
|
|
713
|
+
return { kind: 'none' };
|
|
714
|
+
}
|
|
715
|
+
if (event.key === 'enter') {
|
|
716
|
+
await submitFlag('other', ctx);
|
|
717
|
+
return { kind: 'none' };
|
|
718
|
+
}
|
|
719
|
+
if (event.key === 'backspace') {
|
|
720
|
+
fm.notes = fm.notes.slice(0, -1);
|
|
721
|
+
return { kind: 'none' };
|
|
722
|
+
}
|
|
723
|
+
if (event.key.length === 1 && !event.ctrl) {
|
|
724
|
+
fm.notes += event.key;
|
|
725
|
+
return { kind: 'none' };
|
|
726
|
+
}
|
|
727
|
+
return { kind: 'none' };
|
|
728
|
+
}
|
|
729
|
+
if (event.key === 'esc') {
|
|
730
|
+
state.flagModal = null;
|
|
731
|
+
return { kind: 'none' };
|
|
732
|
+
}
|
|
733
|
+
const reasonMap = {
|
|
734
|
+
'1': 'spam',
|
|
735
|
+
'2': 'harassment',
|
|
736
|
+
'3': 'undisclosed-llm',
|
|
737
|
+
'4': 'malicious',
|
|
738
|
+
'5': 'other'
|
|
739
|
+
};
|
|
740
|
+
const reason = reasonMap[event.key];
|
|
741
|
+
if (reason) {
|
|
742
|
+
if (reason === 'other') {
|
|
743
|
+
fm.awaitingNotes = true;
|
|
744
|
+
return { kind: 'none' };
|
|
745
|
+
}
|
|
746
|
+
await submitFlag(reason, ctx);
|
|
747
|
+
return { kind: 'none' };
|
|
748
|
+
}
|
|
749
|
+
return { kind: 'none' };
|
|
750
|
+
}
|
|
751
|
+
// ── Composer ────────────────────────────────────────────────────────────
|
|
752
|
+
if (state.composer?.active) {
|
|
753
|
+
const comp = state.composer;
|
|
754
|
+
if (comp.status === 'sending')
|
|
755
|
+
return { kind: 'none' };
|
|
756
|
+
if (event.ctrl && event.key === 's') {
|
|
757
|
+
await sendComposer(ctx);
|
|
758
|
+
return { kind: 'none' };
|
|
759
|
+
}
|
|
760
|
+
if (event.key === 'esc') {
|
|
761
|
+
const hasContent = comp.lines.some((l) => l.length > 0);
|
|
762
|
+
if (hasContent) {
|
|
763
|
+
// confirm discard — just close for now
|
|
764
|
+
}
|
|
765
|
+
closeComposer();
|
|
766
|
+
return { kind: 'none' };
|
|
767
|
+
}
|
|
768
|
+
if (event.key === 'enter') {
|
|
769
|
+
// If new-thread and title not yet set, move to content
|
|
770
|
+
if (comp.mode === 'new-thread' && comp.title === '') {
|
|
771
|
+
comp.title = comp.lines.join('').trim();
|
|
772
|
+
comp.lines = [''];
|
|
773
|
+
comp.cursorLine = 0;
|
|
774
|
+
comp.cursorCol = 0;
|
|
775
|
+
return { kind: 'none' };
|
|
776
|
+
}
|
|
777
|
+
composerNewline();
|
|
778
|
+
return { kind: 'none' };
|
|
779
|
+
}
|
|
780
|
+
if (event.key === 'backspace') {
|
|
781
|
+
if (comp.mode === 'new-thread' &&
|
|
782
|
+
typeof comp.title === 'string' &&
|
|
783
|
+
comp.title.length === 0 &&
|
|
784
|
+
comp.lines.join('') === '') {
|
|
785
|
+
// editing title still
|
|
786
|
+
}
|
|
787
|
+
composerBackspace();
|
|
788
|
+
return { kind: 'none' };
|
|
789
|
+
}
|
|
790
|
+
if (event.key === 'up' && comp.cursorLine > 0) {
|
|
791
|
+
comp.cursorLine--;
|
|
792
|
+
comp.cursorCol = Math.min(comp.cursorCol, (comp.lines[comp.cursorLine] ?? '').length);
|
|
793
|
+
return { kind: 'none' };
|
|
794
|
+
}
|
|
795
|
+
if (event.key === 'down' && comp.cursorLine < comp.lines.length - 1) {
|
|
796
|
+
comp.cursorLine++;
|
|
797
|
+
comp.cursorCol = Math.min(comp.cursorCol, (comp.lines[comp.cursorLine] ?? '').length);
|
|
798
|
+
return { kind: 'none' };
|
|
799
|
+
}
|
|
800
|
+
if (event.key === 'left' && comp.cursorCol > 0) {
|
|
801
|
+
comp.cursorCol--;
|
|
802
|
+
return { kind: 'none' };
|
|
803
|
+
}
|
|
804
|
+
if (event.key === 'right') {
|
|
805
|
+
const lineLen = (comp.lines[comp.cursorLine] ?? '').length;
|
|
806
|
+
if (comp.cursorCol < lineLen)
|
|
807
|
+
comp.cursorCol++;
|
|
808
|
+
return { kind: 'none' };
|
|
809
|
+
}
|
|
810
|
+
if (event.key.length === 1 && !event.ctrl && !event.meta) {
|
|
811
|
+
// If new-thread mode and title not set yet, type into title field
|
|
812
|
+
if (comp.mode === 'new-thread' && typeof comp.title === 'string' && !comp.title) {
|
|
813
|
+
// title is filled by user hitting enter — for now just accumulate in lines
|
|
814
|
+
}
|
|
815
|
+
composerInsertChar(event.key);
|
|
816
|
+
return { kind: 'none' };
|
|
817
|
+
}
|
|
818
|
+
if (event.key === 'space') {
|
|
819
|
+
composerInsertChar(' ');
|
|
820
|
+
return { kind: 'none' };
|
|
821
|
+
}
|
|
822
|
+
return { kind: 'none' };
|
|
823
|
+
}
|
|
824
|
+
// ── Filter mode ─────────────────────────────────────────────────────────
|
|
174
825
|
if (state.filtering) {
|
|
175
826
|
if (event.key === 'esc') {
|
|
176
827
|
state.filtering = false;
|
|
@@ -191,6 +842,31 @@ export const communityPage = {
|
|
|
191
842
|
}
|
|
192
843
|
return { kind: 'none' };
|
|
193
844
|
}
|
|
845
|
+
// ── g/G global jumps ────────────────────────────────────────────────────
|
|
846
|
+
if (event.key === 'g') {
|
|
847
|
+
if (state.view === 'boards')
|
|
848
|
+
state.boardCur = 0;
|
|
849
|
+
else if (state.view === 'threads')
|
|
850
|
+
state.threadCur = 0;
|
|
851
|
+
else
|
|
852
|
+
state.replyCur = 0;
|
|
853
|
+
return { kind: 'none' };
|
|
854
|
+
}
|
|
855
|
+
if (event.key === 'G') {
|
|
856
|
+
if (state.view === 'boards') {
|
|
857
|
+
state.boardCur = Math.max(0, cachedBoards.length - 1);
|
|
858
|
+
}
|
|
859
|
+
else if (state.view === 'threads') {
|
|
860
|
+
const list = cachedThreads.filter((t) => !state.filter || t.title.toLowerCase().includes(state.filter.toLowerCase()));
|
|
861
|
+
state.threadCur = Math.max(0, list.length - 1);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
const flatReplies = flattenReplies(cachedReplies);
|
|
865
|
+
state.replyCur = Math.max(0, flatReplies.length - 1);
|
|
866
|
+
}
|
|
867
|
+
return { kind: 'none' };
|
|
868
|
+
}
|
|
869
|
+
// ── Board view ──────────────────────────────────────────────────────────
|
|
194
870
|
if (state.view === 'boards') {
|
|
195
871
|
switch (event.key) {
|
|
196
872
|
case 'j':
|
|
@@ -214,25 +890,44 @@ export const communityPage = {
|
|
|
214
890
|
}
|
|
215
891
|
return { kind: 'none' };
|
|
216
892
|
}
|
|
217
|
-
case 'n':
|
|
893
|
+
case 'n': {
|
|
894
|
+
const boardId = (cachedBoards[state.boardCur]?.id ?? 'meta');
|
|
895
|
+
openComposer('new-thread');
|
|
896
|
+
if (state.composer)
|
|
897
|
+
state.composer.board = boardId;
|
|
898
|
+
return { kind: 'none' };
|
|
899
|
+
}
|
|
218
900
|
case '/':
|
|
219
|
-
|
|
901
|
+
openSearch('all');
|
|
902
|
+
return { kind: 'none' };
|
|
903
|
+
default:
|
|
220
904
|
return { kind: 'none' };
|
|
221
|
-
default: return { kind: 'none' };
|
|
222
905
|
}
|
|
223
906
|
}
|
|
907
|
+
// ── Thread list view ────────────────────────────────────────────────────
|
|
224
908
|
if (state.view === 'threads') {
|
|
909
|
+
const list = cachedThreads.filter((t) => !state.filter || t.title.toLowerCase().includes(state.filter.toLowerCase()));
|
|
225
910
|
switch (event.key) {
|
|
226
911
|
case 'j':
|
|
227
912
|
case 'down':
|
|
228
|
-
state.threadCur = Math.min(
|
|
913
|
+
state.threadCur = Math.min(list.length - 1, state.threadCur + 1);
|
|
229
914
|
return { kind: 'none' };
|
|
230
915
|
case 'k':
|
|
231
916
|
case 'up':
|
|
232
917
|
state.threadCur = Math.max(0, state.threadCur - 1);
|
|
233
918
|
return { kind: 'none' };
|
|
919
|
+
case 'o': {
|
|
920
|
+
const sortOrder = ['top', 'new', 'active'];
|
|
921
|
+
state.threadSort =
|
|
922
|
+
sortOrder[(sortOrder.indexOf(state.threadSort) + 1) % sortOrder.length] ?? 'top';
|
|
923
|
+
const board = state.board ?? cachedBoards[state.boardCur]?.id;
|
|
924
|
+
if (board) {
|
|
925
|
+
loadThreads(board, ctx).then(() => ctx.repaint()).catch(() => { });
|
|
926
|
+
}
|
|
927
|
+
return { kind: 'none' };
|
|
928
|
+
}
|
|
234
929
|
case 'enter': {
|
|
235
|
-
const t =
|
|
930
|
+
const t = list[state.threadCur];
|
|
236
931
|
if (t) {
|
|
237
932
|
const opts = buildSourceOptions(ctx);
|
|
238
933
|
const result = await communityThreadSource(opts, t.id);
|
|
@@ -243,16 +938,62 @@ export const communityPage = {
|
|
|
243
938
|
}
|
|
244
939
|
return { kind: 'none' };
|
|
245
940
|
}
|
|
941
|
+
case 'X': {
|
|
942
|
+
const t = list[state.threadCur];
|
|
943
|
+
if (t) {
|
|
944
|
+
if (state.expandedItems.has(t.id))
|
|
945
|
+
state.expandedItems.delete(t.id);
|
|
946
|
+
else
|
|
947
|
+
state.expandedItems.add(t.id);
|
|
948
|
+
}
|
|
949
|
+
return { kind: 'none' };
|
|
950
|
+
}
|
|
951
|
+
case '+':
|
|
952
|
+
case '=': {
|
|
953
|
+
const t = list[state.threadCur];
|
|
954
|
+
if (t)
|
|
955
|
+
await doVote(t.id, 'discussion', 1, ctx);
|
|
956
|
+
return { kind: 'none' };
|
|
957
|
+
}
|
|
958
|
+
case '-': {
|
|
959
|
+
const t = list[state.threadCur];
|
|
960
|
+
if (t)
|
|
961
|
+
await doVote(t.id, 'discussion', -1, ctx);
|
|
962
|
+
return { kind: 'none' };
|
|
963
|
+
}
|
|
964
|
+
case 'f': {
|
|
965
|
+
const t = list[state.threadCur];
|
|
966
|
+
if (t) {
|
|
967
|
+
state.flagModal = {
|
|
968
|
+
active: true,
|
|
969
|
+
targetId: t.id,
|
|
970
|
+
targetType: 'discussion',
|
|
971
|
+
awaitingNotes: false,
|
|
972
|
+
notes: ''
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
return { kind: 'none' };
|
|
976
|
+
}
|
|
246
977
|
case 'esc':
|
|
247
978
|
state.view = 'boards';
|
|
248
979
|
return { kind: 'none' };
|
|
249
|
-
case 'n':
|
|
250
|
-
|
|
251
|
-
|
|
980
|
+
case 'n': {
|
|
981
|
+
const boardId = (state.board ?? 'meta');
|
|
982
|
+
openComposer('new-thread');
|
|
983
|
+
if (state.composer)
|
|
984
|
+
state.composer.board = boardId;
|
|
985
|
+
return { kind: 'none' };
|
|
986
|
+
}
|
|
987
|
+
case '/': {
|
|
988
|
+
const scope = state.board ?? 'all';
|
|
989
|
+
openSearch(scope);
|
|
990
|
+
return { kind: 'none' };
|
|
991
|
+
}
|
|
992
|
+
default:
|
|
252
993
|
return { kind: 'none' };
|
|
253
|
-
default: return { kind: 'none' };
|
|
254
994
|
}
|
|
255
995
|
}
|
|
996
|
+
// ── Reader view ─────────────────────────────────────────────────────────
|
|
256
997
|
const flatReplies = flattenReplies(cachedReplies);
|
|
257
998
|
switch (event.key) {
|
|
258
999
|
case 'j':
|
|
@@ -266,11 +1007,88 @@ export const communityPage = {
|
|
|
266
1007
|
case 'esc':
|
|
267
1008
|
state.view = 'threads';
|
|
268
1009
|
return { kind: 'none' };
|
|
269
|
-
case '
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1010
|
+
case '/': {
|
|
1011
|
+
const scope = state.board ?? 'all';
|
|
1012
|
+
openSearch(scope);
|
|
1013
|
+
return { kind: 'none' };
|
|
1014
|
+
}
|
|
1015
|
+
case 'r': {
|
|
1016
|
+
const threadId = state.thread ?? '';
|
|
1017
|
+
if (state.replyCur >= 0 && flatReplies.length > 0) {
|
|
1018
|
+
const focused = flatReplies[state.replyCur];
|
|
1019
|
+
openComposer('reply', focused?.reply.id ?? threadId);
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
openComposer('reply', threadId);
|
|
1023
|
+
}
|
|
1024
|
+
return { kind: 'none' };
|
|
1025
|
+
}
|
|
1026
|
+
case '+':
|
|
1027
|
+
case '=': {
|
|
1028
|
+
if (flatReplies.length > 0) {
|
|
1029
|
+
const focused = flatReplies[state.replyCur];
|
|
1030
|
+
if (focused)
|
|
1031
|
+
await doVote(focused.reply.id, 'reply', 1, ctx);
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
const t = cachedThreads.find((x) => x.id === state.thread);
|
|
1035
|
+
if (t)
|
|
1036
|
+
await doVote(t.id, 'discussion', 1, ctx);
|
|
1037
|
+
}
|
|
1038
|
+
return { kind: 'none' };
|
|
1039
|
+
}
|
|
1040
|
+
case '-': {
|
|
1041
|
+
if (flatReplies.length > 0) {
|
|
1042
|
+
const focused = flatReplies[state.replyCur];
|
|
1043
|
+
if (focused)
|
|
1044
|
+
await doVote(focused.reply.id, 'reply', -1, ctx);
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
const t = cachedThreads.find((x) => x.id === state.thread);
|
|
1048
|
+
if (t)
|
|
1049
|
+
await doVote(t.id, 'discussion', -1, ctx);
|
|
1050
|
+
}
|
|
1051
|
+
return { kind: 'none' };
|
|
1052
|
+
}
|
|
1053
|
+
case 'f': {
|
|
1054
|
+
const focused = flatReplies[state.replyCur];
|
|
1055
|
+
if (focused) {
|
|
1056
|
+
state.flagModal = {
|
|
1057
|
+
active: true,
|
|
1058
|
+
targetId: focused.reply.id,
|
|
1059
|
+
targetType: 'reply',
|
|
1060
|
+
awaitingNotes: false,
|
|
1061
|
+
notes: ''
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
const t = cachedThreads.find((x) => x.id === state.thread);
|
|
1066
|
+
if (t) {
|
|
1067
|
+
state.flagModal = {
|
|
1068
|
+
active: true,
|
|
1069
|
+
targetId: t.id,
|
|
1070
|
+
targetType: 'discussion',
|
|
1071
|
+
awaitingNotes: false,
|
|
1072
|
+
notes: ''
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return { kind: 'none' };
|
|
1077
|
+
}
|
|
1078
|
+
case 'X': {
|
|
1079
|
+
const focused = flatReplies[state.replyCur];
|
|
1080
|
+
if (focused) {
|
|
1081
|
+
const id = focused.reply.id;
|
|
1082
|
+
if (state.expandedItems.has(id))
|
|
1083
|
+
state.expandedItems.delete(id);
|
|
1084
|
+
else
|
|
1085
|
+
state.expandedItems.add(id);
|
|
1086
|
+
}
|
|
1087
|
+
return { kind: 'none' };
|
|
1088
|
+
}
|
|
1089
|
+
default:
|
|
1090
|
+
return { kind: 'none' };
|
|
273
1091
|
}
|
|
274
|
-
}
|
|
1092
|
+
}
|
|
275
1093
|
};
|
|
276
1094
|
//# sourceMappingURL=community.js.map
|