@tenyokjs/devdevian-cli 0.5.0-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 CHANGED
@@ -31,12 +31,25 @@ Pages have a bounded 30-second in-memory cache. Refresh explicitly with Shift+R.
31
31
 
32
32
  Ordinary reply deletion replaces your text with a placeholder and preserves nested replies. Its attachments become unavailable. Deleting an accepted answer reopens the question. D/d works on a selected reply or while reading it in full; confirmation is required.
33
33
 
34
+ ## Meet Devi
35
+
36
+ Devi is the Devdevian mascot: a small ghost with a laptop and an orbit. The terminal uses colored ASCII art, so it needs no image protocol or image downloads. A compact drawing is used when space is limited; authentication codes, addresses and controls take priority over decoration.
37
+
38
+ - **“Devi missed you.”** appears while checking your saved session and loading the first page. It disappears as soon as the page is ready; there is no minimum splash duration or fake progress bar.
39
+ - **“Devi saved you a seat.”** welcomes you to browser-approved sign-in. Sign-in instructions stay visible in small terminals.
40
+ - Empty feed, Inbox, communities, local drafts and search views show a still mascot with a relevant next step. Errors remain error states; they do not become a happy empty view.
41
+ - Subtle mode animates a blink only during startup work or pending browser approval. **Reduced motion** keeps the drawing static. Disable it entirely in **Appearance → Devi greeting**, with `devdevian appearance --welcome off`, or just for one launch with `devdevian tui --welcome off`.
42
+ - A failed session check offers **R / Enter** to retry the existing session. **Ctrl+C** exits during loading. Script commands and `--json` output never print the mascot.
43
+
44
+ All mascot captions are in English. Welcome preferences are local and compatible with existing 0.5.0 configuration files.
45
+
34
46
  ## Themes and layout
35
47
 
36
48
  Open **Appearance** in navigation, type `/appearance`, or press **,** from a reading/list screen. Use arrows to choose options and Tab to move between them. The entire interface previews each change, including code colors, the file picker and navigation. Wide terminals include a sample card next to the controls.
37
49
 
38
50
  - **Midnight** preserves the original blue/purple design; **Nebula** uses a deeper violet surface; **Daylight** is light; **Contrast** emphasizes readable foregrounds.
39
51
  - Choose a **blue, violet, cyan or rose accent**, comfortable/compact lists, automatic/hidden sidebar, automatic/stacked review panels, rounded/straight frames and subtle/reduced loading indicators.
52
+ - **Devi greeting** shows/hides the terminal mascot on startup, sign-in and supported empty sections. Reduced motion keeps Devi still.
40
53
  - **Ctrl+S** saves locally. **Esc** cancels the preview. **0** restores the default preview; save to keep it, or cancel to retain existing settings.
41
54
  - Hidden navigation stays available with **Tab**. Stacked review panes switch with **W**. Narrow terminals automatically avoid a permanent sidebar and split review layout.
42
55
 
package/VERSIONS.md CHANGED
@@ -8,6 +8,15 @@ Every published npm artifact has a unique immutable version. We record each vers
8
8
 
9
9
  **0.5.0 focuses on themes, interface customization and local appearance preferences.** Projects and Events parity remains a separate future milestone.
10
10
 
11
+ ## 0.5.1-alpha.1 — Meet Devi
12
+
13
+ - Added a terminal-native ASCII mascot based on the ghost/laptop/orbit logo, with full and compact drawings and English greetings: “Devi missed you.” and “Devi saved you a seat.”
14
+ - Startup follows the actual session check and first page request, with no minimum duration or artificial progress. Connection errors stop the loading state and offer an existing-session retry.
15
+ - Supported empty sections show a still Devi and a contextual next step. Sign-in codes, addresses and controls take priority on small screens.
16
+ - Subtle motion blinks only during active startup or browser approval; Reduced motion is static. Appearance includes a persistent welcome on/off option, also available to offline commands and session-only launch flags.
17
+ - Script/JSON commands remain banner-free. No additional image dependency, API change, schema migration or production-data change. Existing preferences default the new setting to on.
18
+ - Checked delayed/failing startup, retry, animation/static/off modes, English captions, first-page handoff, empty Inbox, script output and 60×18 device approval in unit and pseudo-terminal regressions.
19
+
11
20
  ## 0.5.0-alpha.1 — Make the terminal yours
12
21
 
13
22
  **Changes**
package/bin/devdevian.mjs CHANGED
@@ -120,6 +120,7 @@ const help = `Devdevian CLI ${version} · alpha
120
120
  --review-layout MODE auto | stacked
121
121
  --frame STYLE round | single
122
122
  --motion MODE subtle | reduced
123
+ --welcome MODE on | off (Devi greeting)
123
124
  --no-live Disable background reads in the live workspace
124
125
  --project PATH Root directory for the live file/diff picker (default: current directory)
125
126
  --community ID Publish to an approved private community
@@ -72,6 +72,14 @@ export const appearanceFields = [
72
72
  ["reduced", "Reduced · static indicators"],
73
73
  ],
74
74
  },
75
+ {
76
+ key: "welcome",
77
+ label: "Devi greeting",
78
+ options: [
79
+ ["on", "Show Devi"],
80
+ ["off", "Hide Devi"],
81
+ ],
82
+ },
75
83
  ];
76
84
  export const defaultAppearance = Object.freeze(
77
85
  Object.fromEntries(
package/lib/devi.mjs ADDED
@@ -0,0 +1,137 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Box, Text } from "ink";
3
+ import stringWidth from "string-width";
4
+ import { clip, wrap } from "./tui-model.mjs";
5
+ import { useTheme } from "./theme.mjs";
6
+ const h = React.createElement;
7
+ // Terminal-native mascot: ghost, laptop and orbit, drawn without image protocols.
8
+ const full = [
9
+ ' .-""""-.',
10
+ " / \\",
11
+ " / .----. \\ +",
12
+ " | ( o o ) |",
13
+ " .---| '----' .-------.",
14
+ " .-' | / </> / '-.",
15
+ "( | _ /______/ )",
16
+ " '---.._ \\_/ \\_/ _..---'",
17
+ " '----------'",
18
+ ];
19
+ const small = [
20
+ " .---. .-----.",
21
+ " ( o o ) / </> /",
22
+ " /_ _\\ /_____/",
23
+ " '---'",
24
+ ];
25
+ export function deviArt(compact = false, frame = 0) {
26
+ const blink = frame % 6 === 4;
27
+ return (compact ? small : full).map((line) =>
28
+ blink ? line.replace("o o", "- -").replace("o o", "- -") : line,
29
+ );
30
+ }
31
+ // Required status and controls get space before decoration. No minimum display timer.
32
+ export function deviLayout({
33
+ width,
34
+ height,
35
+ message,
36
+ status = [],
37
+ footer = [],
38
+ show = true,
39
+ frame = 0,
40
+ }) {
41
+ width = Math.max(1, width);
42
+ height = Math.max(1, height);
43
+ const instructions = footer.flatMap((line) => wrap(line, width));
44
+ const details = status.flatMap((line) => wrap(line, width));
45
+ const footerRows = instructions.slice(0, height);
46
+ const budget = Math.max(0, height - footerRows.length);
47
+ const detailRows = details.slice(0, budget);
48
+ let remaining = height - footerRows.length - detailRows.length;
49
+ const title = message ? wrap(message, width) : [];
50
+ let art = [];
51
+ if (show) {
52
+ for (const compact of [false, true]) {
53
+ const candidate = deviArt(compact, frame);
54
+ if (
55
+ candidate.every((line) => stringWidth(line) <= width) &&
56
+ candidate.length + title.length + 2 <= remaining
57
+ ) {
58
+ art = candidate;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ const rows = [];
64
+ if (art.length) {
65
+ rows.push(
66
+ ...art.map((text) => ({
67
+ text: text.padEnd(Math.max(...art.map((line) => stringWidth(line)))),
68
+ role: text.includes("</>") ? "blue" : "purple",
69
+ })),
70
+ { text: "" },
71
+ );
72
+ remaining -= art.length + 1;
73
+ }
74
+ if (title.length && title.length <= remaining) {
75
+ rows.push(...title.map((text) => ({ text, role: "text", bold: true })));
76
+ remaining -= title.length;
77
+ }
78
+ if (remaining && rows.length) rows.push({ text: "" });
79
+ rows.push(
80
+ ...detailRows.map((text) => ({ text, role: "text" })),
81
+ ...footerRows.map((text) => ({ text, role: "muted" })),
82
+ );
83
+ return rows
84
+ .slice(0, height)
85
+ .map((row) => ({ ...row, text: clip(row.text, width) }));
86
+ }
87
+ export function DeviPanel({
88
+ width,
89
+ height,
90
+ message = "Devi missed you.",
91
+ status,
92
+ footer,
93
+ show = true,
94
+ animate = false,
95
+ motion = "subtle",
96
+ }) {
97
+ const c = useTheme(),
98
+ [frame, setFrame] = useState(0);
99
+ useEffect(() => {
100
+ if (!show || !animate || motion === "reduced") return;
101
+ const timer = setInterval(() => setFrame((value) => (value + 1) % 6), 400);
102
+ return () => clearInterval(timer);
103
+ }, [show, animate, motion]);
104
+ const rows = deviLayout({
105
+ width,
106
+ height,
107
+ message,
108
+ status,
109
+ footer,
110
+ show,
111
+ frame: motion === "reduced" ? 0 : frame,
112
+ });
113
+ return h(
114
+ Box,
115
+ {
116
+ width,
117
+ height,
118
+ flexDirection: "column",
119
+ justifyContent: "center",
120
+ overflow: "hidden",
121
+ },
122
+ ...rows.map((row, index) =>
123
+ h(
124
+ Text,
125
+ {
126
+ key: index,
127
+ color: c[row.role] || c.text,
128
+ bold: row.bold,
129
+ wrap: "truncate",
130
+ },
131
+ " ".repeat(
132
+ Math.max(0, Math.floor((width - stringWidth(row.text)) / 2)),
133
+ ) + row.text,
134
+ ),
135
+ ),
136
+ );
137
+ }
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";
@@ -523,7 +524,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
523
524
  ),
524
525
  [user, setUser] = useState(null),
525
526
  [device, setDevice] = useState(null),
526
- [authVersion, setAuthVersion] = useState(0);
527
+ [authVersion, setAuthVersion] = useState(0),
528
+ [booting, setBooting] = useState(true);
527
529
  const [route, setRoute] = useState({ view: "feed", page: 1 }),
528
530
  [data, setData] = useState(null),
529
531
  [loading, setLoading] = useState(false),
@@ -693,6 +695,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
693
695
  setPreview(null);
694
696
  setDevice(null);
695
697
  setUser(null);
698
+ setBooting(true);
696
699
  setReading(null);
697
700
  setActionIndex(null);
698
701
  setUnread(0);
@@ -716,6 +719,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
716
719
  } else setError(problemMessage(e));
717
720
  }
718
721
  function navigate(next, { replace = false } = {}) {
722
+ setBooting(false);
719
723
  if (!replace)
720
724
  history.current.push({
721
725
  route,
@@ -898,7 +902,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
898
902
  if (!controller.signal.aborted) fail(e);
899
903
  });
900
904
  return () => controller.abort();
901
- }, [phase, api]);
905
+ }, [phase, api, authVersion]);
902
906
  useEffect(() => {
903
907
  if (phase !== "ready" || !api || form || route.view === "help") return;
904
908
  let active = true;
@@ -964,7 +968,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
964
968
  if (active) fail(e);
965
969
  })
966
970
  .finally(() => {
967
- if (active) setLoading(false);
971
+ if (active) {
972
+ setLoading(false);
973
+ setBooting(false);
974
+ }
968
975
  });
969
976
  return () => {
970
977
  active = false;
@@ -996,6 +1003,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
996
1003
  if (controller.signal.aborted) return;
997
1004
  resetAccount();
998
1005
  setConfig(saved);
1006
+ setBooting(true);
999
1007
  setPhase("checking");
1000
1008
  setNotice("Terminal connected.");
1001
1009
  return;
@@ -1062,6 +1070,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
1062
1070
  );
1063
1071
  }
1064
1072
  function openForm(next) {
1073
+ setBooting(false);
1065
1074
  setForm({
1066
1075
  ...next,
1067
1076
  draftId: next.draftId || randomBytes(32).toString("hex"),
@@ -1772,6 +1781,14 @@ function App({ initialConfig, flags, root, initialAppearance }) {
1772
1781
  setDevice(null);
1773
1782
  return;
1774
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
+ }
1775
1792
  if (input === "q") exit();
1776
1793
  else if (keyEvent.return && !busy.current) {
1777
1794
  if (phase === "pending" && device) openBrowser(device.verification_uri);
@@ -2257,44 +2274,82 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2257
2274
  },
2258
2275
  });
2259
2276
  else if (phase !== "ready") {
2260
- const lines = [
2261
- "A place for curious developers.",
2262
- "Your conversations, without leaving the terminal.",
2263
- "",
2264
- ...(phase === "pending" && device
2277
+ const statusLines =
2278
+ phase === "pending" && device
2265
2279
  ? [
2266
2280
  "Confirm this code: " + device.user_code,
2267
- "Waiting for your approval " + spin,
2268
2281
  device.verification_uri,
2269
- "Enter reopens the browser · Esc cancels",
2282
+ "Waiting for your approval...",
2270
2283
  ]
2271
2284
  : phase === "checking"
2272
- ? ["Opening your workspace " + spin]
2285
+ ? error
2286
+ ? ["Connection could not be completed.", error]
2287
+ : ["Connecting to Devdevian..."]
2273
2288
  : [
2274
2289
  "Sign in with your existing Devdevian account.",
2275
- "",
2276
- "Enter connects this terminal through your browser.",
2277
2290
  "Google, GitHub and email sign-in stay on the website.",
2278
- "",
2279
- "Q exits.",
2280
- ]),
2281
- ];
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
+ ];
2282
2301
  center = h(
2283
2302
  Box,
2284
2303
  {
2285
2304
  borderStyle: colors.frame,
2286
2305
  borderColor: colors.purple,
2287
2306
  flexDirection: "column",
2288
- paddingX: 2,
2289
- paddingY: 1,
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,
2290
2342
  height: bodyHeight,
2291
2343
  overflow: "hidden",
2292
2344
  },
2293
- h(Text, { bold: true, color: colors.purple }, "YOUR DEVELOPER SPACE"),
2294
- h(FrameRows, {
2295
- rows: lines.flatMap((line) => wrap(line, width - 8)),
2296
- width: width - 8,
2297
- height: Math.max(1, bodyHeight - 5),
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,
2298
2353
  }),
2299
2354
  );
2300
2355
  } else if (dialog) {
@@ -2405,7 +2460,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2405
2460
  width: contentWidth,
2406
2461
  height: contentHeight,
2407
2462
  });
2408
- else if (loading && !current)
2463
+ else if ((loading || booting) && !current)
2409
2464
  body = h(
2410
2465
  Text,
2411
2466
  { color: colors.muted },
@@ -2500,30 +2555,49 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2500
2555
  height: contentHeight,
2501
2556
  offset: Math.min(scroll, Math.max(0, lines.length - contentHeight)),
2502
2557
  });
2503
- else if (!current?.items?.length)
2504
- body = h(
2505
- Box,
2506
- { flexDirection: "column", paddingTop: 1 },
2507
- h(
2508
- Text,
2509
- { color: colors.text },
2510
- route.view === "search"
2511
- ? "Find your next useful conversation."
2512
- : error
2513
- ? "Could not open this view."
2514
- : "Nothing here yet.",
2515
- ),
2516
- h(
2517
- Text,
2518
- { color: colors.muted },
2519
- error
2520
- ? "Press Shift+R to retry."
2521
- : route.view === "feed"
2522
- ? "N starts a conversation. A asks a question."
2523
- : "Use / to search or Tab to explore another section.",
2524
- ),
2525
- );
2526
- else {
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 {
2527
2601
  const visible = Math.max(1, Math.floor(contentHeight / stride)),
2528
2602
  first = Math.max(0, index - visible + 1),
2529
2603
  rows = listRows(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenyokjs/devdevian-cli",
3
- "version": "0.5.0-alpha.1",
3
+ "version": "0.5.1-alpha.1",
4
4
  "description": "Devdevian in your terminal: discussions, questions and human code reviews.",
5
5
  "type": "module",
6
6
  "bin": {