@tenyokjs/devdevian-cli 0.1.0-alpha.1 → 0.3.0-alpha.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/README.md +43 -3
- package/bin/devdevian.mjs +22 -57
- package/lib/client.mjs +6 -3
- package/lib/drafts.mjs +119 -0
- package/lib/picker.mjs +421 -0
- package/lib/tui-data.mjs +202 -0
- package/lib/tui-model.mjs +168 -0
- package/lib/tui.mjs +2100 -0
- package/lib/window.mjs +119 -0
- package/lib/workspace.mjs +386 -0
- package/package.json +10 -1
package/lib/picker.mjs
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { basename, dirname } from "node:path";
|
|
4
|
+
import { clip, editText, graphemes } from "./tui-model.mjs";
|
|
5
|
+
import {
|
|
6
|
+
listFiles,
|
|
7
|
+
listChanges,
|
|
8
|
+
readCodeFile,
|
|
9
|
+
readDiff,
|
|
10
|
+
selectLines,
|
|
11
|
+
} from "./workspace.mjs";
|
|
12
|
+
const h = React.createElement,
|
|
13
|
+
c = {
|
|
14
|
+
text: "#e0e7ff",
|
|
15
|
+
muted: "#8190af",
|
|
16
|
+
blue: "#79b8ff",
|
|
17
|
+
purple: "#b29aff",
|
|
18
|
+
green: "#70d6b0",
|
|
19
|
+
red: "#ff9baf",
|
|
20
|
+
bg: "#12192b",
|
|
21
|
+
selected: "#29345b",
|
|
22
|
+
};
|
|
23
|
+
export function WorkspacePicker({
|
|
24
|
+
root,
|
|
25
|
+
width,
|
|
26
|
+
height,
|
|
27
|
+
initial = "files",
|
|
28
|
+
onSelect,
|
|
29
|
+
onCancel,
|
|
30
|
+
}) {
|
|
31
|
+
const [mode, setMode] = useState(initial),
|
|
32
|
+
[folder, setFolder] = useState("."),
|
|
33
|
+
[hidden, setHidden] = useState(false),
|
|
34
|
+
[staged, setStaged] = useState(false),
|
|
35
|
+
[query, setQuery] = useState(""),
|
|
36
|
+
[queryCursor, setQueryCursor] = useState(0),
|
|
37
|
+
[searching, setSearching] = useState(false),
|
|
38
|
+
[items, setItems] = useState([]),
|
|
39
|
+
[index, setIndex] = useState(0),
|
|
40
|
+
[loading, setLoading] = useState(false),
|
|
41
|
+
[error, setError] = useState(""),
|
|
42
|
+
[selected, setSelected] = useState(null),
|
|
43
|
+
[line, setLine] = useState(0),
|
|
44
|
+
[anchor, setAnchor] = useState(null),
|
|
45
|
+
[hunk, setHunk] = useState(-1),
|
|
46
|
+
[horizontal, setHorizontal] = useState(0);
|
|
47
|
+
const alive = useRef(true),
|
|
48
|
+
selectionController = useRef(null);
|
|
49
|
+
useEffect(
|
|
50
|
+
() => () => {
|
|
51
|
+
alive.current = false;
|
|
52
|
+
selectionController.current?.abort();
|
|
53
|
+
},
|
|
54
|
+
[],
|
|
55
|
+
);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
setLoading(true);
|
|
59
|
+
setError("");
|
|
60
|
+
setItems([]);
|
|
61
|
+
setIndex(0);
|
|
62
|
+
const timer = setTimeout(
|
|
63
|
+
() => {
|
|
64
|
+
const load =
|
|
65
|
+
mode === "files"
|
|
66
|
+
? listFiles(root, folder, {
|
|
67
|
+
hidden,
|
|
68
|
+
query,
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
})
|
|
71
|
+
: listChanges(root, { staged, query, signal: controller.signal });
|
|
72
|
+
load
|
|
73
|
+
.then((rows) => {
|
|
74
|
+
if (!controller.signal.aborted) setItems(rows);
|
|
75
|
+
})
|
|
76
|
+
.catch((e) => {
|
|
77
|
+
if (!controller.signal.aborted) setError(e.message);
|
|
78
|
+
})
|
|
79
|
+
.finally(() => {
|
|
80
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
query ? 120 : 0,
|
|
84
|
+
);
|
|
85
|
+
return () => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
controller.abort();
|
|
88
|
+
};
|
|
89
|
+
}, [root, folder, hidden, mode, staged, query]);
|
|
90
|
+
const content = selected
|
|
91
|
+
? selected.kind === "diff"
|
|
92
|
+
? hunk < 0
|
|
93
|
+
? selected.patch
|
|
94
|
+
: selected.hunks[hunk].patch
|
|
95
|
+
: selected.code
|
|
96
|
+
: "",
|
|
97
|
+
lines = content.split("\n"),
|
|
98
|
+
panelHeight = Math.max(6, height - 2),
|
|
99
|
+
viewHeight = Math.max(1, panelHeight - 6),
|
|
100
|
+
split = !!selected && width >= 110,
|
|
101
|
+
listWidth = split ? 36 : width,
|
|
102
|
+
previewWidth = split ? width - listWidth - 1 : width;
|
|
103
|
+
function clearSearch() {
|
|
104
|
+
setQuery("");
|
|
105
|
+
setQueryCursor(0);
|
|
106
|
+
setSearching(false);
|
|
107
|
+
}
|
|
108
|
+
async function choose() {
|
|
109
|
+
const item = items[index];
|
|
110
|
+
if (!item || loading) return;
|
|
111
|
+
if (item.directory) {
|
|
112
|
+
setFolder(item.path);
|
|
113
|
+
clearSearch();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setLoading(true);
|
|
117
|
+
setError("");
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
selectionController.current = controller;
|
|
120
|
+
try {
|
|
121
|
+
const data =
|
|
122
|
+
mode === "files"
|
|
123
|
+
? await readCodeFile(root, item.path)
|
|
124
|
+
: await readDiff(root, item.path, {
|
|
125
|
+
staged,
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
if (!alive.current) return;
|
|
129
|
+
setSelected({ ...data, kind: mode === "files" ? "file" : "diff" });
|
|
130
|
+
setLine(0);
|
|
131
|
+
setAnchor(null);
|
|
132
|
+
setHunk(-1);
|
|
133
|
+
setHorizontal(0);
|
|
134
|
+
setSearching(false);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (alive.current) setError(e.message);
|
|
137
|
+
} finally {
|
|
138
|
+
if (alive.current) setLoading(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function accept() {
|
|
142
|
+
try {
|
|
143
|
+
if (selected.kind === "file") {
|
|
144
|
+
const start = anchor === null ? null : Math.min(anchor, line) + 1,
|
|
145
|
+
end = anchor === null ? null : Math.max(anchor, line) + 1;
|
|
146
|
+
selectLines(selected.code, start || 1, end || lines.length);
|
|
147
|
+
onSelect({ kind: "file", path: selected.path, start, end });
|
|
148
|
+
} else {
|
|
149
|
+
const part = hunk < 0 ? selected : selected.hunks[hunk];
|
|
150
|
+
selectLines(part.patch);
|
|
151
|
+
onSelect({
|
|
152
|
+
kind: "diff",
|
|
153
|
+
path: selected.path,
|
|
154
|
+
staged: selected.staged,
|
|
155
|
+
hunk: hunk < 0 ? null : hunk,
|
|
156
|
+
hash: part.hash,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
setError(e.message);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
useInput((input, key) => {
|
|
164
|
+
if (key.ctrl && input === "c") {
|
|
165
|
+
onCancel();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (searching) {
|
|
169
|
+
if (key.escape) {
|
|
170
|
+
setSearching(false);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (key.return) {
|
|
174
|
+
setSearching(false);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const next = editText(query, queryCursor, input, key, { max: 160 });
|
|
178
|
+
setQuery(next.value);
|
|
179
|
+
setQueryCursor(next.cursor);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (selected) {
|
|
183
|
+
if (key.escape || key.backspace) {
|
|
184
|
+
setSelected(null);
|
|
185
|
+
setError("");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (key.upArrow || key.downArrow || key.pageUp || key.pageDown) {
|
|
189
|
+
const step = key.pageUp || key.pageDown ? viewHeight : 1,
|
|
190
|
+
delta = key.upArrow || key.pageUp ? -step : step;
|
|
191
|
+
setLine((v) => Math.max(0, Math.min(lines.length - 1, v + delta)));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (key.leftArrow || key.rightArrow) {
|
|
195
|
+
setHorizontal((v) => Math.max(0, v + (key.leftArrow ? -8 : 8)));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (input === " " && selected.kind === "file") {
|
|
199
|
+
setAnchor((v) => (v === null ? line : null));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if ((input === "[" || input === "]") && selected.kind === "diff") {
|
|
203
|
+
setHunk((v) =>
|
|
204
|
+
Math.max(
|
|
205
|
+
-1,
|
|
206
|
+
Math.min(selected.hunks.length - 1, v + (input === "]" ? 1 : -1)),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
setLine(0);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (key.return) accept();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (key.escape) {
|
|
216
|
+
onCancel();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (loading) return;
|
|
220
|
+
if (key.upArrow || key.downArrow || key.pageUp || key.pageDown) {
|
|
221
|
+
const step = key.pageUp || key.pageDown ? viewHeight : 1,
|
|
222
|
+
delta = key.upArrow || key.pageUp ? -step : step;
|
|
223
|
+
setIndex((v) => Math.max(0, Math.min(items.length - 1, v + delta)));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (key.return) {
|
|
227
|
+
choose();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (key.leftArrow || key.backspace) {
|
|
231
|
+
setFolder(folder === "." ? "." : dirname(folder));
|
|
232
|
+
clearSearch();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (input === "/") {
|
|
236
|
+
setSearching(true);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (input === "g") {
|
|
240
|
+
setMode((v) => (v === "files" ? "changes" : "files"));
|
|
241
|
+
clearSearch();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (input === "t" && mode === "changes") {
|
|
245
|
+
setStaged((v) => !v);
|
|
246
|
+
clearSearch();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (input === "h" && mode === "files") setHidden((v) => !v);
|
|
250
|
+
});
|
|
251
|
+
function row(text, color = c.text, key, bg) {
|
|
252
|
+
return h(
|
|
253
|
+
Box,
|
|
254
|
+
{ key, width: "100%", backgroundColor: bg },
|
|
255
|
+
h(
|
|
256
|
+
Text,
|
|
257
|
+
{ color, wrap: "truncate" },
|
|
258
|
+
clip(
|
|
259
|
+
text,
|
|
260
|
+
(selected && (!split || key?.startsWith?.("code"))
|
|
261
|
+
? previewWidth
|
|
262
|
+
: listWidth) - 4,
|
|
263
|
+
),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const visible = viewHeight,
|
|
268
|
+
first = Math.max(0, index - visible + 1);
|
|
269
|
+
const browser = h(
|
|
270
|
+
Box,
|
|
271
|
+
{
|
|
272
|
+
width: listWidth,
|
|
273
|
+
height: panelHeight,
|
|
274
|
+
key: "browser",
|
|
275
|
+
flexDirection: "column",
|
|
276
|
+
borderStyle: "round",
|
|
277
|
+
borderColor: c.purple,
|
|
278
|
+
paddingX: 1,
|
|
279
|
+
overflow: "hidden",
|
|
280
|
+
},
|
|
281
|
+
row("PROJECT · " + basename(root), c.purple, "title"),
|
|
282
|
+
row(
|
|
283
|
+
mode === "files"
|
|
284
|
+
? "Files · " + folder
|
|
285
|
+
: "Git changes · " + (staged ? "staged" : "unstaged"),
|
|
286
|
+
c.blue,
|
|
287
|
+
"mode",
|
|
288
|
+
),
|
|
289
|
+
row(
|
|
290
|
+
searching ? "Find: " + query + "▏" : query ? "Filter: " + query : " ",
|
|
291
|
+
c.muted,
|
|
292
|
+
"query",
|
|
293
|
+
),
|
|
294
|
+
h(
|
|
295
|
+
Box,
|
|
296
|
+
{ height: visible, flexDirection: "column", overflow: "hidden" },
|
|
297
|
+
...(items.length
|
|
298
|
+
? items
|
|
299
|
+
.slice(first, first + visible)
|
|
300
|
+
.map((item, i) =>
|
|
301
|
+
row(
|
|
302
|
+
(first + i === index ? "› " : " ") +
|
|
303
|
+
(item.directory ? "▸ " : "") +
|
|
304
|
+
item.name +
|
|
305
|
+
(mode === "changes"
|
|
306
|
+
? ` +${item.added} −${item.removed}`
|
|
307
|
+
: ""),
|
|
308
|
+
first + i === index ? c.blue : c.text,
|
|
309
|
+
"entry" + i,
|
|
310
|
+
first + i === index ? c.selected : undefined,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
: [
|
|
314
|
+
row(
|
|
315
|
+
loading ? "Reading project…" : "No matching files or changes.",
|
|
316
|
+
c.muted,
|
|
317
|
+
"empty",
|
|
318
|
+
),
|
|
319
|
+
]),
|
|
320
|
+
),
|
|
321
|
+
row(
|
|
322
|
+
`${Math.min(index + 1, items.length)} / ${items.length}`,
|
|
323
|
+
c.muted,
|
|
324
|
+
"count",
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
const start = Math.max(
|
|
328
|
+
0,
|
|
329
|
+
Math.min(line - Math.floor(viewHeight / 2), lines.length - viewHeight),
|
|
330
|
+
),
|
|
331
|
+
from = Math.min(anchor ?? line, line),
|
|
332
|
+
to = Math.max(anchor ?? line, line);
|
|
333
|
+
const preview = selected
|
|
334
|
+
? h(
|
|
335
|
+
Box,
|
|
336
|
+
{
|
|
337
|
+
width: previewWidth,
|
|
338
|
+
height: panelHeight,
|
|
339
|
+
key: "preview",
|
|
340
|
+
flexDirection: "column",
|
|
341
|
+
borderStyle: "round",
|
|
342
|
+
borderColor: c.blue,
|
|
343
|
+
paddingX: 1,
|
|
344
|
+
overflow: "hidden",
|
|
345
|
+
},
|
|
346
|
+
row(selected.path, c.purple, "code-title"),
|
|
347
|
+
row(
|
|
348
|
+
selected.kind === "diff"
|
|
349
|
+
? (staged ? "Staged" : "Unstaged") +
|
|
350
|
+
" · " +
|
|
351
|
+
(hunk < 0
|
|
352
|
+
? "All hunks"
|
|
353
|
+
: `Hunk ${hunk + 1}/${selected.hunks.length}`)
|
|
354
|
+
: anchor === null
|
|
355
|
+
? "Whole file · Space selects a range"
|
|
356
|
+
: `Selected lines ${from + 1}–${to + 1}`,
|
|
357
|
+
c.blue,
|
|
358
|
+
"code-mode",
|
|
359
|
+
),
|
|
360
|
+
row(" ", c.muted, "code-blank"),
|
|
361
|
+
h(
|
|
362
|
+
Box,
|
|
363
|
+
{ height: viewHeight, flexDirection: "column", overflow: "hidden" },
|
|
364
|
+
...lines.slice(start, start + viewHeight).map((text, i) => {
|
|
365
|
+
const n = start + i,
|
|
366
|
+
highlight =
|
|
367
|
+
n === line || (anchor !== null && n >= from && n <= to),
|
|
368
|
+
color =
|
|
369
|
+
selected.kind === "diff"
|
|
370
|
+
? text.startsWith("+")
|
|
371
|
+
? c.green
|
|
372
|
+
: text.startsWith("-")
|
|
373
|
+
? c.red
|
|
374
|
+
: c.text
|
|
375
|
+
: c.text;
|
|
376
|
+
return row(
|
|
377
|
+
(n === line ? "›" : " ") +
|
|
378
|
+
String(n + 1).padStart(4) +
|
|
379
|
+
" " +
|
|
380
|
+
graphemes(text).slice(horizontal).join(""),
|
|
381
|
+
color,
|
|
382
|
+
"code" + i,
|
|
383
|
+
highlight ? c.selected : undefined,
|
|
384
|
+
);
|
|
385
|
+
}),
|
|
386
|
+
),
|
|
387
|
+
row(
|
|
388
|
+
`${line + 1}/${lines.length} · Enter uses selection`,
|
|
389
|
+
c.muted,
|
|
390
|
+
"code-footer",
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
: null;
|
|
394
|
+
return h(
|
|
395
|
+
Box,
|
|
396
|
+
{
|
|
397
|
+
flexDirection: "column",
|
|
398
|
+
height,
|
|
399
|
+
overflow: "hidden",
|
|
400
|
+
backgroundColor: c.bg,
|
|
401
|
+
},
|
|
402
|
+
h(
|
|
403
|
+
Box,
|
|
404
|
+
{ height: height - 2, flexDirection: "row", gap: split ? 1 : 0 },
|
|
405
|
+
selected ? (split ? [browser, preview] : preview) : browser,
|
|
406
|
+
),
|
|
407
|
+
h(
|
|
408
|
+
Text,
|
|
409
|
+
{ color: error ? c.red : c.muted, wrap: "truncate" },
|
|
410
|
+
clip(
|
|
411
|
+
error ||
|
|
412
|
+
(selected
|
|
413
|
+
? selected.kind === "file"
|
|
414
|
+
? "↑↓ move · Space start/reset range · Enter use · Esc files"
|
|
415
|
+
: "[ ] all / individual hunks · Enter use · Esc changes"
|
|
416
|
+
: "G files / git diff · T staged / unstaged · H hidden · / filter · Esc cancel"),
|
|
417
|
+
width,
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
}
|
package/lib/tui-data.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export const navigation = [
|
|
2
|
+
["inbox", "Inbox", "Replies & mentions"],
|
|
3
|
+
["feed", "Public feed", "Developer commons"],
|
|
4
|
+
["open", "Open conversation", "Jump to a post"],
|
|
5
|
+
["ask", "Ask a question", "Find a way forward"],
|
|
6
|
+
["review", "Request code review", "Another pair of eyes"],
|
|
7
|
+
["drafts", "Local drafts", "Resume your writing"],
|
|
8
|
+
["communities", "Communities", "Your private spaces"],
|
|
9
|
+
["search", "Search", "Find a conversation"],
|
|
10
|
+
["devices", "Connected terminals", "Manage CLI access"],
|
|
11
|
+
["help", "Help", "Keys & commands"],
|
|
12
|
+
];
|
|
13
|
+
export async function loadPage(api, route, signal) {
|
|
14
|
+
const get = (path) => api(path, { signal }),
|
|
15
|
+
page = route.page || 1;
|
|
16
|
+
if (route.view === "feed")
|
|
17
|
+
return {
|
|
18
|
+
...(await get("/posts?limit=12&page=" + page)),
|
|
19
|
+
title: "Public feed",
|
|
20
|
+
subtitle: "Questions, ideas and code worth discussing.",
|
|
21
|
+
};
|
|
22
|
+
if (route.view === "inbox")
|
|
23
|
+
return {
|
|
24
|
+
...(await get("/notifications?page=" + page)),
|
|
25
|
+
title: "Inbox",
|
|
26
|
+
subtitle: "Your replies, mentions and review updates.",
|
|
27
|
+
};
|
|
28
|
+
if (route.view === "communities")
|
|
29
|
+
return {
|
|
30
|
+
...(await get("/communities?limit=12&page=" + page)),
|
|
31
|
+
title: "Communities",
|
|
32
|
+
subtitle: "Private spaces, shared context. Enter opens a community.",
|
|
33
|
+
};
|
|
34
|
+
if (route.view === "search")
|
|
35
|
+
return {
|
|
36
|
+
...(await get(
|
|
37
|
+
"/search?type=" +
|
|
38
|
+
(route.type || "posts") +
|
|
39
|
+
"&q=" +
|
|
40
|
+
encodeURIComponent(route.query || "") +
|
|
41
|
+
"&page=" +
|
|
42
|
+
page,
|
|
43
|
+
)),
|
|
44
|
+
title: "Search",
|
|
45
|
+
subtitle: route.query
|
|
46
|
+
? "Results for “" + route.query + "”"
|
|
47
|
+
: "Press / and type /search followed by your query.",
|
|
48
|
+
};
|
|
49
|
+
if (route.view === "devices")
|
|
50
|
+
return {
|
|
51
|
+
...(await get("/cli/sessions")),
|
|
52
|
+
title: "Connected terminals",
|
|
53
|
+
subtitle: "Enter to revoke a terminal. Revoking this one signs it out.",
|
|
54
|
+
};
|
|
55
|
+
if (route.view === "community") {
|
|
56
|
+
const community = await get("/communities/" + encodeURIComponent(route.id));
|
|
57
|
+
const posts = community.is_member
|
|
58
|
+
? await get(
|
|
59
|
+
"/posts?limit=12&community=" +
|
|
60
|
+
encodeURIComponent(route.id) +
|
|
61
|
+
"&page=" +
|
|
62
|
+
page,
|
|
63
|
+
)
|
|
64
|
+
: { items: [] };
|
|
65
|
+
return {
|
|
66
|
+
...posts,
|
|
67
|
+
community,
|
|
68
|
+
title: community.name,
|
|
69
|
+
subtitle: community.is_member
|
|
70
|
+
? "Members-only conversations. N creates a conversation here."
|
|
71
|
+
: community.requested
|
|
72
|
+
? "Your access request is pending."
|
|
73
|
+
: "Private community. J requests access.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (route.view === "post") {
|
|
77
|
+
const post = await get("/posts/" + route.id + "?reply_page=" + page);
|
|
78
|
+
let items = post.replies,
|
|
79
|
+
pagination = post.reply_pagination;
|
|
80
|
+
if (route.tab === "feedback") {
|
|
81
|
+
const feedback = await get(
|
|
82
|
+
"/posts/" + route.id + "/code-reviews?page=" + page,
|
|
83
|
+
);
|
|
84
|
+
items = feedback.items;
|
|
85
|
+
pagination = feedback.pagination;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
post,
|
|
89
|
+
items,
|
|
90
|
+
pagination,
|
|
91
|
+
title: post.title,
|
|
92
|
+
subtitle:
|
|
93
|
+
(post.community_id ? "Private community" : "Public feed") +
|
|
94
|
+
" · " +
|
|
95
|
+
post.kind +
|
|
96
|
+
" · by " +
|
|
97
|
+
post.name,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (route.view === "thread") {
|
|
101
|
+
const post = await get("/posts/" + route.postId),
|
|
102
|
+
replies = await get(
|
|
103
|
+
`/posts/${route.postId}/replies?parent_id=${route.parent.id}&page=${page}`,
|
|
104
|
+
);
|
|
105
|
+
return {
|
|
106
|
+
post,
|
|
107
|
+
...replies,
|
|
108
|
+
title: "Reply thread",
|
|
109
|
+
subtitle: "Replying to " + route.parent.name,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (route.view === "feedback") {
|
|
113
|
+
const [post, notes] = await Promise.all([
|
|
114
|
+
get("/posts/" + route.postId),
|
|
115
|
+
get(`/posts/${route.postId}/code-reviews?target_id=${route.id}`),
|
|
116
|
+
]);
|
|
117
|
+
const note = notes.items.find((item) => item.id === Number(route.id));
|
|
118
|
+
if (!note) throw Error("This feedback is no longer available.");
|
|
119
|
+
const replies = await get(
|
|
120
|
+
`/posts/${route.postId}/code-reviews/${route.id}/replies?page=${page}${route.targetReply ? "&target_id=" + route.targetReply : ""}`,
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
post,
|
|
124
|
+
note,
|
|
125
|
+
items: [
|
|
126
|
+
{ ...note, entryType: "note" },
|
|
127
|
+
...replies.items.map((item) => ({
|
|
128
|
+
...item,
|
|
129
|
+
entryType: "feedbackReply",
|
|
130
|
+
})),
|
|
131
|
+
],
|
|
132
|
+
pagination: replies.pagination,
|
|
133
|
+
title: "Code feedback",
|
|
134
|
+
subtitle: `Lines ${note.start_line}–${note.end_line} · ${note.resolved ? "resolved" : "open"}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { items: [], title: "Your workspace", subtitle: "" };
|
|
138
|
+
}
|
|
139
|
+
export function contentLines(route, data) {
|
|
140
|
+
if (route.view === "help")
|
|
141
|
+
return [
|
|
142
|
+
"A developer workspace, in your terminal.",
|
|
143
|
+
"",
|
|
144
|
+
"Tab Switch navigation / content / command bar",
|
|
145
|
+
"↑ ↓ Select a section, conversation or reply",
|
|
146
|
+
"Enter Open the selected item",
|
|
147
|
+
"Esc Back / leave an input",
|
|
148
|
+
"← → Switch conversation tabs",
|
|
149
|
+
"Page Up / Down Scroll a conversation or list",
|
|
150
|
+
"N / A New conversation / ask a question",
|
|
151
|
+
"R Reply in a conversation; refresh a list",
|
|
152
|
+
"C Add feedback on code lines",
|
|
153
|
+
"S Resolve / reopen selected code feedback",
|
|
154
|
+
"E / D Edit / delete your own feedback or reply",
|
|
155
|
+
"O Open this conversation in your browser",
|
|
156
|
+
"/ Focus the command / search bar",
|
|
157
|
+
"Ctrl+S Preview the current form",
|
|
158
|
+
"Ctrl+O / Ctrl+G Select a code file / Git diff in a review form",
|
|
159
|
+
"/drafts Restore locally saved drafts for this account/project",
|
|
160
|
+
"Ctrl+C Exit and restore the original terminal",
|
|
161
|
+
"",
|
|
162
|
+
"Commands: /feed /inbox /search words /open 142",
|
|
163
|
+
" /ask /review /communities /devices /logout /quit",
|
|
164
|
+
"",
|
|
165
|
+
"Text fields: arrows move the caret; Home/End move within a line.",
|
|
166
|
+
"Multiline fields: Enter adds a line; Tab changes field.",
|
|
167
|
+
"Nothing is published until you confirm the preview.",
|
|
168
|
+
"A -> Z: your browser account and this terminal use the same data.",
|
|
169
|
+
];
|
|
170
|
+
if (route.view === "community" && !data.community?.is_member)
|
|
171
|
+
return [
|
|
172
|
+
data.community?.description || "",
|
|
173
|
+
"",
|
|
174
|
+
data.community?.requested
|
|
175
|
+
? "Access requested. The owner or validators will review it."
|
|
176
|
+
: "Press J to request access.",
|
|
177
|
+
];
|
|
178
|
+
if (route.view === "post" && (!route.tab || route.tab === "overview")) {
|
|
179
|
+
const p = data.post;
|
|
180
|
+
if (!p) return [];
|
|
181
|
+
const lines = [p.body || ""];
|
|
182
|
+
if (p.review_focus) lines.push("", "REVIEW FOCUS", p.review_focus);
|
|
183
|
+
if (p.code)
|
|
184
|
+
lines.push(
|
|
185
|
+
"",
|
|
186
|
+
"CODE SNIPPET",
|
|
187
|
+
...p.code
|
|
188
|
+
.split("\n")
|
|
189
|
+
.map((line, index) => String(index + 1).padStart(4) + " " + line),
|
|
190
|
+
);
|
|
191
|
+
if (p.links?.length)
|
|
192
|
+
lines.push(
|
|
193
|
+
"",
|
|
194
|
+
"LINKS",
|
|
195
|
+
...p.links.map((link) => link.label + " · " + link.url),
|
|
196
|
+
);
|
|
197
|
+
if (p.archived_at || p.locked_at)
|
|
198
|
+
lines.push("", "Replies are paused for this conversation.");
|
|
199
|
+
return lines;
|
|
200
|
+
}
|
|
201
|
+
return [];
|
|
202
|
+
}
|