@tenyokjs/devdevian-cli 0.2.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 +27 -3
- package/bin/devdevian.mjs +3 -1
- package/lib/drafts.mjs +119 -0
- package/lib/picker.mjs +421 -0
- package/lib/tui-data.mjs +3 -0
- package/lib/tui-model.mjs +2 -1
- package/lib/tui.mjs +259 -68
- package/lib/workspace.mjs +386 -0
- package/package.json +1 -1
package/lib/tui.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { render, Box, Text, useApp, useInput, useWindowSize } from "ink";
|
|
3
|
-
import {
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { homedir, hostname } from "node:os";
|
|
5
|
-
import {
|
|
5
|
+
import { workspaceRoot, reviewSource } from "./workspace.mjs";
|
|
6
|
+
import { WorkspacePicker } from "./picker.mjs";
|
|
7
|
+
import { DraftStore, draftIdentity } from "./drafts.mjs";
|
|
6
8
|
import { spawn } from "node:child_process";
|
|
7
9
|
import { setTimeout as delay } from "node:timers/promises";
|
|
8
10
|
import {
|
|
@@ -98,17 +100,21 @@ function fieldsFor(form) {
|
|
|
98
100
|
? [
|
|
99
101
|
{
|
|
100
102
|
key: "file",
|
|
101
|
-
label: "Code file",
|
|
103
|
+
label: form.source?.kind === "diff" ? "Git diff" : "Code file",
|
|
102
104
|
min: 1,
|
|
103
105
|
max: 1500,
|
|
104
|
-
hint: "
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
key: "lines",
|
|
108
|
-
label: "File lines (optional)",
|
|
109
|
-
max: 30,
|
|
110
|
-
hint: "START:END; published snippet line numbers start at 1.",
|
|
106
|
+
hint: "Ctrl+O file explorer · Ctrl+G Git diff · Enter browses an empty field.",
|
|
111
107
|
},
|
|
108
|
+
...(form.source?.kind === "diff"
|
|
109
|
+
? []
|
|
110
|
+
: [
|
|
111
|
+
{
|
|
112
|
+
key: "lines",
|
|
113
|
+
label: "File lines (optional)",
|
|
114
|
+
max: 30,
|
|
115
|
+
hint: "START:END; published snippet line numbers start at 1.",
|
|
116
|
+
},
|
|
117
|
+
]),
|
|
112
118
|
{
|
|
113
119
|
key: "review_focus",
|
|
114
120
|
label: "Review focus",
|
|
@@ -283,7 +289,15 @@ function listRows(route, data, index, width) {
|
|
|
283
289
|
let title = item.title || item.name || "",
|
|
284
290
|
meta = "",
|
|
285
291
|
body = "";
|
|
286
|
-
if (route.view === "
|
|
292
|
+
if (route.view === "drafts") {
|
|
293
|
+
title = item.form.values.title || item.form.title;
|
|
294
|
+
meta = "Local draft · " + stamp(item.savedAt);
|
|
295
|
+
body =
|
|
296
|
+
item.form.values.body ||
|
|
297
|
+
item.form.values.description ||
|
|
298
|
+
item.form.values.file ||
|
|
299
|
+
"";
|
|
300
|
+
} else if (route.view === "inbox") {
|
|
287
301
|
title =
|
|
288
302
|
item.actor_name +
|
|
289
303
|
" · " +
|
|
@@ -350,7 +364,7 @@ function listRows(route, data, index, width) {
|
|
|
350
364
|
];
|
|
351
365
|
});
|
|
352
366
|
}
|
|
353
|
-
function App({ initialConfig, flags }) {
|
|
367
|
+
function App({ initialConfig, flags, root }) {
|
|
354
368
|
const { exit } = useApp(),
|
|
355
369
|
{ columns, rows } = useWindowSize();
|
|
356
370
|
const [config, setConfig] = useState(initialConfig),
|
|
@@ -380,12 +394,24 @@ function App({ initialConfig, flags }) {
|
|
|
380
394
|
[yes, setYes] = useState(false),
|
|
381
395
|
[pending, setPending] = useState(false),
|
|
382
396
|
[tick, setTick] = useState(0),
|
|
383
|
-
[reading, setReading] = useState(null)
|
|
397
|
+
[reading, setReading] = useState(null),
|
|
398
|
+
[picker, setPicker] = useState(null),
|
|
399
|
+
[draftSaved, setDraftSaved] = useState(false);
|
|
384
400
|
const cache = useRef(new PageCache()),
|
|
385
401
|
history = useRef([]),
|
|
386
402
|
busy = useRef(false),
|
|
387
403
|
alive = useRef(true),
|
|
388
|
-
authController = useRef(null)
|
|
404
|
+
authController = useRef(null),
|
|
405
|
+
draftQueue = useRef(Promise.resolve()),
|
|
406
|
+
draftTimer = useRef(null),
|
|
407
|
+
draftGeneration = useRef(0);
|
|
408
|
+
const drafts = useMemo(
|
|
409
|
+
() =>
|
|
410
|
+
config && user
|
|
411
|
+
? new DraftStore({ origin: config.origin, userId: user.id, root })
|
|
412
|
+
: null,
|
|
413
|
+
[config?.origin, user?.id, root],
|
|
414
|
+
);
|
|
389
415
|
const api = useMemo(
|
|
390
416
|
() => (config ? client(config.origin, config.token) : null),
|
|
391
417
|
[config?.origin, config?.token],
|
|
@@ -405,6 +431,7 @@ function App({ initialConfig, flags }) {
|
|
|
405
431
|
history.current = [];
|
|
406
432
|
setData(null);
|
|
407
433
|
setForm(null);
|
|
434
|
+
setPicker(null);
|
|
408
435
|
setPreview(null);
|
|
409
436
|
setDevice(null);
|
|
410
437
|
setUser(null);
|
|
@@ -462,6 +489,79 @@ function App({ initialConfig, flags }) {
|
|
|
462
489
|
setRefresh((n) => n + 1);
|
|
463
490
|
if (message) setNotice(message);
|
|
464
491
|
}
|
|
492
|
+
function queueDraft(work) {
|
|
493
|
+
const result = draftQueue.current.catch(() => {}).then(work);
|
|
494
|
+
draftQueue.current = result;
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
async function forgetDraft(value) {
|
|
498
|
+
clearTimeout(draftTimer.current);
|
|
499
|
+
cache.current.clear();
|
|
500
|
+
if (drafts && value)
|
|
501
|
+
await queueDraft(() => drafts.remove(draftIdentity(value)));
|
|
502
|
+
}
|
|
503
|
+
async function restoreDraft(item) {
|
|
504
|
+
await task(async () => {
|
|
505
|
+
const saved = await drafts.read(item.id),
|
|
506
|
+
next = { ...saved.form, draftId: item.id };
|
|
507
|
+
if (next.kind === "publication") {
|
|
508
|
+
const [topics, communities] = await Promise.all([
|
|
509
|
+
api("/topics"),
|
|
510
|
+
api("/communities?joined=1&limit=50"),
|
|
511
|
+
]);
|
|
512
|
+
next.topics = itemsOf(topics).map((t) => [t.id, t.name]);
|
|
513
|
+
next.audiences = [
|
|
514
|
+
["", "Public feed · everyone"],
|
|
515
|
+
...itemsOf(communities).map((c) => [c.id, "Private · " + c.name]),
|
|
516
|
+
];
|
|
517
|
+
if (
|
|
518
|
+
next.values.community_id &&
|
|
519
|
+
!next.audiences.some(
|
|
520
|
+
(c) => String(c[0]) === String(next.values.community_id),
|
|
521
|
+
)
|
|
522
|
+
) {
|
|
523
|
+
let space;
|
|
524
|
+
try {
|
|
525
|
+
space = await api(
|
|
526
|
+
"/communities/" + encodeURIComponent(next.values.community_id),
|
|
527
|
+
);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
if (![403, 404].includes(e.status)) throw e;
|
|
530
|
+
}
|
|
531
|
+
if (space?.is_member)
|
|
532
|
+
next.audiences.push([space.id, "Private · " + space.name]);
|
|
533
|
+
else {
|
|
534
|
+
next.unavailableAudience = next.values.community_id;
|
|
535
|
+
next.audiences.push([
|
|
536
|
+
next.values.community_id,
|
|
537
|
+
"Unavailable private audience · choose another",
|
|
538
|
+
]);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
openForm(next);
|
|
543
|
+
setNotice(
|
|
544
|
+
"Restored local draft. Check the code selection before publishing.",
|
|
545
|
+
);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
useEffect(() => {
|
|
549
|
+
const generation = ++draftGeneration.current;
|
|
550
|
+
if (!drafts || !form) return;
|
|
551
|
+
setDraftSaved(false);
|
|
552
|
+
draftTimer.current = setTimeout(() => {
|
|
553
|
+
queueDraft(() => drafts.save(form))
|
|
554
|
+
.then((id) => {
|
|
555
|
+
if (id && alive.current && generation === draftGeneration.current)
|
|
556
|
+
setDraftSaved(true);
|
|
557
|
+
})
|
|
558
|
+
.catch((e) => {
|
|
559
|
+
if (alive.current)
|
|
560
|
+
setError("Could not save local draft: " + clean(e.message));
|
|
561
|
+
});
|
|
562
|
+
}, 450);
|
|
563
|
+
return () => clearTimeout(draftTimer.current);
|
|
564
|
+
}, [drafts, form]);
|
|
465
565
|
function askConfirm(title, description, action) {
|
|
466
566
|
setYes(false);
|
|
467
567
|
setDialog({ title, description, action });
|
|
@@ -505,7 +605,15 @@ function App({ initialConfig, flags }) {
|
|
|
505
605
|
setLoading(!cached);
|
|
506
606
|
setError("");
|
|
507
607
|
if (!cached)
|
|
508
|
-
|
|
608
|
+
(route.view === "drafts"
|
|
609
|
+
? drafts.list().then((items) => ({
|
|
610
|
+
items,
|
|
611
|
+
title: "Local drafts",
|
|
612
|
+
subtitle:
|
|
613
|
+
"Private to this account and project · Enter restore · D delete",
|
|
614
|
+
}))
|
|
615
|
+
: loadPage(api, route, controller.signal)
|
|
616
|
+
)
|
|
509
617
|
.then((result) => {
|
|
510
618
|
if (!active) return;
|
|
511
619
|
cache.current.set(key, result);
|
|
@@ -605,7 +713,11 @@ function App({ initialConfig, flags }) {
|
|
|
605
713
|
}
|
|
606
714
|
}
|
|
607
715
|
function openForm(next) {
|
|
608
|
-
setForm(
|
|
716
|
+
setForm({
|
|
717
|
+
...next,
|
|
718
|
+
draftId: next.draftId || randomBytes(32).toString("hex"),
|
|
719
|
+
});
|
|
720
|
+
setDraftSaved(false);
|
|
609
721
|
setField(0);
|
|
610
722
|
setCursor(graphemes(next.values[fieldsFor(next)[0].key] || "").length);
|
|
611
723
|
setPreview(null);
|
|
@@ -704,6 +816,13 @@ function App({ initialConfig, flags }) {
|
|
|
704
816
|
async function prepare() {
|
|
705
817
|
if (!form) return;
|
|
706
818
|
await task(async () => {
|
|
819
|
+
if (
|
|
820
|
+
form.unavailableAudience &&
|
|
821
|
+
String(form.values.community_id) === String(form.unavailableAudience)
|
|
822
|
+
)
|
|
823
|
+
throw Error(
|
|
824
|
+
"Choose an available audience explicitly before publishing this draft.",
|
|
825
|
+
);
|
|
707
826
|
const fields = fieldsFor(form);
|
|
708
827
|
for (let i = 0; i < fields.length; i++) {
|
|
709
828
|
const f = fields[i],
|
|
@@ -736,36 +855,31 @@ function App({ initialConfig, flags }) {
|
|
|
736
855
|
};
|
|
737
856
|
url = "/posts";
|
|
738
857
|
if (payload.kind === "review") {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
throw Error("The range is outside the selected file.");
|
|
764
|
-
code = lines.slice(first - 1, last).join("\n");
|
|
858
|
+
let source = form.source;
|
|
859
|
+
if (source?.kind !== "diff") {
|
|
860
|
+
source = {
|
|
861
|
+
kind: "file",
|
|
862
|
+
path: form.values.file.replace(/^~(?=\/)/, homedir()),
|
|
863
|
+
};
|
|
864
|
+
if (form.values.lines) {
|
|
865
|
+
const match = form.values.lines.match(/^([1-9]\d*):([1-9]\d*)$/);
|
|
866
|
+
if (!match) throw Error("Use START:END for file lines.");
|
|
867
|
+
source.start = Number(match[1]);
|
|
868
|
+
source.end = Number(match[2]);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const chosen = await reviewSource(root, source);
|
|
872
|
+
payload.code = chosen.code;
|
|
873
|
+
if (chosen.kind === "diff") {
|
|
874
|
+
payload.body +=
|
|
875
|
+
"\n\nReview source: " +
|
|
876
|
+
clean(chosen.label).replace(/\n/g, " ") +
|
|
877
|
+
"\nReview line numbers refer to the published patch, including its headers.";
|
|
878
|
+
if (payload.body.length > 15000)
|
|
879
|
+
throw Error(
|
|
880
|
+
"Shorten the context to leave room for the Git diff source label.",
|
|
881
|
+
);
|
|
765
882
|
}
|
|
766
|
-
if (code.trim().length < 10)
|
|
767
|
-
throw Error("The selected snippet needs at least 10 characters.");
|
|
768
|
-
payload.code = code;
|
|
769
883
|
payload.review_focus = form.values.review_focus;
|
|
770
884
|
}
|
|
771
885
|
if (payload.kind === "showcase")
|
|
@@ -823,10 +937,16 @@ function App({ initialConfig, flags }) {
|
|
|
823
937
|
body: preview.payload,
|
|
824
938
|
});
|
|
825
939
|
cache.current.clear();
|
|
940
|
+
let localWarning = "";
|
|
941
|
+
try {
|
|
942
|
+
await forgetDraft(form);
|
|
943
|
+
} catch {
|
|
944
|
+
localWarning =
|
|
945
|
+
" Published successfully; the local draft could not be removed. Do not publish it again.";
|
|
946
|
+
}
|
|
826
947
|
setPreview(null);
|
|
827
948
|
setForm(null);
|
|
828
949
|
setScroll(0);
|
|
829
|
-
setNotice("Saved. Your web account sees the same update.");
|
|
830
950
|
if (preview.url === "/posts")
|
|
831
951
|
navigate({ view: "post", id: response.id, tab: "overview", page: 1 });
|
|
832
952
|
else if (preview.url === "/communities")
|
|
@@ -841,12 +961,17 @@ function App({ initialConfig, flags }) {
|
|
|
841
961
|
page: 1,
|
|
842
962
|
});
|
|
843
963
|
}
|
|
964
|
+
setNotice("Saved. Your web account sees the same update." + localWarning);
|
|
844
965
|
});
|
|
845
966
|
}
|
|
846
967
|
async function openSelected() {
|
|
847
968
|
if (route.view === "community" && !current?.community?.is_member) return;
|
|
848
969
|
const item = selected;
|
|
849
970
|
if (!item) return;
|
|
971
|
+
if (route.view === "drafts") {
|
|
972
|
+
await restoreDraft(item);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
850
975
|
if (route.view === "devices") {
|
|
851
976
|
askConfirm("Revoke this terminal?", item.name, () =>
|
|
852
977
|
task(async () => {
|
|
@@ -978,6 +1103,18 @@ function App({ initialConfig, flags }) {
|
|
|
978
1103
|
});
|
|
979
1104
|
}
|
|
980
1105
|
function deleteSelected() {
|
|
1106
|
+
if (route.view === "drafts" && selected) {
|
|
1107
|
+
askConfirm(
|
|
1108
|
+
"Delete saved draft?",
|
|
1109
|
+
"Only this local draft will be removed.",
|
|
1110
|
+
() =>
|
|
1111
|
+
task(async () => {
|
|
1112
|
+
await queueDraft(() => drafts.remove(selected.id));
|
|
1113
|
+
invalidate("Draft deleted.");
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
981
1118
|
const target = editable();
|
|
982
1119
|
if (
|
|
983
1120
|
!target?.feedback ||
|
|
@@ -1074,8 +1211,13 @@ function App({ initialConfig, flags }) {
|
|
|
1074
1211
|
if (form) {
|
|
1075
1212
|
askConfirm(
|
|
1076
1213
|
"Leave this draft?",
|
|
1077
|
-
"
|
|
1078
|
-
() =>
|
|
1214
|
+
"Save this draft locally and exit? Resume it from Local drafts in this project.",
|
|
1215
|
+
() =>
|
|
1216
|
+
task(async () => {
|
|
1217
|
+
clearTimeout(draftTimer.current);
|
|
1218
|
+
if (drafts) await queueDraft(() => drafts.save(form));
|
|
1219
|
+
exit();
|
|
1220
|
+
}),
|
|
1079
1221
|
);
|
|
1080
1222
|
} else exit();
|
|
1081
1223
|
}
|
|
@@ -1090,6 +1232,7 @@ function App({ initialConfig, flags }) {
|
|
|
1090
1232
|
);
|
|
1091
1233
|
}
|
|
1092
1234
|
useInput((input, keyEvent) => {
|
|
1235
|
+
if (picker) return;
|
|
1093
1236
|
if (keyEvent.ctrl && input === "c") {
|
|
1094
1237
|
if (dialog) {
|
|
1095
1238
|
exit();
|
|
@@ -1183,15 +1326,26 @@ function App({ initialConfig, flags }) {
|
|
|
1183
1326
|
return;
|
|
1184
1327
|
}
|
|
1185
1328
|
if (form) {
|
|
1329
|
+
if (
|
|
1330
|
+
keyEvent.ctrl &&
|
|
1331
|
+
["o", "g"].includes(input) &&
|
|
1332
|
+
form.kind === "publication" &&
|
|
1333
|
+
form.values.kind === "review"
|
|
1334
|
+
) {
|
|
1335
|
+
setPicker(input === "g" ? "changes" : "files");
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1186
1338
|
if (keyEvent.escape) {
|
|
1187
1339
|
askConfirm(
|
|
1188
1340
|
"Discard this draft?",
|
|
1189
1341
|
"Nothing has been published. Stay to keep editing.",
|
|
1190
|
-
() =>
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1342
|
+
() =>
|
|
1343
|
+
task(async () => {
|
|
1344
|
+
await forgetDraft(form);
|
|
1345
|
+
setForm(null);
|
|
1346
|
+
setFocus("content");
|
|
1347
|
+
setError("");
|
|
1348
|
+
}),
|
|
1195
1349
|
);
|
|
1196
1350
|
return;
|
|
1197
1351
|
}
|
|
@@ -1236,6 +1390,10 @@ function App({ initialConfig, flags }) {
|
|
|
1236
1390
|
return;
|
|
1237
1391
|
}
|
|
1238
1392
|
if (keyEvent.return && !active.multiline) {
|
|
1393
|
+
if (active.key === "file" && !form.values.file) {
|
|
1394
|
+
setPicker("files");
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1239
1397
|
changeField(field + 1);
|
|
1240
1398
|
return;
|
|
1241
1399
|
}
|
|
@@ -1248,6 +1406,7 @@ function App({ initialConfig, flags }) {
|
|
|
1248
1406
|
);
|
|
1249
1407
|
setForm((old) => ({
|
|
1250
1408
|
...old,
|
|
1409
|
+
...(active.key === "file" ? { source: null } : {}),
|
|
1251
1410
|
values: { ...old.values, [active.key]: updated.value },
|
|
1252
1411
|
}));
|
|
1253
1412
|
setCursor(updated.cursor);
|
|
@@ -1466,6 +1625,34 @@ function App({ initialConfig, flags }) {
|
|
|
1466
1625
|
h(Text, null, "Enlarge this terminal to at least 60 × 18."),
|
|
1467
1626
|
h(Text, null, "Q or Ctrl+C exits."),
|
|
1468
1627
|
);
|
|
1628
|
+
else if (picker)
|
|
1629
|
+
center = h(WorkspacePicker, {
|
|
1630
|
+
root,
|
|
1631
|
+
width,
|
|
1632
|
+
height: bodyHeight,
|
|
1633
|
+
initial: picker,
|
|
1634
|
+
onCancel: () => setPicker(null),
|
|
1635
|
+
onSelect: (source) => {
|
|
1636
|
+
setForm((old) => ({
|
|
1637
|
+
...old,
|
|
1638
|
+
source,
|
|
1639
|
+
values: {
|
|
1640
|
+
...old.values,
|
|
1641
|
+
file: source.path,
|
|
1642
|
+
lines:
|
|
1643
|
+
source.kind === "file" && source.start
|
|
1644
|
+
? source.start + ":" + source.end
|
|
1645
|
+
: "",
|
|
1646
|
+
},
|
|
1647
|
+
}));
|
|
1648
|
+
setPicker(null);
|
|
1649
|
+
setNotice(
|
|
1650
|
+
source.kind === "diff"
|
|
1651
|
+
? "Git changes selected. Preview before publishing."
|
|
1652
|
+
: "File selected. Preview before publishing.",
|
|
1653
|
+
);
|
|
1654
|
+
},
|
|
1655
|
+
});
|
|
1469
1656
|
else if (phase !== "ready") {
|
|
1470
1657
|
const lines = [
|
|
1471
1658
|
"A place for curious developers.",
|
|
@@ -1743,7 +1930,8 @@ function App({ initialConfig, flags }) {
|
|
|
1743
1930
|
)
|
|
1744
1931
|
: h(TextLine, {
|
|
1745
1932
|
text: form
|
|
1746
|
-
? "Tab next field ·
|
|
1933
|
+
? "Tab next field · Ctrl+S preview" +
|
|
1934
|
+
(draftSaved ? " · Draft saved locally" : "")
|
|
1747
1935
|
: preview
|
|
1748
1936
|
? "Scroll to review · ← → choose · Enter confirms"
|
|
1749
1937
|
: current?.subtitle || " ",
|
|
@@ -1810,19 +1998,21 @@ function App({ initialConfig, flags }) {
|
|
|
1810
1998
|
compact && focus === "nav" ? null : pane,
|
|
1811
1999
|
);
|
|
1812
2000
|
}
|
|
1813
|
-
const hint =
|
|
1814
|
-
? "
|
|
1815
|
-
:
|
|
1816
|
-
? "
|
|
1817
|
-
:
|
|
1818
|
-
? "
|
|
1819
|
-
:
|
|
1820
|
-
? "
|
|
1821
|
-
:
|
|
1822
|
-
? "
|
|
1823
|
-
: route.view === "
|
|
1824
|
-
? "
|
|
1825
|
-
:
|
|
2001
|
+
const hint = picker
|
|
2002
|
+
? "Project files stay local until publication is confirmed"
|
|
2003
|
+
: reading
|
|
2004
|
+
? "↑↓ scroll · R reply · E edit · Esc close"
|
|
2005
|
+
: preview
|
|
2006
|
+
? "← → choose · Enter confirm · Esc edit"
|
|
2007
|
+
: form
|
|
2008
|
+
? "Tab field · Ctrl+O files · Ctrl+G diff · Ctrl+S preview · Esc discard"
|
|
2009
|
+
: phase !== "ready"
|
|
2010
|
+
? "Enter connect / open browser · Ctrl+C exit"
|
|
2011
|
+
: route.view === "feedback"
|
|
2012
|
+
? "↑↓ select · R reply · E edit · D delete · S resolve · ! report"
|
|
2013
|
+
: route.view === "post"
|
|
2014
|
+
? "←→ tabs · ↑↓ scroll/select · R reply · C code remark · Space full text · Tab navigate"
|
|
2015
|
+
: "↑↓ select · Enter open · Tab sections · [ ] pages · N new · / search";
|
|
1826
2016
|
return h(
|
|
1827
2017
|
Box,
|
|
1828
2018
|
{
|
|
@@ -1892,7 +2082,8 @@ export async function startTui(flags = {}) {
|
|
|
1892
2082
|
let config = await loadConfig();
|
|
1893
2083
|
if (flags.host && config && originOf(flags.host) !== config.origin)
|
|
1894
2084
|
config = null;
|
|
1895
|
-
const
|
|
2085
|
+
const root = await workspaceRoot(flags.project || process.cwd());
|
|
2086
|
+
const app = render(h(App, { initialConfig: config, flags, root }), {
|
|
1896
2087
|
alternateScreen: true,
|
|
1897
2088
|
exitOnCtrlC: false,
|
|
1898
2089
|
patchConsole: true,
|