@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 +7 -0
- package/package.json +7 -7
- package/src/commands.ts +89 -15
- package/src/monitor.ts +1 -1
- package/src/shapes.ts +3 -0
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.
|
|
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.
|
|
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.
|
|
52
|
-
"@pi-unipi/kanboard-linux-arm64": "3.0.0-alpha.
|
|
53
|
-
"@pi-unipi/kanboard-darwin-x64": "3.0.0-alpha.
|
|
54
|
-
"@pi-unipi/kanboard-darwin-arm64": "3.0.0-alpha.
|
|
55
|
-
"@pi-unipi/kanboard-win32-x64": "3.0.0-alpha.
|
|
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
|
-
|
|
580
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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) ?? [],
|