@tenyokjs/devdevian-cli 0.7.1-alpha.1 → 0.7.2-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
@@ -69,6 +69,10 @@ In full-text reading (**Space** on a reply or remark), **/** opens local text se
69
69
 
70
70
  Active fields explain their input: Space stays a space, Enter adds a line in multiline fields, and Tab leaves the field. Project feedback accepts text and URLs; dedicated website/repository/additional links are in **People & links**. Additional links use one `Label | https://example.com` per line. Context hints stay below the editor on larger terminals and use the status/hint lines on compact ones. **Ctrl+S** checks the complete form and opens a preview; drafts autosave locally and publication still requires explicit confirmation.
71
71
 
72
+ Multiline fields show the current logical line and total line count. While editing, the command bar gives its space to the form; the caret stays visible as you add lines or move through the text. Previous lines remain part of the same field, including after Enter. On short terminals only a window of a long message is visible; use ↑/↓ to revisit earlier lines or Ctrl+E for the external editor.
73
+
74
+ Conversation overviews and publication previews display Demo/Repository URL fields alongside any labeled links, with duplicate destinations shown once.
75
+
72
76
  ## Meet Devi
73
77
 
74
78
  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.
package/VERSIONS.md CHANGED
@@ -8,6 +8,12 @@ Every published npm artifact has a unique immutable version. We record each vers
8
8
 
9
9
  **0.7.0 focuses on everyday terminal use**: discoverable quick actions, staged forms and resumable reading.
10
10
 
11
+ ## 0.7.2-alpha.1 — Visible links and multiline writing
12
+
13
+ - Conversation overviews now include Demo and Repository URL fields alongside labeled links. Duplicate URLs appear once, preserving a supplied label; publication previews show the same destinations.
14
+ - Multiline editing follows the caret and displays the logical line position/count. Enter retains the preceding visible line when space permits. While editing, the inactive command bar yields its rows to the form, including at 60×18.
15
+ - Real terminal/API regressions cover saved Demo URLs together with labeled links, newline visibility, cursor movement and editing in compact terminals. No API/schema or credential changes.
16
+
11
17
  ## 0.7.1-alpha.1 — Clearer forms
12
18
 
13
19
  - Added visible, keyboard-focusable Back / Next controls to staged forms. Tab reaches the controls at step boundaries, arrows choose an available action and Enter activates it. The final action is Preview; publication remains a separate confirmation. Ctrl+N/P shortcuts remain available.
@@ -0,0 +1,13 @@
1
+ // Keep legacy URL fields visible alongside labeled links, without repeating a URL.
2
+ export function conversationLinks(post = {}) {
3
+ const seen = new Set();
4
+ return [
5
+ ...(post.links || []),
6
+ ...(post.demo_url ? [{ label: "Demo", url: post.demo_url }] : []),
7
+ ...(post.repo_url ? [{ label: "Repository", url: post.repo_url }] : []),
8
+ ].filter((link) => {
9
+ if (!link.url || seen.has(link.url)) return false;
10
+ seen.add(link.url);
11
+ return true;
12
+ });
13
+ }
package/lib/tui-data.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { loadCatalog, isCatalog } from "./catalog.mjs";
2
2
  import { createHash } from "node:crypto";
3
+ import { conversationLinks } from "./conversation-links.mjs";
3
4
  export function assertSnippet(code, hash) {
4
5
  if (
5
6
  createHash("sha256")
@@ -288,11 +289,12 @@ export function contentLines(route, data) {
288
289
  .split("\n")
289
290
  .map((line, index) => String(index + 1).padStart(4) + " " + line),
290
291
  );
291
- if (p.links?.length)
292
+ const links = conversationLinks(p);
293
+ if (links.length)
292
294
  lines.push(
293
295
  "",
294
296
  "LINKS",
295
- ...p.links.map((link) => link.label + " · " + link.url),
297
+ ...links.map((link) => link.label + " · " + link.url),
296
298
  );
297
299
  if (p.archived_at || p.locked_at)
298
300
  lines.push("", "Replies are paused for this conversation.");
package/lib/tui.mjs CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  import { contextActions } from "./context-actions.mjs";
56
56
  import { editExternal } from "./external-editor.mjs";
57
57
  import { markdownRows } from "./rich-text.mjs";
58
+ import { conversationLinks } from "./conversation-links.mjs";
58
59
  import { problemMessage } from "./problems.mjs";
59
60
  import { ReviewWorkspace, codeRange } from "./review-workspace.mjs";
60
61
  import {
@@ -306,9 +307,14 @@ function FormView({ form, active, cursor, width, height, navFocus }) {
306
307
  navRows = step && height >= 3 ? 1 : 0;
307
308
  const editorHeight = Math.max(1, height - helpRows - navRows);
308
309
  let selectedLine = rows.length;
310
+ let caretRow = null;
309
311
  fields.forEach((field, index) => {
310
312
  if (step && !step.indexes.includes(index)) return;
311
313
  if (index === active) selectedLine = rows.length;
314
+ const value = form.values[field.key] || "";
315
+ const lineNumber = field.multiline
316
+ ? graphemes(value).slice(0, cursor).join("").split("\n").length
317
+ : 0;
312
318
  rows.push({
313
319
  text: (index === active && !navFocus ? "› " : " ") + field.label,
314
320
  color: index === active && !navFocus ? colors.purple : colors.muted,
@@ -324,7 +330,6 @@ function FormView({ form, active, cursor, width, height, navFocus }) {
324
330
  color: index === active && !navFocus ? colors.blue : colors.text,
325
331
  });
326
332
  } else {
327
- const value = form.values[field.key] || "";
328
333
  const text =
329
334
  index === active && !navFocus
330
335
  ? cursorLines(value, cursor, width - 3).lines
@@ -339,12 +344,18 @@ function FormView({ form, active, cursor, width, height, navFocus }) {
339
344
  const visible = field.multiline
340
345
  ? Math.min(
341
346
  index === active
342
- ? Math.max(1, Math.min(12, editorHeight - 2))
347
+ ? Math.max(1, Math.min(12, editorHeight - 1))
343
348
  : 3,
344
349
  Math.max(2, text.length),
345
350
  )
346
351
  : 1,
347
352
  start = index === active ? Math.max(0, caretLine - visible + 1) : 0;
353
+ if (index === active && !navFocus && field.multiline) {
354
+ rows.at(-1).text += ` · line ${lineNumber}/${value.split("\n").length}`;
355
+ // Follow the caret, rather than anchoring the viewport to the field label.
356
+ // A newline keeps its preceding row visible whenever the terminal has room.
357
+ caretRow = rows.length + caretLine - start;
358
+ }
348
359
  rows.push(
349
360
  ...text.slice(start, start + visible).map((line) => ({
350
361
  text: " " + line,
@@ -369,13 +380,15 @@ function FormView({ form, active, cursor, width, height, navFocus }) {
369
380
  color: colors.blue,
370
381
  bold: true,
371
382
  });
372
- const offset = Math.max(
383
+ let offset = Math.max(
373
384
  0,
374
385
  Math.min(
375
386
  selectedLine - (editorHeight >= 4 ? 1 : 0),
376
387
  rows.length - editorHeight,
377
388
  ),
378
389
  );
390
+ if (caretRow !== null && caretRow >= offset + editorHeight)
391
+ offset = caretRow - editorHeight + 1;
379
392
  return h(
380
393
  Box,
381
394
  { flexDirection: "column", height, overflow: "hidden" },
@@ -554,12 +567,11 @@ function documentRows(route, data, width, colors) {
554
567
  ...markdownRows(fence + "\n" + post.code + "\n" + fence, width, colors),
555
568
  );
556
569
  }
557
- if (post.links?.length)
570
+ const links = conversationLinks(post);
571
+ if (links.length)
558
572
  rows.push(
559
573
  { text: "LINKS", bold: true },
560
- ...post.links.flatMap((link) =>
561
- wrap(link.label + " · " + link.url, width),
562
- ),
574
+ ...links.flatMap((link) => wrap(link.label + " · " + link.url, width)),
563
575
  );
564
576
  if (post.archived_at || post.locked_at)
565
577
  rows.push({
@@ -708,7 +720,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
708
720
  height = Math.max(1, rows - 1),
709
721
  compact = width < 90 || effectiveAppearance.sidebar === "hidden",
710
722
  sidebar = compact ? 0 : 25,
711
- bodyHeight = Math.max(1, height - 9),
723
+ editingForm = !!form && !preview && !picker,
724
+ bodyHeight = Math.max(1, height - (editingForm ? 6 : 9)),
712
725
  contentWidth = Math.max(1, width - sidebar - 6),
713
726
  contentHeight = Math.max(1, bodyHeight - 6);
714
727
  const key = JSON.stringify(route),
@@ -3150,6 +3163,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
3150
3163
  ? [payload.title || payload.name, ""]
3151
3164
  : []),
3152
3165
  payload.body || payload.reason || payload.description || "",
3166
+ ...conversationLinks(payload).flatMap((link) => [
3167
+ "",
3168
+ link.label + " · " + link.url,
3169
+ ]),
3153
3170
  ...(preview.catalog
3154
3171
  ? [
3155
3172
  "",
@@ -3614,31 +3631,33 @@ function App({ initialConfig, flags, root, initialAppearance }) {
3614
3631
  width,
3615
3632
  color: error ? colors.red : notice ? colors.green : colors.muted,
3616
3633
  }),
3617
- h(
3618
- Box,
3619
- {
3620
- height: 3,
3621
- borderStyle: colors.frame,
3622
- borderColor: focus === "prompt" ? colors.blue : colors.line,
3623
- paddingX: 1,
3624
- overflow: "hidden",
3625
- },
3626
- h(Text, { color: colors.purple, bold: true }, "› "),
3627
- focus === "prompt"
3628
- ? h(InputText, {
3629
- value: command,
3630
- cursor: commandCursor,
3631
- width: Math.max(1, width - 6),
3632
- })
3633
- : h(
3634
- Text,
3635
- { color: colors.muted },
3636
- clip(
3637
- "Search or /command · Press / to type · X actions · , appearance",
3638
- width - 6,
3639
- ),
3640
- ),
3641
- ),
3634
+ editingForm
3635
+ ? null
3636
+ : h(
3637
+ Box,
3638
+ {
3639
+ height: 3,
3640
+ borderStyle: colors.frame,
3641
+ borderColor: focus === "prompt" ? colors.blue : colors.line,
3642
+ paddingX: 1,
3643
+ overflow: "hidden",
3644
+ },
3645
+ h(Text, { color: colors.purple, bold: true }, "› "),
3646
+ focus === "prompt"
3647
+ ? h(InputText, {
3648
+ value: command,
3649
+ cursor: commandCursor,
3650
+ width: Math.max(1, width - 6),
3651
+ })
3652
+ : h(
3653
+ Text,
3654
+ { color: colors.muted },
3655
+ clip(
3656
+ "Search or /command · Press / to type · X actions · , appearance",
3657
+ width - 6,
3658
+ ),
3659
+ ),
3660
+ ),
3642
3661
  h(TextLine, { text: hint, width, color: colors.muted }),
3643
3662
  ),
3644
3663
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenyokjs/devdevian-cli",
3
- "version": "0.7.1-alpha.1",
3
+ "version": "0.7.2-alpha.1",
4
4
  "description": "Devdevian in your terminal: discussions, questions and human code reviews.",
5
5
  "type": "module",
6
6
  "bin": {