kankaku 1.0.2 → 1.2.0
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/README.md +120 -24
- package/dist/adapters/hub-manager/credentials.js +58 -0
- package/dist/adapters/hub-manager/install.js +93 -14
- package/dist/adapters/hub-manager/port-probe.js +38 -0
- package/dist/adapters/hub-reassign.js +115 -0
- package/dist/adapters/package-versions.js +24 -0
- package/dist/adapters/setup/hub.js +1 -1
- package/dist/adapters/setup/json-writer.js +8 -3
- package/dist/cli.js +100 -20
- package/dist/domain/local-hub-model.js +40 -0
- package/dist/domain/nav-model.js +8 -0
- package/dist/domain/reassign-model.js +101 -0
- package/dist/domain/reassign-picker.js +258 -0
- package/dist/domain/setup-wizard.js +23 -4
- package/dist/domain/version-info.js +5 -0
- package/dist/ports/reassign-actions.js +1 -0
- package/dist/ui/app.js +9 -4
- package/dist/ui/components/table.js +2 -2
- package/dist/ui/layout.js +3 -2
- package/dist/ui/reassign-panel.js +36 -0
- package/dist/ui/setup/wizard-screen.js +31 -4
- package/dist/ui/tasks-screen.js +167 -10
- package/package.json +3 -3
- package/vendor/kankaku-pi/package.json +3 -3
package/dist/ui/tasks-screen.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import { formatMinutes } from "../domain/today-model.js";
|
|
5
5
|
import { SCREENS, hintsFor } from "../domain/nav-model.js";
|
|
6
6
|
import { wrapText } from "../domain/text-wrap.js";
|
|
7
|
+
import { NOT_ON_HUB, describeAssignment, eligibleForBulk } from "../domain/reassign-model.js";
|
|
8
|
+
import { advance, back, finishApplying, jumpSelection, moveSelection, openPicker, pickerHints, pickerRowCount } from "../domain/reassign-picker.js";
|
|
9
|
+
import { ReassignPanel, pickerMaxRows } from "./reassign-panel.js";
|
|
7
10
|
import { Layout } from "./layout.js";
|
|
8
11
|
import { Panel } from "./components/panel.js";
|
|
9
12
|
import { Table } from "./components/table.js";
|
|
@@ -63,7 +66,7 @@ const DETAIL_CHROME_ROWS = 2;
|
|
|
63
66
|
* `muted` `… N more lines` line instead of letting Ink's own overflow
|
|
64
67
|
* clipping silently swallow the rest with no visible cue.
|
|
65
68
|
*/
|
|
66
|
-
function DetailPanel({ row, width, height }) {
|
|
69
|
+
function DetailPanel({ row, hub, width, height }) {
|
|
67
70
|
const theme = useTheme();
|
|
68
71
|
if (row === undefined) {
|
|
69
72
|
return (_jsx(Panel, { title: "Task", width: width, height: height, children: _jsx(Text, { dimColor: true, children: "no task selected" }) }));
|
|
@@ -77,6 +80,8 @@ function DetailPanel({ row, width, height }) {
|
|
|
77
80
|
metaLines.push({ text: `project ${row.projectName}`, dim: true });
|
|
78
81
|
if (row.hubTaskTitle !== undefined)
|
|
79
82
|
metaLines.push({ text: `task ${row.hubTaskTitle}`, dim: true });
|
|
83
|
+
if (hub !== undefined)
|
|
84
|
+
metaLines.push({ text: `hub ${hub}`, dim: true });
|
|
80
85
|
metaLines.push({ text: `wall ${formatMinutes(row.wallMs)} work ${formatMinutes(row.workMs)} wait ${formatMinutes(row.waitingMs)}`, dim: false });
|
|
81
86
|
metaLines.push({ text: `cost ${formatCost(row.cost)}${row.cacheHit !== undefined ? ` cache hit ${Math.round(row.cacheHit * 100)}%` : ""}`, dim: false });
|
|
82
87
|
metaLines.push({ text: `subagents ${row.subagentCount}`, dim: false });
|
|
@@ -86,38 +91,186 @@ function DetailPanel({ row, width, height }) {
|
|
|
86
91
|
const visiblePromptLines = truncated ? wrappedPrompt.slice(0, Math.max(promptBudget - 1, 0)) : wrappedPrompt.slice(0, promptBudget);
|
|
87
92
|
const hiddenCount = wrappedPrompt.length - visiblePromptLines.length;
|
|
88
93
|
const showIndicator = truncated && promptBudget > 0;
|
|
89
|
-
return (_jsx(Panel, { title: "Task", width: width, height: height, children: _jsxs(Box, { flexDirection: "column", children: [visiblePromptLines.map((line, index) => (_jsx(Text, { children: line }, `prompt-${index}`))), showIndicator && _jsx(Text, { color: theme.muted, children: `… ${hiddenCount} more lines` }), metaLines.map((line, index) => (_jsx(Text, { dimColor: line.dim, children: line.text }, `meta-${index}`)))] }) }));
|
|
94
|
+
return (_jsx(Panel, { title: "Task", width: width, height: height, children: _jsxs(Box, { flexDirection: "column", children: [visiblePromptLines.map((line, index) => (_jsx(Text, { children: line }, `prompt-${index}`))), showIndicator && _jsx(Text, { color: theme.muted, children: `… ${hiddenCount} more lines` }), metaLines.map((line, index) => (_jsx(Text, { dimColor: line.dim, wrap: "truncate-end", children: line.text }, `meta-${index}`)))] }) }));
|
|
90
95
|
}
|
|
91
96
|
const KEY_HINTS = [
|
|
92
97
|
{ key: "↑↓", label: "select" },
|
|
93
98
|
{ key: "a", label: "today/all" },
|
|
99
|
+
{ key: "m", label: "move" },
|
|
100
|
+
{ key: "M", label: "move all" },
|
|
94
101
|
{ key: "r", label: "refresh" },
|
|
95
|
-
{ key: "esc", label: "clear filter" },
|
|
96
102
|
{ key: "1-4", label: "screens" },
|
|
97
103
|
{ key: "q", label: "quit" },
|
|
98
104
|
];
|
|
105
|
+
/** With a project filter set, `esc` clears it; listed only then, so the footer stays on one line in narrower terminals. */
|
|
106
|
+
const FILTERED_KEY_HINTS = [...KEY_HINTS.slice(0, 5), { key: "esc", label: "clear filter" }, ...KEY_HINTS.slice(5)];
|
|
99
107
|
/** Below this main-content width, the detail panel stacks under the table instead of beside it. */
|
|
100
108
|
const DETAIL_BREAKPOINT = 70;
|
|
101
109
|
/** Rows the `Panel` chrome (top border line + bottom border line) and the `Table`'s own header line take, outside its data rows. */
|
|
102
110
|
const TABLE_CHROME_ROWS = 3;
|
|
111
|
+
/** Footer messages of the reassignment flow. */
|
|
112
|
+
const ASKING_HUB = "asking the hub…";
|
|
113
|
+
const REASSIGN_UNAVAILABLE = "reassignment is not available: no hub connection in this session";
|
|
114
|
+
const NOTHING_UNASSIGNED = "no unassigned tasks on the hub in this view";
|
|
115
|
+
function toTarget(row) {
|
|
116
|
+
return { taskId: row.id, label: row.prompt };
|
|
117
|
+
}
|
|
103
118
|
/**
|
|
104
119
|
* The Tasks screen: a table of tasks (via `domain/tasks-model.ts`, never
|
|
105
120
|
* reimplemented here) with a `[ Task ]` detail panel for the selected row.
|
|
106
121
|
* `a` toggles today/all, `r` reloads, `↑↓` move the selection. When
|
|
107
122
|
* `projectFilter` is set (via Today's `enter`), the table is restricted to
|
|
108
|
-
* that project and `esc` clears it through `onClearFilter`.
|
|
109
|
-
*
|
|
123
|
+
* that project and `esc` clears it through `onClearFilter`.
|
|
124
|
+
*
|
|
125
|
+
* `m` moves (reassigns) the selected task on the hub and `M` every task of the
|
|
126
|
+
* current view that is unassigned there: the screen asks the hub for the
|
|
127
|
+
* rows involved (`reassign.prepare`, a progress message in the footer
|
|
128
|
+
* meanwhile), then opens the picker (`domain/reassign-picker.ts`) as a
|
|
129
|
+
* panel over the content zone. The picker is a modal flow: while it is
|
|
130
|
+
* open (or the hub is being asked) every other key of the screen is inert.
|
|
131
|
+
* Nothing is written to disk; the hub is written only by `reassign.apply`,
|
|
132
|
+
* after the review step.
|
|
110
133
|
*/
|
|
111
|
-
export function TasksScreen({ load, roots, version, columns, rows, focused = true, projectFilter, onClearFilter }) {
|
|
134
|
+
export function TasksScreen({ load, roots, version, columns, rows, focused = true, projectFilter, onClearFilter, reassign, onModalChange }) {
|
|
112
135
|
const allRef = useRef(false);
|
|
113
136
|
const [all, setAll] = useState(false);
|
|
114
137
|
const [model, setModel] = useState(() => load({ all: false }));
|
|
115
138
|
const [selected, setSelected] = useState(0);
|
|
139
|
+
const [picker, setPickerState] = useState(undefined);
|
|
140
|
+
const pickerRef = useRef(undefined);
|
|
141
|
+
const [asking, setAsking] = useState(false);
|
|
142
|
+
const askingRef = useRef(false);
|
|
143
|
+
const [note, setNote] = useState(undefined);
|
|
144
|
+
/** The hub's assignment (by names) for the tasks asked about in this session, keyed by kankaku task id. */
|
|
145
|
+
const [hubAssignments, setHubAssignments] = useState({});
|
|
116
146
|
const visibleRows = useMemo(() => (projectFilter !== undefined ? model.rows.filter((row) => row.project === projectFilter) : model.rows), [model.rows, projectFilter]);
|
|
117
147
|
const maxRowsRef = useRef(0);
|
|
148
|
+
const pickerRowsRef = useRef(1);
|
|
149
|
+
const setPicker = (next) => {
|
|
150
|
+
pickerRef.current = next;
|
|
151
|
+
setPickerState(next);
|
|
152
|
+
};
|
|
153
|
+
const modalOpen = asking || picker !== undefined;
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
onModalChange?.(modalOpen);
|
|
156
|
+
}, [modalOpen]);
|
|
157
|
+
const rememberHubRows = (found, catalog) => {
|
|
158
|
+
setHubAssignments((current) => {
|
|
159
|
+
const next = { ...current };
|
|
160
|
+
for (const [taskId, hubRow] of found)
|
|
161
|
+
next[taskId] = describeAssignment(hubRow, catalog);
|
|
162
|
+
return next;
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
const startReassign = (mode) => {
|
|
166
|
+
if (reassign === undefined) {
|
|
167
|
+
setNote(REASSIGN_UNAVAILABLE);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const chosen = mode === "single" ? [visibleRows[Math.min(selected, Math.max(visibleRows.length - 1, 0))]] : visibleRows;
|
|
171
|
+
const targets = chosen.filter((row) => row !== undefined).map(toTarget);
|
|
172
|
+
if (targets.length === 0) {
|
|
173
|
+
setNote("no tasks to reassign");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
askingRef.current = true;
|
|
177
|
+
setAsking(true);
|
|
178
|
+
setNote(ASKING_HUB);
|
|
179
|
+
void reassign
|
|
180
|
+
.prepare(targets.map((target) => target.taskId))
|
|
181
|
+
.then((prepared) => {
|
|
182
|
+
if (!prepared.ok) {
|
|
183
|
+
setNote(prepared.message);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
rememberHubRows(prepared.rows, prepared.catalog);
|
|
187
|
+
if (mode === "single" && !prepared.rows.has(targets[0].taskId)) {
|
|
188
|
+
setNote(NOT_ON_HUB);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (mode === "bulk" && eligibleForBulk(targets, prepared.rows, prepared.catalog).length === 0) {
|
|
192
|
+
setNote(NOTHING_UNASSIGNED);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
setNote(undefined);
|
|
196
|
+
setPicker(openPicker({ mode, catalog: prepared.catalog, targets, rows: prepared.rows }));
|
|
197
|
+
})
|
|
198
|
+
.catch((error) => setNote(error instanceof Error ? error.message : String(error)))
|
|
199
|
+
.finally(() => {
|
|
200
|
+
askingRef.current = false;
|
|
201
|
+
setAsking(false);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
const startApply = (state) => {
|
|
205
|
+
const plan = state.plan;
|
|
206
|
+
if (reassign === undefined || plan === undefined || !plan.ok)
|
|
207
|
+
return;
|
|
208
|
+
const settle = (outcomes) => {
|
|
209
|
+
const current = pickerRef.current ?? state;
|
|
210
|
+
setPicker(finishApplying(current, outcomes));
|
|
211
|
+
setHubAssignments((assignments) => {
|
|
212
|
+
const next = { ...assignments };
|
|
213
|
+
for (const line of plan.plan.lines) {
|
|
214
|
+
if (line.kind === "reassign" && outcomes.some((outcome) => outcome.taskId === line.taskId && outcome.status === "reassigned"))
|
|
215
|
+
next[line.taskId] = line.to;
|
|
216
|
+
}
|
|
217
|
+
return next;
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
void reassign.apply(plan.plan).then(settle, (error) => {
|
|
221
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
222
|
+
settle(plan.plan.lines.filter((line) => line.kind === "reassign").map((line) => ({ taskId: line.taskId, status: "failed", reason })));
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
const handlePickerKey = (current, key) => {
|
|
226
|
+
if (current.step === "applying")
|
|
227
|
+
return;
|
|
228
|
+
const page = Math.max(pickerRowsRef.current, 1);
|
|
229
|
+
if (key.escape || key.leftArrow) {
|
|
230
|
+
setPicker(back(current));
|
|
231
|
+
}
|
|
232
|
+
else if (key.return) {
|
|
233
|
+
const next = advance(current);
|
|
234
|
+
setPicker(next);
|
|
235
|
+
if (next !== undefined && next.step === "applying" && current.step === "review")
|
|
236
|
+
startApply(next);
|
|
237
|
+
}
|
|
238
|
+
else if (key.downArrow) {
|
|
239
|
+
setPicker(moveSelection(current, 1));
|
|
240
|
+
}
|
|
241
|
+
else if (key.upArrow) {
|
|
242
|
+
setPicker(moveSelection(current, -1));
|
|
243
|
+
}
|
|
244
|
+
else if (key.pageDown) {
|
|
245
|
+
setPicker(moveSelection(current, page));
|
|
246
|
+
}
|
|
247
|
+
else if (key.pageUp) {
|
|
248
|
+
setPicker(moveSelection(current, -page));
|
|
249
|
+
}
|
|
250
|
+
else if (key.home) {
|
|
251
|
+
setPicker(jumpSelection(current, "first"));
|
|
252
|
+
}
|
|
253
|
+
else if (key.end) {
|
|
254
|
+
setPicker(jumpSelection(current, "last"));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
118
257
|
useInput((input, key) => {
|
|
258
|
+
if (askingRef.current)
|
|
259
|
+
return;
|
|
260
|
+
setNote(undefined);
|
|
261
|
+
const openPickerState = pickerRef.current;
|
|
262
|
+
if (openPickerState !== undefined) {
|
|
263
|
+
handlePickerKey(openPickerState, key);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
119
266
|
const lastIndex = Math.max(visibleRows.length - 1, 0);
|
|
120
|
-
if (input === "
|
|
267
|
+
if (input === "m") {
|
|
268
|
+
startReassign("single");
|
|
269
|
+
}
|
|
270
|
+
else if (input === "M") {
|
|
271
|
+
startReassign("bulk");
|
|
272
|
+
}
|
|
273
|
+
else if (input === "a") {
|
|
121
274
|
const nextAll = !allRef.current;
|
|
122
275
|
allRef.current = nextAll;
|
|
123
276
|
setAll(nextAll);
|
|
@@ -149,7 +302,11 @@ export function TasksScreen({ load, roots, version, columns, rows, focused = tru
|
|
|
149
302
|
onClearFilter?.();
|
|
150
303
|
}
|
|
151
304
|
}, { isActive: focused });
|
|
152
|
-
return (_jsx(Layout, { columns: columns, rows: rows, headerLeft: `>_ kankaku ${version}`, headerRight: projectFilter !== undefined ? `filtered: ${projectFilter}` : undefined, sidebarItems: SCREENS, activeId: "tasks", sidebarStats: [`tasks ${visibleRows.length}`, all ? "scope all" : "scope today"], keyHints: hintsFor("tasks", focused ? "main" : "sidebar", KEY_HINTS), focus: focused ? "main" : "sidebar", children: ({ mainWidth, mainHeight }) => {
|
|
305
|
+
return (_jsx(Layout, { columns: columns, rows: rows, headerLeft: `>_ kankaku ${version}`, headerRight: projectFilter !== undefined ? `filtered: ${projectFilter}` : undefined, sidebarItems: SCREENS, activeId: "tasks", sidebarStats: [`tasks ${visibleRows.length}`, all ? "scope all" : "scope today"], keyHints: picker !== undefined ? pickerHints(picker) : hintsFor("tasks", focused ? "main" : "sidebar", projectFilter !== undefined ? FILTERED_KEY_HINTS : KEY_HINTS), ...(note !== undefined ? { footerNote: note } : {}), focus: focused ? "main" : "sidebar", children: ({ mainWidth, mainHeight }) => {
|
|
306
|
+
if (picker !== undefined) {
|
|
307
|
+
pickerRowsRef.current = pickerMaxRows(mainHeight);
|
|
308
|
+
return _jsx(ReassignPanel, { state: picker, width: mainWidth, height: mainHeight });
|
|
309
|
+
}
|
|
153
310
|
const wide = mainWidth >= DETAIL_BREAKPOINT;
|
|
154
311
|
const tableWidth = wide ? Math.floor(mainWidth * 0.62) : mainWidth;
|
|
155
312
|
const detailWidth = wide ? Math.max(mainWidth - tableWidth - 1, 1) : mainWidth;
|
|
@@ -160,7 +317,7 @@ export function TasksScreen({ load, roots, version, columns, rows, focused = tru
|
|
|
160
317
|
const maxRows = Math.max(tableHeight - TABLE_CHROME_ROWS, 1);
|
|
161
318
|
maxRowsRef.current = maxRows;
|
|
162
319
|
const table = (_jsx(Panel, { title: "Tasks", width: tableWidth, height: wide ? mainHeight : tableHeight, active: focused, children: _jsx(Table, { columns: taskColumns(tableWidth), rows: visibleRows, rowKey: (row) => row.id, cell: taskCell, selectedIndex: selected, emptyText: "no tasks", maxRows: maxRows }) }));
|
|
163
|
-
const detail = _jsx(DetailPanel, { row: selectedRow, width: detailWidth, height: detailHeight });
|
|
320
|
+
const detail = _jsx(DetailPanel, { row: selectedRow, hub: selectedRow !== undefined ? hubAssignments[selectedRow.id] : undefined, width: detailWidth, height: detailHeight });
|
|
164
321
|
return wide ? (_jsxs(Box, { flexDirection: "row", children: [table, _jsx(Box, { width: 1 }), detail] })) : (_jsxs(Box, { flexDirection: "column", children: [table, detail] }));
|
|
165
322
|
} }));
|
|
166
323
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kankaku",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Terminal app that shows today's kankaku work across every project on disk",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"ink": "^7.1.1",
|
|
41
|
-
"kankaku-pi": "^1.0
|
|
42
|
-
"kankaku-claude": "^1.0
|
|
41
|
+
"kankaku-pi": "^1.2.0",
|
|
42
|
+
"kankaku-claude": "^1.2.0",
|
|
43
43
|
"kankaku-hub": "^0.2.0",
|
|
44
44
|
"react": "^19.3.0"
|
|
45
45
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kankaku-pi",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "pi extension that records agent work time per prompt, excluding waits for the user, with subagent linkage and task/session views",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "soyunninja",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"prepublishOnly": "npm run check"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
62
|
-
"@earendil-works/pi-tui": "^0.
|
|
61
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
62
|
+
"@earendil-works/pi-tui": "^0.87.1",
|
|
63
63
|
"@types/node": "^24.13.4",
|
|
64
64
|
"typescript": "^5.7.0"
|
|
65
65
|
}
|