@tenyokjs/devdevian-cli 0.2.0-alpha.1 → 0.3.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 +30 -4
- package/bin/devdevian.mjs +3 -1
- package/lib/drafts.mjs +119 -0
- package/lib/picker.mjs +421 -0
- package/lib/tui-data.mjs +21 -4
- package/lib/tui-model.mjs +2 -1
- package/lib/tui.mjs +292 -82
- 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
|
" · " +
|
|
@@ -319,8 +333,14 @@ function listRows(route, data, index, width) {
|
|
|
319
333
|
item.name +
|
|
320
334
|
(item.entryType === "note"
|
|
321
335
|
? " · original remark"
|
|
322
|
-
:
|
|
323
|
-
|
|
336
|
+
: item.entryType === "parent"
|
|
337
|
+
? " · parent reply"
|
|
338
|
+
: " · reply #" + item.id);
|
|
339
|
+
meta =
|
|
340
|
+
stamp(item.created) +
|
|
341
|
+
((item.edited_at || item.updated_at) && !item.deleted_at
|
|
342
|
+
? " · edited"
|
|
343
|
+
: "");
|
|
324
344
|
body = item.deleted_at ? "[deleted — discussion preserved]" : item.body;
|
|
325
345
|
} else {
|
|
326
346
|
meta = [
|
|
@@ -350,7 +370,7 @@ function listRows(route, data, index, width) {
|
|
|
350
370
|
];
|
|
351
371
|
});
|
|
352
372
|
}
|
|
353
|
-
function App({ initialConfig, flags }) {
|
|
373
|
+
function App({ initialConfig, flags, root }) {
|
|
354
374
|
const { exit } = useApp(),
|
|
355
375
|
{ columns, rows } = useWindowSize();
|
|
356
376
|
const [config, setConfig] = useState(initialConfig),
|
|
@@ -380,12 +400,24 @@ function App({ initialConfig, flags }) {
|
|
|
380
400
|
[yes, setYes] = useState(false),
|
|
381
401
|
[pending, setPending] = useState(false),
|
|
382
402
|
[tick, setTick] = useState(0),
|
|
383
|
-
[reading, setReading] = useState(null)
|
|
403
|
+
[reading, setReading] = useState(null),
|
|
404
|
+
[picker, setPicker] = useState(null),
|
|
405
|
+
[draftSaved, setDraftSaved] = useState(false);
|
|
384
406
|
const cache = useRef(new PageCache()),
|
|
385
407
|
history = useRef([]),
|
|
386
408
|
busy = useRef(false),
|
|
387
409
|
alive = useRef(true),
|
|
388
|
-
authController = useRef(null)
|
|
410
|
+
authController = useRef(null),
|
|
411
|
+
draftQueue = useRef(Promise.resolve()),
|
|
412
|
+
draftTimer = useRef(null),
|
|
413
|
+
draftGeneration = useRef(0);
|
|
414
|
+
const drafts = useMemo(
|
|
415
|
+
() =>
|
|
416
|
+
config && user
|
|
417
|
+
? new DraftStore({ origin: config.origin, userId: user.id, root })
|
|
418
|
+
: null,
|
|
419
|
+
[config?.origin, user?.id, root],
|
|
420
|
+
);
|
|
389
421
|
const api = useMemo(
|
|
390
422
|
() => (config ? client(config.origin, config.token) : null),
|
|
391
423
|
[config?.origin, config?.token],
|
|
@@ -396,7 +428,7 @@ function App({ initialConfig, flags }) {
|
|
|
396
428
|
sidebar = compact ? 0 : 25,
|
|
397
429
|
bodyHeight = Math.max(1, height - 9),
|
|
398
430
|
contentWidth = Math.max(1, width - sidebar - 6),
|
|
399
|
-
contentHeight = Math.max(1, bodyHeight -
|
|
431
|
+
contentHeight = Math.max(1, bodyHeight - 6);
|
|
400
432
|
const key = JSON.stringify(route),
|
|
401
433
|
current = data?.key === key ? data : null;
|
|
402
434
|
const selected = current?.items?.[index];
|
|
@@ -405,6 +437,7 @@ function App({ initialConfig, flags }) {
|
|
|
405
437
|
history.current = [];
|
|
406
438
|
setData(null);
|
|
407
439
|
setForm(null);
|
|
440
|
+
setPicker(null);
|
|
408
441
|
setPreview(null);
|
|
409
442
|
setDevice(null);
|
|
410
443
|
setUser(null);
|
|
@@ -462,6 +495,79 @@ function App({ initialConfig, flags }) {
|
|
|
462
495
|
setRefresh((n) => n + 1);
|
|
463
496
|
if (message) setNotice(message);
|
|
464
497
|
}
|
|
498
|
+
function queueDraft(work) {
|
|
499
|
+
const result = draftQueue.current.catch(() => {}).then(work);
|
|
500
|
+
draftQueue.current = result;
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
async function forgetDraft(value) {
|
|
504
|
+
clearTimeout(draftTimer.current);
|
|
505
|
+
cache.current.clear();
|
|
506
|
+
if (drafts && value)
|
|
507
|
+
await queueDraft(() => drafts.remove(draftIdentity(value)));
|
|
508
|
+
}
|
|
509
|
+
async function restoreDraft(item) {
|
|
510
|
+
await task(async () => {
|
|
511
|
+
const saved = await drafts.read(item.id),
|
|
512
|
+
next = { ...saved.form, draftId: item.id };
|
|
513
|
+
if (next.kind === "publication") {
|
|
514
|
+
const [topics, communities] = await Promise.all([
|
|
515
|
+
api("/topics"),
|
|
516
|
+
api("/communities?joined=1&limit=50"),
|
|
517
|
+
]);
|
|
518
|
+
next.topics = itemsOf(topics).map((t) => [t.id, t.name]);
|
|
519
|
+
next.audiences = [
|
|
520
|
+
["", "Public feed · everyone"],
|
|
521
|
+
...itemsOf(communities).map((c) => [c.id, "Private · " + c.name]),
|
|
522
|
+
];
|
|
523
|
+
if (
|
|
524
|
+
next.values.community_id &&
|
|
525
|
+
!next.audiences.some(
|
|
526
|
+
(c) => String(c[0]) === String(next.values.community_id),
|
|
527
|
+
)
|
|
528
|
+
) {
|
|
529
|
+
let space;
|
|
530
|
+
try {
|
|
531
|
+
space = await api(
|
|
532
|
+
"/communities/" + encodeURIComponent(next.values.community_id),
|
|
533
|
+
);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
if (![403, 404].includes(e.status)) throw e;
|
|
536
|
+
}
|
|
537
|
+
if (space?.is_member)
|
|
538
|
+
next.audiences.push([space.id, "Private · " + space.name]);
|
|
539
|
+
else {
|
|
540
|
+
next.unavailableAudience = next.values.community_id;
|
|
541
|
+
next.audiences.push([
|
|
542
|
+
next.values.community_id,
|
|
543
|
+
"Unavailable private audience · choose another",
|
|
544
|
+
]);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
openForm(next);
|
|
549
|
+
setNotice(
|
|
550
|
+
"Restored local draft. Check the code selection before publishing.",
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
const generation = ++draftGeneration.current;
|
|
556
|
+
if (!drafts || !form) return;
|
|
557
|
+
setDraftSaved(false);
|
|
558
|
+
draftTimer.current = setTimeout(() => {
|
|
559
|
+
queueDraft(() => drafts.save(form))
|
|
560
|
+
.then((id) => {
|
|
561
|
+
if (id && alive.current && generation === draftGeneration.current)
|
|
562
|
+
setDraftSaved(true);
|
|
563
|
+
})
|
|
564
|
+
.catch((e) => {
|
|
565
|
+
if (alive.current)
|
|
566
|
+
setError("Could not save local draft: " + clean(e.message));
|
|
567
|
+
});
|
|
568
|
+
}, 450);
|
|
569
|
+
return () => clearTimeout(draftTimer.current);
|
|
570
|
+
}, [drafts, form]);
|
|
465
571
|
function askConfirm(title, description, action) {
|
|
466
572
|
setYes(false);
|
|
467
573
|
setDialog({ title, description, action });
|
|
@@ -505,7 +611,15 @@ function App({ initialConfig, flags }) {
|
|
|
505
611
|
setLoading(!cached);
|
|
506
612
|
setError("");
|
|
507
613
|
if (!cached)
|
|
508
|
-
|
|
614
|
+
(route.view === "drafts"
|
|
615
|
+
? drafts.list().then((items) => ({
|
|
616
|
+
items,
|
|
617
|
+
title: "Local drafts",
|
|
618
|
+
subtitle:
|
|
619
|
+
"Private to this account and project · Enter restore · D delete",
|
|
620
|
+
}))
|
|
621
|
+
: loadPage(api, route, controller.signal)
|
|
622
|
+
)
|
|
509
623
|
.then((result) => {
|
|
510
624
|
if (!active) return;
|
|
511
625
|
cache.current.set(key, result);
|
|
@@ -605,7 +719,11 @@ function App({ initialConfig, flags }) {
|
|
|
605
719
|
}
|
|
606
720
|
}
|
|
607
721
|
function openForm(next) {
|
|
608
|
-
setForm(
|
|
722
|
+
setForm({
|
|
723
|
+
...next,
|
|
724
|
+
draftId: next.draftId || randomBytes(32).toString("hex"),
|
|
725
|
+
});
|
|
726
|
+
setDraftSaved(false);
|
|
609
727
|
setField(0);
|
|
610
728
|
setCursor(graphemes(next.values[fieldsFor(next)[0].key] || "").length);
|
|
611
729
|
setPreview(null);
|
|
@@ -704,6 +822,13 @@ function App({ initialConfig, flags }) {
|
|
|
704
822
|
async function prepare() {
|
|
705
823
|
if (!form) return;
|
|
706
824
|
await task(async () => {
|
|
825
|
+
if (
|
|
826
|
+
form.unavailableAudience &&
|
|
827
|
+
String(form.values.community_id) === String(form.unavailableAudience)
|
|
828
|
+
)
|
|
829
|
+
throw Error(
|
|
830
|
+
"Choose an available audience explicitly before publishing this draft.",
|
|
831
|
+
);
|
|
707
832
|
const fields = fieldsFor(form);
|
|
708
833
|
for (let i = 0; i < fields.length; i++) {
|
|
709
834
|
const f = fields[i],
|
|
@@ -736,36 +861,31 @@ function App({ initialConfig, flags }) {
|
|
|
736
861
|
};
|
|
737
862
|
url = "/posts";
|
|
738
863
|
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");
|
|
864
|
+
let source = form.source;
|
|
865
|
+
if (source?.kind !== "diff") {
|
|
866
|
+
source = {
|
|
867
|
+
kind: "file",
|
|
868
|
+
path: form.values.file.replace(/^~(?=\/)/, homedir()),
|
|
869
|
+
};
|
|
870
|
+
if (form.values.lines) {
|
|
871
|
+
const match = form.values.lines.match(/^([1-9]\d*):([1-9]\d*)$/);
|
|
872
|
+
if (!match) throw Error("Use START:END for file lines.");
|
|
873
|
+
source.start = Number(match[1]);
|
|
874
|
+
source.end = Number(match[2]);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const chosen = await reviewSource(root, source);
|
|
878
|
+
payload.code = chosen.code;
|
|
879
|
+
if (chosen.kind === "diff") {
|
|
880
|
+
payload.body +=
|
|
881
|
+
"\n\nReview source: " +
|
|
882
|
+
clean(chosen.label).replace(/\n/g, " ") +
|
|
883
|
+
"\nReview line numbers refer to the published patch, including its headers.";
|
|
884
|
+
if (payload.body.length > 15000)
|
|
885
|
+
throw Error(
|
|
886
|
+
"Shorten the context to leave room for the Git diff source label.",
|
|
887
|
+
);
|
|
765
888
|
}
|
|
766
|
-
if (code.trim().length < 10)
|
|
767
|
-
throw Error("The selected snippet needs at least 10 characters.");
|
|
768
|
-
payload.code = code;
|
|
769
889
|
payload.review_focus = form.values.review_focus;
|
|
770
890
|
}
|
|
771
891
|
if (payload.kind === "showcase")
|
|
@@ -823,10 +943,16 @@ function App({ initialConfig, flags }) {
|
|
|
823
943
|
body: preview.payload,
|
|
824
944
|
});
|
|
825
945
|
cache.current.clear();
|
|
946
|
+
let localWarning = "";
|
|
947
|
+
try {
|
|
948
|
+
await forgetDraft(form);
|
|
949
|
+
} catch {
|
|
950
|
+
localWarning =
|
|
951
|
+
" Published successfully; the local draft could not be removed. Do not publish it again.";
|
|
952
|
+
}
|
|
826
953
|
setPreview(null);
|
|
827
954
|
setForm(null);
|
|
828
955
|
setScroll(0);
|
|
829
|
-
setNotice("Saved. Your web account sees the same update.");
|
|
830
956
|
if (preview.url === "/posts")
|
|
831
957
|
navigate({ view: "post", id: response.id, tab: "overview", page: 1 });
|
|
832
958
|
else if (preview.url === "/communities")
|
|
@@ -841,12 +967,17 @@ function App({ initialConfig, flags }) {
|
|
|
841
967
|
page: 1,
|
|
842
968
|
});
|
|
843
969
|
}
|
|
970
|
+
setNotice("Saved. Your web account sees the same update." + localWarning);
|
|
844
971
|
});
|
|
845
972
|
}
|
|
846
973
|
async function openSelected() {
|
|
847
974
|
if (route.view === "community" && !current?.community?.is_member) return;
|
|
848
975
|
const item = selected;
|
|
849
976
|
if (!item) return;
|
|
977
|
+
if (route.view === "drafts") {
|
|
978
|
+
await restoreDraft(item);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
850
981
|
if (route.view === "devices") {
|
|
851
982
|
askConfirm("Revoke this terminal?", item.name, () =>
|
|
852
983
|
task(async () => {
|
|
@@ -910,6 +1041,11 @@ function App({ initialConfig, flags }) {
|
|
|
910
1041
|
navigate({ view: "feedback", postId: route.id, id: item.id, page: 1 });
|
|
911
1042
|
return;
|
|
912
1043
|
}
|
|
1044
|
+
if (route.view === "thread" && item.entryType === "parent") {
|
|
1045
|
+
setReading(item);
|
|
1046
|
+
setScroll(0);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
913
1049
|
if (
|
|
914
1050
|
(route.view === "post" && route.tab === "replies") ||
|
|
915
1051
|
route.view === "thread"
|
|
@@ -978,25 +1114,43 @@ function App({ initialConfig, flags }) {
|
|
|
978
1114
|
});
|
|
979
1115
|
}
|
|
980
1116
|
function deleteSelected() {
|
|
1117
|
+
if (route.view === "drafts" && selected) {
|
|
1118
|
+
askConfirm(
|
|
1119
|
+
"Delete saved draft?",
|
|
1120
|
+
"Only this local draft will be removed.",
|
|
1121
|
+
() =>
|
|
1122
|
+
task(async () => {
|
|
1123
|
+
await queueDraft(() => drafts.remove(selected.id));
|
|
1124
|
+
invalidate("Draft deleted.");
|
|
1125
|
+
}),
|
|
1126
|
+
);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
981
1129
|
const target = editable();
|
|
982
|
-
if (
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
) {
|
|
987
|
-
setError("Select your own line feedback or feedback reply to delete.");
|
|
1130
|
+
if (!target || target.row.user_id !== user.id || target.row.deleted_at) {
|
|
1131
|
+
setError(
|
|
1132
|
+
"Select your own reply or code feedback in the Replies / Code feedback tab to delete.",
|
|
1133
|
+
);
|
|
988
1134
|
return;
|
|
989
1135
|
}
|
|
990
1136
|
askConfirm(
|
|
991
|
-
"Delete your feedback?",
|
|
992
|
-
"Your text is removed. Other members’ replies are preserved."
|
|
1137
|
+
target.feedback ? "Delete your feedback?" : "Delete your reply?",
|
|
1138
|
+
"Your text is removed. Other members’ replies are preserved." +
|
|
1139
|
+
(!target.feedback && current.post?.accepted_reply_id === target.row.id
|
|
1140
|
+
? " Its accepted-solution mark is cleared."
|
|
1141
|
+
: ""),
|
|
993
1142
|
() =>
|
|
994
1143
|
task(async () => {
|
|
995
1144
|
await api(target.url, {
|
|
996
1145
|
method: "DELETE",
|
|
997
1146
|
body: { revision: target.row.revision },
|
|
998
1147
|
});
|
|
999
|
-
|
|
1148
|
+
setReading(null);
|
|
1149
|
+
invalidate(
|
|
1150
|
+
target.feedback
|
|
1151
|
+
? "Feedback deleted; the discussion is preserved."
|
|
1152
|
+
: "Reply deleted; other members’ replies are preserved.",
|
|
1153
|
+
);
|
|
1000
1154
|
}),
|
|
1001
1155
|
);
|
|
1002
1156
|
}
|
|
@@ -1074,8 +1228,13 @@ function App({ initialConfig, flags }) {
|
|
|
1074
1228
|
if (form) {
|
|
1075
1229
|
askConfirm(
|
|
1076
1230
|
"Leave this draft?",
|
|
1077
|
-
"
|
|
1078
|
-
() =>
|
|
1231
|
+
"Save this draft locally and exit? Resume it from Local drafts in this project.",
|
|
1232
|
+
() =>
|
|
1233
|
+
task(async () => {
|
|
1234
|
+
clearTimeout(draftTimer.current);
|
|
1235
|
+
if (drafts) await queueDraft(() => drafts.save(form));
|
|
1236
|
+
exit();
|
|
1237
|
+
}),
|
|
1079
1238
|
);
|
|
1080
1239
|
} else exit();
|
|
1081
1240
|
}
|
|
@@ -1090,6 +1249,7 @@ function App({ initialConfig, flags }) {
|
|
|
1090
1249
|
);
|
|
1091
1250
|
}
|
|
1092
1251
|
useInput((input, keyEvent) => {
|
|
1252
|
+
if (picker) return;
|
|
1093
1253
|
if (keyEvent.ctrl && input === "c") {
|
|
1094
1254
|
if (dialog) {
|
|
1095
1255
|
exit();
|
|
@@ -1155,9 +1315,11 @@ function App({ initialConfig, flags }) {
|
|
|
1155
1315
|
else if (input === "r") {
|
|
1156
1316
|
setReading(null);
|
|
1157
1317
|
reply();
|
|
1158
|
-
} else if (input === "e") {
|
|
1318
|
+
} else if (input === "e" || input === "E") {
|
|
1159
1319
|
setReading(null);
|
|
1160
1320
|
editSelected();
|
|
1321
|
+
} else if (input === "d" || input === "D") {
|
|
1322
|
+
deleteSelected();
|
|
1161
1323
|
}
|
|
1162
1324
|
return;
|
|
1163
1325
|
}
|
|
@@ -1183,15 +1345,26 @@ function App({ initialConfig, flags }) {
|
|
|
1183
1345
|
return;
|
|
1184
1346
|
}
|
|
1185
1347
|
if (form) {
|
|
1348
|
+
if (
|
|
1349
|
+
keyEvent.ctrl &&
|
|
1350
|
+
["o", "g"].includes(input) &&
|
|
1351
|
+
form.kind === "publication" &&
|
|
1352
|
+
form.values.kind === "review"
|
|
1353
|
+
) {
|
|
1354
|
+
setPicker(input === "g" ? "changes" : "files");
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1186
1357
|
if (keyEvent.escape) {
|
|
1187
1358
|
askConfirm(
|
|
1188
1359
|
"Discard this draft?",
|
|
1189
1360
|
"Nothing has been published. Stay to keep editing.",
|
|
1190
|
-
() =>
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1361
|
+
() =>
|
|
1362
|
+
task(async () => {
|
|
1363
|
+
await forgetDraft(form);
|
|
1364
|
+
setForm(null);
|
|
1365
|
+
setFocus("content");
|
|
1366
|
+
setError("");
|
|
1367
|
+
}),
|
|
1195
1368
|
);
|
|
1196
1369
|
return;
|
|
1197
1370
|
}
|
|
@@ -1236,6 +1409,10 @@ function App({ initialConfig, flags }) {
|
|
|
1236
1409
|
return;
|
|
1237
1410
|
}
|
|
1238
1411
|
if (keyEvent.return && !active.multiline) {
|
|
1412
|
+
if (active.key === "file" && !form.values.file) {
|
|
1413
|
+
setPicker("files");
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1239
1416
|
changeField(field + 1);
|
|
1240
1417
|
return;
|
|
1241
1418
|
}
|
|
@@ -1248,6 +1425,7 @@ function App({ initialConfig, flags }) {
|
|
|
1248
1425
|
);
|
|
1249
1426
|
setForm((old) => ({
|
|
1250
1427
|
...old,
|
|
1428
|
+
...(active.key === "file" ? { source: null } : {}),
|
|
1251
1429
|
values: { ...old.values, [active.key]: updated.value },
|
|
1252
1430
|
}));
|
|
1253
1431
|
setCursor(updated.cursor);
|
|
@@ -1406,7 +1584,7 @@ function App({ initialConfig, flags }) {
|
|
|
1406
1584
|
editSelected();
|
|
1407
1585
|
return;
|
|
1408
1586
|
}
|
|
1409
|
-
if (input === "d") {
|
|
1587
|
+
if (input === "d" || input === "D") {
|
|
1410
1588
|
deleteSelected();
|
|
1411
1589
|
return;
|
|
1412
1590
|
}
|
|
@@ -1466,6 +1644,34 @@ function App({ initialConfig, flags }) {
|
|
|
1466
1644
|
h(Text, null, "Enlarge this terminal to at least 60 × 18."),
|
|
1467
1645
|
h(Text, null, "Q or Ctrl+C exits."),
|
|
1468
1646
|
);
|
|
1647
|
+
else if (picker)
|
|
1648
|
+
center = h(WorkspacePicker, {
|
|
1649
|
+
root,
|
|
1650
|
+
width,
|
|
1651
|
+
height: bodyHeight,
|
|
1652
|
+
initial: picker,
|
|
1653
|
+
onCancel: () => setPicker(null),
|
|
1654
|
+
onSelect: (source) => {
|
|
1655
|
+
setForm((old) => ({
|
|
1656
|
+
...old,
|
|
1657
|
+
source,
|
|
1658
|
+
values: {
|
|
1659
|
+
...old.values,
|
|
1660
|
+
file: source.path,
|
|
1661
|
+
lines:
|
|
1662
|
+
source.kind === "file" && source.start
|
|
1663
|
+
? source.start + ":" + source.end
|
|
1664
|
+
: "",
|
|
1665
|
+
},
|
|
1666
|
+
}));
|
|
1667
|
+
setPicker(null);
|
|
1668
|
+
setNotice(
|
|
1669
|
+
source.kind === "diff"
|
|
1670
|
+
? "Git changes selected. Preview before publishing."
|
|
1671
|
+
: "File selected. Preview before publishing.",
|
|
1672
|
+
);
|
|
1673
|
+
},
|
|
1674
|
+
});
|
|
1469
1675
|
else if (phase !== "ready") {
|
|
1470
1676
|
const lines = [
|
|
1471
1677
|
"A place for curious developers.",
|
|
@@ -1743,7 +1949,8 @@ function App({ initialConfig, flags }) {
|
|
|
1743
1949
|
)
|
|
1744
1950
|
: h(TextLine, {
|
|
1745
1951
|
text: form
|
|
1746
|
-
? "Tab next field ·
|
|
1952
|
+
? "Tab next field · Ctrl+S preview" +
|
|
1953
|
+
(draftSaved ? " · Draft saved locally" : "")
|
|
1747
1954
|
: preview
|
|
1748
1955
|
? "Scroll to review · ← → choose · Enter confirms"
|
|
1749
1956
|
: current?.subtitle || " ",
|
|
@@ -1810,19 +2017,21 @@ function App({ initialConfig, flags }) {
|
|
|
1810
2017
|
compact && focus === "nav" ? null : pane,
|
|
1811
2018
|
);
|
|
1812
2019
|
}
|
|
1813
|
-
const hint =
|
|
1814
|
-
? "
|
|
1815
|
-
:
|
|
1816
|
-
? "
|
|
1817
|
-
:
|
|
1818
|
-
? "
|
|
1819
|
-
:
|
|
1820
|
-
? "
|
|
1821
|
-
:
|
|
1822
|
-
? "
|
|
1823
|
-
: route.view === "
|
|
1824
|
-
? "
|
|
1825
|
-
:
|
|
2020
|
+
const hint = picker
|
|
2021
|
+
? "Project files stay local until publication is confirmed"
|
|
2022
|
+
: reading
|
|
2023
|
+
? "↑↓ scroll · R reply · E edit · D delete · Esc close"
|
|
2024
|
+
: preview
|
|
2025
|
+
? "← → choose · Enter confirm · Esc edit"
|
|
2026
|
+
: form
|
|
2027
|
+
? "Tab field · Ctrl+O files · Ctrl+G diff · Ctrl+S preview · Esc discard"
|
|
2028
|
+
: phase !== "ready"
|
|
2029
|
+
? "Enter connect / open browser · Ctrl+C exit"
|
|
2030
|
+
: route.view === "feedback"
|
|
2031
|
+
? "↑↓ select · R reply · E edit · D delete · S resolve · ! report"
|
|
2032
|
+
: route.view === "post"
|
|
2033
|
+
? "←→ tabs · ↑↓ select · R reply · E edit · D delete · Space read"
|
|
2034
|
+
: "↑↓ select · Enter open · Tab sections · [ ] pages · N new · / search";
|
|
1826
2035
|
return h(
|
|
1827
2036
|
Box,
|
|
1828
2037
|
{
|
|
@@ -1892,7 +2101,8 @@ export async function startTui(flags = {}) {
|
|
|
1892
2101
|
let config = await loadConfig();
|
|
1893
2102
|
if (flags.host && config && originOf(flags.host) !== config.origin)
|
|
1894
2103
|
config = null;
|
|
1895
|
-
const
|
|
2104
|
+
const root = await workspaceRoot(flags.project || process.cwd());
|
|
2105
|
+
const app = render(h(App, { initialConfig: config, flags, root }), {
|
|
1896
2106
|
alternateScreen: true,
|
|
1897
2107
|
exitOnCtrlC: false,
|
|
1898
2108
|
patchConsole: true,
|