@pi-unipi/kanboard 2.4.0 → 2.4.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/commands.ts CHANGED
@@ -9,7 +9,6 @@ import * as path from "node:path";
9
9
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
10
10
  import { UNIPI_PREFIX, KANBOARD_COMMANDS, KANBOARD_DIRS, UNIPI_EVENTS, emitEvent } from "@pi-unipi/core";
11
11
  import { startServer, KanboardServer } from "./server/index.js";
12
- import { renderKanboardOverlay } from "./tui/kanboard-overlay.js";
13
12
 
14
13
  /** Module-level reference to running server */
15
14
  let runningServer: KanboardServer | null = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@pi-unipi/core": "2.4.0"
42
+ "@pi-unipi/core": "2.4.1"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,300 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Kanboard TUI Overlay
3
- *
4
- * Two tabs: Tasks list and Kanban Board.
5
- * Uses pi-tui overlay API (same pattern as MCP add overlay).
6
- */
7
-
8
- import {
9
- Key,
10
- matchesKey,
11
- truncateToWidth,
12
- type TUI,
13
- visibleWidth,
14
- } from "@earendil-works/pi-tui";
15
- import type { Theme, KeybindingsManager } from "@earendil-works/pi-coding-agent";
16
- import type { ParsedDoc, ParsedItem, ItemStatus } from "../types.js";
17
- import { createDefaultRegistry } from "../parser/index.js";
18
-
19
- type Tab = "tasks" | "board";
20
-
21
- interface KanboardState {
22
- tab: Tab;
23
- tasks: ParsedItem[];
24
- taskIndex: number;
25
- taskScroll: number;
26
- /** Board columns: index into tasks array grouped by status */
27
- boardColumns: {
28
- todo: number[];
29
- inProgress: number[];
30
- done: number[];
31
- };
32
- boardCol: number; // 0=todo, 1=inProgress, 2=done
33
- boardIndex: number[];
34
- pendingG: boolean;
35
- docsRoot: string;
36
- }
37
-
38
- /** Status icon */
39
- function statusIcon(status: ItemStatus): string {
40
- switch (status) {
41
- case "done":
42
- return "✓";
43
- case "in-progress":
44
- return "◐";
45
- default:
46
- return "○";
47
- }
48
- }
49
-
50
- /** Pad string to visible width */
51
- function padVisible(content: string, targetWidth: number): string {
52
- const pad = Math.max(0, targetWidth - visibleWidth(content));
53
- return content + " ".repeat(pad);
54
- }
55
-
56
- /**
57
- * Render the kanboard overlay.
58
- */
59
- export function renderKanboardOverlay(params?: {
60
- docsRoot?: string;
61
- onComplete?: () => void;
62
- }) {
63
- return (
64
- tui: TUI,
65
- theme: Theme,
66
- _kb: KeybindingsManager,
67
- done: (result: { viewed: boolean } | null) => void,
68
- ) => {
69
- const state: KanboardState = {
70
- tab: "tasks",
71
- tasks: [],
72
- taskIndex: 0,
73
- taskScroll: 0,
74
- boardColumns: { todo: [], inProgress: [], done: [] },
75
- boardCol: 0,
76
- boardIndex: [0, 0, 0],
77
- pendingG: false,
78
- docsRoot: params?.docsRoot ?? ".unipi/docs",
79
- };
80
-
81
- // Load data
82
- let loaded = false;
83
- const ensureLoaded = async () => {
84
- if (loaded) return;
85
- const registry = await createDefaultRegistry();
86
- const docs = registry.parseAll(state.docsRoot);
87
- state.tasks = docs.flatMap((d) => d.items);
88
-
89
- // Build board columns
90
- state.boardColumns = { todo: [], inProgress: [], done: [] };
91
- state.tasks.forEach((item, idx) => {
92
- if (item.status === "done") state.boardColumns.done.push(idx);
93
- else if (item.status === "in-progress") state.boardColumns.inProgress.push(idx);
94
- else state.boardColumns.todo.push(idx);
95
- });
96
-
97
- // Init board indices
98
- state.boardIndex = [
99
- 0,
100
- 0,
101
- 0,
102
- ];
103
-
104
- loaded = true;
105
- };
106
-
107
- const render = async () => {
108
- await ensureLoaded();
109
-
110
- const width = tui.terminal?.columns ?? 80;
111
- const height = tui.terminal?.rows ?? 24;
112
- const lines: string[] = [];
113
-
114
- // Header
115
- const header = " 📋 Kanboard ";
116
- const tabLine =
117
- " [T]asks | [B]oard ".replace(
118
- state.tab === "tasks" ? "[T]" : "T",
119
- state.tab === "tasks" ? "▸T" : " T",
120
- ).replace(
121
- state.tab === "board" ? "[B]" : "B",
122
- state.tab === "board" ? "▸B" : " B",
123
- );
124
- lines.push(truncateToWidth(header, width));
125
- lines.push(truncateToWidth(tabLine, width));
126
- lines.push("─".repeat(width));
127
-
128
- if (state.tab === "tasks") {
129
- renderTasksTab(lines, width, height - 5);
130
- } else {
131
- renderBoardTab(lines, width, height - 5);
132
- }
133
-
134
- // Footer
135
- lines.push("─".repeat(width));
136
- const footer = " j/k: navigate Tab: switch tab q/Esc: close ";
137
- lines.push(truncateToWidth(footer, width));
138
-
139
- (tui as unknown as { setContent(lines: string[]): void }).setContent(lines);
140
- };
141
-
142
- const renderTasksTab = (lines: string[], width: number, maxLines: number) => {
143
- if (state.tasks.length === 0) {
144
- lines.push(truncateToWidth(" No tasks found.", width));
145
- return;
146
- }
147
-
148
- // Ensure index in range
149
- state.taskIndex = Math.min(state.taskIndex, state.tasks.length - 1);
150
- state.taskIndex = Math.max(0, state.taskIndex);
151
-
152
- // Scroll to keep selected visible
153
- if (state.taskIndex < state.taskScroll) state.taskScroll = state.taskIndex;
154
- if (state.taskIndex >= state.taskScroll + maxLines) {
155
- state.taskScroll = state.taskIndex - maxLines + 1;
156
- }
157
-
158
- const visible = state.tasks.slice(
159
- state.taskScroll,
160
- state.taskScroll + maxLines,
161
- );
162
-
163
- for (let i = 0; i < visible.length; i++) {
164
- const item = visible[i];
165
- const globalIdx = state.taskScroll + i;
166
- const selected = globalIdx === state.taskIndex;
167
- const prefix = selected ? " ▸ " : " ";
168
- const icon = statusIcon(item.status);
169
- const source = `[${item.sourceFile}]`;
170
- const line = `${prefix}${icon} ${item.text} ${source}`;
171
- lines.push(
172
- truncateToWidth(
173
- selected ? `\x1b[7m${padVisible(line, width)}\x1b[0m` : line,
174
- width,
175
- ),
176
- );
177
- }
178
- };
179
-
180
- const renderBoardTab = (lines: string[], width: number, maxLines: number) => {
181
- const colWidth = Math.floor(width / 3);
182
- const cols: Array<{ title: string; indices: number[]; colIdx: number }> = [
183
- { title: "To Do", indices: state.boardColumns.todo, colIdx: 0 },
184
- { title: "In Progress", indices: state.boardColumns.inProgress, colIdx: 1 },
185
- { title: "Done", indices: state.boardColumns.done, colIdx: 2 },
186
- ];
187
-
188
- // Column headers
189
- let headerLine = "";
190
- for (const col of cols) {
191
- const isActive = col.colIdx === state.boardCol;
192
- const title = isActive ? `▸ ${col.title}` : ` ${col.title}`;
193
- headerLine += padVisible(` ${title} (${col.indices.length})`, colWidth);
194
- }
195
- lines.push(truncateToWidth(headerLine, width));
196
- lines.push("─".repeat(width));
197
-
198
- // Column items
199
- const maxItems = Math.max(
200
- ...cols.map((c) => c.indices.length),
201
- maxLines - 2,
202
- );
203
- for (let row = 0; row < Math.min(maxItems, maxLines - 2); row++) {
204
- let rowLine = "";
205
- for (const col of cols) {
206
- const active = col.colIdx === state.boardCol;
207
- const boardIdx = state.boardIndex[col.colIdx] ?? 0;
208
- if (row < col.indices.length) {
209
- const item = state.tasks[col.indices[row]];
210
- const selected = active && row === boardIdx;
211
- const icon = statusIcon(item.status);
212
- const text = truncateToWidth(` ${icon} ${item.text}`, colWidth - 1);
213
- if (selected) {
214
- rowLine += `\x1b[7m${padVisible(text, colWidth)}\x1b[0m`;
215
- } else {
216
- rowLine += padVisible(text, colWidth);
217
- }
218
- } else {
219
- rowLine += padVisible("", colWidth);
220
- }
221
- }
222
- lines.push(truncateToWidth(rowLine, width));
223
- }
224
- };
225
-
226
- const handleKey = async (key: string) => {
227
- await ensureLoaded();
228
-
229
- // Close
230
- if (matchesKey(key, "q") || matchesKey(key, "escape")) {
231
- done({ viewed: true });
232
- return;
233
- }
234
-
235
- // Tab switching
236
- if (matchesKey(key, "tab") || matchesKey(key, "b")) {
237
- state.tab = state.tab === "tasks" ? "board" : "tasks";
238
- render();
239
- return;
240
- }
241
-
242
- if (matchesKey(key, "t")) {
243
- state.tab = "tasks";
244
- render();
245
- return;
246
- }
247
-
248
- // Navigation
249
- if (state.tab === "tasks") {
250
- if (matchesKey(key, "j") || matchesKey(key, "down")) {
251
- state.taskIndex = Math.min(state.taskIndex + 1, state.tasks.length - 1);
252
- } else if (matchesKey(key, "k") || matchesKey(key, "up")) {
253
- state.taskIndex = Math.max(state.taskIndex - 1, 0);
254
- } else if (matchesKey(key, "g")) {
255
- if (state.pendingG) {
256
- state.taskIndex = 0;
257
- state.pendingG = false;
258
- } else {
259
- state.pendingG = true;
260
- setTimeout(() => {
261
- state.pendingG = false;
262
- }, 500);
263
- }
264
- } else if (matchesKey(key, "shift+g")) {
265
- state.taskIndex = state.tasks.length - 1;
266
- }
267
- } else {
268
- // Board navigation
269
- const cols = [
270
- state.boardColumns.todo,
271
- state.boardColumns.inProgress,
272
- state.boardColumns.done,
273
- ];
274
-
275
- if (matchesKey(key, "h") || matchesKey(key, "left")) {
276
- state.boardCol = Math.max(0, state.boardCol - 1);
277
- } else if (matchesKey(key, "l") || matchesKey(key, "right")) {
278
- state.boardCol = Math.min(2, state.boardCol + 1);
279
- } else if (matchesKey(key, "j") || matchesKey(key, "down")) {
280
- const col = cols[state.boardCol];
281
- state.boardIndex[state.boardCol] = Math.min(
282
- state.boardIndex[state.boardCol] + 1,
283
- col.length - 1,
284
- );
285
- } else if (matchesKey(key, "k") || matchesKey(key, "up")) {
286
- state.boardIndex[state.boardCol] = Math.max(
287
- state.boardIndex[state.boardCol] - 1,
288
- 0,
289
- );
290
- }
291
- }
292
-
293
- render();
294
- };
295
-
296
- // Start
297
- render();
298
- (tui as unknown as { on(event: string, handler: (data: string) => void): void }).on("key", handleKey);
299
- };
300
- }