@pi-unipi/kanboard 3.0.0-alpha.17 → 3.0.0-alpha.19

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/index.ts CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  syncPiRuntime,
45
45
  runRotateTokenAction,
46
46
  runStopDaemon,
47
+ captureToBacklog,
47
48
  type CommandDeps,
48
49
  } from "./src/commands.js";
49
50
  import { createWriteGuard } from "./src/guard.js";
@@ -224,6 +225,12 @@ export default function (pi: ExtensionAPI) {
224
225
  registerProgressRenderer(pi);
225
226
  registerKanboardCommands(pi, buildDeps());
226
227
 
228
+ // ── API for other extensions (input-shortcuts K chord) ──────────────
229
+ // Add the editor text to this project's Backlog as a body-only task.
230
+ (globalThis as { __unipi_kanboard_api?: unknown }).__unipi_kanboard_api = {
231
+ captureToBacklog: (opts: { cwd: string; text: string }) => captureToBacklog(buildDeps(), opts),
232
+ };
233
+
227
234
  // R1 progress reminders for hand-worked tasks (silent in child sessions).
228
235
  registerProgressReminders(
229
236
  pi,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "3.0.0-alpha.17",
3
+ "version": "3.0.0-alpha.19",
4
4
  "description": "Kanboard v3 — deferred-work board: CLI/daemon bridge, task runner and skill",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@pi-unipi/core": "3.0.0-alpha.17"
34
+ "@pi-unipi/core": "3.0.0-alpha.19"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -48,10 +48,10 @@
48
48
  "themes": []
49
49
  },
50
50
  "optionalDependencies": {
51
- "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.17",
52
- "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.17",
53
- "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.17",
54
- "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.17",
55
- "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.17"
51
+ "@pi-unipi/kanboard-linux-x64": "3.0.0-alpha.19",
52
+ "@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.19",
53
+ "@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.19",
54
+ "@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.19",
55
+ "@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.19"
56
56
  }
57
57
  }
package/src/commands.ts CHANGED
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  import { promisify } from "node:util";
12
12
 
13
13
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
14
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
14
15
  import { getSettings } from "@pi-unipi/core";
15
16
 
16
17
  import { KanboardCliError, type KanboardCli } from "./bin.js";
@@ -576,15 +577,18 @@ export async function runAdd(deps: CommandDeps, ctx: ExtensionCommandContext, te
576
577
  ctx.ui.notify(`kanboard: ${parsed.error} — /unipi:kanboard-add [-p 1-5] [--after ID] [--status backlog|todo] <title>`, "warning");
577
578
  return;
578
579
  }
579
- if (!parsed.title) {
580
- ctx.ui.notify("kanboard: add needs a title — /unipi:kanboard-add <title>", "warning");
580
+ const hasDescription = parsed.description.trim().length > 0;
581
+ if (!parsed.title && !hasDescription) {
582
+ ctx.ui.notify("kanboard: add needs a title or a description — /unipi:kanboard-add <title> (lines below are the description)", "warning");
581
583
  return;
582
584
  }
583
585
  if (!(await ensureOnboarded(deps, ctx))) return;
584
586
 
585
587
  let bodyFile: string | null = null;
586
588
  try {
587
- const argv = ["add", parsed.title, ...parsed.flags];
589
+ // A description-only add omits the title positional entirely (the binary
590
+ // stores an empty title and derives the display title from the body).
591
+ const argv = ["add", ...(parsed.title ? [parsed.title] : []), ...parsed.flags];
588
592
  if (parsed.description) {
589
593
  bodyFile = join(mkdtempSync(join(tmpdir(), "kb-add-")), "body.md");
590
594
  writeFileSync(bodyFile, parsed.description);
@@ -606,6 +610,55 @@ export async function runAdd(deps: CommandDeps, ctx: ExtensionCommandContext, te
606
610
  }
607
611
  }
608
612
 
613
+ export type CaptureToBacklogResult =
614
+ | { ok: true; id: string; attachments: number }
615
+ | { ok: false; reason: string };
616
+
617
+ /**
618
+ * Capture editor text as a Backlog task body — the entry point the
619
+ * input-shortcuts K chord calls through `globalThis.__unipi_kanboard_api`.
620
+ * No title: the body carries the point (the binary derives the display
621
+ * title). No prompts: when the folder is not onboarded the capture is
622
+ * refused with the reason instead.
623
+ */
624
+ export async function captureToBacklog(
625
+ deps: CommandDeps,
626
+ opts: { cwd: string; text: string },
627
+ ): Promise<CaptureToBacklogResult> {
628
+ const client = deps.cli;
629
+ if (!client) return { ok: false, reason: deps.unavailable ?? "kanboard binary unavailable" };
630
+ let slug: string | null = null;
631
+ try {
632
+ const settings = getSettings("kanboard", opts.cwd) as { slug?: string };
633
+ slug = typeof settings.slug === "string" && settings.slug.length > 0 ? settings.slug : null;
634
+ } catch {
635
+ slug = null;
636
+ }
637
+ if (!slug) {
638
+ return { ok: false, reason: "kanboard is not set up here — run /unipi:kanboard" };
639
+ }
640
+ const text = opts.text.trim();
641
+ if (!text) return { ok: false, reason: "nothing to add" };
642
+ const attaches = detectFilePaths(text);
643
+ let bodyFile: string | null = null;
644
+ try {
645
+ bodyFile = join(mkdtempSync(join(tmpdir(), "kb-capture-")), "body.md");
646
+ writeFileSync(bodyFile, text);
647
+ const argv = ["--project", slug, "add", "--status", "backlog", "--body-file", bodyFile];
648
+ for (const file of attaches) argv.push("--attach", file);
649
+ const task = asTask("add", await client.run<unknown>(argv, { cwd: opts.cwd }));
650
+ deps.debug(`captureToBacklog ${task.id} (${attaches.length} attachments)`);
651
+ return { ok: true, id: task.id, attachments: attaches.length };
652
+ } catch (error) {
653
+ return {
654
+ ok: false,
655
+ reason: error instanceof KanboardCliError ? error.message : String(error),
656
+ };
657
+ } finally {
658
+ if (bodyFile) rmSync(join(bodyFile, ".."), { recursive: true, force: true });
659
+ }
660
+ }
661
+
609
662
  export async function runStatus(deps: CommandDeps, ctx: ExtensionCommandContext | ExtensionContext): Promise<void> {
610
663
  const client = deps.cli;
611
664
  const lines: string[] = [];
@@ -806,6 +859,8 @@ export async function runRotateTokenAction(
806
859
  interface ShowTask {
807
860
  id: string;
808
861
  title: string;
862
+ /** Derived title (Rust `display_title`) — shown when `title` is empty. */
863
+ displayTitle?: string;
809
864
  status: string;
810
865
  priority?: string;
811
866
  order?: number;
@@ -831,6 +886,11 @@ const SHOW_EXTRA_LANES: Array<{ id: string; label: string }> = [
831
886
  const PRIO_GLYPH: Record<string, string> = { urgent: "⇈", high: "↑", medium: "·", low: "↓", none: " " };
832
887
  const PRIO_RANK: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3, none: 4 };
833
888
 
889
+ /** The title a row shows: the derived one when the raw title is empty. */
890
+ function showTitle(task: ShowTask): string {
891
+ return task.displayTitle || task.title || "(untitled)";
892
+ }
893
+
834
894
  /** Same ordering as the CLI's claim_sort: priority, then lane order, then id. */
835
895
  export function claimOrder(tasks: ShowTask[]): ShowTask[] {
836
896
  return [...tasks].sort(
@@ -875,7 +935,7 @@ function laneRows(tasks: ShowTask[], lane: string): ShowRow[] {
875
935
  number: `${number}.`,
876
936
  id: task.id,
877
937
  prio: PRIO_GLYPH[task.priority ?? "none"] ?? " ",
878
- title: task.title,
938
+ title: showTitle(task),
879
939
  extra:
880
940
  task.ready === false || waits.length > 0
881
941
  ? `waits for ${waits.join(", ")}`
@@ -898,7 +958,7 @@ function laneRows(tasks: ShowTask[], lane: string): ShowRow[] {
898
958
  branch: "",
899
959
  id: task.id,
900
960
  prio: PRIO_GLYPH[task.priority ?? "none"] ?? " ",
901
- title: task.title,
961
+ title: showTitle(task),
902
962
  extra:
903
963
  lane === "in_progress" && task.run?.session
904
964
  ? `· ${task.run.session}`
@@ -952,15 +1012,29 @@ export function showRenderer(
952
1012
  lines.push(theme.fg("accent", lane.label));
953
1013
  for (const row of laneRows(inLane, lane.id)) {
954
1014
  const head = ` ${row.branch}${row.number ? `${row.number} ` : ""}${theme.fg("muted", row.id)} ${row.prio} `;
955
- // visible length so far, then truncate the title to fit.
956
- const prefixLen = 2 + row.branch.length + (row.number ? row.number.length + 1 : 0) + row.id.length + 2;
957
- const budget = Math.max(10, width - prefixLen - (row.extra ? row.extra.length + 2 : 0));
958
- const title = row.title.length > budget ? `${row.title.slice(0, Math.max(1, budget - 1))}…` : row.title;
959
- const extra = row.extra ? ` ${row.tone === "error" ? theme.fg("error", row.extra) : theme.fg("dim", row.extra)}` : "";
960
- lines.push(`${head}${title}${extra}`);
1015
+ const styledExtra = row.extra
1016
+ ? ` ${row.tone === "error" ? theme.fg("error", row.extra) : theme.fg("dim", row.extra)}`
1017
+ : "";
1018
+ // Budgets in visible cells (CJK ≈ 2 cells, ANSI = 0 — .length lies on both).
1019
+ // Title takes all the room left after the extra when both fit comfortably,
1020
+ // else it is capped at 10 cells; the extra then takes whatever remains, so a
1021
+ // huge blockedReason is truncated instead of overflowing the terminal (which
1022
+ // hard-crashes pi). Final truncate is the belt-and-braces guarantee.
1023
+ const avail = Math.max(0, width - visibleWidth(head));
1024
+ const extraW = styledExtra ? visibleWidth(styledExtra) : 0;
1025
+ const titleBudget = Math.max(0, avail - extraW >= 10 ? avail - extraW : Math.min(avail, 10));
1026
+ const title = truncateToWidth(row.title, titleBudget, "…");
1027
+ let line = `${head}${title}`;
1028
+ if (styledExtra) {
1029
+ const extraBudget = avail - visibleWidth(title) - 2;
1030
+ if (extraBudget > 0) line += truncateToWidth(styledExtra, extraBudget + 2, "…");
1031
+ }
1032
+ lines.push(line);
961
1033
  }
962
1034
  }
963
- return lines;
1035
+ // Hard guarantee: a line wider than the terminal hard-crashes pi, so every
1036
+ // emitted line — header and lane labels included — is truncated to `width`.
1037
+ return lines.map((line) => truncateToWidth(line, width));
964
1038
  },
965
1039
  invalidate: () => undefined,
966
1040
  };
@@ -1094,19 +1168,19 @@ export async function taskCompletions(
1094
1168
  const needle = prefix.toLowerCase();
1095
1169
  const items = cache.tasks
1096
1170
  .filter((task) => !["cancelled", "archived"].includes(task.status))
1097
- .filter((task) => task.id.toLowerCase().startsWith(needle) || (titleSearch && task.title.toLowerCase().includes(needle)))
1171
+ .filter((task) => task.id.toLowerCase().startsWith(needle) || (titleSearch && showTitle(task).toLowerCase().includes(needle)))
1098
1172
  .slice(0, 12)
1099
1173
  .map((task) =>
1100
1174
  rowStyle === "id-title"
1101
1175
  ? {
1102
1176
  value: fullArgs(before, task.id),
1103
- label: `${task.id} ${task.title.slice(0, 64)}`,
1177
+ label: `${task.id} ${showTitle(task).slice(0, 64)}`,
1104
1178
  description: task.status,
1105
1179
  }
1106
1180
  : {
1107
1181
  value: fullArgs(before, task.id),
1108
1182
  label: task.id,
1109
- description: `${task.status} — ${task.title}`,
1183
+ description: `${task.status} — ${showTitle(task)}`,
1110
1184
  },
1111
1185
  );
1112
1186
  return items.length > 0 ? items : null;
package/src/monitor.ts CHANGED
@@ -294,7 +294,7 @@ export function createKanboardMonitor(deps: MonitorDeps): KanboardMonitor {
294
294
  source: "kanboard",
295
295
  priority: AUTOWORK_PRIORITY,
296
296
  customType: AUTOWORK_NUDGE_CUSTOM_TYPE,
297
- content: `↻ next ready: ${next.id} ${next.title} — show it, start it, work it (autowork) ${ANTI_POISONING_SUFFIX}`,
297
+ content: `↻ next ready: ${next.id} ${next.displayTitle || next.title || "(untitled)"} — show it, start it, work it (autowork) ${ANTI_POISONING_SUFFIX}`,
298
298
  display: true,
299
299
  onDelivered: () => {
300
300
  offersPerTask.set(next.id, offers); // counts on DELIVERY, not proposal
package/src/shapes.ts CHANGED
@@ -45,6 +45,8 @@ export interface KanboardActivity {
45
45
  export interface KanboardTask {
46
46
  id: string;
47
47
  title: string;
48
+ /** Title derived from the first body line when `title` is empty (Rust `Task::display_title`). */
49
+ displayTitle?: string;
48
50
  body?: string;
49
51
  status: string;
50
52
  priority?: string;
@@ -115,6 +117,7 @@ export function asTask(command: string, value: unknown): KanboardTask {
115
117
  ...record,
116
118
  id: requireString(command, record, "id"),
117
119
  title: requireString(command, record, "title"),
120
+ displayTitle: typeof record.displayTitle === "string" ? record.displayTitle : undefined,
118
121
  status: requireString(command, record, "status"),
119
122
  deps: optionalArray<string>(record.deps) ?? [],
120
123
  activity: optionalArray<KanboardActivity>(record.activity) ?? [],