@tenyokjs/devdevian-cli 0.7.0-alpha.1 → 0.7.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
@@ -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,12 @@ 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
+
70
72
  ## Meet Devi
71
73
 
72
74
  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,14 @@ 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.1-alpha.1 — Clearer forms
12
+
13
+ - 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.
14
+ - 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.
15
+ - Clarified additional-link syntax, preview versus autosaved drafts, and optional URL fields. Opening a new form clears stale success notices.
16
+ - 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.
17
+ - 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.
18
+
11
19
  ## 0.7.0-alpha.1 — Everyday flow
12
20
 
13
21
  - 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,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.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,
@@ -289,21 +290,29 @@ function InputText({ value, cursor, width, height = 1 }) {
289
290
  }),
290
291
  );
291
292
  }
292
- function FormView({ form, active, cursor, width, height }) {
293
+ function FormView({ form, active, cursor, width, height, navFocus }) {
293
294
  const colors = useTheme();
294
295
  const fields = fieldsFor(form),
295
296
  rows = [];
296
297
  const steps = formSteps(form, fields),
297
298
  stepIndex = stepAt(steps, active, fields.length),
298
299
  step = steps[stepIndex];
300
+ const guidance = fieldGuidance(navFocus ? null : fields[active], {
301
+ wizard: !!step,
302
+ project: form.kind === "catalog" && form.entity === "project",
303
+ });
304
+ const helpRows =
305
+ form.kind === "appearance" ? 0 : height >= 8 ? 2 : height >= 5 ? 1 : 0,
306
+ navRows = step && height >= 3 ? 1 : 0;
307
+ const editorHeight = Math.max(1, height - helpRows - navRows);
299
308
  let selectedLine = rows.length;
300
309
  fields.forEach((field, index) => {
301
310
  if (step && !step.indexes.includes(index)) return;
302
311
  if (index === active) selectedLine = rows.length;
303
312
  rows.push({
304
- text: (index === active ? "› " : " ") + field.label,
305
- color: index === active ? colors.purple : colors.muted,
306
- bold: index === active,
313
+ text: (index === active && !navFocus ? "› " : " ") + field.label,
314
+ color: index === active && !navFocus ? colors.purple : colors.muted,
315
+ bold: index === active && !navFocus,
307
316
  });
308
317
  if (field.options) {
309
318
  const option = field.options.find(
@@ -312,12 +321,12 @@ function FormView({ form, active, cursor, width, height }) {
312
321
  rows.push({
313
322
  text:
314
323
  " ‹ " + (option?.[1] || form.values[field.key] || "Choose") + " ›",
315
- color: index === active ? colors.blue : colors.text,
324
+ color: index === active && !navFocus ? colors.blue : colors.text,
316
325
  });
317
326
  } else {
318
327
  const value = form.values[field.key] || "";
319
328
  const text =
320
- index === active
329
+ index === active && !navFocus
321
330
  ? cursorLines(value, cursor, width - 3).lines
322
331
  : wrap(value || "…", width - 3);
323
332
  const caretLine =
@@ -329,7 +338,9 @@ function FormView({ form, active, cursor, width, height }) {
329
338
  : 0;
330
339
  const visible = field.multiline
331
340
  ? Math.min(
332
- index === active ? Math.max(1, Math.min(12, height - 3)) : 3,
341
+ index === active
342
+ ? Math.max(1, Math.min(12, editorHeight - 2))
343
+ : 3,
333
344
  Math.max(2, text.length),
334
345
  )
335
346
  : 1,
@@ -337,16 +348,14 @@ function FormView({ form, active, cursor, width, height }) {
337
348
  rows.push(
338
349
  ...text.slice(start, start + visible).map((line) => ({
339
350
  text: " " + line,
340
- color: index === active ? colors.text : colors.muted,
351
+ color: index === active && !navFocus ? colors.text : colors.muted,
341
352
  })),
342
353
  );
343
354
  }
344
- if (field.hint && active === index)
345
- rows.push({ text: " " + field.hint, color: colors.muted });
346
355
  rows.push("");
347
356
  });
348
357
  if (active === fields.length) selectedLine = rows.length;
349
- if (!step || stepIndex === steps.length - 1)
358
+ if (!step)
350
359
  rows.push({
351
360
  text:
352
361
  (active === fields.length ? "› " : " ") +
@@ -362,9 +371,56 @@ function FormView({ form, active, cursor, width, height }) {
362
371
  });
363
372
  const offset = Math.max(
364
373
  0,
365
- Math.min(selectedLine - (height >= 4 ? 1 : 0), rows.length - height),
374
+ Math.min(
375
+ selectedLine - (editorHeight >= 4 ? 1 : 0),
376
+ rows.length - editorHeight,
377
+ ),
378
+ );
379
+ return h(
380
+ Box,
381
+ { flexDirection: "column", height, overflow: "hidden" },
382
+ h(FrameRows, { rows, width, height: editorHeight, offset }),
383
+ helpRows === 2
384
+ ? h(TextLine, {
385
+ text:
386
+ guidance.detail ||
387
+ (form.kind === "catalogFilter"
388
+ ? "Ctrl+S applies these filters."
389
+ : form.kind === "calendar"
390
+ ? "Ctrl+S exports a new calendar file."
391
+ : "Ctrl+S opens a preview; publishing requires separate confirmation."),
392
+ width,
393
+ color: colors.muted,
394
+ })
395
+ : null,
396
+ helpRows
397
+ ? h(TextLine, { text: guidance.keys, width, color: colors.muted })
398
+ : null,
399
+ navRows
400
+ ? h(
401
+ Text,
402
+ { wrap: "truncate" },
403
+ ...stepControls(steps, stepIndex).map((control) =>
404
+ h(
405
+ Text,
406
+ {
407
+ key: control.id,
408
+ color: !control.enabled
409
+ ? colors.muted
410
+ : navFocus === control.id
411
+ ? colors.blue
412
+ : colors.purple,
413
+ bold: navFocus === control.id,
414
+ dimColor: !control.enabled,
415
+ backgroundColor:
416
+ navFocus === control.id ? colors.selected : undefined,
417
+ },
418
+ ` ${navFocus === control.id ? "›" : " "}[ ${control.label} ] `,
419
+ ),
420
+ ),
421
+ )
422
+ : null,
366
423
  );
367
- return h(FrameRows, { rows, width, height, offset });
368
424
  }
369
425
  function listRows(route, data, index, width, colors, density = "comfortable") {
370
426
  const items = data.items || [];
@@ -591,6 +647,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
591
647
  [notice, setNotice] = useState(initialAppearance.warning || "");
592
648
  const [form, setForm] = useState(null),
593
649
  [field, setField] = useState(0),
650
+ [formNav, setFormNav] = useState(null),
594
651
  [cursor, setCursor] = useState(0),
595
652
  [preview, setPreview] = useState(null),
596
653
  [dialog, setDialog] = useState(null),
@@ -893,6 +950,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
893
950
  setReaderFind({ query: "", cursor: 0, editing: false });
894
951
  }
895
952
  function moveStep(direction) {
953
+ setFormNav(null);
896
954
  const fields = fieldsFor(form),
897
955
  steps = formSteps(form, fields),
898
956
  currentStep = stepAt(steps, field, fields.length);
@@ -1392,6 +1450,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
1392
1450
  );
1393
1451
  }
1394
1452
  function openForm(next) {
1453
+ setFormNav(null);
1454
+ setNotice("");
1395
1455
  setBooting(false);
1396
1456
  setForm({
1397
1457
  ...next,
@@ -1556,6 +1616,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
1556
1616
  }
1557
1617
  async function prepare() {
1558
1618
  if (!form) return;
1619
+ setFormNav(null);
1559
1620
  if (form.kind === "catalogFilter") {
1560
1621
  await task(async () => {
1561
1622
  const values = validateFilter(form.values);
@@ -2128,7 +2189,19 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2128
2189
  }
2129
2190
  function changeField(next) {
2130
2191
  const fields = fieldsFor(form),
2131
- selected = (next + fields.length + 1) % (fields.length + 1);
2192
+ steps = formSteps(form, fields),
2193
+ stepIndex = stepAt(steps, field, fields.length),
2194
+ step = steps[stepIndex];
2195
+ if (
2196
+ step &&
2197
+ !formNav &&
2198
+ ((next > field && field === step.indexes.at(-1)) ||
2199
+ (next < field && field === step.indexes[0]))
2200
+ ) {
2201
+ setFormNav(next < field && stepIndex > 0 ? "back" : "next");
2202
+ return;
2203
+ }
2204
+ const selected = (next + fields.length + 1) % (fields.length + 1);
2132
2205
  setField(selected);
2133
2206
  setCursor(
2134
2207
  selected < fields.length
@@ -2324,7 +2397,12 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2324
2397
  return;
2325
2398
  }
2326
2399
  if (form) {
2327
- if (keyEvent.ctrl && input === "o" && form.kind === "catalog") {
2400
+ if (
2401
+ keyEvent.ctrl &&
2402
+ input === "o" &&
2403
+ form.kind === "catalog" &&
2404
+ !formNav
2405
+ ) {
2328
2406
  const active = fieldsFor(form)[field];
2329
2407
  if (["logo", "banner", "screenshots"].includes(active?.key)) {
2330
2408
  setPicker({ mediaKey: active.key });
@@ -2359,7 +2437,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2359
2437
  moveStep(input === "n" ? 1 : -1);
2360
2438
  return;
2361
2439
  }
2362
- if (keyEvent.ctrl && input === "e") {
2440
+ if (keyEvent.ctrl && input === "e" && !formNav) {
2363
2441
  externalEdit();
2364
2442
  return;
2365
2443
  }
@@ -2367,6 +2445,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2367
2445
  keyEvent.ctrl &&
2368
2446
  ["o", "g"].includes(input) &&
2369
2447
  form.kind === "publication" &&
2448
+ !formNav &&
2370
2449
  form.values.kind === "review"
2371
2450
  ) {
2372
2451
  setPicker(input === "g" ? "changes" : "files");
@@ -2390,6 +2469,39 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2390
2469
  prepare();
2391
2470
  return;
2392
2471
  }
2472
+ if (formNav) {
2473
+ const fields = fieldsFor(form),
2474
+ steps = formSteps(form, fields),
2475
+ i = stepAt(steps, field, fields.length),
2476
+ step = steps[i];
2477
+ if (keyEvent.return) {
2478
+ moveStep(formNav === "back" ? -1 : 1);
2479
+ return;
2480
+ }
2481
+ if (keyEvent.leftArrow || keyEvent.upArrow) {
2482
+ setFormNav(i > 0 ? "back" : "next");
2483
+ return;
2484
+ }
2485
+ if (keyEvent.rightArrow || keyEvent.downArrow) {
2486
+ setFormNav("next");
2487
+ return;
2488
+ }
2489
+ if (keyEvent.tab) {
2490
+ if (keyEvent.shift && formNav === "next" && i > 0) {
2491
+ setFormNav("back");
2492
+ return;
2493
+ }
2494
+ if (!keyEvent.shift && formNav === "back") {
2495
+ setFormNav("next");
2496
+ return;
2497
+ }
2498
+ const next = keyEvent.shift ? step.indexes.at(-1) : step.indexes[0];
2499
+ setFormNav(null);
2500
+ setField(next);
2501
+ setCursor(graphemes(form.values[fields[next].key] || "").length);
2502
+ }
2503
+ return;
2504
+ }
2393
2505
  if (keyEvent.tab) {
2394
2506
  changeField(field + (keyEvent.shift ? -1 : 1));
2395
2507
  return;
@@ -2735,8 +2847,16 @@ function App({ initialConfig, flags, root, initialAppearance }) {
2735
2847
  reportSelected();
2736
2848
  }
2737
2849
  });
2850
+ const compactFieldHelp =
2851
+ form && !formNav && form.kind !== "appearance" && contentHeight < 8
2852
+ ? fieldGuidance(fieldsFor(form)[field], {
2853
+ wizard: !!activeSteps.length,
2854
+ project: form.kind === "catalog" && form.entity === "project",
2855
+ }).detail
2856
+ : "";
2738
2857
  const status =
2739
2858
  error ||
2859
+ compactFieldHelp ||
2740
2860
  liveStatus ||
2741
2861
  notice ||
2742
2862
  (pending
@@ -3087,6 +3207,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
3087
3207
  body = h(form.kind === "appearance" ? AppearanceView : FormView, {
3088
3208
  form,
3089
3209
  active: field,
3210
+ navFocus: formNav,
3090
3211
  cursor,
3091
3212
  width: contentWidth,
3092
3213
  height: contentHeight,
@@ -3432,7 +3553,11 @@ function App({ initialConfig, flags, root, initialAppearance }) {
3432
3553
  : preview
3433
3554
  ? "← → choose · Enter confirm · Esc edit"
3434
3555
  : form
3435
- ? "Tab field · Ctrl+N/P steps · Ctrl+E editor · Ctrl+S preview"
3556
+ ? formNav
3557
+ ? `${stepControls(activeSteps, activeStep).find((control) => control.id === formNav)?.label || "Next"} · Enter select · Tab fields · Ctrl+S preview`
3558
+ : fieldsFor(form)[field]?.multiline
3559
+ ? "Enter new line · Tab next field · Ctrl+N next step · Ctrl+S preview"
3560
+ : "Enter/Tab next field · Ctrl+N/P steps · Ctrl+S preview"
3436
3561
  : phase !== "ready"
3437
3562
  ? "Enter connect / open browser · Ctrl+C exit"
3438
3563
  : route.view === "feedback"
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.1-alpha.1",
4
4
  "description": "Devdevian in your terminal: discussions, questions and human code reviews.",
5
5
  "type": "module",
6
6
  "bin": {