@tenyokjs/devdevian-cli 0.7.0-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
@@ -38,7 +38,7 @@ The default `devdevian` command and `devdevian tui` open an alternate terminal s
38
38
  - **N** creates a conversation (a community on the Communities screen); **A** asks a question; **R** replies inside a conversation; **Shift+R** refreshes.
39
39
  - **C** comments on snippet lines; **E/D** edit/delete your own ordinary replies or code feedback; **S** resolves/reopens a code remark or accepts/removes a reply as the solution (question/review author only); **!** reports selected code feedback.
40
40
  - **Ctrl+E** opens the current multiline field in `$VISUAL` / `$EDITOR` (use `code --wait` for VS Code). Closing the editor restores the form; publishing still requires preview and confirmation. **Ctrl+L** saves an active draft and returns to sign-in.
41
- - In forms, **Tab/Shift+Tab** change fields, arrows choose options or move the text cursor, **Enter** adds a line, **Ctrl+S** opens the preview. Publication requires a separate confirmation; Escape asks before discarding a draft. Drafts auto-save locally after a short pause and can be restored from **Local drafts** or `/drafts`.
41
+ - In forms, **Tab/Shift+Tab** change fields or focus the step controls, arrows choose options or move the text cursor, **Enter** adds a line inside multiline text, and **Ctrl+S** opens the preview. Publication requires a separate confirmation; Escape asks before discarding a draft. Drafts auto-save locally after a short pause and can be restored from **Local drafts** or `/drafts`.
42
42
  - **Ctrl+C** exits. With an unsent draft, confirm saving and leaving or press Ctrl+C again to force exit.
43
43
 
44
44
  `devdevian open` launches Terminal on macOS, Windows Terminal on Windows, or GNOME Terminal/Konsole/xterm on a Linux desktop. A graphical desktop and an installed supported terminal are required; over SSH and inside an editor, use `devdevian tui`. Window launchers still need platform-specific user testing; terminal rendering and input are covered by macOS/Linux pseudo-terminal checks.
@@ -63,10 +63,16 @@ Forms autosave locally. Successful upload IDs are retained if validation fails;
63
63
 
64
64
  Press **Ctrl+K** outside a form or command input, or run **/actions**, to search actions, sections and creation flows. Recent views stay in memory for the current account/session; the palette offers up to eight. Type `project 7`, `event 9` or `open 142` for an explicit ID, or choose the forum-search result for a text query. Typing stays local; actions run only after Enter. Destructive actions retain their separate confirmation. **Esc** returns to the previous view without moving your selection.
65
65
 
66
- Conversation, Project and Event forms show one step at a time. **Ctrl+N** validates the current step and moves forward; **Ctrl+P** returns to the previous step. **Tab** still moves through all fields and crosses steps. **Ctrl+S** checks every required field and opens the full publication preview; a missing value returns you to its step. Step changes preserve text, image paths, code selections and locally saved drafts. No upload or publication happens just by advancing a step.
66
+ Conversation, Project and Event forms show one step at a time. **Ctrl+N** validates the current step and moves forward; **Ctrl+P** returns to the previous step. **Tab / Shift+Tab** move through the current step and its **Back / Next** controls. Use arrows to choose a control and **Enter** to activate it; the last step offers **Preview**, not immediate publication. **Ctrl+S** checks every required field and opens the full publication preview; a missing value returns you to its step. Step changes preserve text, image paths, code selections and locally saved drafts. No upload or publication happens just by advancing a step.
67
67
 
68
68
  In full-text reading (**Space** on a reply or remark), **/** opens local text search. **Enter** locates a matching rendered line; **n / Shift+N** cycles forward/backward. **Home / End** jumps to the start/end. **Esc** returns to the underlying view, and reopening the same unchanged item restores your place. This is bounded session memory, not cross-device or permanent reading history. In text fields and the command bar, **Ctrl+K** continues to delete to the end of the line.
69
69
 
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
+
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
+
70
76
  ## Meet Devi
71
77
 
72
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,20 @@ 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
+
17
+ ## 0.7.1-alpha.1 — Clearer forms
18
+
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.
20
+ - Pinned input guidance distinguishes Space, multiline Enter, field navigation and image/code pickers. Project help explicitly points to the dedicated People & links step. Compact layouts prioritize the caret and keep guidance in status/hint lines.
21
+ - Clarified additional-link syntax, preview versus autosaved drafts, and optional URL fields. Opening a new form clears stale success notices.
22
+ - Regression coverage publishes a project through the real terminal and isolated API, checks text spaces/newlines and multiple labeled links from a second account, exercises Back/Next/Preview, preserves Git/image selections and checks multiline editing at 60×18.
23
+ - No API/schema change, new dependency or production-data reset. Per-version history is recorded here; no separate GitHub release is required for this patch.
24
+
11
25
  ## 0.7.0-alpha.1 — Everyday flow
12
26
 
13
27
  - Added a searchable quick-action workspace via `Ctrl+K` or `/actions`: scoped actions, navigation, creation, up to eight recent views, explicit ID opening and a regular forum-search fallback. Typing performs no requests or mutations; existing server permissions and confirmation dialogs still apply.
package/bin/devdevian.mjs CHANGED
@@ -117,6 +117,7 @@ const help = `Devdevian CLI ${version} · alpha
117
117
  Inside the workspace: /projects, /project ID, /events, /event ID
118
118
  Ctrl+K or /actions opens quick actions; type to filter
119
119
  Ctrl+N / Ctrl+P moves between form steps; Ctrl+S validates and previews
120
+ Forms: Tab fields/buttons, Enter selects Back/Next or adds a line in multiline text
120
121
  Full reply text: / find, n/N matches, Home/End, Esc remembers your place
121
122
  N creates · F filters · E edits your publication · X opens context actions
122
123
 
@@ -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
+ }
@@ -0,0 +1,38 @@
1
+ export function fieldGuidance(field, { wizard = false, project = false } = {}) {
2
+ if (!field)
3
+ return {
4
+ detail: "",
5
+ keys: wizard
6
+ ? "Enter selects the highlighted action"
7
+ : "Enter opens the preview",
8
+ };
9
+ let detail = field.hint || "";
10
+ if (project && field.key === "help")
11
+ detail =
12
+ "Text and links are allowed. Dedicated links: People & links step.";
13
+ if (["url", "repo", "demo_url"].includes(field.key) && !detail)
14
+ detail = "Use a complete https:// or http:// URL.";
15
+ if (["logo", "banner"].includes(field.key))
16
+ return { detail, keys: "Ctrl+O selects an image · Tab next field" };
17
+ if (field.key === "file")
18
+ return { detail, keys: "Ctrl+O file · Ctrl+G Git diff · Tab next field" };
19
+ const keys = field.options
20
+ ? "← / → choose · Tab next field"
21
+ : field.multiline
22
+ ? "Space inserts a space · Enter adds a line · Tab next field"
23
+ : "Space inserts a space · Enter / Tab next field";
24
+ return { detail, keys };
25
+ }
26
+ export function stepControls(steps, index) {
27
+ return [
28
+ { id: "back", label: "← Back", enabled: index > 0 },
29
+ {
30
+ id: "next",
31
+ label:
32
+ index === steps.length - 1
33
+ ? "Preview →"
34
+ : "Next: " + steps[index + 1]?.label + " →",
35
+ enabled: true,
36
+ },
37
+ ];
38
+ }
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
@@ -43,6 +43,7 @@ import {
43
43
  appearanceFlags,
44
44
  } from "./appearance.mjs";
45
45
  import { liveLoop, anchoredIndex } from "./live.mjs";
46
+ import { fieldGuidance, stepControls } from "./form-guidance.mjs";
46
47
  import { paletteItems } from "./palette.mjs";
47
48
  import {
48
49
  formSteps,
@@ -54,6 +55,7 @@ import {
54
55
  import { contextActions } from "./context-actions.mjs";
55
56
  import { editExternal } from "./external-editor.mjs";
56
57
  import { markdownRows } from "./rich-text.mjs";
58
+ import { conversationLinks } from "./conversation-links.mjs";
57
59
  import { problemMessage } from "./problems.mjs";
58
60
  import { ReviewWorkspace, codeRange } from "./review-workspace.mjs";
59
61
  import {
@@ -289,21 +291,34 @@ function InputText({ value, cursor, width, height = 1 }) {
289
291
  }),
290
292
  );
291
293
  }
292
- function FormView({ form, active, cursor, width, height }) {
294
+ function FormView({ form, active, cursor, width, height, navFocus }) {
293
295
  const colors = useTheme();
294
296
  const fields = fieldsFor(form),
295
297
  rows = [];
296
298
  const steps = formSteps(form, fields),
297
299
  stepIndex = stepAt(steps, active, fields.length),
298
300
  step = steps[stepIndex];
301
+ const guidance = fieldGuidance(navFocus ? null : fields[active], {
302
+ wizard: !!step,
303
+ project: form.kind === "catalog" && form.entity === "project",
304
+ });
305
+ const helpRows =
306
+ form.kind === "appearance" ? 0 : height >= 8 ? 2 : height >= 5 ? 1 : 0,
307
+ navRows = step && height >= 3 ? 1 : 0;
308
+ const editorHeight = Math.max(1, height - helpRows - navRows);
299
309
  let selectedLine = rows.length;
310
+ let caretRow = null;
300
311
  fields.forEach((field, index) => {
301
312
  if (step && !step.indexes.includes(index)) return;
302
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;
303
318
  rows.push({
304
- text: (index === active ? "› " : " ") + field.label,
305
- color: index === active ? colors.purple : colors.muted,
306
- bold: index === active,
319
+ text: (index === active && !navFocus ? "› " : " ") + field.label,
320
+ color: index === active && !navFocus ? colors.purple : colors.muted,
321
+ bold: index === active && !navFocus,
307
322
  });
308
323
  if (field.options) {
309
324
  const option = field.options.find(
@@ -312,12 +327,11 @@ function FormView({ form, active, cursor, width, height }) {
312
327
  rows.push({
313
328
  text:
314
329
  " ‹ " + (option?.[1] || form.values[field.key] || "Choose") + " ›",
315
- color: index === active ? colors.blue : colors.text,
330
+ color: index === active && !navFocus ? colors.blue : colors.text,
316
331
  });
317
332
  } else {
318
- const value = form.values[field.key] || "";
319
333
  const text =
320
- index === active
334
+ index === active && !navFocus
321
335
  ? cursorLines(value, cursor, width - 3).lines
322
336
  : wrap(value || "…", width - 3);
323
337
  const caretLine =
@@ -329,24 +343,30 @@ function FormView({ form, active, cursor, width, height }) {
329
343
  : 0;
330
344
  const visible = field.multiline
331
345
  ? Math.min(
332
- index === active ? Math.max(1, Math.min(12, height - 3)) : 3,
346
+ index === active
347
+ ? Math.max(1, Math.min(12, editorHeight - 1))
348
+ : 3,
333
349
  Math.max(2, text.length),
334
350
  )
335
351
  : 1,
336
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
+ }
337
359
  rows.push(
338
360
  ...text.slice(start, start + visible).map((line) => ({
339
361
  text: " " + line,
340
- color: index === active ? colors.text : colors.muted,
362
+ color: index === active && !navFocus ? colors.text : colors.muted,
341
363
  })),
342
364
  );
343
365
  }
344
- if (field.hint && active === index)
345
- rows.push({ text: " " + field.hint, color: colors.muted });
346
366
  rows.push("");
347
367
  });
348
368
  if (active === fields.length) selectedLine = rows.length;
349
- if (!step || stepIndex === steps.length - 1)
369
+ if (!step)
350
370
  rows.push({
351
371
  text:
352
372
  (active === fields.length ? "› " : " ") +
@@ -360,11 +380,60 @@ function FormView({ form, active, cursor, width, height }) {
360
380
  color: colors.blue,
361
381
  bold: true,
362
382
  });
363
- const offset = Math.max(
383
+ let offset = Math.max(
364
384
  0,
365
- Math.min(selectedLine - (height >= 4 ? 1 : 0), rows.length - height),
385
+ Math.min(
386
+ selectedLine - (editorHeight >= 4 ? 1 : 0),
387
+ rows.length - editorHeight,
388
+ ),
389
+ );
390
+ if (caretRow !== null && caretRow >= offset + editorHeight)
391
+ offset = caretRow - editorHeight + 1;
392
+ return h(
393
+ Box,
394
+ { flexDirection: "column", height, overflow: "hidden" },
395
+ h(FrameRows, { rows, width, height: editorHeight, offset }),
396
+ helpRows === 2
397
+ ? h(TextLine, {
398
+ text:
399
+ guidance.detail ||
400
+ (form.kind === "catalogFilter"
401
+ ? "Ctrl+S applies these filters."
402
+ : form.kind === "calendar"
403
+ ? "Ctrl+S exports a new calendar file."
404
+ : "Ctrl+S opens a preview; publishing requires separate confirmation."),
405
+ width,
406
+ color: colors.muted,
407
+ })
408
+ : null,
409
+ helpRows
410
+ ? h(TextLine, { text: guidance.keys, width, color: colors.muted })
411
+ : null,
412
+ navRows
413
+ ? h(
414
+ Text,
415
+ { wrap: "truncate" },
416
+ ...stepControls(steps, stepIndex).map((control) =>
417
+ h(
418
+ Text,
419
+ {
420
+ key: control.id,
421
+ color: !control.enabled
422
+ ? colors.muted
423
+ : navFocus === control.id
424
+ ? colors.blue
425
+ : colors.purple,
426
+ bold: navFocus === control.id,
427
+ dimColor: !control.enabled,
428
+ backgroundColor:
429
+ navFocus === control.id ? colors.selected : undefined,
430
+ },
431
+ ` ${navFocus === control.id ? "›" : " "}[ ${control.label} ] `,
432
+ ),
433
+ ),
434
+ )
435
+ : null,
366
436
  );
367
- return h(FrameRows, { rows, width, height, offset });
368
437
  }
369
438
  function listRows(route, data, index, width, colors, density = "comfortable") {
370
439
  const items = data.items || [];
@@ -498,12 +567,11 @@ function documentRows(route, data, width, colors) {
498
567
  ...markdownRows(fence + "\n" + post.code + "\n" + fence, width, colors),
499
568
  );
500
569
  }
501
- if (post.links?.length)
570
+ const links = conversationLinks(post);
571
+ if (links.length)
502
572
  rows.push(
503
573
  { text: "LINKS", bold: true },
504
- ...post.links.flatMap((link) =>
505
- wrap(link.label + " · " + link.url, width),
506
- ),
574
+ ...links.flatMap((link) => wrap(link.label + " · " + link.url, width)),
507
575
  );
508
576
  if (post.archived_at || post.locked_at)
509
577
  rows.push({
@@ -591,6 +659,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
591
659
  [notice, setNotice] = useState(initialAppearance.warning || "");
592
660
  const [form, setForm] = useState(null),
593
661
  [field, setField] = useState(0),
662
+ [formNav, setFormNav] = useState(null),
594
663
  [cursor, setCursor] = useState(0),
595
664
  [preview, setPreview] = useState(null),
596
665
  [dialog, setDialog] = useState(null),
@@ -651,7 +720,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
651
720
  height = Math.max(1, rows - 1),
652
721
  compact = width < 90 || effectiveAppearance.sidebar === "hidden",
653
722
  sidebar = compact ? 0 : 25,
654
- bodyHeight = Math.max(1, height - 9),
723
+ editingForm = !!form && !preview && !picker,
724
+ bodyHeight = Math.max(1, height - (editingForm ? 6 : 9)),
655
725
  contentWidth = Math.max(1, width - sidebar - 6),
656
726
  contentHeight = Math.max(1, bodyHeight - 6);
657
727
  const key = JSON.stringify(route),
@@ -893,6 +963,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
893
963
  setReaderFind({ query: "", cursor: 0, editing: false });
894
964
  }
895
965
  function moveStep(direction) {
966
+ setFormNav(null);
896
967
  const fields = fieldsFor(form),
897
968
  steps = formSteps(form, fields),
898
969
  currentStep = stepAt(steps, field, fields.length);
@@ -1392,6 +1463,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
1392
1463
  );
1393
1464
  }
1394
1465
  function openForm(next) {
1466
+ setFormNav(null);
1467
+ setNotice("");
1395
1468
  setBooting(false);
1396
1469
  setForm({
1397
1470
  ...next,
@@ -1556,6 +1629,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
1556
1629
  }
1557
1630
  async function prepare() {
1558
1631
  if (!form) return;
1632
+ setFormNav(null);
1559
1633
  if (form.kind === "catalogFilter") {
1560
1634
  await task(async () => {
1561
1635
  const values = validateFilter(form.values);
@@ -2128,7 +2202,19 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2128
2202
  }
2129
2203
  function changeField(next) {
2130
2204
  const fields = fieldsFor(form),
2131
- selected = (next + fields.length + 1) % (fields.length + 1);
2205
+ steps = formSteps(form, fields),
2206
+ stepIndex = stepAt(steps, field, fields.length),
2207
+ step = steps[stepIndex];
2208
+ if (
2209
+ step &&
2210
+ !formNav &&
2211
+ ((next > field && field === step.indexes.at(-1)) ||
2212
+ (next < field && field === step.indexes[0]))
2213
+ ) {
2214
+ setFormNav(next < field && stepIndex > 0 ? "back" : "next");
2215
+ return;
2216
+ }
2217
+ const selected = (next + fields.length + 1) % (fields.length + 1);
2132
2218
  setField(selected);
2133
2219
  setCursor(
2134
2220
  selected < fields.length
@@ -2324,7 +2410,12 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2324
2410
  return;
2325
2411
  }
2326
2412
  if (form) {
2327
- if (keyEvent.ctrl && input === "o" && form.kind === "catalog") {
2413
+ if (
2414
+ keyEvent.ctrl &&
2415
+ input === "o" &&
2416
+ form.kind === "catalog" &&
2417
+ !formNav
2418
+ ) {
2328
2419
  const active = fieldsFor(form)[field];
2329
2420
  if (["logo", "banner", "screenshots"].includes(active?.key)) {
2330
2421
  setPicker({ mediaKey: active.key });
@@ -2359,7 +2450,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2359
2450
  moveStep(input === "n" ? 1 : -1);
2360
2451
  return;
2361
2452
  }
2362
- if (keyEvent.ctrl && input === "e") {
2453
+ if (keyEvent.ctrl && input === "e" && !formNav) {
2363
2454
  externalEdit();
2364
2455
  return;
2365
2456
  }
@@ -2367,6 +2458,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2367
2458
  keyEvent.ctrl &&
2368
2459
  ["o", "g"].includes(input) &&
2369
2460
  form.kind === "publication" &&
2461
+ !formNav &&
2370
2462
  form.values.kind === "review"
2371
2463
  ) {
2372
2464
  setPicker(input === "g" ? "changes" : "files");
@@ -2390,6 +2482,39 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2390
2482
  prepare();
2391
2483
  return;
2392
2484
  }
2485
+ if (formNav) {
2486
+ const fields = fieldsFor(form),
2487
+ steps = formSteps(form, fields),
2488
+ i = stepAt(steps, field, fields.length),
2489
+ step = steps[i];
2490
+ if (keyEvent.return) {
2491
+ moveStep(formNav === "back" ? -1 : 1);
2492
+ return;
2493
+ }
2494
+ if (keyEvent.leftArrow || keyEvent.upArrow) {
2495
+ setFormNav(i > 0 ? "back" : "next");
2496
+ return;
2497
+ }
2498
+ if (keyEvent.rightArrow || keyEvent.downArrow) {
2499
+ setFormNav("next");
2500
+ return;
2501
+ }
2502
+ if (keyEvent.tab) {
2503
+ if (keyEvent.shift && formNav === "next" && i > 0) {
2504
+ setFormNav("back");
2505
+ return;
2506
+ }
2507
+ if (!keyEvent.shift && formNav === "back") {
2508
+ setFormNav("next");
2509
+ return;
2510
+ }
2511
+ const next = keyEvent.shift ? step.indexes.at(-1) : step.indexes[0];
2512
+ setFormNav(null);
2513
+ setField(next);
2514
+ setCursor(graphemes(form.values[fields[next].key] || "").length);
2515
+ }
2516
+ return;
2517
+ }
2393
2518
  if (keyEvent.tab) {
2394
2519
  changeField(field + (keyEvent.shift ? -1 : 1));
2395
2520
  return;
@@ -2735,8 +2860,16 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2735
2860
  reportSelected();
2736
2861
  }
2737
2862
  });
2863
+ const compactFieldHelp =
2864
+ form && !formNav && form.kind !== "appearance" && contentHeight < 8
2865
+ ? fieldGuidance(fieldsFor(form)[field], {
2866
+ wizard: !!activeSteps.length,
2867
+ project: form.kind === "catalog" && form.entity === "project",
2868
+ }).detail
2869
+ : "";
2738
2870
  const status =
2739
2871
  error ||
2872
+ compactFieldHelp ||
2740
2873
  liveStatus ||
2741
2874
  notice ||
2742
2875
  (pending
@@ -3030,6 +3163,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
3030
3163
  ? [payload.title || payload.name, ""]
3031
3164
  : []),
3032
3165
  payload.body || payload.reason || payload.description || "",
3166
+ ...conversationLinks(payload).flatMap((link) => [
3167
+ "",
3168
+ link.label + " · " + link.url,
3169
+ ]),
3033
3170
  ...(preview.catalog
3034
3171
  ? [
3035
3172
  "",
@@ -3087,6 +3224,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
3087
3224
  body = h(form.kind === "appearance" ? AppearanceView : FormView, {
3088
3225
  form,
3089
3226
  active: field,
3227
+ navFocus: formNav,
3090
3228
  cursor,
3091
3229
  width: contentWidth,
3092
3230
  height: contentHeight,
@@ -3432,7 +3570,11 @@ function App({ initialConfig, flags, root, initialAppearance }) {
3432
3570
  : preview
3433
3571
  ? "← → choose · Enter confirm · Esc edit"
3434
3572
  : form
3435
- ? "Tab field · Ctrl+N/P steps · Ctrl+E editor · Ctrl+S preview"
3573
+ ? formNav
3574
+ ? `${stepControls(activeSteps, activeStep).find((control) => control.id === formNav)?.label || "Next"} · Enter select · Tab fields · Ctrl+S preview`
3575
+ : fieldsFor(form)[field]?.multiline
3576
+ ? "Enter new line · Tab next field · Ctrl+N next step · Ctrl+S preview"
3577
+ : "Enter/Tab next field · Ctrl+N/P steps · Ctrl+S preview"
3436
3578
  : phase !== "ready"
3437
3579
  ? "Enter connect / open browser · Ctrl+C exit"
3438
3580
  : route.view === "feedback"
@@ -3489,31 +3631,33 @@ function App({ initialConfig, flags, root, initialAppearance }) {
3489
3631
  width,
3490
3632
  color: error ? colors.red : notice ? colors.green : colors.muted,
3491
3633
  }),
3492
- h(
3493
- Box,
3494
- {
3495
- height: 3,
3496
- borderStyle: colors.frame,
3497
- borderColor: focus === "prompt" ? colors.blue : colors.line,
3498
- paddingX: 1,
3499
- overflow: "hidden",
3500
- },
3501
- h(Text, { color: colors.purple, bold: true }, "› "),
3502
- focus === "prompt"
3503
- ? h(InputText, {
3504
- value: command,
3505
- cursor: commandCursor,
3506
- width: Math.max(1, width - 6),
3507
- })
3508
- : h(
3509
- Text,
3510
- { color: colors.muted },
3511
- clip(
3512
- "Search or /command · Press / to type · X actions · , appearance",
3513
- width - 6,
3514
- ),
3515
- ),
3516
- ),
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
+ ),
3517
3661
  h(TextLine, { text: hint, width, color: colors.muted }),
3518
3662
  ),
3519
3663
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenyokjs/devdevian-cli",
3
- "version": "0.7.0-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": {