@tenyokjs/devdevian-cli 0.4.1-alpha.1 → 0.5.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 +36 -0
- package/VERSIONS.md +26 -3
- package/bin/devdevian.mjs +47 -1
- package/lib/appearance.mjs +187 -0
- package/lib/devi.mjs +137 -0
- package/lib/picker.mjs +6 -5
- package/lib/review-workspace.mjs +8 -5
- package/lib/rich-text.mjs +4 -4
- package/lib/theme.mjs +66 -13
- package/lib/tui-data.mjs +2 -0
- package/lib/tui-model.mjs +2 -1
- package/lib/tui.mjs +425 -178
- package/package.json +1 -1
package/lib/tui.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { render, Box, Text, useApp, useInput, useWindowSize } from "ink";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { homedir, hostname } from "node:os";
|
|
5
5
|
import { workspaceRoot, reviewSource } from "./workspace.mjs";
|
|
6
|
+
import { DeviPanel } from "./devi.mjs";
|
|
6
7
|
import { WorkspacePicker } from "./picker.mjs";
|
|
7
8
|
import { DraftStore, draftIdentity } from "./drafts.mjs";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
@@ -32,7 +33,15 @@ import {
|
|
|
32
33
|
notificationRoute,
|
|
33
34
|
} from "./tui-data.mjs";
|
|
34
35
|
import { basename } from "node:path";
|
|
35
|
-
import {
|
|
36
|
+
import { themeFor, ThemeContext, useTheme } from "./theme.mjs";
|
|
37
|
+
import {
|
|
38
|
+
appearanceFields,
|
|
39
|
+
defaultAppearance,
|
|
40
|
+
loadAppearance,
|
|
41
|
+
saveAppearance,
|
|
42
|
+
normalizeAppearance,
|
|
43
|
+
appearanceFlags,
|
|
44
|
+
} from "./appearance.mjs";
|
|
36
45
|
import { liveLoop, anchoredIndex } from "./live.mjs";
|
|
37
46
|
import { contextActions } from "./context-actions.mjs";
|
|
38
47
|
import { editExternal } from "./external-editor.mjs";
|
|
@@ -83,6 +92,7 @@ function openBrowser(url) {
|
|
|
83
92
|
child.unref();
|
|
84
93
|
}
|
|
85
94
|
function fieldsFor(form) {
|
|
95
|
+
if (form.kind === "appearance") return appearanceFields;
|
|
86
96
|
if (form.kind === "publication")
|
|
87
97
|
return [
|
|
88
98
|
{
|
|
@@ -190,10 +200,16 @@ function fieldsFor(form) {
|
|
|
190
200
|
},
|
|
191
201
|
];
|
|
192
202
|
}
|
|
193
|
-
function TextLine({ text, width, color
|
|
194
|
-
|
|
203
|
+
function TextLine({ text, width, color, bold = false }) {
|
|
204
|
+
const colors = useTheme();
|
|
205
|
+
return h(
|
|
206
|
+
Text,
|
|
207
|
+
{ color: color || colors.text, bold, wrap: "truncate" },
|
|
208
|
+
clip(text, width),
|
|
209
|
+
);
|
|
195
210
|
}
|
|
196
211
|
function FrameRows({ rows, width, height, offset = 0 }) {
|
|
212
|
+
const colors = useTheme();
|
|
197
213
|
return h(
|
|
198
214
|
Box,
|
|
199
215
|
{ flexDirection: "column", height, overflow: "hidden" },
|
|
@@ -226,6 +242,7 @@ function FrameRows({ rows, width, height, offset = 0 }) {
|
|
|
226
242
|
);
|
|
227
243
|
}
|
|
228
244
|
function InputText({ value, cursor, width, height = 1 }) {
|
|
245
|
+
const colors = useTheme();
|
|
229
246
|
const result = cursorLines(value, cursor, width),
|
|
230
247
|
start = Math.max(0, result.line - height + 1);
|
|
231
248
|
return h(
|
|
@@ -244,6 +261,7 @@ function InputText({ value, cursor, width, height = 1 }) {
|
|
|
244
261
|
);
|
|
245
262
|
}
|
|
246
263
|
function FormView({ form, active, cursor, width, height }) {
|
|
264
|
+
const colors = useTheme();
|
|
247
265
|
const fields = fieldsFor(form),
|
|
248
266
|
rows = [];
|
|
249
267
|
let selectedLine = 0;
|
|
@@ -295,14 +313,19 @@ function FormView({ form, active, cursor, width, height }) {
|
|
|
295
313
|
rows.push({
|
|
296
314
|
text:
|
|
297
315
|
(active === fields.length ? "› " : " ") +
|
|
298
|
-
|
|
316
|
+
(form.kind === "appearance"
|
|
317
|
+
? "Save appearance [Ctrl+S]"
|
|
318
|
+
: "Preview publication [Ctrl+S]"),
|
|
299
319
|
color: colors.blue,
|
|
300
320
|
bold: true,
|
|
301
321
|
});
|
|
302
|
-
const offset = Math.max(
|
|
322
|
+
const offset = Math.max(
|
|
323
|
+
0,
|
|
324
|
+
Math.min(selectedLine - (height >= 4 ? 1 : 0), rows.length - height),
|
|
325
|
+
);
|
|
303
326
|
return h(FrameRows, { rows, width, height, offset });
|
|
304
327
|
}
|
|
305
|
-
function listRows(route, data, index, width) {
|
|
328
|
+
function listRows(route, data, index, width, colors, density = "comfortable") {
|
|
306
329
|
const items = data.items || [];
|
|
307
330
|
return items.flatMap((item, i) => {
|
|
308
331
|
let title = item.title || item.name || "",
|
|
@@ -388,21 +411,28 @@ function listRows(route, data, index, width) {
|
|
|
388
411
|
color: selected ? colors.text : colors.muted,
|
|
389
412
|
selected,
|
|
390
413
|
},
|
|
391
|
-
|
|
414
|
+
...(density === "compact"
|
|
415
|
+
? []
|
|
416
|
+
: [
|
|
417
|
+
{
|
|
418
|
+
text: " " + "─".repeat(Math.max(0, width - 4)),
|
|
419
|
+
color: colors.line,
|
|
420
|
+
},
|
|
421
|
+
]),
|
|
392
422
|
];
|
|
393
423
|
});
|
|
394
424
|
}
|
|
395
|
-
function documentRows(route, data, width) {
|
|
425
|
+
function documentRows(route, data, width, colors) {
|
|
396
426
|
if (route.view !== "post" || (route.tab && route.tab !== "overview"))
|
|
397
427
|
return contentLines(route, data).flatMap((line) => wrap(line, width));
|
|
398
428
|
const post = data?.post;
|
|
399
429
|
if (!post) return [];
|
|
400
|
-
const rows = markdownRows(post.body || "", width);
|
|
430
|
+
const rows = markdownRows(post.body || "", width, colors);
|
|
401
431
|
if (post.review_focus)
|
|
402
432
|
rows.push(
|
|
403
433
|
{ text: "" },
|
|
404
434
|
{ text: "REVIEW FOCUS", color: colors.purple, bold: true },
|
|
405
|
-
...markdownRows(post.review_focus, width),
|
|
435
|
+
...markdownRows(post.review_focus, width, colors),
|
|
406
436
|
);
|
|
407
437
|
if (post.code) {
|
|
408
438
|
// Pick a fence longer than any backtick run in the snippet.
|
|
@@ -414,7 +444,7 @@ function documentRows(route, data, width) {
|
|
|
414
444
|
);
|
|
415
445
|
rows.push(
|
|
416
446
|
{ text: "" },
|
|
417
|
-
...markdownRows(fence + "\n" + post.code + "\n" + fence, width),
|
|
447
|
+
...markdownRows(fence + "\n" + post.code + "\n" + fence, width, colors),
|
|
418
448
|
);
|
|
419
449
|
}
|
|
420
450
|
if (post.links?.length)
|
|
@@ -431,16 +461,71 @@ function documentRows(route, data, width) {
|
|
|
431
461
|
});
|
|
432
462
|
return rows;
|
|
433
463
|
}
|
|
434
|
-
function
|
|
464
|
+
function AppearanceView({ form, active, cursor, width, height }) {
|
|
465
|
+
const c = useTheme(),
|
|
466
|
+
wide = width >= 100;
|
|
467
|
+
return h(
|
|
468
|
+
Box,
|
|
469
|
+
{ height, overflow: "hidden", gap: wide ? 2 : 0 },
|
|
470
|
+
h(
|
|
471
|
+
Box,
|
|
472
|
+
{ width: wide ? width - 36 : width, flexDirection: "column" },
|
|
473
|
+
h(FormView, {
|
|
474
|
+
form,
|
|
475
|
+
active,
|
|
476
|
+
cursor,
|
|
477
|
+
width: wide ? width - 36 : width,
|
|
478
|
+
height,
|
|
479
|
+
}),
|
|
480
|
+
),
|
|
481
|
+
wide
|
|
482
|
+
? h(
|
|
483
|
+
Box,
|
|
484
|
+
{
|
|
485
|
+
width: 34,
|
|
486
|
+
height,
|
|
487
|
+
borderStyle: c.frame,
|
|
488
|
+
borderColor: c.line,
|
|
489
|
+
paddingX: 1,
|
|
490
|
+
flexDirection: "column",
|
|
491
|
+
overflow: "hidden",
|
|
492
|
+
},
|
|
493
|
+
h(Text, { color: c.purple, bold: true }, "LIVE PREVIEW"),
|
|
494
|
+
h(Text, { color: c.muted }, "Only this terminal changes."),
|
|
495
|
+
h(Text, null, " "),
|
|
496
|
+
h(
|
|
497
|
+
Text,
|
|
498
|
+
{ color: c.text, bold: true },
|
|
499
|
+
"A good conversation starts here",
|
|
500
|
+
),
|
|
501
|
+
h(
|
|
502
|
+
Text,
|
|
503
|
+
{ color: c.blue, backgroundColor: c.selected },
|
|
504
|
+
"▸ Your selected conversation",
|
|
505
|
+
),
|
|
506
|
+
h(Text, { color: c.muted }, "Ada · code review · just now"),
|
|
507
|
+
h(Text, { color: c.green }, "+ ship with confidence"),
|
|
508
|
+
h(Text, { color: c.red }, "− leave the guesswork behind"),
|
|
509
|
+
h(Text, null, " "),
|
|
510
|
+
h(Text, { color: c.purple }, "← → change · Tab next"),
|
|
511
|
+
h(Text, { color: c.text }, "Ctrl+S save · Esc cancel"),
|
|
512
|
+
h(Text, { color: c.muted }, "0 restores the default preview"),
|
|
513
|
+
)
|
|
514
|
+
: null,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
function App({ initialConfig, flags, root, initialAppearance }) {
|
|
435
518
|
const { exit, suspendTerminal } = useApp(),
|
|
436
519
|
{ columns, rows } = useWindowSize();
|
|
520
|
+
const [appearance, setAppearance] = useState(initialAppearance.settings);
|
|
437
521
|
const [config, setConfig] = useState(initialConfig),
|
|
438
522
|
[phase, setPhase] = useState(
|
|
439
523
|
initialConfig && !flags.initialLogin ? "checking" : "auth",
|
|
440
524
|
),
|
|
441
525
|
[user, setUser] = useState(null),
|
|
442
526
|
[device, setDevice] = useState(null),
|
|
443
|
-
[authVersion, setAuthVersion] = useState(0)
|
|
527
|
+
[authVersion, setAuthVersion] = useState(0),
|
|
528
|
+
[booting, setBooting] = useState(true);
|
|
444
529
|
const [route, setRoute] = useState({ view: "feed", page: 1 }),
|
|
445
530
|
[data, setData] = useState(null),
|
|
446
531
|
[loading, setLoading] = useState(false),
|
|
@@ -452,7 +537,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
452
537
|
[command, setCommand] = useState(""),
|
|
453
538
|
[commandCursor, setCommandCursor] = useState(0),
|
|
454
539
|
[error, setError] = useState(""),
|
|
455
|
-
[notice, setNotice] = useState("");
|
|
540
|
+
[notice, setNotice] = useState(initialAppearance.warning || "");
|
|
456
541
|
const [form, setForm] = useState(null),
|
|
457
542
|
[field, setField] = useState(0),
|
|
458
543
|
[cursor, setCursor] = useState(0),
|
|
@@ -496,9 +581,13 @@ function App({ initialConfig, flags, root }) {
|
|
|
496
581
|
() => (config ? client(config.origin, config.token) : null),
|
|
497
582
|
[config?.origin, config?.token],
|
|
498
583
|
);
|
|
584
|
+
const effectiveAppearance =
|
|
585
|
+
form?.kind === "appearance" ? normalizeAppearance(form.values) : appearance;
|
|
586
|
+
const colors = themeFor(effectiveAppearance),
|
|
587
|
+
stride = effectiveAppearance.density === "compact" ? 3 : 4;
|
|
499
588
|
const width = Math.max(1, columns),
|
|
500
589
|
height = Math.max(1, rows - 1),
|
|
501
|
-
compact = width < 90,
|
|
590
|
+
compact = width < 90 || effectiveAppearance.sidebar === "hidden",
|
|
502
591
|
sidebar = compact ? 0 : 25,
|
|
503
592
|
bodyHeight = Math.max(1, height - 9),
|
|
504
593
|
contentWidth = Math.max(1, width - sidebar - 6),
|
|
@@ -606,6 +695,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
606
695
|
setPreview(null);
|
|
607
696
|
setDevice(null);
|
|
608
697
|
setUser(null);
|
|
698
|
+
setBooting(true);
|
|
609
699
|
setReading(null);
|
|
610
700
|
setActionIndex(null);
|
|
611
701
|
setUnread(0);
|
|
@@ -629,6 +719,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
629
719
|
} else setError(problemMessage(e));
|
|
630
720
|
}
|
|
631
721
|
function navigate(next, { replace = false } = {}) {
|
|
722
|
+
setBooting(false);
|
|
632
723
|
if (!replace)
|
|
633
724
|
history.current.push({
|
|
634
725
|
route,
|
|
@@ -786,10 +877,14 @@ function App({ initialConfig, flags, root }) {
|
|
|
786
877
|
};
|
|
787
878
|
}, []);
|
|
788
879
|
useEffect(() => {
|
|
789
|
-
if (
|
|
880
|
+
if (
|
|
881
|
+
effectiveAppearance.motion === "reduced" ||
|
|
882
|
+
(!loading && !pending && phase !== "pending")
|
|
883
|
+
)
|
|
884
|
+
return;
|
|
790
885
|
const timer = setInterval(() => setTick((v) => (v + 1) % 4), 180);
|
|
791
886
|
return () => clearInterval(timer);
|
|
792
|
-
}, [loading, pending, phase]);
|
|
887
|
+
}, [loading, pending, phase, effectiveAppearance.motion]);
|
|
793
888
|
useEffect(() => {
|
|
794
889
|
if (phase !== "checking" || !api) return;
|
|
795
890
|
const controller = new AbortController();
|
|
@@ -807,7 +902,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
807
902
|
if (!controller.signal.aborted) fail(e);
|
|
808
903
|
});
|
|
809
904
|
return () => controller.abort();
|
|
810
|
-
}, [phase, api]);
|
|
905
|
+
}, [phase, api, authVersion]);
|
|
811
906
|
useEffect(() => {
|
|
812
907
|
if (phase !== "ready" || !api || form || route.view === "help") return;
|
|
813
908
|
let active = true;
|
|
@@ -873,7 +968,10 @@ function App({ initialConfig, flags, root }) {
|
|
|
873
968
|
if (active) fail(e);
|
|
874
969
|
})
|
|
875
970
|
.finally(() => {
|
|
876
|
-
if (active)
|
|
971
|
+
if (active) {
|
|
972
|
+
setLoading(false);
|
|
973
|
+
setBooting(false);
|
|
974
|
+
}
|
|
877
975
|
});
|
|
878
976
|
return () => {
|
|
879
977
|
active = false;
|
|
@@ -905,6 +1003,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
905
1003
|
if (controller.signal.aborted) return;
|
|
906
1004
|
resetAccount();
|
|
907
1005
|
setConfig(saved);
|
|
1006
|
+
setBooting(true);
|
|
908
1007
|
setPhase("checking");
|
|
909
1008
|
setNotice("Terminal connected.");
|
|
910
1009
|
return;
|
|
@@ -944,6 +1043,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
944
1043
|
async function chooseNav(i) {
|
|
945
1044
|
const name = navigation[i]?.[0];
|
|
946
1045
|
setNavIndex(i);
|
|
1046
|
+
if (name === "appearance") return openAppearance();
|
|
947
1047
|
if (name === "ask" || name === "review")
|
|
948
1048
|
return composePublication(name === "review" ? "review" : "question");
|
|
949
1049
|
if (name === "open")
|
|
@@ -959,7 +1059,18 @@ function App({ initialConfig, flags, root }) {
|
|
|
959
1059
|
setCommandCursor(0);
|
|
960
1060
|
}
|
|
961
1061
|
}
|
|
1062
|
+
function openAppearance() {
|
|
1063
|
+
openForm({
|
|
1064
|
+
kind: "appearance",
|
|
1065
|
+
title: "Appearance",
|
|
1066
|
+
values: { ...appearance },
|
|
1067
|
+
});
|
|
1068
|
+
setNotice(
|
|
1069
|
+
"Live preview · ← → change · Ctrl+S save · Esc cancel · 0 defaults",
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
962
1072
|
function openForm(next) {
|
|
1073
|
+
setBooting(false);
|
|
963
1074
|
setForm({
|
|
964
1075
|
...next,
|
|
965
1076
|
draftId: next.draftId || randomBytes(32).toString("hex"),
|
|
@@ -1123,6 +1234,18 @@ function App({ initialConfig, flags, root }) {
|
|
|
1123
1234
|
}
|
|
1124
1235
|
async function prepare() {
|
|
1125
1236
|
if (!form) return;
|
|
1237
|
+
if (form.kind === "appearance") {
|
|
1238
|
+
await task(async () => {
|
|
1239
|
+
const saved = await saveAppearance(form.values);
|
|
1240
|
+
setAppearance(saved);
|
|
1241
|
+
setForm(null);
|
|
1242
|
+
setFocus("content");
|
|
1243
|
+
setNotice(
|
|
1244
|
+
"Appearance saved locally. Your account and other devices are unchanged.",
|
|
1245
|
+
);
|
|
1246
|
+
});
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1126
1249
|
await task(async () => {
|
|
1127
1250
|
if (
|
|
1128
1251
|
form.unavailableAudience &&
|
|
@@ -1571,6 +1694,10 @@ function App({ initialConfig, flags, root }) {
|
|
|
1571
1694
|
);
|
|
1572
1695
|
return;
|
|
1573
1696
|
}
|
|
1697
|
+
if (parsed.command === "appearance") {
|
|
1698
|
+
openAppearance();
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1574
1701
|
if (parsed.command === "open") {
|
|
1575
1702
|
navigate({ view: "post", id: parsed.arg, tab: "overview", page: 1 });
|
|
1576
1703
|
return;
|
|
@@ -1591,6 +1718,10 @@ function App({ initialConfig, flags, root }) {
|
|
|
1591
1718
|
}
|
|
1592
1719
|
}
|
|
1593
1720
|
function askQuit() {
|
|
1721
|
+
if (form?.kind === "appearance") {
|
|
1722
|
+
exit();
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1594
1725
|
if (form) {
|
|
1595
1726
|
askConfirm(
|
|
1596
1727
|
"Leave this draft?",
|
|
@@ -1650,6 +1781,14 @@ function App({ initialConfig, flags, root }) {
|
|
|
1650
1781
|
setDevice(null);
|
|
1651
1782
|
return;
|
|
1652
1783
|
}
|
|
1784
|
+
if (phase === "checking") {
|
|
1785
|
+
if (input === "q") exit();
|
|
1786
|
+
else if (error && (keyEvent.return || input === "r" || input === "R")) {
|
|
1787
|
+
setError("");
|
|
1788
|
+
setAuthVersion((v) => v + 1);
|
|
1789
|
+
}
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1653
1792
|
if (input === "q") exit();
|
|
1654
1793
|
else if (keyEvent.return && !busy.current) {
|
|
1655
1794
|
if (phase === "pending" && device) openBrowser(device.verification_uri);
|
|
@@ -1677,6 +1816,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
1677
1816
|
? "[deleted — discussion preserved]"
|
|
1678
1817
|
: reading.body || "",
|
|
1679
1818
|
contentWidth,
|
|
1819
|
+
colors,
|
|
1680
1820
|
),
|
|
1681
1821
|
maximum = Math.max(0, lines.length - contentHeight);
|
|
1682
1822
|
if (keyEvent.escape || keyEvent.return || input === " ") {
|
|
@@ -1725,6 +1865,17 @@ function App({ initialConfig, flags, root }) {
|
|
|
1725
1865
|
return;
|
|
1726
1866
|
}
|
|
1727
1867
|
if (form) {
|
|
1868
|
+
if (form.kind === "appearance" && keyEvent.escape) {
|
|
1869
|
+
setForm(null);
|
|
1870
|
+
setFocus("content");
|
|
1871
|
+
setNotice("Appearance preview cancelled. Saved preferences kept.");
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (form.kind === "appearance" && input === "0") {
|
|
1875
|
+
setForm((old) => ({ ...old, values: { ...defaultAppearance } }));
|
|
1876
|
+
setNotice("Default preview restored. Ctrl+S saves; Esc cancels.");
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1728
1879
|
if (keyEvent.ctrl && input === "e") {
|
|
1729
1880
|
externalEdit();
|
|
1730
1881
|
return;
|
|
@@ -1775,20 +1926,20 @@ function App({ initialConfig, flags, root }) {
|
|
|
1775
1926
|
keyEvent.downArrow
|
|
1776
1927
|
) {
|
|
1777
1928
|
const opts = active.options,
|
|
1778
|
-
|
|
1929
|
+
step = keyEvent.leftArrow || keyEvent.upArrow ? -1 : 1;
|
|
1930
|
+
setForm((old) => {
|
|
1931
|
+
const i = Math.max(
|
|
1779
1932
|
0,
|
|
1780
1933
|
opts.findIndex(
|
|
1781
1934
|
(item) =>
|
|
1782
|
-
String(item[0]) === String(
|
|
1935
|
+
String(item[0]) === String(old.values[active.key] || ""),
|
|
1783
1936
|
),
|
|
1784
|
-
)
|
|
1785
|
-
|
|
1786
|
-
next
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
values: { ...old.values, [active.key]: next[0] },
|
|
1791
|
-
}));
|
|
1937
|
+
);
|
|
1938
|
+
const next = opts[(i + step + opts.length) % opts.length];
|
|
1939
|
+
return next
|
|
1940
|
+
? { ...old, values: { ...old.values, [active.key]: next[0] } }
|
|
1941
|
+
: old;
|
|
1942
|
+
});
|
|
1792
1943
|
} else if (keyEvent.return) changeField(field + 1);
|
|
1793
1944
|
return;
|
|
1794
1945
|
}
|
|
@@ -1862,6 +2013,10 @@ function App({ initialConfig, flags, root }) {
|
|
|
1862
2013
|
else if (keyEvent.rightArrow) setFocus("content");
|
|
1863
2014
|
return;
|
|
1864
2015
|
}
|
|
2016
|
+
if (input === ",") {
|
|
2017
|
+
openAppearance();
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
1865
2020
|
if (input === "x" || input === "X" || input === "?") {
|
|
1866
2021
|
setActionIndex(0);
|
|
1867
2022
|
return;
|
|
@@ -1948,13 +2103,13 @@ function App({ initialConfig, flags, root }) {
|
|
|
1948
2103
|
) {
|
|
1949
2104
|
const step =
|
|
1950
2105
|
keyEvent.pageUp || keyEvent.pageDown
|
|
1951
|
-
? Math.max(1, Math.floor(contentHeight /
|
|
2106
|
+
? Math.max(1, Math.floor(contentHeight / stride))
|
|
1952
2107
|
: 1,
|
|
1953
2108
|
delta = keyEvent.upArrow || keyEvent.pageUp ? -step : step;
|
|
1954
2109
|
if (prose) {
|
|
1955
2110
|
const maximum = Math.max(
|
|
1956
2111
|
0,
|
|
1957
|
-
documentRows(route, current || {}, contentWidth).length -
|
|
2112
|
+
documentRows(route, current || {}, contentWidth, colors).length -
|
|
1958
2113
|
contentHeight,
|
|
1959
2114
|
);
|
|
1960
2115
|
const amount =
|
|
@@ -2079,7 +2234,8 @@ function App({ initialConfig, flags, root }) {
|
|
|
2079
2234
|
: loading
|
|
2080
2235
|
? "Loading…"
|
|
2081
2236
|
: "Shared account · shared conversations · same permissions");
|
|
2082
|
-
const spin =
|
|
2237
|
+
const spin =
|
|
2238
|
+
effectiveAppearance.motion === "reduced" ? "·" : ["◐", "◓", "◑", "◒"][tick];
|
|
2083
2239
|
let center;
|
|
2084
2240
|
if (width < 60 || rows < 18)
|
|
2085
2241
|
center = h(
|
|
@@ -2118,51 +2274,89 @@ function App({ initialConfig, flags, root }) {
|
|
|
2118
2274
|
},
|
|
2119
2275
|
});
|
|
2120
2276
|
else if (phase !== "ready") {
|
|
2121
|
-
const
|
|
2122
|
-
|
|
2123
|
-
"Your conversations, without leaving the terminal.",
|
|
2124
|
-
"",
|
|
2125
|
-
...(phase === "pending" && device
|
|
2277
|
+
const statusLines =
|
|
2278
|
+
phase === "pending" && device
|
|
2126
2279
|
? [
|
|
2127
2280
|
"Confirm this code: " + device.user_code,
|
|
2128
|
-
"Waiting for your approval " + spin,
|
|
2129
2281
|
device.verification_uri,
|
|
2130
|
-
"
|
|
2282
|
+
"Waiting for your approval...",
|
|
2131
2283
|
]
|
|
2132
2284
|
: phase === "checking"
|
|
2133
|
-
?
|
|
2285
|
+
? error
|
|
2286
|
+
? ["Connection could not be completed.", error]
|
|
2287
|
+
: ["Connecting to Devdevian..."]
|
|
2134
2288
|
: [
|
|
2135
2289
|
"Sign in with your existing Devdevian account.",
|
|
2136
|
-
"",
|
|
2137
|
-
"Enter connects this terminal through your browser.",
|
|
2138
2290
|
"Google, GitHub and email sign-in stay on the website.",
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2291
|
+
];
|
|
2292
|
+
const footer =
|
|
2293
|
+
phase === "pending"
|
|
2294
|
+
? ["Enter reopens the browser · Esc cancels"]
|
|
2295
|
+
: phase === "checking"
|
|
2296
|
+
? [error ? "R or Enter retries · Ctrl+C exits" : "Ctrl+C exits"]
|
|
2297
|
+
: [
|
|
2298
|
+
"Enter connects this terminal through your browser.",
|
|
2299
|
+
"Q or Ctrl+C exits.",
|
|
2300
|
+
];
|
|
2143
2301
|
center = h(
|
|
2144
2302
|
Box,
|
|
2145
2303
|
{
|
|
2146
|
-
borderStyle:
|
|
2304
|
+
borderStyle: colors.frame,
|
|
2147
2305
|
borderColor: colors.purple,
|
|
2148
2306
|
flexDirection: "column",
|
|
2149
|
-
paddingX:
|
|
2150
|
-
|
|
2307
|
+
paddingX: 1,
|
|
2308
|
+
height: bodyHeight,
|
|
2309
|
+
overflow: "hidden",
|
|
2310
|
+
},
|
|
2311
|
+
h(DeviPanel, {
|
|
2312
|
+
width: Math.max(1, width - 4),
|
|
2313
|
+
height: Math.max(1, bodyHeight - 2),
|
|
2314
|
+
message:
|
|
2315
|
+
effectiveAppearance.welcome === "off"
|
|
2316
|
+
? ""
|
|
2317
|
+
: phase === "checking"
|
|
2318
|
+
? "Devi missed you."
|
|
2319
|
+
: "Devi saved you a seat.",
|
|
2320
|
+
status: statusLines,
|
|
2321
|
+
footer,
|
|
2322
|
+
show: effectiveAppearance.welcome === "on",
|
|
2323
|
+
animate: phase !== "auth" && !error,
|
|
2324
|
+
motion: effectiveAppearance.motion,
|
|
2325
|
+
}),
|
|
2326
|
+
);
|
|
2327
|
+
} else if (
|
|
2328
|
+
booting &&
|
|
2329
|
+
!current &&
|
|
2330
|
+
effectiveAppearance.welcome === "on" &&
|
|
2331
|
+
!error &&
|
|
2332
|
+
!dialog &&
|
|
2333
|
+
!form &&
|
|
2334
|
+
!picker
|
|
2335
|
+
) {
|
|
2336
|
+
center = h(
|
|
2337
|
+
Box,
|
|
2338
|
+
{
|
|
2339
|
+
borderStyle: colors.frame,
|
|
2340
|
+
borderColor: colors.purple,
|
|
2341
|
+
paddingX: 1,
|
|
2151
2342
|
height: bodyHeight,
|
|
2152
2343
|
overflow: "hidden",
|
|
2153
2344
|
},
|
|
2154
|
-
h(
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2345
|
+
h(DeviPanel, {
|
|
2346
|
+
width: Math.max(1, width - 4),
|
|
2347
|
+
height: Math.max(1, bodyHeight - 2),
|
|
2348
|
+
message: "Devi missed you.",
|
|
2349
|
+
status: ["Loading your " + labelOf(route).toLowerCase() + "..."],
|
|
2350
|
+
footer: ["Ctrl+C exits"],
|
|
2351
|
+
motion: effectiveAppearance.motion,
|
|
2352
|
+
animate: true,
|
|
2159
2353
|
}),
|
|
2160
2354
|
);
|
|
2161
2355
|
} else if (dialog) {
|
|
2162
2356
|
center = h(
|
|
2163
2357
|
Box,
|
|
2164
2358
|
{
|
|
2165
|
-
borderStyle:
|
|
2359
|
+
borderStyle: colors.frame,
|
|
2166
2360
|
borderColor: colors.purple,
|
|
2167
2361
|
flexDirection: "column",
|
|
2168
2362
|
paddingX: 2,
|
|
@@ -2202,6 +2396,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
2202
2396
|
? "[deleted — discussion preserved]"
|
|
2203
2397
|
: reading.body || "",
|
|
2204
2398
|
contentWidth,
|
|
2399
|
+
colors,
|
|
2205
2400
|
);
|
|
2206
2401
|
body = h(FrameRows, {
|
|
2207
2402
|
rows: lines,
|
|
@@ -2258,14 +2453,14 @@ function App({ initialConfig, flags, root }) {
|
|
|
2258
2453
|
),
|
|
2259
2454
|
);
|
|
2260
2455
|
} else if (form)
|
|
2261
|
-
body = h(FormView, {
|
|
2456
|
+
body = h(form.kind === "appearance" ? AppearanceView : FormView, {
|
|
2262
2457
|
form,
|
|
2263
2458
|
active: field,
|
|
2264
2459
|
cursor,
|
|
2265
2460
|
width: contentWidth,
|
|
2266
2461
|
height: contentHeight,
|
|
2267
2462
|
});
|
|
2268
|
-
else if (loading && !current)
|
|
2463
|
+
else if ((loading || booting) && !current)
|
|
2269
2464
|
body = h(
|
|
2270
2465
|
Text,
|
|
2271
2466
|
{ color: colors.muted },
|
|
@@ -2324,7 +2519,17 @@ function App({ initialConfig, flags, root }) {
|
|
|
2324
2519
|
line: reviewLine,
|
|
2325
2520
|
anchor: reviewAnchor,
|
|
2326
2521
|
horizontal: reviewHorizontal,
|
|
2327
|
-
|
|
2522
|
+
layout: effectiveAppearance.reviewLayout,
|
|
2523
|
+
stride,
|
|
2524
|
+
rows: (w) =>
|
|
2525
|
+
listRows(
|
|
2526
|
+
route,
|
|
2527
|
+
current,
|
|
2528
|
+
index,
|
|
2529
|
+
w,
|
|
2530
|
+
colors,
|
|
2531
|
+
effectiveAppearance.density,
|
|
2532
|
+
),
|
|
2328
2533
|
});
|
|
2329
2534
|
else if (error && !current)
|
|
2330
2535
|
body = h(FrameRows, {
|
|
@@ -2342,7 +2547,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
2342
2547
|
height: contentHeight,
|
|
2343
2548
|
});
|
|
2344
2549
|
else {
|
|
2345
|
-
const lines = documentRows(route, current || {}, contentWidth);
|
|
2550
|
+
const lines = documentRows(route, current || {}, contentWidth, colors);
|
|
2346
2551
|
if (lines.length)
|
|
2347
2552
|
body = h(FrameRows, {
|
|
2348
2553
|
rows: lines,
|
|
@@ -2350,36 +2555,59 @@ function App({ initialConfig, flags, root }) {
|
|
|
2350
2555
|
height: contentHeight,
|
|
2351
2556
|
offset: Math.min(scroll, Math.max(0, lines.length - contentHeight)),
|
|
2352
2557
|
});
|
|
2353
|
-
else if (!current?.items?.length)
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2558
|
+
else if (!current?.items?.length) {
|
|
2559
|
+
const messages = {
|
|
2560
|
+
inbox: [
|
|
2561
|
+
"You're all caught up.",
|
|
2562
|
+
"Replies, mentions and review updates will appear here.",
|
|
2563
|
+
],
|
|
2564
|
+
drafts: [
|
|
2565
|
+
"No local drafts yet.",
|
|
2566
|
+
"Start writing to save a private draft on this device.",
|
|
2567
|
+
],
|
|
2568
|
+
communities: [
|
|
2569
|
+
"Find your people.",
|
|
2570
|
+
"Use / to search or N to create a private community.",
|
|
2571
|
+
],
|
|
2572
|
+
feed: [
|
|
2573
|
+
"Nothing here yet.",
|
|
2574
|
+
"N starts a conversation. A asks a question.",
|
|
2575
|
+
],
|
|
2576
|
+
search: route.query
|
|
2577
|
+
? [
|
|
2578
|
+
"No conversations matched your search.",
|
|
2579
|
+
"Try fewer words or a different phrase.",
|
|
2580
|
+
]
|
|
2581
|
+
: [
|
|
2582
|
+
"Find your next useful conversation.",
|
|
2583
|
+
"Press / and type /search followed by your query.",
|
|
2584
|
+
],
|
|
2585
|
+
};
|
|
2586
|
+
const message = messages[route.view] || [
|
|
2587
|
+
"Nothing here yet.",
|
|
2588
|
+
"Use / to search or Tab to explore another section.",
|
|
2589
|
+
];
|
|
2590
|
+
body = h(DeviPanel, {
|
|
2591
|
+
width: contentWidth,
|
|
2592
|
+
height: contentHeight,
|
|
2593
|
+
message: message[0],
|
|
2594
|
+
status: [message[1]],
|
|
2595
|
+
show:
|
|
2596
|
+
effectiveAppearance.welcome === "on" &&
|
|
2597
|
+
Object.hasOwn(messages, route.view),
|
|
2598
|
+
motion: effectiveAppearance.motion,
|
|
2599
|
+
});
|
|
2600
|
+
} else {
|
|
2601
|
+
const visible = Math.max(1, Math.floor(contentHeight / stride)),
|
|
2378
2602
|
first = Math.max(0, index - visible + 1),
|
|
2379
|
-
rows = listRows(
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2603
|
+
rows = listRows(
|
|
2604
|
+
route,
|
|
2605
|
+
current,
|
|
2606
|
+
index,
|
|
2607
|
+
contentWidth,
|
|
2608
|
+
colors,
|
|
2609
|
+
effectiveAppearance.density,
|
|
2610
|
+
).slice(first * stride, first * stride + contentHeight);
|
|
2383
2611
|
body = h(
|
|
2384
2612
|
Box,
|
|
2385
2613
|
{
|
|
@@ -2419,7 +2647,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
2419
2647
|
const pane = h(
|
|
2420
2648
|
Box,
|
|
2421
2649
|
{
|
|
2422
|
-
borderStyle:
|
|
2650
|
+
borderStyle: colors.frame,
|
|
2423
2651
|
borderColor: focus === "nav" && !compact ? colors.line : colors.purple,
|
|
2424
2652
|
backgroundColor: colors.panel,
|
|
2425
2653
|
flexDirection: "column",
|
|
@@ -2470,7 +2698,9 @@ function App({ initialConfig, flags, root }) {
|
|
|
2470
2698
|
)
|
|
2471
2699
|
: h(TextLine, {
|
|
2472
2700
|
text: form
|
|
2473
|
-
?
|
|
2701
|
+
? (form.kind === "appearance"
|
|
2702
|
+
? "Local preferences · Preview now · Ctrl+S save · Esc cancel"
|
|
2703
|
+
: "Tab next field · Ctrl+E editor · Ctrl+S preview") +
|
|
2474
2704
|
(draftSaved ? " · Draft saved locally" : "")
|
|
2475
2705
|
: preview
|
|
2476
2706
|
? "Scroll to review · ← → choose · Enter confirms"
|
|
@@ -2493,7 +2723,7 @@ function App({ initialConfig, flags, root }) {
|
|
|
2493
2723
|
? h(
|
|
2494
2724
|
Box,
|
|
2495
2725
|
{
|
|
2496
|
-
borderStyle:
|
|
2726
|
+
borderStyle: colors.frame,
|
|
2497
2727
|
borderColor: focus === "nav" ? colors.blue : colors.line,
|
|
2498
2728
|
backgroundColor: colors.panel,
|
|
2499
2729
|
flexDirection: "column",
|
|
@@ -2546,97 +2776,103 @@ function App({ initialConfig, flags, root }) {
|
|
|
2546
2776
|
);
|
|
2547
2777
|
}
|
|
2548
2778
|
const hint =
|
|
2549
|
-
|
|
2550
|
-
? "
|
|
2551
|
-
:
|
|
2552
|
-
? "
|
|
2553
|
-
:
|
|
2554
|
-
? "
|
|
2555
|
-
:
|
|
2556
|
-
? "
|
|
2557
|
-
:
|
|
2558
|
-
? "
|
|
2559
|
-
:
|
|
2560
|
-
? "
|
|
2561
|
-
:
|
|
2562
|
-
? "
|
|
2563
|
-
:
|
|
2564
|
-
? "
|
|
2565
|
-
: route.view === "
|
|
2566
|
-
? "
|
|
2567
|
-
:
|
|
2779
|
+
form?.kind === "appearance"
|
|
2780
|
+
? "← → choose · Tab next · Ctrl+S save · Esc cancel · 0 default preview"
|
|
2781
|
+
: actionIndex !== null
|
|
2782
|
+
? "Actions are scoped to the selected item · Esc closes"
|
|
2783
|
+
: reviewing && !form && !preview && !reading
|
|
2784
|
+
? "W code / discussion · Space select · C comment · X actions · F filter"
|
|
2785
|
+
: picker
|
|
2786
|
+
? "Project files stay local until publication is confirmed"
|
|
2787
|
+
: reading
|
|
2788
|
+
? "↑↓ scroll · R reply · E edit · D delete · Esc close"
|
|
2789
|
+
: preview
|
|
2790
|
+
? "← → choose · Enter confirm · Esc edit"
|
|
2791
|
+
: form
|
|
2792
|
+
? "Tab field · Ctrl+E editor · Ctrl+O files · Ctrl+G diff · Ctrl+S preview"
|
|
2793
|
+
: phase !== "ready"
|
|
2794
|
+
? "Enter connect / open browser · Ctrl+C exit"
|
|
2795
|
+
: route.view === "feedback"
|
|
2796
|
+
? "↑↓ select · R reply · E edit · D delete · S resolve · ! report"
|
|
2797
|
+
: route.view === "post"
|
|
2798
|
+
? "←→ tabs · ↑↓ select · R reply · E edit · D delete · Space read"
|
|
2799
|
+
: "↑↓ select · Enter open · Tab sections · [ ] pages · N new · / search";
|
|
2568
2800
|
return h(
|
|
2569
|
-
|
|
2570
|
-
{
|
|
2571
|
-
flexDirection: "column",
|
|
2572
|
-
width,
|
|
2573
|
-
height,
|
|
2574
|
-
overflow: "hidden",
|
|
2575
|
-
backgroundColor: colors.bg,
|
|
2576
|
-
},
|
|
2801
|
+
ThemeContext.Provider,
|
|
2802
|
+
{ value: colors },
|
|
2577
2803
|
h(
|
|
2578
2804
|
Box,
|
|
2579
2805
|
{
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
paddingX: 1,
|
|
2584
|
-
justifyContent: "space-between",
|
|
2806
|
+
flexDirection: "column",
|
|
2807
|
+
width,
|
|
2808
|
+
height,
|
|
2585
2809
|
overflow: "hidden",
|
|
2810
|
+
backgroundColor: colors.bg,
|
|
2586
2811
|
},
|
|
2587
2812
|
h(
|
|
2588
|
-
|
|
2589
|
-
{
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2813
|
+
Box,
|
|
2814
|
+
{
|
|
2815
|
+
height: 3,
|
|
2816
|
+
borderStyle: colors.frame,
|
|
2817
|
+
borderColor: colors.line,
|
|
2818
|
+
paddingX: 1,
|
|
2819
|
+
justifyContent: "space-between",
|
|
2820
|
+
overflow: "hidden",
|
|
2821
|
+
},
|
|
2822
|
+
h(
|
|
2823
|
+
Text,
|
|
2824
|
+
{ color: colors.purple, bold: true },
|
|
2825
|
+
clip("◈ DEVDEVIAN · " + basename(root), Math.max(10, width - 36)),
|
|
2826
|
+
),
|
|
2827
|
+
h(
|
|
2828
|
+
Text,
|
|
2829
|
+
{ color: colors.blue },
|
|
2830
|
+
clip(
|
|
2831
|
+
user
|
|
2832
|
+
? "@" +
|
|
2833
|
+
user.username +
|
|
2834
|
+
" · " +
|
|
2835
|
+
unread +
|
|
2836
|
+
" unread · " +
|
|
2837
|
+
(live ? "● Live" : "○ Paused")
|
|
2838
|
+
: "CLI · ALPHA",
|
|
2839
|
+
32,
|
|
2840
|
+
),
|
|
2605
2841
|
),
|
|
2606
2842
|
),
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2843
|
+
center,
|
|
2844
|
+
h(TextLine, {
|
|
2845
|
+
text: clip((pending || loading ? spin + " " : "") + status, width),
|
|
2846
|
+
width,
|
|
2847
|
+
color: error ? colors.red : notice ? colors.green : colors.muted,
|
|
2848
|
+
}),
|
|
2849
|
+
h(
|
|
2850
|
+
Box,
|
|
2851
|
+
{
|
|
2852
|
+
height: 3,
|
|
2853
|
+
borderStyle: colors.frame,
|
|
2854
|
+
borderColor: focus === "prompt" ? colors.blue : colors.line,
|
|
2855
|
+
paddingX: 1,
|
|
2856
|
+
overflow: "hidden",
|
|
2857
|
+
},
|
|
2858
|
+
h(Text, { color: colors.purple, bold: true }, "› "),
|
|
2859
|
+
focus === "prompt"
|
|
2860
|
+
? h(InputText, {
|
|
2861
|
+
value: command,
|
|
2862
|
+
cursor: commandCursor,
|
|
2863
|
+
width: Math.max(1, width - 6),
|
|
2864
|
+
})
|
|
2865
|
+
: h(
|
|
2866
|
+
Text,
|
|
2867
|
+
{ color: colors.muted },
|
|
2868
|
+
clip(
|
|
2869
|
+
"Search or /command · Press / to type · X actions · , appearance",
|
|
2870
|
+
width - 6,
|
|
2871
|
+
),
|
|
2636
2872
|
),
|
|
2637
|
-
|
|
2873
|
+
),
|
|
2874
|
+
h(TextLine, { text: hint, width, color: colors.muted }),
|
|
2638
2875
|
),
|
|
2639
|
-
h(TextLine, { text: hint, width, color: colors.muted }),
|
|
2640
2876
|
);
|
|
2641
2877
|
}
|
|
2642
2878
|
export async function startTui(flags = {}) {
|
|
@@ -2648,13 +2884,24 @@ export async function startTui(flags = {}) {
|
|
|
2648
2884
|
if (flags.host && config && originOf(flags.host) !== config.origin)
|
|
2649
2885
|
config = null;
|
|
2650
2886
|
const root = await workspaceRoot(flags.project || process.cwd());
|
|
2651
|
-
const
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2887
|
+
const loaded = await loadAppearance();
|
|
2888
|
+
const initialAppearance = {
|
|
2889
|
+
...loaded,
|
|
2890
|
+
settings: normalizeAppearance({
|
|
2891
|
+
...loaded.settings,
|
|
2892
|
+
...appearanceFlags(flags),
|
|
2893
|
+
}),
|
|
2894
|
+
};
|
|
2895
|
+
const app = render(
|
|
2896
|
+
h(App, { initialConfig: config, flags, root, initialAppearance }),
|
|
2897
|
+
{
|
|
2898
|
+
alternateScreen: true,
|
|
2899
|
+
exitOnCtrlC: false,
|
|
2900
|
+
patchConsole: true,
|
|
2901
|
+
maxFps: 20,
|
|
2902
|
+
incrementalRendering: false,
|
|
2903
|
+
},
|
|
2904
|
+
);
|
|
2658
2905
|
try {
|
|
2659
2906
|
await app.waitUntilExit();
|
|
2660
2907
|
} finally {
|