@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 +0 -1
- package/package.json +2 -2
- package/tui/kanboard-overlay.ts +0 -300
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.
|
|
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.
|
|
42
|
+
"@pi-unipi/core": "2.4.1"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
package/tui/kanboard-overlay.ts
DELETED
|
@@ -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
|
-
}
|