@tarcisiopgs/lisa 1.6.0 → 1.7.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/dist/{chunk-KAME5MG7.js → chunk-MUBWKMRZ.js} +22 -3
- package/dist/index.js +47 -4
- package/dist/kanban-BY7QNVEE.js +489 -0
- package/package.json +61 -59
- package/dist/kanban-KIPKQ2IL.js +0 -127
|
@@ -8,11 +8,15 @@ var KanbanEmitter = class extends EventEmitter {
|
|
|
8
8
|
var kanbanEmitter = new KanbanEmitter();
|
|
9
9
|
function useKanbanState() {
|
|
10
10
|
const [cards, setCards] = useState([]);
|
|
11
|
+
const [isEmpty, setIsEmpty] = useState(false);
|
|
12
|
+
const [workComplete, setWorkComplete] = useState(
|
|
13
|
+
null
|
|
14
|
+
);
|
|
11
15
|
useEffect(() => {
|
|
12
16
|
const onQueued = (issue) => {
|
|
13
17
|
setCards((prev) => {
|
|
14
18
|
if (prev.some((c) => c.id === issue.id)) return prev;
|
|
15
|
-
return [...prev, { id: issue.id, title: issue.title, column: "backlog" }];
|
|
19
|
+
return [...prev, { id: issue.id, title: issue.title, column: "backlog", outputLog: "" }];
|
|
16
20
|
});
|
|
17
21
|
};
|
|
18
22
|
const onStarted = (issueId) => {
|
|
@@ -24,7 +28,9 @@ function useKanbanState() {
|
|
|
24
28
|
};
|
|
25
29
|
const onDone = (issueId, prUrl) => {
|
|
26
30
|
setCards(
|
|
27
|
-
(prev) => prev.map(
|
|
31
|
+
(prev) => prev.map(
|
|
32
|
+
(c) => c.id === issueId ? { ...c, column: "done", prUrl, finishedAt: Date.now() } : c
|
|
33
|
+
)
|
|
28
34
|
);
|
|
29
35
|
};
|
|
30
36
|
const onReverted = (issueId) => {
|
|
@@ -34,18 +40,31 @@ function useKanbanState() {
|
|
|
34
40
|
)
|
|
35
41
|
);
|
|
36
42
|
};
|
|
43
|
+
const onOutput = (issueId, text) => {
|
|
44
|
+
setCards(
|
|
45
|
+
(prev) => prev.map((c) => c.id === issueId ? { ...c, outputLog: c.outputLog + text } : c)
|
|
46
|
+
);
|
|
47
|
+
};
|
|
37
48
|
kanbanEmitter.on("issue:queued", onQueued);
|
|
38
49
|
kanbanEmitter.on("issue:started", onStarted);
|
|
39
50
|
kanbanEmitter.on("issue:done", onDone);
|
|
40
51
|
kanbanEmitter.on("issue:reverted", onReverted);
|
|
52
|
+
kanbanEmitter.on("issue:output", onOutput);
|
|
53
|
+
const onEmpty = () => setIsEmpty(true);
|
|
54
|
+
const onComplete = (data) => setWorkComplete(data);
|
|
55
|
+
kanbanEmitter.on("work:empty", onEmpty);
|
|
56
|
+
kanbanEmitter.on("work:complete", onComplete);
|
|
41
57
|
return () => {
|
|
42
58
|
kanbanEmitter.off("issue:queued", onQueued);
|
|
43
59
|
kanbanEmitter.off("issue:started", onStarted);
|
|
44
60
|
kanbanEmitter.off("issue:done", onDone);
|
|
45
61
|
kanbanEmitter.off("issue:reverted", onReverted);
|
|
62
|
+
kanbanEmitter.off("issue:output", onOutput);
|
|
63
|
+
kanbanEmitter.off("work:empty", onEmpty);
|
|
64
|
+
kanbanEmitter.off("work:complete", onComplete);
|
|
46
65
|
};
|
|
47
66
|
}, []);
|
|
48
|
-
return { cards };
|
|
67
|
+
return { cards, isEmpty, workComplete };
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
export {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
kanbanEmitter
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MUBWKMRZ.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { execSync as execSync8 } from "child_process";
|
|
@@ -317,7 +317,9 @@ function error(message) {
|
|
|
317
317
|
emitJson("error", message);
|
|
318
318
|
return;
|
|
319
319
|
}
|
|
320
|
-
|
|
320
|
+
if (shouldPrintToConsole()) {
|
|
321
|
+
console.error(`${pc.red("[lisa]")} ${pc.dim(timestamp())} ${message}`);
|
|
322
|
+
}
|
|
321
323
|
writeToFile("error", message);
|
|
322
324
|
}
|
|
323
325
|
function ok(message) {
|
|
@@ -388,6 +390,7 @@ function setTitle(title) {
|
|
|
388
390
|
}
|
|
389
391
|
function startSpinner(message) {
|
|
390
392
|
if (!isTTY()) return;
|
|
393
|
+
if (getOutputMode() === "tui") return;
|
|
391
394
|
stopSpinner();
|
|
392
395
|
spinnerFrame = 0;
|
|
393
396
|
writeOSC(`${SPINNER_FRAMES[0]} Lisa \u2014 ${message}`);
|
|
@@ -402,6 +405,7 @@ function stopSpinner(message) {
|
|
|
402
405
|
spinnerTimer = null;
|
|
403
406
|
}
|
|
404
407
|
if (!isTTY()) return;
|
|
408
|
+
if (getOutputMode() === "tui") return;
|
|
405
409
|
if (message) {
|
|
406
410
|
writeOSC(message);
|
|
407
411
|
}
|
|
@@ -1036,6 +1040,9 @@ var AiderProvider = class {
|
|
|
1036
1040
|
proc.stdout.on("data", (chunk) => {
|
|
1037
1041
|
const text2 = chunk.toString();
|
|
1038
1042
|
if (getOutputMode() !== "tui") process.stdout.write(text2);
|
|
1043
|
+
if (opts.issueId) {
|
|
1044
|
+
kanbanEmitter.emit("issue:output", opts.issueId, text2);
|
|
1045
|
+
}
|
|
1039
1046
|
chunks.push(text2);
|
|
1040
1047
|
try {
|
|
1041
1048
|
appendFileSync3(opts.logFile, text2);
|
|
@@ -1117,6 +1124,9 @@ var ClaudeProvider = class {
|
|
|
1117
1124
|
proc.stdout.on("data", (chunk) => {
|
|
1118
1125
|
const text2 = chunk.toString();
|
|
1119
1126
|
if (getOutputMode() !== "tui") process.stdout.write(text2);
|
|
1127
|
+
if (opts.issueId) {
|
|
1128
|
+
kanbanEmitter.emit("issue:output", opts.issueId, text2);
|
|
1129
|
+
}
|
|
1120
1130
|
chunks.push(text2);
|
|
1121
1131
|
try {
|
|
1122
1132
|
appendFileSync4(opts.logFile, text2);
|
|
@@ -1191,6 +1201,9 @@ var CopilotProvider = class {
|
|
|
1191
1201
|
proc.stdout.on("data", (chunk) => {
|
|
1192
1202
|
const text2 = chunk.toString();
|
|
1193
1203
|
if (getOutputMode() !== "tui") process.stdout.write(text2);
|
|
1204
|
+
if (opts.issueId) {
|
|
1205
|
+
kanbanEmitter.emit("issue:output", opts.issueId, text2);
|
|
1206
|
+
}
|
|
1194
1207
|
chunks.push(text2);
|
|
1195
1208
|
try {
|
|
1196
1209
|
appendFileSync5(opts.logFile, text2);
|
|
@@ -1283,6 +1296,9 @@ var CursorProvider = class {
|
|
|
1283
1296
|
proc.stdout.on("data", (chunk) => {
|
|
1284
1297
|
const text2 = chunk.toString();
|
|
1285
1298
|
if (getOutputMode() !== "tui") process.stdout.write(text2);
|
|
1299
|
+
if (opts.issueId) {
|
|
1300
|
+
kanbanEmitter.emit("issue:output", opts.issueId, text2);
|
|
1301
|
+
}
|
|
1286
1302
|
chunks.push(text2);
|
|
1287
1303
|
try {
|
|
1288
1304
|
appendFileSync6(opts.logFile, text2);
|
|
@@ -1358,6 +1374,9 @@ var GeminiProvider = class {
|
|
|
1358
1374
|
proc.stdout.on("data", (chunk) => {
|
|
1359
1375
|
const text2 = chunk.toString();
|
|
1360
1376
|
if (getOutputMode() !== "tui") process.stdout.write(text2);
|
|
1377
|
+
if (opts.issueId) {
|
|
1378
|
+
kanbanEmitter.emit("issue:output", opts.issueId, text2);
|
|
1379
|
+
}
|
|
1361
1380
|
chunks.push(text2);
|
|
1362
1381
|
try {
|
|
1363
1382
|
appendFileSync7(opts.logFile, text2);
|
|
@@ -1433,6 +1452,9 @@ var GooseProvider = class {
|
|
|
1433
1452
|
proc.stdout.on("data", (chunk) => {
|
|
1434
1453
|
const text2 = chunk.toString();
|
|
1435
1454
|
if (getOutputMode() !== "tui") process.stdout.write(text2);
|
|
1455
|
+
if (opts.issueId) {
|
|
1456
|
+
kanbanEmitter.emit("issue:output", opts.issueId, text2);
|
|
1457
|
+
}
|
|
1436
1458
|
chunks.push(text2);
|
|
1437
1459
|
try {
|
|
1438
1460
|
appendFileSync8(opts.logFile, text2);
|
|
@@ -1507,6 +1529,9 @@ var OpenCodeProvider = class {
|
|
|
1507
1529
|
proc.stdout.on("data", (chunk) => {
|
|
1508
1530
|
const text2 = chunk.toString();
|
|
1509
1531
|
if (getOutputMode() !== "tui") process.stdout.write(text2);
|
|
1532
|
+
if (opts.issueId) {
|
|
1533
|
+
kanbanEmitter.emit("issue:output", opts.issueId, text2);
|
|
1534
|
+
}
|
|
1510
1535
|
chunks.push(text2);
|
|
1511
1536
|
try {
|
|
1512
1537
|
appendFileSync9(opts.logFile, text2);
|
|
@@ -3262,8 +3287,14 @@ function installSignalHandlers() {
|
|
|
3262
3287
|
`Failed to revert ${issueId}: ${err instanceof Error ? err.message : String(err)}`
|
|
3263
3288
|
);
|
|
3264
3289
|
}
|
|
3290
|
+
kanbanEmitter.emit("issue:reverted", issueId);
|
|
3291
|
+
}
|
|
3292
|
+
const hasTUI = kanbanEmitter.listenerCount("tui:exit") > 0;
|
|
3293
|
+
kanbanEmitter.emit("tui:exit");
|
|
3294
|
+
if (hasTUI) {
|
|
3295
|
+
await new Promise((resolve6) => setTimeout(resolve6, 250));
|
|
3265
3296
|
}
|
|
3266
|
-
process.exit(
|
|
3297
|
+
process.exit(0);
|
|
3267
3298
|
};
|
|
3268
3299
|
process.on("SIGINT", () => {
|
|
3269
3300
|
cleanup("SIGINT");
|
|
@@ -3322,6 +3353,8 @@ async function runLoop(config2, opts) {
|
|
|
3322
3353
|
}
|
|
3323
3354
|
}
|
|
3324
3355
|
let session = 0;
|
|
3356
|
+
const loopStart = Date.now();
|
|
3357
|
+
let completedCount = 0;
|
|
3325
3358
|
while (true) {
|
|
3326
3359
|
session++;
|
|
3327
3360
|
if (opts.limit > 0 && session > opts.limit) {
|
|
@@ -3370,6 +3403,9 @@ async function runLoop(config2, opts) {
|
|
|
3370
3403
|
error(`Issue '${opts.issueId}' not found.`);
|
|
3371
3404
|
} else {
|
|
3372
3405
|
ok(`No more issues with label '${config2.source_config.label}'. Done.`);
|
|
3406
|
+
if (session === 1) {
|
|
3407
|
+
kanbanEmitter.emit("work:empty");
|
|
3408
|
+
}
|
|
3373
3409
|
}
|
|
3374
3410
|
break;
|
|
3375
3411
|
}
|
|
@@ -3479,6 +3515,7 @@ async function runLoop(config2, opts) {
|
|
|
3479
3515
|
for (const prUrl of sessionResult.prUrls) {
|
|
3480
3516
|
kanbanEmitter.emit("issue:done", issue2.id, prUrl);
|
|
3481
3517
|
}
|
|
3518
|
+
completedCount++;
|
|
3482
3519
|
if (labelToRemove) {
|
|
3483
3520
|
ok(`Removed label "${labelToRemove}" from ${issue2.id}`);
|
|
3484
3521
|
}
|
|
@@ -3496,6 +3533,12 @@ async function runLoop(config2, opts) {
|
|
|
3496
3533
|
setTitle("Lisa \u2014 cooling down...");
|
|
3497
3534
|
await sleep(config2.loop.cooldown * 1e3);
|
|
3498
3535
|
}
|
|
3536
|
+
if (completedCount > 0) {
|
|
3537
|
+
kanbanEmitter.emit("work:complete", {
|
|
3538
|
+
total: completedCount,
|
|
3539
|
+
duration: Date.now() - loopStart
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3499
3542
|
resetTitle();
|
|
3500
3543
|
ok(`lisa finished. ${session} session(s) run.`);
|
|
3501
3544
|
}
|
|
@@ -4079,7 +4122,7 @@ Add them to your ${shell} and run: source ${shell}`));
|
|
|
4079
4122
|
if (isTUI) {
|
|
4080
4123
|
const { render } = await import("ink");
|
|
4081
4124
|
const { createElement } = await import("react");
|
|
4082
|
-
const { KanbanApp } = await import("./kanban-
|
|
4125
|
+
const { KanbanApp } = await import("./kanban-BY7QNVEE.js");
|
|
4083
4126
|
render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
|
|
4084
4127
|
}
|
|
4085
4128
|
await runLoop(merged, {
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
kanbanEmitter,
|
|
4
|
+
useKanbanState
|
|
5
|
+
} from "./chunk-MUBWKMRZ.js";
|
|
6
|
+
|
|
7
|
+
// src/ui/kanban.tsx
|
|
8
|
+
import { Box as Box6, useApp, useInput as useInput2 } from "ink";
|
|
9
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
10
|
+
|
|
11
|
+
// src/ui/board.tsx
|
|
12
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
13
|
+
|
|
14
|
+
// src/ui/column.tsx
|
|
15
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
16
|
+
|
|
17
|
+
// src/ui/card.tsx
|
|
18
|
+
import { Box, Text } from "ink";
|
|
19
|
+
import Spinner from "ink-spinner";
|
|
20
|
+
import { useEffect, useState } from "react";
|
|
21
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
|
+
function formatElapsed(ms) {
|
|
23
|
+
const seconds = Math.floor(ms / 1e3);
|
|
24
|
+
const minutes = Math.floor(seconds / 60);
|
|
25
|
+
const remainingSeconds = seconds % 60;
|
|
26
|
+
if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
|
|
27
|
+
return `${seconds}s`;
|
|
28
|
+
}
|
|
29
|
+
function Card({ card, isSelected = false }) {
|
|
30
|
+
const [now, setNow] = useState(Date.now());
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (card.column !== "in_progress") return;
|
|
33
|
+
const interval = setInterval(() => setNow(Date.now()), 1e3);
|
|
34
|
+
return () => clearInterval(interval);
|
|
35
|
+
}, [card.column]);
|
|
36
|
+
let statusGlyph;
|
|
37
|
+
let statusColor;
|
|
38
|
+
if (card.hasError) {
|
|
39
|
+
statusGlyph = "\u2716";
|
|
40
|
+
statusColor = "red";
|
|
41
|
+
} else if (card.column === "in_progress") {
|
|
42
|
+
statusGlyph = "\u25C9";
|
|
43
|
+
statusColor = "yellow";
|
|
44
|
+
} else if (card.column === "done") {
|
|
45
|
+
statusGlyph = "\u2714";
|
|
46
|
+
statusColor = "green";
|
|
47
|
+
} else {
|
|
48
|
+
statusGlyph = "\u25CB";
|
|
49
|
+
statusColor = "white";
|
|
50
|
+
}
|
|
51
|
+
const selectionBar = isSelected ? "\u2590" : " ";
|
|
52
|
+
const selectionColor = isSelected ? "yellow" : "white";
|
|
53
|
+
const truncated = card.title.length > 30 ? `${card.title.slice(0, 27)}\u2026` : card.title;
|
|
54
|
+
return /* @__PURE__ */ jsxs(
|
|
55
|
+
Box,
|
|
56
|
+
{
|
|
57
|
+
flexDirection: "row",
|
|
58
|
+
paddingX: 0,
|
|
59
|
+
marginBottom: 0,
|
|
60
|
+
borderStyle: "single",
|
|
61
|
+
borderColor: card.hasError ? "red" : isSelected ? "yellow" : "gray",
|
|
62
|
+
children: [
|
|
63
|
+
/* @__PURE__ */ jsx(Text, { color: selectionColor, children: selectionBar }),
|
|
64
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
|
|
65
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [
|
|
66
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", bold: isSelected, children: card.id }),
|
|
67
|
+
/* @__PURE__ */ jsx(Text, { color: statusColor, children: statusGlyph })
|
|
68
|
+
] }),
|
|
69
|
+
/* @__PURE__ */ jsx(Text, { bold: isSelected, dimColor: !isSelected, children: truncated }),
|
|
70
|
+
card.column === "in_progress" && card.startedAt !== void 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", marginTop: 0, children: [
|
|
71
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
|
|
72
|
+
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
73
|
+
" ",
|
|
74
|
+
formatElapsed(now - card.startedAt)
|
|
75
|
+
] })
|
|
76
|
+
] }),
|
|
77
|
+
card.column === "done" && card.startedAt !== void 0 && card.finishedAt !== void 0 && /* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
78
|
+
"\u2714 ",
|
|
79
|
+
formatElapsed(card.finishedAt - card.startedAt)
|
|
80
|
+
] }),
|
|
81
|
+
card.hasError && /* @__PURE__ */ jsx(Text, { color: "red", children: "FAILED" })
|
|
82
|
+
] })
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/ui/column.tsx
|
|
89
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
90
|
+
var CARD_HEIGHT = 5;
|
|
91
|
+
var HEADER_ROWS = 4;
|
|
92
|
+
function Column({ label, cards, isFocused = false, activeCardIndex = 0 }) {
|
|
93
|
+
const terminalRows = process.stdout.rows ?? 24;
|
|
94
|
+
const visibleCount = Math.max(1, Math.floor((terminalRows - HEADER_ROWS) / CARD_HEIGHT));
|
|
95
|
+
let scrollOffset = 0;
|
|
96
|
+
if (activeCardIndex >= visibleCount) {
|
|
97
|
+
scrollOffset = activeCardIndex - visibleCount + 1;
|
|
98
|
+
}
|
|
99
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, cards.length - visibleCount)));
|
|
100
|
+
const visibleCards = cards.slice(scrollOffset, scrollOffset + visibleCount);
|
|
101
|
+
const hiddenAbove = scrollOffset;
|
|
102
|
+
const hiddenBelow = Math.max(0, cards.length - scrollOffset - visibleCount);
|
|
103
|
+
const borderColor = isFocused ? "yellow" : "gray";
|
|
104
|
+
const headerColor = isFocused ? "yellow" : "white";
|
|
105
|
+
const runningCount = cards.filter((c) => c.column === "in_progress").length;
|
|
106
|
+
const errorCount = cards.filter((c) => c.hasError).length;
|
|
107
|
+
return /* @__PURE__ */ jsxs2(
|
|
108
|
+
Box2,
|
|
109
|
+
{
|
|
110
|
+
flexDirection: "column",
|
|
111
|
+
flexGrow: 1,
|
|
112
|
+
borderStyle: "single",
|
|
113
|
+
borderColor,
|
|
114
|
+
paddingX: 1,
|
|
115
|
+
paddingY: 0,
|
|
116
|
+
children: [
|
|
117
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [
|
|
118
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
|
|
119
|
+
isFocused && /* @__PURE__ */ jsx2(Text2, { color: "yellow", bold: true, children: "\u25B6 " }),
|
|
120
|
+
!isFocused && /* @__PURE__ */ jsx2(Text2, { color: "gray", children: " " }),
|
|
121
|
+
/* @__PURE__ */ jsx2(Text2, { color: headerColor, bold: true, children: label.toUpperCase() })
|
|
122
|
+
] }),
|
|
123
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
|
|
124
|
+
errorCount > 0 && /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: `!${errorCount} ` }),
|
|
125
|
+
runningCount > 0 && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: `~${runningCount} ` }),
|
|
126
|
+
/* @__PURE__ */ jsx2(Text2, { color: headerColor, children: `[${cards.length}]` })
|
|
127
|
+
] })
|
|
128
|
+
] }),
|
|
129
|
+
hiddenAbove > 0 && /* @__PURE__ */ jsx2(Box2, { justifyContent: "center", children: /* @__PURE__ */ jsx2(Text2, { color: "yellow", dimColor: true, children: `\u2191 ${hiddenAbove} more` }) }),
|
|
130
|
+
visibleCards.map((card, idx) => {
|
|
131
|
+
const absoluteIdx = scrollOffset + idx;
|
|
132
|
+
const isSelected = isFocused && absoluteIdx === activeCardIndex;
|
|
133
|
+
return /* @__PURE__ */ jsx2(Card, { card, isSelected }, card.id);
|
|
134
|
+
}),
|
|
135
|
+
cards.length === 0 && /* @__PURE__ */ jsx2(Box2, { justifyContent: "center", paddingY: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: "\u2014 empty \u2014" }) }),
|
|
136
|
+
hiddenBelow > 0 && /* @__PURE__ */ jsx2(Box2, { justifyContent: "center", children: /* @__PURE__ */ jsx2(Text2, { color: "yellow", dimColor: true, children: `\u2193 ${hiddenBelow} more` }) })
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/ui/board.tsx
|
|
143
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
144
|
+
function formatDuration(ms) {
|
|
145
|
+
const totalSeconds = Math.floor(ms / 1e3);
|
|
146
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
147
|
+
const seconds = totalSeconds % 60;
|
|
148
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
149
|
+
return `${seconds}s`;
|
|
150
|
+
}
|
|
151
|
+
function Board({
|
|
152
|
+
cards,
|
|
153
|
+
labels,
|
|
154
|
+
isEmpty,
|
|
155
|
+
workComplete,
|
|
156
|
+
activeColIndex = 0,
|
|
157
|
+
activeCardIndex = 0
|
|
158
|
+
}) {
|
|
159
|
+
const backlog = cards.filter((c) => c.column === "backlog");
|
|
160
|
+
const inProgress = cards.filter((c) => c.column === "in_progress");
|
|
161
|
+
const done = cards.filter((c) => c.column === "done");
|
|
162
|
+
if (isEmpty) {
|
|
163
|
+
return /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs3(
|
|
164
|
+
Box3,
|
|
165
|
+
{
|
|
166
|
+
flexDirection: "column",
|
|
167
|
+
borderStyle: "single",
|
|
168
|
+
borderColor: "yellow",
|
|
169
|
+
paddingX: 3,
|
|
170
|
+
paddingY: 1,
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", bold: true, children: "\u25C8 NO ISSUES FOUND" }),
|
|
173
|
+
/* @__PURE__ */ jsx3(Box3, { height: 1 }),
|
|
174
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", dimColor: true, children: "No issues match the current label and status filters." }),
|
|
175
|
+
/* @__PURE__ */ jsx3(Box3, { height: 1 }),
|
|
176
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Check your source configuration and labels." })
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
) });
|
|
180
|
+
}
|
|
181
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexGrow: 1, flexDirection: "column", children: [
|
|
182
|
+
workComplete && /* @__PURE__ */ jsxs3(Box3, { borderStyle: "single", borderColor: "green", paddingX: 2, paddingY: 0, marginBottom: 0, children: [
|
|
183
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "\u2714 ALL SESSIONS COMPLETE" }),
|
|
184
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", children: " \u2014 " }),
|
|
185
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", bold: true, children: workComplete.total }),
|
|
186
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", children: ` issue${workComplete.total !== 1 ? "s" : ""} resolved in ` }),
|
|
187
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: formatDuration(workComplete.duration) })
|
|
188
|
+
] }),
|
|
189
|
+
/* @__PURE__ */ jsxs3(Box3, { flexGrow: 1, flexDirection: "row", children: [
|
|
190
|
+
/* @__PURE__ */ jsx3(
|
|
191
|
+
Column,
|
|
192
|
+
{
|
|
193
|
+
label: labels.backlog,
|
|
194
|
+
cards: backlog,
|
|
195
|
+
isFocused: activeColIndex === 0,
|
|
196
|
+
activeCardIndex: activeColIndex === 0 ? activeCardIndex : 0
|
|
197
|
+
}
|
|
198
|
+
),
|
|
199
|
+
/* @__PURE__ */ jsx3(
|
|
200
|
+
Column,
|
|
201
|
+
{
|
|
202
|
+
label: labels.inProgress,
|
|
203
|
+
cards: inProgress,
|
|
204
|
+
isFocused: activeColIndex === 1,
|
|
205
|
+
activeCardIndex: activeColIndex === 1 ? activeCardIndex : 0
|
|
206
|
+
}
|
|
207
|
+
),
|
|
208
|
+
/* @__PURE__ */ jsx3(
|
|
209
|
+
Column,
|
|
210
|
+
{
|
|
211
|
+
label: labels.done,
|
|
212
|
+
cards: done,
|
|
213
|
+
isFocused: activeColIndex === 2,
|
|
214
|
+
activeCardIndex: activeColIndex === 2 ? activeCardIndex : 0
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
] })
|
|
218
|
+
] });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/ui/detail.tsx
|
|
222
|
+
import { Box as Box4, Text as Text4, useInput } from "ink";
|
|
223
|
+
import Spinner2 from "ink-spinner";
|
|
224
|
+
import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
225
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
226
|
+
function formatElapsed2(ms) {
|
|
227
|
+
const seconds = Math.floor(ms / 1e3);
|
|
228
|
+
const minutes = Math.floor(seconds / 60);
|
|
229
|
+
const remainingSeconds = seconds % 60;
|
|
230
|
+
if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
|
|
231
|
+
return `${seconds}s`;
|
|
232
|
+
}
|
|
233
|
+
function statusLabel(column, hasError) {
|
|
234
|
+
if (hasError) return { text: "FAILED", color: "red" };
|
|
235
|
+
if (column === "in_progress") return { text: "IN PROGRESS", color: "yellow" };
|
|
236
|
+
if (column === "done") return { text: "DONE", color: "green" };
|
|
237
|
+
return { text: "QUEUED", color: "white" };
|
|
238
|
+
}
|
|
239
|
+
function IssueDetail({ card, onBack }) {
|
|
240
|
+
const [now, setNow] = useState2(Date.now());
|
|
241
|
+
const [logScrollOffset, setLogScrollOffset] = useState2(0);
|
|
242
|
+
const [userScrolled, setUserScrolled] = useState2(false);
|
|
243
|
+
const prevOutputLen = useRef(card.outputLog.length);
|
|
244
|
+
useEffect2(() => {
|
|
245
|
+
if (card.column !== "in_progress") return;
|
|
246
|
+
const interval = setInterval(() => setNow(Date.now()), 1e3);
|
|
247
|
+
return () => clearInterval(interval);
|
|
248
|
+
}, [card.column]);
|
|
249
|
+
useEffect2(() => {
|
|
250
|
+
if (!userScrolled && card.outputLog.length !== prevOutputLen.current) {
|
|
251
|
+
prevOutputLen.current = card.outputLog.length;
|
|
252
|
+
setLogScrollOffset(0);
|
|
253
|
+
}
|
|
254
|
+
}, [card.outputLog, userScrolled]);
|
|
255
|
+
useInput((_input, key) => {
|
|
256
|
+
if (key.escape) {
|
|
257
|
+
onBack();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (key.upArrow) {
|
|
261
|
+
setUserScrolled(true);
|
|
262
|
+
setLogScrollOffset((prev) => prev + 1);
|
|
263
|
+
}
|
|
264
|
+
if (key.downArrow) {
|
|
265
|
+
setLogScrollOffset((prev) => {
|
|
266
|
+
const next = Math.max(0, prev - 1);
|
|
267
|
+
if (next === 0) setUserScrolled(false);
|
|
268
|
+
return next;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
const terminalCols = process.stdout.columns ?? 80;
|
|
273
|
+
const terminalRows = process.stdout.rows ?? 24;
|
|
274
|
+
const bodyRows = Math.max(1, terminalRows - 10);
|
|
275
|
+
const lines = card.outputLog.split("\n");
|
|
276
|
+
const startLine = Math.max(0, lines.length - bodyRows - logScrollOffset);
|
|
277
|
+
const visibleLines = lines.slice(startLine, startLine + bodyRows);
|
|
278
|
+
const status = statusLabel(card.column, card.hasError);
|
|
279
|
+
let elapsedDisplay = null;
|
|
280
|
+
let isRunning = false;
|
|
281
|
+
if (card.column === "in_progress" && card.startedAt !== void 0) {
|
|
282
|
+
elapsedDisplay = formatElapsed2(now - card.startedAt);
|
|
283
|
+
isRunning = true;
|
|
284
|
+
} else if (card.column === "done" && card.startedAt !== void 0 && card.finishedAt !== void 0) {
|
|
285
|
+
elapsedDisplay = formatElapsed2(card.finishedAt - card.startedAt);
|
|
286
|
+
}
|
|
287
|
+
const SIDEBAR_TOTAL_WIDTH = 28;
|
|
288
|
+
const separatorInner = Math.max(0, terminalCols - SIDEBAR_TOTAL_WIDTH - 4);
|
|
289
|
+
const separator = `\u2560${"\u2550".repeat(Math.max(0, separatorInner - 2))}\u2563`;
|
|
290
|
+
const totalLines = lines.length;
|
|
291
|
+
const scrollPct = totalLines <= bodyRows ? "100%" : `${Math.round((startLine + bodyRows) / totalLines * 100)}%`;
|
|
292
|
+
return /* @__PURE__ */ jsxs4(
|
|
293
|
+
Box4,
|
|
294
|
+
{
|
|
295
|
+
flexGrow: 1,
|
|
296
|
+
flexDirection: "column",
|
|
297
|
+
borderStyle: "single",
|
|
298
|
+
borderColor: "yellow",
|
|
299
|
+
paddingX: 1,
|
|
300
|
+
paddingY: 0,
|
|
301
|
+
children: [
|
|
302
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", justifyContent: "space-between", marginTop: 0, children: [
|
|
303
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
|
|
304
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", bold: true, children: card.id }),
|
|
305
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", children: " \u2502 " }),
|
|
306
|
+
/* @__PURE__ */ jsx4(Text4, { color: status.color, bold: true, children: status.text })
|
|
307
|
+
] }),
|
|
308
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", children: [
|
|
309
|
+
isRunning && elapsedDisplay && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", marginRight: 2, children: [
|
|
310
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
|
|
311
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", bold: true, children: ` ${elapsedDisplay}` })
|
|
312
|
+
] }),
|
|
313
|
+
!isRunning && elapsedDisplay && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", marginRight: 2, children: [
|
|
314
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2714 " }),
|
|
315
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", bold: true, children: elapsedDisplay })
|
|
316
|
+
] })
|
|
317
|
+
] })
|
|
318
|
+
] }),
|
|
319
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 0, children: /* @__PURE__ */ jsx4(Text4, { color: "white", bold: true, children: card.title }) }),
|
|
320
|
+
card.prUrl !== void 0 && card.prUrl.length > 0 && /* @__PURE__ */ jsxs4(Box4, { marginTop: 0, children: [
|
|
321
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: "PR: " }),
|
|
322
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", children: card.prUrl })
|
|
323
|
+
] }),
|
|
324
|
+
/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: separator }) }),
|
|
325
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", justifyContent: "space-between", children: [
|
|
326
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "PROVIDER OUTPUT" }),
|
|
327
|
+
userScrolled && /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: `scroll ${scrollPct}` }),
|
|
328
|
+
!userScrolled && totalLines > bodyRows && /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "auto-scroll" })
|
|
329
|
+
] }),
|
|
330
|
+
/* @__PURE__ */ jsx4(Box4, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: card.outputLog.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "row", marginTop: 1, children: [
|
|
331
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
|
|
332
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: " Waiting for provider output..." })
|
|
333
|
+
] }) : visibleLines.map((line, i) => (
|
|
334
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: log lines have no stable key
|
|
335
|
+
/* @__PURE__ */ jsx4(Text4, { color: "white", dimColor: true, children: line }, i)
|
|
336
|
+
)) }),
|
|
337
|
+
/* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
|
|
338
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "[\u2191\u2193] scroll" }),
|
|
339
|
+
/* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: "[Esc] back to board" })
|
|
340
|
+
] })
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/ui/sidebar.tsx
|
|
347
|
+
import { basename } from "path";
|
|
348
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
349
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
350
|
+
function Sidebar({ provider, source, cwd }) {
|
|
351
|
+
const dir = basename(cwd);
|
|
352
|
+
return /* @__PURE__ */ jsxs5(
|
|
353
|
+
Box5,
|
|
354
|
+
{
|
|
355
|
+
flexDirection: "column",
|
|
356
|
+
width: 28,
|
|
357
|
+
borderStyle: "single",
|
|
358
|
+
borderColor: "yellow",
|
|
359
|
+
paddingX: 1,
|
|
360
|
+
paddingY: 0,
|
|
361
|
+
children: [
|
|
362
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
363
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557" }),
|
|
364
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
|
|
365
|
+
"\u2551",
|
|
366
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", bold: true, children: " L I S A v 2 " }),
|
|
367
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u2551" })
|
|
368
|
+
] }),
|
|
369
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D" })
|
|
370
|
+
] }),
|
|
371
|
+
/* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, children: [
|
|
372
|
+
/* @__PURE__ */ jsx5(Text5, { color: "green", children: "\u25B6 " }),
|
|
373
|
+
/* @__PURE__ */ jsx5(Text5, { color: "green", bold: true, children: "RUNNING" })
|
|
374
|
+
] }),
|
|
375
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
|
|
376
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
|
|
377
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", dimColor: true, children: "PROVIDER" }),
|
|
378
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
379
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u25B8 " }),
|
|
380
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", bold: true, children: provider.toUpperCase() })
|
|
381
|
+
] })
|
|
382
|
+
] }),
|
|
383
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
|
|
384
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", dimColor: true, children: "SOURCE" }),
|
|
385
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
386
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u25B8 " }),
|
|
387
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", bold: true, children: source.toUpperCase() })
|
|
388
|
+
] })
|
|
389
|
+
] }),
|
|
390
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
|
|
391
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", dimColor: true, children: "WORKSPACE" }),
|
|
392
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
393
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u25B8 " }),
|
|
394
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", bold: true, children: dir.length > 18 ? `${dir.slice(0, 15)}\u2026` : dir })
|
|
395
|
+
] })
|
|
396
|
+
] }),
|
|
397
|
+
/* @__PURE__ */ jsx5(Box5, { flexGrow: 1 }),
|
|
398
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
|
|
399
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
400
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[Tab] next column" }),
|
|
401
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[\u2191\u2193] navigate " }),
|
|
402
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[\u21B5] view detail " }),
|
|
403
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "[q] quit " })
|
|
404
|
+
] })
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/ui/kanban.tsx
|
|
411
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
412
|
+
function KanbanApp({ config }) {
|
|
413
|
+
const { exit } = useApp();
|
|
414
|
+
const { cards, isEmpty, workComplete } = useKanbanState();
|
|
415
|
+
const [activeView, setActiveView] = useState3("board");
|
|
416
|
+
const [activeColIndex, setActiveColIndex] = useState3(0);
|
|
417
|
+
const [activeCardIndex, setActiveCardIndex] = useState3(0);
|
|
418
|
+
useEffect3(() => {
|
|
419
|
+
const onExit = () => exit();
|
|
420
|
+
kanbanEmitter.on("tui:exit", onExit);
|
|
421
|
+
return () => {
|
|
422
|
+
kanbanEmitter.off("tui:exit", onExit);
|
|
423
|
+
};
|
|
424
|
+
}, [exit]);
|
|
425
|
+
const backlog = cards.filter((c) => c.column === "backlog");
|
|
426
|
+
const inProgress = cards.filter((c) => c.column === "in_progress");
|
|
427
|
+
const done = cards.filter((c) => c.column === "done");
|
|
428
|
+
const columnCards = [backlog, inProgress, done];
|
|
429
|
+
useInput2((input, key) => {
|
|
430
|
+
if (input === "q") {
|
|
431
|
+
process.emit("SIGINT");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (activeView === "detail") {
|
|
435
|
+
if (key.escape) setActiveView("board");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (key.tab && !key.shift) {
|
|
439
|
+
const nextCol = (activeColIndex + 1) % 3;
|
|
440
|
+
setActiveColIndex(nextCol);
|
|
441
|
+
const colLen = columnCards[nextCol]?.length ?? 0;
|
|
442
|
+
setActiveCardIndex(Math.min(activeCardIndex, Math.max(0, colLen - 1)));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (key.tab && key.shift) {
|
|
446
|
+
const prevCol = (activeColIndex + 2) % 3;
|
|
447
|
+
setActiveColIndex(prevCol);
|
|
448
|
+
const colLen = columnCards[prevCol]?.length ?? 0;
|
|
449
|
+
setActiveCardIndex(Math.min(activeCardIndex, Math.max(0, colLen - 1)));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (key.downArrow) {
|
|
453
|
+
const colLen = columnCards[activeColIndex]?.length ?? 0;
|
|
454
|
+
setActiveCardIndex((prev) => Math.min(prev + 1, Math.max(0, colLen - 1)));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (key.upArrow) {
|
|
458
|
+
setActiveCardIndex((prev) => Math.max(0, prev - 1));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (key.return) {
|
|
462
|
+
const colLen = columnCards[activeColIndex]?.length ?? 0;
|
|
463
|
+
if (colLen > 0) setActiveView("detail");
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
const labels = {
|
|
467
|
+
backlog: config.source_config.pick_from,
|
|
468
|
+
inProgress: config.source_config.in_progress,
|
|
469
|
+
done: config.source_config.done
|
|
470
|
+
};
|
|
471
|
+
const selectedCard = activeView === "detail" ? columnCards[activeColIndex]?.[activeCardIndex] ?? null : null;
|
|
472
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", height: process.stdout.rows, children: [
|
|
473
|
+
/* @__PURE__ */ jsx6(Sidebar, { provider: config.provider, source: config.source, cwd: process.cwd() }),
|
|
474
|
+
activeView === "board" || !selectedCard ? /* @__PURE__ */ jsx6(
|
|
475
|
+
Board,
|
|
476
|
+
{
|
|
477
|
+
cards,
|
|
478
|
+
labels,
|
|
479
|
+
isEmpty,
|
|
480
|
+
workComplete,
|
|
481
|
+
activeColIndex,
|
|
482
|
+
activeCardIndex
|
|
483
|
+
}
|
|
484
|
+
) : /* @__PURE__ */ jsx6(IssueDetail, { card: selectedCard, onBack: () => setActiveView("board") })
|
|
485
|
+
] });
|
|
486
|
+
}
|
|
487
|
+
export {
|
|
488
|
+
KanbanApp
|
|
489
|
+
};
|
package/package.json
CHANGED
|
@@ -1,60 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
2
|
+
"name": "@tarcisiopgs/lisa",
|
|
3
|
+
"version": "1.7.1",
|
|
4
|
+
"description": "Deterministic autonomous issue resolver — structured AI agent loop for Linear/Trello",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"lisa": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"check": "biome check src/",
|
|
14
|
+
"format": "biome format --write src/",
|
|
15
|
+
"lint": "biome lint src/",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"test:coverage": "vitest run --coverage",
|
|
20
|
+
"ci": "concurrently -n lint,typecheck,test \"pnpm lint\" \"pnpm typecheck\" \"pnpm test\"",
|
|
21
|
+
"prepare": "husky"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@clack/prompts": "^1.0.1",
|
|
25
|
+
"citty": "^0.2.1",
|
|
26
|
+
"execa": "^9.6.1",
|
|
27
|
+
"ink": "^6.8.0",
|
|
28
|
+
"ink-spinner": "^5.0.0",
|
|
29
|
+
"picocolors": "^1.1.1",
|
|
30
|
+
"react": "^19.2.4",
|
|
31
|
+
"yaml": "^2.8.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "^2.4.3",
|
|
35
|
+
"@types/node": "^22.13.4",
|
|
36
|
+
"@types/react": "^19.2.14",
|
|
37
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
38
|
+
"concurrently": "^9.2.1",
|
|
39
|
+
"husky": "^9.1.7",
|
|
40
|
+
"lint-staged": "^16.2.7",
|
|
41
|
+
"tsup": "^8.4.0",
|
|
42
|
+
"tsx": "^4.19.3",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
45
|
+
},
|
|
46
|
+
"packageManager": "pnpm@10.29.3",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/tarcisiopgs/lisa.git"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist"
|
|
56
|
+
],
|
|
57
|
+
"lint-staged": {
|
|
58
|
+
"*.{ts,tsx}": [
|
|
59
|
+
"biome check --write"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/kanban-KIPKQ2IL.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
useKanbanState
|
|
4
|
-
} from "./chunk-KAME5MG7.js";
|
|
5
|
-
|
|
6
|
-
// src/ui/kanban.tsx
|
|
7
|
-
import { Box as Box5, useApp, useInput } from "ink";
|
|
8
|
-
|
|
9
|
-
// src/ui/board.tsx
|
|
10
|
-
import { Box as Box3 } from "ink";
|
|
11
|
-
|
|
12
|
-
// src/ui/column.tsx
|
|
13
|
-
import { Box as Box2, Text as Text2 } from "ink";
|
|
14
|
-
|
|
15
|
-
// src/ui/card.tsx
|
|
16
|
-
import { Box, Text } from "ink";
|
|
17
|
-
import Spinner from "ink-spinner";
|
|
18
|
-
import { useEffect, useState } from "react";
|
|
19
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
20
|
-
function formatElapsed(ms) {
|
|
21
|
-
const seconds = Math.floor(ms / 1e3);
|
|
22
|
-
const minutes = Math.floor(seconds / 60);
|
|
23
|
-
const remainingSeconds = seconds % 60;
|
|
24
|
-
if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
|
|
25
|
-
return `${seconds}s`;
|
|
26
|
-
}
|
|
27
|
-
function Card({ card }) {
|
|
28
|
-
const [now, setNow] = useState(Date.now());
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (card.column !== "in_progress") return;
|
|
31
|
-
const interval = setInterval(() => setNow(Date.now()), 1e3);
|
|
32
|
-
return () => clearInterval(interval);
|
|
33
|
-
}, [card.column]);
|
|
34
|
-
const truncated = card.title.length > 22 ? `${card.title.slice(0, 19)}...` : card.title;
|
|
35
|
-
const borderColor = card.hasError ? "red" : card.column === "done" ? "green" : "white";
|
|
36
|
-
return /* @__PURE__ */ jsxs(
|
|
37
|
-
Box,
|
|
38
|
-
{
|
|
39
|
-
borderStyle: "round",
|
|
40
|
-
borderColor,
|
|
41
|
-
flexDirection: "column",
|
|
42
|
-
paddingX: 1,
|
|
43
|
-
marginBottom: 1,
|
|
44
|
-
children: [
|
|
45
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: card.id }),
|
|
46
|
-
/* @__PURE__ */ jsx(Text, { children: truncated }),
|
|
47
|
-
card.column === "in_progress" && card.startedAt !== void 0 && /* @__PURE__ */ jsxs(Box, { children: [
|
|
48
|
-
/* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
|
|
49
|
-
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
50
|
-
" ",
|
|
51
|
-
formatElapsed(now - card.startedAt)
|
|
52
|
-
] })
|
|
53
|
-
] })
|
|
54
|
-
]
|
|
55
|
-
}
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/ui/column.tsx
|
|
60
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
61
|
-
function Column({ label, cards }) {
|
|
62
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, borderStyle: "single", paddingX: 1, children: [
|
|
63
|
-
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
|
|
64
|
-
label,
|
|
65
|
-
" (",
|
|
66
|
-
cards.length,
|
|
67
|
-
")"
|
|
68
|
-
] }),
|
|
69
|
-
/* @__PURE__ */ jsx2(Box2, { height: 1 }),
|
|
70
|
-
cards.map((card) => /* @__PURE__ */ jsx2(Card, { card }, card.id))
|
|
71
|
-
] });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// src/ui/board.tsx
|
|
75
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
76
|
-
function Board({ cards, labels }) {
|
|
77
|
-
const backlog = cards.filter((c) => c.column === "backlog");
|
|
78
|
-
const inProgress = cards.filter((c) => c.column === "in_progress");
|
|
79
|
-
const done = cards.filter((c) => c.column === "done");
|
|
80
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexGrow: 1, flexDirection: "row", children: [
|
|
81
|
-
/* @__PURE__ */ jsx3(Column, { label: labels.backlog, cards: backlog }),
|
|
82
|
-
/* @__PURE__ */ jsx3(Column, { label: labels.inProgress, cards: inProgress }),
|
|
83
|
-
/* @__PURE__ */ jsx3(Column, { label: labels.done, cards: done })
|
|
84
|
-
] });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// src/ui/sidebar.tsx
|
|
88
|
-
import { Box as Box4, Text as Text3 } from "ink";
|
|
89
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
90
|
-
function Sidebar({ provider, source }) {
|
|
91
|
-
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width: 18, borderStyle: "single", paddingX: 1, children: [
|
|
92
|
-
/* @__PURE__ */ jsx4(Text3, { bold: true, color: "yellow", children: "\u25C6 LISA" }),
|
|
93
|
-
/* @__PURE__ */ jsx4(Box4, { height: 1 }),
|
|
94
|
-
/* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "Provider" }),
|
|
95
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: provider }),
|
|
96
|
-
/* @__PURE__ */ jsx4(Box4, { height: 1 }),
|
|
97
|
-
/* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "Source" }),
|
|
98
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", children: source }),
|
|
99
|
-
/* @__PURE__ */ jsx4(Box4, { flexGrow: 1 }),
|
|
100
|
-
/* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "[q] Quit" })
|
|
101
|
-
] });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// src/ui/kanban.tsx
|
|
105
|
-
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
106
|
-
function KanbanApp({ config }) {
|
|
107
|
-
const { exit } = useApp();
|
|
108
|
-
const { cards } = useKanbanState();
|
|
109
|
-
useInput((input) => {
|
|
110
|
-
if (input === "q") {
|
|
111
|
-
process.emit("SIGINT");
|
|
112
|
-
exit();
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
const labels = {
|
|
116
|
-
backlog: config.source_config.pick_from,
|
|
117
|
-
inProgress: config.source_config.in_progress,
|
|
118
|
-
done: config.source_config.done
|
|
119
|
-
};
|
|
120
|
-
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", height: process.stdout.rows, children: [
|
|
121
|
-
/* @__PURE__ */ jsx5(Sidebar, { provider: config.provider, source: config.source }),
|
|
122
|
-
/* @__PURE__ */ jsx5(Board, { cards, labels })
|
|
123
|
-
] });
|
|
124
|
-
}
|
|
125
|
-
export {
|
|
126
|
-
KanbanApp
|
|
127
|
-
};
|