@tenyokjs/devdevian-cli 0.3.1-alpha.1 → 0.4.1-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 +18 -1
- package/VERSIONS.md +57 -0
- package/bin/devdevian.mjs +8 -25
- package/lib/client.mjs +12 -4
- package/lib/context-actions.mjs +53 -0
- package/lib/external-editor.mjs +111 -0
- package/lib/live.mjs +52 -0
- package/lib/picker.mjs +2 -10
- package/lib/problems.mjs +29 -0
- package/lib/review-workspace.mjs +163 -0
- package/lib/rich-text.mjs +96 -0
- package/lib/theme.mjs +13 -0
- package/lib/tui-data.mjs +69 -2
- package/lib/tui.mjs +643 -97
- package/package.json +2 -1
package/lib/tui.mjs
CHANGED
|
@@ -24,20 +24,22 @@ import {
|
|
|
24
24
|
parseCommand,
|
|
25
25
|
PageCache,
|
|
26
26
|
} from "./tui-model.mjs";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
navigation,
|
|
29
|
+
loadPage,
|
|
30
|
+
contentLines,
|
|
31
|
+
assertSnippet,
|
|
32
|
+
notificationRoute,
|
|
33
|
+
} from "./tui-data.mjs";
|
|
34
|
+
import { basename } from "node:path";
|
|
35
|
+
import { colors } from "./theme.mjs";
|
|
36
|
+
import { liveLoop, anchoredIndex } from "./live.mjs";
|
|
37
|
+
import { contextActions } from "./context-actions.mjs";
|
|
38
|
+
import { editExternal } from "./external-editor.mjs";
|
|
39
|
+
import { markdownRows } from "./rich-text.mjs";
|
|
40
|
+
import { problemMessage } from "./problems.mjs";
|
|
41
|
+
import { ReviewWorkspace, codeRange } from "./review-workspace.mjs";
|
|
28
42
|
const h = React.createElement;
|
|
29
|
-
const colors = {
|
|
30
|
-
bg: "#0d1222",
|
|
31
|
-
panel: "#12192b",
|
|
32
|
-
line: "#354266",
|
|
33
|
-
muted: "#8190af",
|
|
34
|
-
text: "#e0e7ff",
|
|
35
|
-
blue: "#79b8ff",
|
|
36
|
-
purple: "#b29aff",
|
|
37
|
-
green: "#70d6b0",
|
|
38
|
-
red: "#ff9baf",
|
|
39
|
-
selected: "#29345b",
|
|
40
|
-
};
|
|
41
43
|
const itemsOf = (value) => (Array.isArray(value) ? value : value?.items || []);
|
|
42
44
|
const stamp = (value) => {
|
|
43
45
|
const date = new Date(
|
|
@@ -196,13 +198,30 @@ function FrameRows({ rows, width, height, offset = 0 }) {
|
|
|
196
198
|
Box,
|
|
197
199
|
{ flexDirection: "column", height, overflow: "hidden" },
|
|
198
200
|
...rows.slice(offset, offset + height).map((row, i) =>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
row.spans
|
|
202
|
+
? h(
|
|
203
|
+
Text,
|
|
204
|
+
{
|
|
205
|
+
key: i,
|
|
206
|
+
color: row.color || colors.text,
|
|
207
|
+
bold: row.bold,
|
|
208
|
+
wrap: "truncate",
|
|
209
|
+
},
|
|
210
|
+
...row.spans.map((span, n) =>
|
|
211
|
+
h(
|
|
212
|
+
Text,
|
|
213
|
+
{ key: n, color: span.color, bold: span.bold || row.bold },
|
|
214
|
+
span.text,
|
|
215
|
+
),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
: h(TextLine, {
|
|
219
|
+
key: i,
|
|
220
|
+
text: typeof row === "string" ? row : row.text,
|
|
221
|
+
width,
|
|
222
|
+
color: row.color || colors.text,
|
|
223
|
+
bold: row.bold || false,
|
|
224
|
+
}),
|
|
206
225
|
),
|
|
207
226
|
);
|
|
208
227
|
}
|
|
@@ -335,7 +354,10 @@ function listRows(route, data, index, width) {
|
|
|
335
354
|
? " · original remark"
|
|
336
355
|
: item.entryType === "parent"
|
|
337
356
|
? " · parent reply"
|
|
338
|
-
: " · reply #" + item.id)
|
|
357
|
+
: " · reply #" + item.id) +
|
|
358
|
+
(data.post?.accepted_reply_id === item.id
|
|
359
|
+
? " · accepted solution"
|
|
360
|
+
: "");
|
|
339
361
|
meta =
|
|
340
362
|
stamp(item.created) +
|
|
341
363
|
((item.edited_at || item.updated_at) && !item.deleted_at
|
|
@@ -355,7 +377,7 @@ function listRows(route, data, index, width) {
|
|
|
355
377
|
const selected = i === index;
|
|
356
378
|
return [
|
|
357
379
|
{
|
|
358
|
-
text: (selected ? "
|
|
380
|
+
text: (selected ? "▸ " : " ") + clip(title, width - 3),
|
|
359
381
|
color: selected ? colors.blue : colors.text,
|
|
360
382
|
bold: selected,
|
|
361
383
|
selected,
|
|
@@ -366,12 +388,51 @@ function listRows(route, data, index, width) {
|
|
|
366
388
|
color: selected ? colors.text : colors.muted,
|
|
367
389
|
selected,
|
|
368
390
|
},
|
|
369
|
-
"",
|
|
391
|
+
{ text: " " + "─".repeat(Math.max(0, width - 4)), color: colors.line },
|
|
370
392
|
];
|
|
371
393
|
});
|
|
372
394
|
}
|
|
395
|
+
function documentRows(route, data, width) {
|
|
396
|
+
if (route.view !== "post" || (route.tab && route.tab !== "overview"))
|
|
397
|
+
return contentLines(route, data).flatMap((line) => wrap(line, width));
|
|
398
|
+
const post = data?.post;
|
|
399
|
+
if (!post) return [];
|
|
400
|
+
const rows = markdownRows(post.body || "", width);
|
|
401
|
+
if (post.review_focus)
|
|
402
|
+
rows.push(
|
|
403
|
+
{ text: "" },
|
|
404
|
+
{ text: "REVIEW FOCUS", color: colors.purple, bold: true },
|
|
405
|
+
...markdownRows(post.review_focus, width),
|
|
406
|
+
);
|
|
407
|
+
if (post.code) {
|
|
408
|
+
// Pick a fence longer than any backtick run in the snippet.
|
|
409
|
+
const fence = "`".repeat(
|
|
410
|
+
Math.max(
|
|
411
|
+
3,
|
|
412
|
+
...(post.code.match(/`+/g) || []).map((run) => run.length + 1),
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
rows.push(
|
|
416
|
+
{ text: "" },
|
|
417
|
+
...markdownRows(fence + "\n" + post.code + "\n" + fence, width),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (post.links?.length)
|
|
421
|
+
rows.push(
|
|
422
|
+
{ text: "LINKS", bold: true },
|
|
423
|
+
...post.links.flatMap((link) =>
|
|
424
|
+
wrap(link.label + " · " + link.url, width),
|
|
425
|
+
),
|
|
426
|
+
);
|
|
427
|
+
if (post.archived_at || post.locked_at)
|
|
428
|
+
rows.push({
|
|
429
|
+
text: "Replies are paused for this conversation.",
|
|
430
|
+
color: colors.muted,
|
|
431
|
+
});
|
|
432
|
+
return rows;
|
|
433
|
+
}
|
|
373
434
|
function App({ initialConfig, flags, root }) {
|
|
374
|
-
const { exit } = useApp(),
|
|
435
|
+
const { exit, suspendTerminal } = useApp(),
|
|
375
436
|
{ columns, rows } = useWindowSize();
|
|
376
437
|
const [config, setConfig] = useState(initialConfig),
|
|
377
438
|
[phase, setPhase] = useState(
|
|
@@ -402,9 +463,22 @@ function App({ initialConfig, flags, root }) {
|
|
|
402
463
|
[tick, setTick] = useState(0),
|
|
403
464
|
[reading, setReading] = useState(null),
|
|
404
465
|
[picker, setPicker] = useState(null),
|
|
405
|
-
[draftSaved, setDraftSaved] = useState(false)
|
|
466
|
+
[draftSaved, setDraftSaved] = useState(false),
|
|
467
|
+
[actionIndex, setActionIndex] = useState(null),
|
|
468
|
+
[reviewFocus, setReviewFocus] = useState("notes"),
|
|
469
|
+
[reviewLine, setReviewLine] = useState(0),
|
|
470
|
+
[reviewAnchor, setReviewAnchor] = useState(null),
|
|
471
|
+
[reviewHorizontal, setReviewHorizontal] = useState(0),
|
|
472
|
+
[live, setLive] = useState(!flags["no-live"]),
|
|
473
|
+
[unread, setUnread] = useState(0),
|
|
474
|
+
[liveStatus, setLiveStatus] = useState("");
|
|
475
|
+
const activity = useRef(Date.now()),
|
|
476
|
+
liveView = useRef(null),
|
|
477
|
+
notificationGeneration = useRef(0);
|
|
406
478
|
const cache = useRef(new PageCache()),
|
|
407
479
|
history = useRef([]),
|
|
480
|
+
restoreSelection = useRef(null),
|
|
481
|
+
seenSnippet = useRef(null),
|
|
408
482
|
busy = useRef(false),
|
|
409
483
|
alive = useRef(true),
|
|
410
484
|
authController = useRef(null),
|
|
@@ -432,6 +506,97 @@ function App({ initialConfig, flags, root }) {
|
|
|
432
506
|
const key = JSON.stringify(route),
|
|
433
507
|
current = data?.key === key ? data : null;
|
|
434
508
|
const selected = current?.items?.[index];
|
|
509
|
+
const reviewing =
|
|
510
|
+
!!current?.post?.code &&
|
|
511
|
+
(route.view === "feedback" ||
|
|
512
|
+
(route.view === "post" && route.tab === "feedback"));
|
|
513
|
+
const actions = contextActions({
|
|
514
|
+
route,
|
|
515
|
+
data: current,
|
|
516
|
+
selected,
|
|
517
|
+
user,
|
|
518
|
+
live,
|
|
519
|
+
});
|
|
520
|
+
liveView.current = {
|
|
521
|
+
route,
|
|
522
|
+
key,
|
|
523
|
+
current,
|
|
524
|
+
index,
|
|
525
|
+
blocked: !!(
|
|
526
|
+
form ||
|
|
527
|
+
preview ||
|
|
528
|
+
picker ||
|
|
529
|
+
dialog ||
|
|
530
|
+
reading ||
|
|
531
|
+
actionIndex !== null ||
|
|
532
|
+
loading ||
|
|
533
|
+
pending ||
|
|
534
|
+
focus === "prompt"
|
|
535
|
+
),
|
|
536
|
+
};
|
|
537
|
+
useEffect(() => {
|
|
538
|
+
if (phase !== "ready" || !api || !live) return;
|
|
539
|
+
return liveLoop(
|
|
540
|
+
async (signal) => {
|
|
541
|
+
const generation = notificationGeneration.current;
|
|
542
|
+
const count = await api("/notifications?limit=1", { signal });
|
|
543
|
+
if (signal.aborted || generation !== notificationGeneration.current)
|
|
544
|
+
return;
|
|
545
|
+
setUnread(count.unread_count || 0);
|
|
546
|
+
const before = liveView.current;
|
|
547
|
+
if (
|
|
548
|
+
before.blocked ||
|
|
549
|
+
!["inbox", "post", "feedback", "thread"].includes(before.route.view)
|
|
550
|
+
) {
|
|
551
|
+
setLiveStatus("");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const next = await loadPage(api, before.route, signal);
|
|
555
|
+
const after = liveView.current;
|
|
556
|
+
if (
|
|
557
|
+
signal.aborted ||
|
|
558
|
+
after.key !== before.key ||
|
|
559
|
+
after.current !== before.current ||
|
|
560
|
+
after.blocked
|
|
561
|
+
)
|
|
562
|
+
return;
|
|
563
|
+
cache.current.set(before.key, next);
|
|
564
|
+
setIndex(
|
|
565
|
+
anchoredIndex(after.current?.items, next.items || [], after.index),
|
|
566
|
+
);
|
|
567
|
+
setData({ ...next, key: before.key });
|
|
568
|
+
setLiveStatus("");
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
activity: () => activity.current,
|
|
572
|
+
onError: (e) => {
|
|
573
|
+
setLiveStatus(
|
|
574
|
+
e.status === 401 || e.status === 403
|
|
575
|
+
? "Live updates paused: sign in again. Local draft preserved."
|
|
576
|
+
: "Live updates delayed · automatic retry",
|
|
577
|
+
);
|
|
578
|
+
if (e.status === 401 || e.status === 403) setLive(false);
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
);
|
|
582
|
+
}, [api, phase, live]);
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
if (current?.unread_count !== undefined) setUnread(current.unread_count);
|
|
585
|
+
}, [current]);
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
const post = current?.post;
|
|
588
|
+
if (!post?.code) return;
|
|
589
|
+
if (
|
|
590
|
+
seenSnippet.current?.id === post.id &&
|
|
591
|
+
seenSnippet.current.code !== post.code
|
|
592
|
+
) {
|
|
593
|
+
setReviewLine(0);
|
|
594
|
+
setReviewAnchor(null);
|
|
595
|
+
setReviewHorizontal(0);
|
|
596
|
+
} else
|
|
597
|
+
setReviewLine((line) => Math.min(line, post.code.split("\n").length - 1));
|
|
598
|
+
seenSnippet.current = { id: post.id, code: post.code };
|
|
599
|
+
}, [current?.post?.id, current?.post?.code]);
|
|
435
600
|
const resetAccount = () => {
|
|
436
601
|
cache.current.clear();
|
|
437
602
|
history.current = [];
|
|
@@ -441,24 +606,56 @@ function App({ initialConfig, flags, root }) {
|
|
|
441
606
|
setPreview(null);
|
|
442
607
|
setDevice(null);
|
|
443
608
|
setUser(null);
|
|
609
|
+
setReading(null);
|
|
610
|
+
setActionIndex(null);
|
|
611
|
+
setUnread(0);
|
|
612
|
+
setLiveStatus("");
|
|
613
|
+
notificationGeneration.current++;
|
|
444
614
|
setRoute({ view: "feed", page: 1 });
|
|
445
615
|
setError("");
|
|
446
616
|
};
|
|
447
617
|
function fail(e) {
|
|
448
618
|
if (!alive.current) return;
|
|
449
|
-
if (e.status === 401) {
|
|
619
|
+
if (e.status === 401 && form) {
|
|
620
|
+
setLive(false);
|
|
621
|
+
setError(problemMessage(e));
|
|
622
|
+
} else if (e.status === 401) {
|
|
450
623
|
resetAccount();
|
|
451
624
|
setPhase("auth");
|
|
452
625
|
setConfig(null);
|
|
453
626
|
setError(
|
|
454
627
|
"Your session expired or was revoked. Press Enter to reconnect.",
|
|
455
628
|
);
|
|
456
|
-
} else setError(
|
|
629
|
+
} else setError(problemMessage(e));
|
|
457
630
|
}
|
|
458
631
|
function navigate(next, { replace = false } = {}) {
|
|
459
|
-
if (!replace)
|
|
632
|
+
if (!replace)
|
|
633
|
+
history.current.push({
|
|
634
|
+
route,
|
|
635
|
+
index,
|
|
636
|
+
scroll,
|
|
637
|
+
selectedId: selected?.id,
|
|
638
|
+
entryType: selected?.entryType,
|
|
639
|
+
reviewFocus,
|
|
640
|
+
reviewLine,
|
|
641
|
+
reviewAnchor,
|
|
642
|
+
reviewHorizontal,
|
|
643
|
+
});
|
|
644
|
+
restoreSelection.current = {
|
|
645
|
+
key: JSON.stringify(next),
|
|
646
|
+
selectedId: next.targetReply,
|
|
647
|
+
entryType:
|
|
648
|
+
next.view === "feedback" && next.targetReply
|
|
649
|
+
? "feedbackReply"
|
|
650
|
+
: undefined,
|
|
651
|
+
index: 0,
|
|
652
|
+
};
|
|
460
653
|
if (history.current.length > 30) history.current.shift();
|
|
461
654
|
setReading(null);
|
|
655
|
+
setReviewLine(0);
|
|
656
|
+
setReviewAnchor(null);
|
|
657
|
+
setReviewHorizontal(0);
|
|
658
|
+
if (next.view === "feedback") setReviewFocus("notes");
|
|
462
659
|
setRoute(next);
|
|
463
660
|
setIndex(0);
|
|
464
661
|
setScroll(0);
|
|
@@ -469,6 +666,15 @@ function App({ initialConfig, flags, root }) {
|
|
|
469
666
|
function back() {
|
|
470
667
|
const previous = history.current.pop();
|
|
471
668
|
if (previous) {
|
|
669
|
+
restoreSelection.current = {
|
|
670
|
+
...previous,
|
|
671
|
+
key: JSON.stringify(previous.route),
|
|
672
|
+
};
|
|
673
|
+
setReading(null);
|
|
674
|
+
setReviewFocus(previous.reviewFocus);
|
|
675
|
+
setReviewLine(previous.reviewLine);
|
|
676
|
+
setReviewAnchor(previous.reviewAnchor);
|
|
677
|
+
setReviewHorizontal(previous.reviewHorizontal);
|
|
472
678
|
setRoute(previous.route);
|
|
473
679
|
setIndex(previous.index);
|
|
474
680
|
setScroll(previous.scroll);
|
|
@@ -609,6 +815,23 @@ function App({ initialConfig, flags, root }) {
|
|
|
609
815
|
cached = cache.current.get(key);
|
|
610
816
|
setData(cached ? { ...cached, key } : null);
|
|
611
817
|
setLoading(!cached);
|
|
818
|
+
if (cached && restoreSelection.current?.key === key) {
|
|
819
|
+
const saved = restoreSelection.current;
|
|
820
|
+
const found = cached.items?.findIndex(
|
|
821
|
+
(item) =>
|
|
822
|
+
item.id === saved.selectedId &&
|
|
823
|
+
(saved.entryType === undefined || item.entryType === saved.entryType),
|
|
824
|
+
);
|
|
825
|
+
setIndex(
|
|
826
|
+
found >= 0
|
|
827
|
+
? found
|
|
828
|
+
: Math.min(
|
|
829
|
+
saved.index || 0,
|
|
830
|
+
Math.max(0, (cached.items?.length || 0) - 1),
|
|
831
|
+
),
|
|
832
|
+
);
|
|
833
|
+
restoreSelection.current = null;
|
|
834
|
+
}
|
|
612
835
|
setError("");
|
|
613
836
|
if (!cached)
|
|
614
837
|
(route.view === "drafts"
|
|
@@ -624,9 +847,27 @@ function App({ initialConfig, flags, root }) {
|
|
|
624
847
|
if (!active) return;
|
|
625
848
|
cache.current.set(key, result);
|
|
626
849
|
setData({ ...result, key });
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
850
|
+
const saved = restoreSelection.current;
|
|
851
|
+
if (saved?.key === key) {
|
|
852
|
+
const found = result.items?.findIndex(
|
|
853
|
+
(item) =>
|
|
854
|
+
item.id === saved.selectedId &&
|
|
855
|
+
(saved.entryType === undefined ||
|
|
856
|
+
item.entryType === saved.entryType),
|
|
857
|
+
);
|
|
858
|
+
setIndex(
|
|
859
|
+
found >= 0
|
|
860
|
+
? found
|
|
861
|
+
: Math.min(
|
|
862
|
+
saved.index || 0,
|
|
863
|
+
Math.max(0, (result.items?.length || 0) - 1),
|
|
864
|
+
),
|
|
865
|
+
);
|
|
866
|
+
restoreSelection.current = null;
|
|
867
|
+
} else
|
|
868
|
+
setIndex((i) =>
|
|
869
|
+
Math.min(i, Math.max(0, (result.items?.length || 0) - 1)),
|
|
870
|
+
);
|
|
630
871
|
})
|
|
631
872
|
.catch((e) => {
|
|
632
873
|
if (active) fail(e);
|
|
@@ -795,6 +1036,46 @@ function App({ initialConfig, flags, root }) {
|
|
|
795
1036
|
values: { body: "" },
|
|
796
1037
|
});
|
|
797
1038
|
}
|
|
1039
|
+
async function externalEdit() {
|
|
1040
|
+
const descriptor = fieldsFor(form)[field];
|
|
1041
|
+
if (!descriptor?.multiline) {
|
|
1042
|
+
setNotice("Ctrl+E opens the editor for a multiline text field.");
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const original = form;
|
|
1046
|
+
await task(async () => {
|
|
1047
|
+
clearTimeout(draftTimer.current);
|
|
1048
|
+
await queueDraft(() => drafts.save(original));
|
|
1049
|
+
let value;
|
|
1050
|
+
await suspendTerminal(async () => {
|
|
1051
|
+
value = await editExternal(original.values[descriptor.key] || "", {
|
|
1052
|
+
max: descriptor.max,
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
const updated = {
|
|
1056
|
+
...original,
|
|
1057
|
+
values: { ...original.values, [descriptor.key]: value },
|
|
1058
|
+
};
|
|
1059
|
+
setForm(updated);
|
|
1060
|
+
setCursor(graphemes(value).length);
|
|
1061
|
+
await queueDraft(() => drafts.save(updated));
|
|
1062
|
+
setDraftSaved(true);
|
|
1063
|
+
setNotice("Text returned from editor. Preview before publishing.");
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
async function reconnect() {
|
|
1067
|
+
await task(async () => {
|
|
1068
|
+
clearTimeout(draftTimer.current);
|
|
1069
|
+
draftGeneration.current++;
|
|
1070
|
+
if (form && drafts) await queueDraft(() => drafts.save(form));
|
|
1071
|
+
resetAccount();
|
|
1072
|
+
setConfig(null);
|
|
1073
|
+
setPhase("auth");
|
|
1074
|
+
setNotice(
|
|
1075
|
+
"Draft saved locally. Enter reconnects; restore it from Local drafts after signing in.",
|
|
1076
|
+
);
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
798
1079
|
async function lineFeedback() {
|
|
799
1080
|
const p = current?.post;
|
|
800
1081
|
if (!p?.code) {
|
|
@@ -805,8 +1086,25 @@ function App({ initialConfig, flags, root }) {
|
|
|
805
1086
|
setError("Replies are paused.");
|
|
806
1087
|
return;
|
|
807
1088
|
}
|
|
1089
|
+
const note = route.view === "feedback" ? current.note : selected;
|
|
1090
|
+
const range = reviewing
|
|
1091
|
+
? reviewFocus === "notes" && note?.start_line
|
|
1092
|
+
? { start: note.start_line, end: note.end_line }
|
|
1093
|
+
: codeRange(reviewLine, reviewAnchor)
|
|
1094
|
+
: { start: 1, end: 1 };
|
|
1095
|
+
if (reviewing && reviewFocus === "notes" && note?.outdated) {
|
|
1096
|
+
setError(
|
|
1097
|
+
"This remark belongs to an earlier snippet. W switches to current code before adding a new remark.",
|
|
1098
|
+
);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (range.end - range.start >= 50) {
|
|
1102
|
+
setError("Select at most 50 code lines for a remark.");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
808
1105
|
await task(async () => {
|
|
809
1106
|
const notes = await api(`/posts/${p.id}/code-reviews`);
|
|
1107
|
+
assertSnippet(p.code, notes.snippet_hash);
|
|
810
1108
|
openForm({
|
|
811
1109
|
kind: "line",
|
|
812
1110
|
title: "Comment on code lines",
|
|
@@ -815,7 +1113,11 @@ function App({ initialConfig, flags, root }) {
|
|
|
815
1113
|
postId: p.id,
|
|
816
1114
|
snippetHash: notes.snippet_hash,
|
|
817
1115
|
lineCount: p.code.split("\n").length,
|
|
818
|
-
values: {
|
|
1116
|
+
values: {
|
|
1117
|
+
start_line: String(range.start),
|
|
1118
|
+
end_line: String(range.end),
|
|
1119
|
+
body: "",
|
|
1120
|
+
},
|
|
819
1121
|
});
|
|
820
1122
|
});
|
|
821
1123
|
}
|
|
@@ -1006,11 +1308,14 @@ function App({ initialConfig, flags, root }) {
|
|
|
1006
1308
|
}
|
|
1007
1309
|
if (route.view === "inbox") {
|
|
1008
1310
|
await task(async () => {
|
|
1009
|
-
if (!item.read_at)
|
|
1311
|
+
if (!item.read_at) {
|
|
1312
|
+
notificationGeneration.current++;
|
|
1010
1313
|
await api("/notifications/" + item.id + "/read", {
|
|
1011
1314
|
method: "PATCH",
|
|
1012
1315
|
body: {},
|
|
1013
1316
|
});
|
|
1317
|
+
setUnread((v) => Math.max(0, v - 1));
|
|
1318
|
+
}
|
|
1014
1319
|
cache.current.clear();
|
|
1015
1320
|
if (item.event_id) {
|
|
1016
1321
|
openBrowser(
|
|
@@ -1019,21 +1324,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
1019
1324
|
setNotice("Event opened in your browser.");
|
|
1020
1325
|
return;
|
|
1021
1326
|
}
|
|
1022
|
-
|
|
1023
|
-
navigate({
|
|
1024
|
-
view: "feedback",
|
|
1025
|
-
postId: item.post_id,
|
|
1026
|
-
id: item.code_review_id,
|
|
1027
|
-
page: 1,
|
|
1028
|
-
targetReply: item.feedback_reply_id || null,
|
|
1029
|
-
});
|
|
1030
|
-
else
|
|
1031
|
-
navigate({
|
|
1032
|
-
view: "post",
|
|
1033
|
-
id: item.post_id,
|
|
1034
|
-
tab: item.reply_id ? "replies" : "overview",
|
|
1035
|
-
page: 1,
|
|
1036
|
-
});
|
|
1327
|
+
navigate(await notificationRoute(api, item));
|
|
1037
1328
|
});
|
|
1038
1329
|
return;
|
|
1039
1330
|
}
|
|
@@ -1176,6 +1467,81 @@ function App({ initialConfig, flags, root }) {
|
|
|
1176
1467
|
invalidate(note.resolved ? "Feedback reopened." : "Feedback resolved.");
|
|
1177
1468
|
});
|
|
1178
1469
|
}
|
|
1470
|
+
function acceptSolution() {
|
|
1471
|
+
const post = current?.post;
|
|
1472
|
+
if (
|
|
1473
|
+
!selected ||
|
|
1474
|
+
selected.deleted_at ||
|
|
1475
|
+
!post ||
|
|
1476
|
+
post.user_id !== user.id ||
|
|
1477
|
+
!["question", "review"].includes(post.kind) ||
|
|
1478
|
+
!(route.view === "thread" || route.tab === "replies")
|
|
1479
|
+
)
|
|
1480
|
+
return;
|
|
1481
|
+
const accepted = post.accepted_reply_id === selected.id;
|
|
1482
|
+
askConfirm(
|
|
1483
|
+
accepted
|
|
1484
|
+
? "Remove accepted solution?"
|
|
1485
|
+
: "Accept this reply as the solution?",
|
|
1486
|
+
"This changes the solution shown to everyone in the conversation.",
|
|
1487
|
+
() =>
|
|
1488
|
+
task(async () => {
|
|
1489
|
+
await api(`/posts/${post.id}/solution`, {
|
|
1490
|
+
method: "PATCH",
|
|
1491
|
+
body: { reply_id: accepted ? null : selected.id },
|
|
1492
|
+
});
|
|
1493
|
+
invalidate(accepted ? "Solution removed." : "Solution accepted.");
|
|
1494
|
+
}),
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
function reportSelected() {
|
|
1498
|
+
const target = editable();
|
|
1499
|
+
if (!target?.feedback || target.row.deleted_at) {
|
|
1500
|
+
setError("Select an existing code remark or feedback reply to report.");
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
const noteId = route.view === "feedback" ? route.id : target.row.id;
|
|
1504
|
+
openForm({
|
|
1505
|
+
kind: "report",
|
|
1506
|
+
feedback: true,
|
|
1507
|
+
title: "Report selected feedback",
|
|
1508
|
+
url: `/posts/${current.post.id}/code-reviews/${noteId}/report`,
|
|
1509
|
+
replyId: target.row.entryType === "feedbackReply" ? target.row.id : 0,
|
|
1510
|
+
audience: "Assigned moderation team",
|
|
1511
|
+
values: { reason: "" },
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
function runAction(id) {
|
|
1515
|
+
setActionIndex(null);
|
|
1516
|
+
if (id === "read") {
|
|
1517
|
+
if (current?.post) {
|
|
1518
|
+
setReading(selected);
|
|
1519
|
+
setScroll(0);
|
|
1520
|
+
} else openSelected();
|
|
1521
|
+
} else if (id === "reply") reply();
|
|
1522
|
+
else if (id === "comment") lineFeedback();
|
|
1523
|
+
else if (id === "edit") editSelected();
|
|
1524
|
+
else if (id === "delete") deleteSelected();
|
|
1525
|
+
else if (id === "resolve") resolveSelected();
|
|
1526
|
+
else if (id === "solution") acceptSolution();
|
|
1527
|
+
else if (id === "report") reportSelected();
|
|
1528
|
+
else if (id === "browser")
|
|
1529
|
+
openBrowser(config.origin + "/app#post/" + current.post.id);
|
|
1530
|
+
else if (id === "sync") invalidate("Updated.");
|
|
1531
|
+
else if (id === "live") {
|
|
1532
|
+
setLive((v) => !v);
|
|
1533
|
+
setLiveStatus("");
|
|
1534
|
+
} else if (id === "read-all")
|
|
1535
|
+
task(async () => {
|
|
1536
|
+
notificationGeneration.current++;
|
|
1537
|
+
await api("/notifications/read-all", {
|
|
1538
|
+
method: "PATCH",
|
|
1539
|
+
body: { through_id: current.latest_id },
|
|
1540
|
+
});
|
|
1541
|
+
setUnread(0);
|
|
1542
|
+
invalidate("Received notifications marked as read.");
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1179
1545
|
async function runCommand() {
|
|
1180
1546
|
const value = command.trim();
|
|
1181
1547
|
if (!value) return;
|
|
@@ -1249,6 +1615,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
1249
1615
|
);
|
|
1250
1616
|
}
|
|
1251
1617
|
useInput((input, keyEvent) => {
|
|
1618
|
+
activity.current = Date.now();
|
|
1252
1619
|
if (picker) return;
|
|
1253
1620
|
if (keyEvent.ctrl && input === "c") {
|
|
1254
1621
|
if (dialog) {
|
|
@@ -1291,8 +1658,21 @@ function App({ initialConfig, flags, root }) {
|
|
|
1291
1658
|
return;
|
|
1292
1659
|
}
|
|
1293
1660
|
if (busy.current) return;
|
|
1661
|
+
if (keyEvent.ctrl && input === "l") {
|
|
1662
|
+
reconnect();
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
if (actionIndex !== null) {
|
|
1666
|
+
if (keyEvent.escape || input === "x" || input === "X")
|
|
1667
|
+
setActionIndex(null);
|
|
1668
|
+
else if (keyEvent.upArrow) setActionIndex((v) => Math.max(0, v - 1));
|
|
1669
|
+
else if (keyEvent.downArrow)
|
|
1670
|
+
setActionIndex((v) => Math.min(actions.length - 1, v + 1));
|
|
1671
|
+
else if (keyEvent.return) runAction(actions[actionIndex]?.id);
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1294
1674
|
if (reading) {
|
|
1295
|
-
const lines =
|
|
1675
|
+
const lines = markdownRows(
|
|
1296
1676
|
reading.deleted_at
|
|
1297
1677
|
? "[deleted — discussion preserved]"
|
|
1298
1678
|
: reading.body || "",
|
|
@@ -1345,6 +1725,10 @@ function App({ initialConfig, flags, root }) {
|
|
|
1345
1725
|
return;
|
|
1346
1726
|
}
|
|
1347
1727
|
if (form) {
|
|
1728
|
+
if (keyEvent.ctrl && input === "e") {
|
|
1729
|
+
externalEdit();
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1348
1732
|
if (
|
|
1349
1733
|
keyEvent.ctrl &&
|
|
1350
1734
|
["o", "g"].includes(input) &&
|
|
@@ -1478,6 +1862,80 @@ function App({ initialConfig, flags, root }) {
|
|
|
1478
1862
|
else if (keyEvent.rightArrow) setFocus("content");
|
|
1479
1863
|
return;
|
|
1480
1864
|
}
|
|
1865
|
+
if (input === "x" || input === "X" || input === "?") {
|
|
1866
|
+
setActionIndex(0);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if ((input === "v" || input === "V") && current?.post?.code) {
|
|
1870
|
+
navigate({ view: "post", id: current.post.id, tab: "feedback", page: 1 });
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
if (reviewing && (input === "w" || input === "W")) {
|
|
1874
|
+
setReviewFocus((v) => (v === "code" ? "notes" : "code"));
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
if (
|
|
1878
|
+
reviewing &&
|
|
1879
|
+
(input === "f" || input === "F") &&
|
|
1880
|
+
route.view === "post"
|
|
1881
|
+
) {
|
|
1882
|
+
const filters = [undefined, "open", "resolved"];
|
|
1883
|
+
navigate(
|
|
1884
|
+
{
|
|
1885
|
+
...route,
|
|
1886
|
+
status: filters[(filters.indexOf(route.status) + 1) % filters.length],
|
|
1887
|
+
page: 1,
|
|
1888
|
+
},
|
|
1889
|
+
{ replace: true },
|
|
1890
|
+
);
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (reviewing && reviewFocus === "code") {
|
|
1894
|
+
if (
|
|
1895
|
+
keyEvent.upArrow ||
|
|
1896
|
+
keyEvent.downArrow ||
|
|
1897
|
+
keyEvent.pageUp ||
|
|
1898
|
+
keyEvent.pageDown
|
|
1899
|
+
) {
|
|
1900
|
+
const delta =
|
|
1901
|
+
(keyEvent.upArrow || keyEvent.pageUp ? -1 : 1) *
|
|
1902
|
+
(keyEvent.pageUp || keyEvent.pageDown
|
|
1903
|
+
? Math.max(1, contentHeight - 2)
|
|
1904
|
+
: 1);
|
|
1905
|
+
setReviewLine((v) =>
|
|
1906
|
+
Math.max(
|
|
1907
|
+
0,
|
|
1908
|
+
Math.min(current.post.code.split("\n").length - 1, v + delta),
|
|
1909
|
+
),
|
|
1910
|
+
);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (keyEvent.leftArrow || keyEvent.rightArrow) {
|
|
1914
|
+
setReviewHorizontal((v) =>
|
|
1915
|
+
Math.max(0, v + (keyEvent.leftArrow ? -8 : 8)),
|
|
1916
|
+
);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (input === " ") {
|
|
1920
|
+
setReviewAnchor((v) => (v === null ? reviewLine : null));
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (keyEvent.return) {
|
|
1924
|
+
const i = current.items.findIndex(
|
|
1925
|
+
(n) =>
|
|
1926
|
+
!n.outdated &&
|
|
1927
|
+
!n.deleted_at &&
|
|
1928
|
+
reviewLine + 1 >= n.start_line &&
|
|
1929
|
+
reviewLine + 1 <= n.end_line,
|
|
1930
|
+
);
|
|
1931
|
+
if (i >= 0) {
|
|
1932
|
+
setIndex(i);
|
|
1933
|
+
setReviewFocus("notes");
|
|
1934
|
+
} else
|
|
1935
|
+
setNotice("No remark on this line on the current page. C adds one.");
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1481
1939
|
const prose =
|
|
1482
1940
|
route.view === "help" ||
|
|
1483
1941
|
(route.view === "post" && (!route.tab || route.tab === "overview")) ||
|
|
@@ -1496,9 +1954,8 @@ function App({ initialConfig, flags, root }) {
|
|
|
1496
1954
|
if (prose) {
|
|
1497
1955
|
const maximum = Math.max(
|
|
1498
1956
|
0,
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
).length - contentHeight,
|
|
1957
|
+
documentRows(route, current || {}, contentWidth).length -
|
|
1958
|
+
contentHeight,
|
|
1502
1959
|
);
|
|
1503
1960
|
const amount =
|
|
1504
1961
|
keyEvent.pageUp || keyEvent.pageDown
|
|
@@ -1572,15 +2029,16 @@ function App({ initialConfig, flags, root }) {
|
|
|
1572
2029
|
invalidate("Updated.");
|
|
1573
2030
|
return;
|
|
1574
2031
|
}
|
|
1575
|
-
if (input === "c") {
|
|
2032
|
+
if (input === "c" || input === "C") {
|
|
1576
2033
|
lineFeedback();
|
|
1577
2034
|
return;
|
|
1578
2035
|
}
|
|
1579
|
-
if (input === "s") {
|
|
1580
|
-
|
|
2036
|
+
if (input === "s" || input === "S") {
|
|
2037
|
+
if (route.view === "thread" || route.tab === "replies") acceptSolution();
|
|
2038
|
+
else resolveSelected();
|
|
1581
2039
|
return;
|
|
1582
2040
|
}
|
|
1583
|
-
if (input === "e") {
|
|
2041
|
+
if (input === "e" || input === "E") {
|
|
1584
2042
|
editSelected();
|
|
1585
2043
|
return;
|
|
1586
2044
|
}
|
|
@@ -1609,25 +2067,12 @@ function App({ initialConfig, flags, root }) {
|
|
|
1609
2067
|
return;
|
|
1610
2068
|
}
|
|
1611
2069
|
if (input === "!" && ["feedback", "post"].includes(route.view)) {
|
|
1612
|
-
|
|
1613
|
-
if (!target?.feedback || target.row.deleted_at) {
|
|
1614
|
-
setError("Select an existing code remark or feedback reply to report.");
|
|
1615
|
-
return;
|
|
1616
|
-
}
|
|
1617
|
-
const noteId = route.view === "feedback" ? route.id : target.row.id;
|
|
1618
|
-
openForm({
|
|
1619
|
-
kind: "report",
|
|
1620
|
-
feedback: true,
|
|
1621
|
-
title: "Report selected feedback",
|
|
1622
|
-
url: `/posts/${current.post.id}/code-reviews/${noteId}/report`,
|
|
1623
|
-
replyId: target.row.entryType === "feedbackReply" ? target.row.id : 0,
|
|
1624
|
-
audience: "Assigned moderation team",
|
|
1625
|
-
values: { reason: "" },
|
|
1626
|
-
});
|
|
2070
|
+
reportSelected();
|
|
1627
2071
|
}
|
|
1628
2072
|
});
|
|
1629
2073
|
const status =
|
|
1630
2074
|
error ||
|
|
2075
|
+
liveStatus ||
|
|
1631
2076
|
notice ||
|
|
1632
2077
|
(pending
|
|
1633
2078
|
? "Saving…"
|
|
@@ -1752,7 +2197,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
1752
2197
|
} else {
|
|
1753
2198
|
let body;
|
|
1754
2199
|
if (reading) {
|
|
1755
|
-
const lines =
|
|
2200
|
+
const lines = markdownRows(
|
|
1756
2201
|
reading.deleted_at
|
|
1757
2202
|
? "[deleted — discussion preserved]"
|
|
1758
2203
|
: reading.body || "",
|
|
@@ -1826,10 +2271,78 @@ function App({ initialConfig, flags, root }) {
|
|
|
1826
2271
|
{ color: colors.muted },
|
|
1827
2272
|
"Opening " + labelOf(route).toLowerCase() + " " + spin,
|
|
1828
2273
|
);
|
|
1829
|
-
else {
|
|
1830
|
-
const
|
|
1831
|
-
|
|
2274
|
+
else if (actionIndex !== null) {
|
|
2275
|
+
const first = Math.max(
|
|
2276
|
+
0,
|
|
2277
|
+
actionIndex - Math.max(1, contentHeight - 2) + 1,
|
|
1832
2278
|
);
|
|
2279
|
+
body = h(
|
|
2280
|
+
Box,
|
|
2281
|
+
{ height: contentHeight, flexDirection: "column", overflow: "hidden" },
|
|
2282
|
+
h(
|
|
2283
|
+
Text,
|
|
2284
|
+
{ bold: true, color: colors.blue },
|
|
2285
|
+
"Actions · ↑↓ choose · Enter run · Esc close",
|
|
2286
|
+
),
|
|
2287
|
+
contentHeight >= 4
|
|
2288
|
+
? h(
|
|
2289
|
+
Text,
|
|
2290
|
+
{ color: colors.muted },
|
|
2291
|
+
"Available for this selection and your role",
|
|
2292
|
+
)
|
|
2293
|
+
: null,
|
|
2294
|
+
...actions
|
|
2295
|
+
.slice(
|
|
2296
|
+
first,
|
|
2297
|
+
first + Math.max(1, contentHeight - (contentHeight >= 4 ? 2 : 1)),
|
|
2298
|
+
)
|
|
2299
|
+
.map((action, i) =>
|
|
2300
|
+
h(
|
|
2301
|
+
Text,
|
|
2302
|
+
{
|
|
2303
|
+
key: action.id,
|
|
2304
|
+
color: i + first === actionIndex ? colors.blue : colors.text,
|
|
2305
|
+
backgroundColor:
|
|
2306
|
+
i + first === actionIndex ? colors.selected : undefined,
|
|
2307
|
+
},
|
|
2308
|
+
clip(
|
|
2309
|
+
(i + first === actionIndex ? "› " : " ") + action.label,
|
|
2310
|
+
contentWidth,
|
|
2311
|
+
),
|
|
2312
|
+
),
|
|
2313
|
+
),
|
|
2314
|
+
);
|
|
2315
|
+
} else if (reviewing)
|
|
2316
|
+
body = h(ReviewWorkspace, {
|
|
2317
|
+
post: current.post,
|
|
2318
|
+
note: route.view === "feedback" ? current.note : null,
|
|
2319
|
+
items: current.items || [],
|
|
2320
|
+
index,
|
|
2321
|
+
width: contentWidth,
|
|
2322
|
+
height: contentHeight,
|
|
2323
|
+
focus: reviewFocus,
|
|
2324
|
+
line: reviewLine,
|
|
2325
|
+
anchor: reviewAnchor,
|
|
2326
|
+
horizontal: reviewHorizontal,
|
|
2327
|
+
rows: (w) => listRows(route, current, index, w),
|
|
2328
|
+
});
|
|
2329
|
+
else if (error && !current)
|
|
2330
|
+
body = h(FrameRows, {
|
|
2331
|
+
rows: [
|
|
2332
|
+
{ text: "Could not open this view", bold: true, color: colors.red },
|
|
2333
|
+
"",
|
|
2334
|
+
...wrap(error, contentWidth),
|
|
2335
|
+
"",
|
|
2336
|
+
...wrap(
|
|
2337
|
+
"Shift+R retries this view · Esc goes back · Ctrl+L reconnects",
|
|
2338
|
+
contentWidth,
|
|
2339
|
+
),
|
|
2340
|
+
],
|
|
2341
|
+
width: contentWidth,
|
|
2342
|
+
height: contentHeight,
|
|
2343
|
+
});
|
|
2344
|
+
else {
|
|
2345
|
+
const lines = documentRows(route, current || {}, contentWidth);
|
|
1833
2346
|
if (lines.length)
|
|
1834
2347
|
body = h(FrameRows, {
|
|
1835
2348
|
rows: lines,
|
|
@@ -1941,15 +2454,23 @@ function App({ initialConfig, flags, root }) {
|
|
|
1941
2454
|
? colors.blue
|
|
1942
2455
|
: colors.muted,
|
|
1943
2456
|
bold: (route.tab || "overview") === id,
|
|
2457
|
+
backgroundColor:
|
|
2458
|
+
(route.tab || "overview") === id
|
|
2459
|
+
? colors.selected
|
|
2460
|
+
: undefined,
|
|
1944
2461
|
},
|
|
1945
2462
|
" " + label + " ",
|
|
1946
2463
|
),
|
|
1947
2464
|
),
|
|
1948
|
-
h(
|
|
2465
|
+
h(
|
|
2466
|
+
Text,
|
|
2467
|
+
{ color: colors.muted },
|
|
2468
|
+
reviewing ? " W focus · F " + (route.status || "all") : " ← →",
|
|
2469
|
+
),
|
|
1949
2470
|
)
|
|
1950
2471
|
: h(TextLine, {
|
|
1951
2472
|
text: form
|
|
1952
|
-
? "Tab next field · Ctrl+S preview" +
|
|
2473
|
+
? "Tab next field · Ctrl+E editor · Ctrl+S preview" +
|
|
1953
2474
|
(draftSaved ? " · Draft saved locally" : "")
|
|
1954
2475
|
: preview
|
|
1955
2476
|
? "Scroll to review · ← → choose · Enter confirms"
|
|
@@ -1981,7 +2502,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
1981
2502
|
paddingX: 1,
|
|
1982
2503
|
overflow: "hidden",
|
|
1983
2504
|
},
|
|
1984
|
-
h(Text, { color: colors.
|
|
2505
|
+
h(Text, { color: colors.purple, bold: true }, "YOUR SPACE"),
|
|
1985
2506
|
h(Text, null, " "),
|
|
1986
2507
|
...navigation
|
|
1987
2508
|
.slice(
|
|
@@ -2005,9 +2526,16 @@ function App({ initialConfig, flags, root }) {
|
|
|
2005
2526
|
backgroundColor: selected ? colors.selected : undefined,
|
|
2006
2527
|
wrap: "truncate",
|
|
2007
2528
|
},
|
|
2008
|
-
(selected ? "› " : " ") +
|
|
2529
|
+
(selected ? "› " : route.view === id ? "● " : " ") +
|
|
2530
|
+
label +
|
|
2531
|
+
(id === "inbox" && unread ? " · " + unread : ""),
|
|
2009
2532
|
);
|
|
2010
2533
|
}),
|
|
2534
|
+
h(
|
|
2535
|
+
Text,
|
|
2536
|
+
{ color: colors.muted, dimColor: true },
|
|
2537
|
+
" Tab sections · / find",
|
|
2538
|
+
),
|
|
2011
2539
|
)
|
|
2012
2540
|
: null;
|
|
2013
2541
|
center = h(
|
|
@@ -2017,21 +2545,26 @@ function App({ initialConfig, flags, root }) {
|
|
|
2017
2545
|
compact && focus === "nav" ? null : pane,
|
|
2018
2546
|
);
|
|
2019
2547
|
}
|
|
2020
|
-
const hint =
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2548
|
+
const hint =
|
|
2549
|
+
actionIndex !== null
|
|
2550
|
+
? "Actions are scoped to the selected item · Esc closes"
|
|
2551
|
+
: reviewing && !form && !preview && !reading
|
|
2552
|
+
? "W code / discussion · Space select · C comment · X actions · F filter"
|
|
2553
|
+
: picker
|
|
2554
|
+
? "Project files stay local until publication is confirmed"
|
|
2555
|
+
: reading
|
|
2556
|
+
? "↑↓ scroll · R reply · E edit · D delete · Esc close"
|
|
2557
|
+
: preview
|
|
2558
|
+
? "← → choose · Enter confirm · Esc edit"
|
|
2559
|
+
: form
|
|
2560
|
+
? "Tab field · Ctrl+E editor · Ctrl+O files · Ctrl+G diff · Ctrl+S preview"
|
|
2561
|
+
: phase !== "ready"
|
|
2562
|
+
? "Enter connect / open browser · Ctrl+C exit"
|
|
2563
|
+
: route.view === "feedback"
|
|
2564
|
+
? "↑↓ select · R reply · E edit · D delete · S resolve · ! report"
|
|
2565
|
+
: route.view === "post"
|
|
2566
|
+
? "←→ tabs · ↑↓ select · R reply · E edit · D delete · Space read"
|
|
2567
|
+
: "↑↓ select · Enter open · Tab sections · [ ] pages · N new · / search";
|
|
2035
2568
|
return h(
|
|
2036
2569
|
Box,
|
|
2037
2570
|
{
|
|
@@ -2054,12 +2587,22 @@ function App({ initialConfig, flags, root }) {
|
|
|
2054
2587
|
h(
|
|
2055
2588
|
Text,
|
|
2056
2589
|
{ color: colors.purple, bold: true },
|
|
2057
|
-
clip("◈ DEVDEVIAN
|
|
2590
|
+
clip("◈ DEVDEVIAN · " + basename(root), Math.max(10, width - 36)),
|
|
2058
2591
|
),
|
|
2059
2592
|
h(
|
|
2060
2593
|
Text,
|
|
2061
2594
|
{ color: colors.blue },
|
|
2062
|
-
clip(
|
|
2595
|
+
clip(
|
|
2596
|
+
user
|
|
2597
|
+
? "@" +
|
|
2598
|
+
user.username +
|
|
2599
|
+
" · " +
|
|
2600
|
+
unread +
|
|
2601
|
+
" unread · " +
|
|
2602
|
+
(live ? "● Live" : "○ Paused")
|
|
2603
|
+
: "CLI · ALPHA",
|
|
2604
|
+
32,
|
|
2605
|
+
),
|
|
2063
2606
|
),
|
|
2064
2607
|
),
|
|
2065
2608
|
center,
|
|
@@ -2087,7 +2630,10 @@ function App({ initialConfig, flags, root }) {
|
|
|
2087
2630
|
: h(
|
|
2088
2631
|
Text,
|
|
2089
2632
|
{ color: colors.muted },
|
|
2090
|
-
clip(
|
|
2633
|
+
clip(
|
|
2634
|
+
"Search or /command · Press / to type · X actions",
|
|
2635
|
+
width - 6,
|
|
2636
|
+
),
|
|
2091
2637
|
),
|
|
2092
2638
|
),
|
|
2093
2639
|
h(TextLine, { text: hint, width, color: colors.muted }),
|