kankaku 0.6.0 → 0.7.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 +103 -14
- package/dist/adapters/cached-catalog.d.ts +8 -2
- package/dist/adapters/cached-catalog.js +13 -3
- package/dist/adapters/pocketbase-catalog.d.ts +7 -5
- package/dist/adapters/pocketbase-catalog.js +22 -5
- package/dist/adapters/pocketbase-sink.d.ts +2 -1
- package/dist/adapters/pocketbase-sink.js +2 -1
- package/dist/domain/hub-entry.d.ts +12 -3
- package/dist/domain/hub-entry.js +14 -4
- package/dist/domain/task-view.d.ts +4 -0
- package/dist/domain/task-view.js +2 -0
- package/dist/domain/work-record.d.ts +18 -0
- package/dist/domain/work-record.js +18 -0
- package/dist/domain/work-target.d.ts +30 -1
- package/dist/domain/work-target.js +33 -11
- package/dist/ports/catalog.d.ts +4 -2
- package/package.json +1 -1
- package/src/adapters/cached-catalog.ts +22 -6
- package/src/adapters/hub-actions.ts +83 -0
- package/src/adapters/kankaku-command.ts +236 -244
- package/src/adapters/panel/kankaku-panel.ts +274 -0
- package/src/adapters/panel/panel-items.ts +77 -0
- package/src/adapters/panel/panel-lines.ts +13 -0
- package/src/adapters/panel/panel-theme.ts +32 -0
- package/src/adapters/panel/screens/about.ts +69 -0
- package/src/adapters/panel/screens/doctor.ts +85 -0
- package/src/adapters/panel/screens/export.ts +119 -0
- package/src/adapters/panel/screens/report.ts +141 -0
- package/src/adapters/panel/screens/sync.ts +126 -0
- package/src/adapters/panel/screens/target.ts +367 -0
- package/src/adapters/pi-tracker.ts +110 -6
- package/src/adapters/pocketbase-catalog.ts +36 -7
- package/src/adapters/pocketbase-sink.ts +15 -3
- package/src/adapters/report-views.ts +98 -0
- package/src/adapters/report.ts +66 -16
- package/src/adapters/session-target.ts +111 -4
- package/src/adapters/target-picker.ts +46 -1
- package/src/domain/hub-entry.ts +20 -5
- package/src/domain/panel-model.ts +267 -0
- package/src/domain/task-view.ts +6 -0
- package/src/domain/work-record.ts +23 -0
- package/src/domain/work-target.ts +60 -11
- package/src/extension.ts +19 -8
- package/src/ports/catalog.ts +4 -2
|
@@ -32,6 +32,19 @@ export interface Project {
|
|
|
32
32
|
active: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A hub `tasks` row: something kankaku can link a session's `task_entries`
|
|
37
|
+
* row to (`task_entries.task`), never create. See `domain/hub-entry.ts` for
|
|
38
|
+
* the create-only assignment rule this feeds.
|
|
39
|
+
*/
|
|
40
|
+
export interface HubTask {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
projectId: string;
|
|
44
|
+
status: "open" | "doing" | "done";
|
|
45
|
+
externalRef?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
export interface WorkTarget {
|
|
36
49
|
clientId: string;
|
|
37
50
|
clientCode: string;
|
|
@@ -39,12 +52,23 @@ export interface WorkTarget {
|
|
|
39
52
|
projectId?: string;
|
|
40
53
|
projectCode?: string;
|
|
41
54
|
projectName?: string;
|
|
55
|
+
/** Hub `tasks` record id, kept only when it belongs to `projectId`. See {@link resolveWorkTarget}. */
|
|
56
|
+
hubTaskId?: string;
|
|
57
|
+
/** Denormalised alongside `hubTaskId` for display. */
|
|
58
|
+
hubTaskTitle?: string;
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
/** ids picked from the session, or from the project's `.kankaku/config.json`. */
|
|
45
62
|
export interface WorkTargetCandidate {
|
|
46
63
|
clientId: string;
|
|
47
64
|
projectId?: string;
|
|
65
|
+
/**
|
|
66
|
+
* A hub task the user picked for this session (`/kankaku task pick`).
|
|
67
|
+
* Only ever set on the session-level candidate — the project config file
|
|
68
|
+
* and `repoPaths` matching never carry one (task linking is session-only,
|
|
69
|
+
* see AGENTS.md).
|
|
70
|
+
*/
|
|
71
|
+
hubTaskId?: string;
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
/**
|
|
@@ -63,6 +87,8 @@ export interface ResolveWorkTargetInput {
|
|
|
63
87
|
cwd: string;
|
|
64
88
|
clients: Client[];
|
|
65
89
|
projects: Project[];
|
|
90
|
+
/** The hub's known tasks, to validate a candidate's `hubTaskId` against. Defaults to `[]` so existing callers compile unchanged. */
|
|
91
|
+
tasks?: HubTask[];
|
|
66
92
|
}
|
|
67
93
|
|
|
68
94
|
export type WorkTargetSourceName = "session" | "project" | "repoPaths";
|
|
@@ -81,7 +107,18 @@ function findUsableProject(projects: Project[], projectId: string, clientId: str
|
|
|
81
107
|
return project;
|
|
82
108
|
}
|
|
83
109
|
|
|
84
|
-
|
|
110
|
+
/**
|
|
111
|
+
* A hub task usable as a link target: exists and belongs to `projectId`.
|
|
112
|
+
* Any {@link HubTask.status} is accepted — a task marked `done` mid-session
|
|
113
|
+
* keeps linking — only the project match matters here.
|
|
114
|
+
*/
|
|
115
|
+
function findUsableTask(tasks: HubTask[], taskId: string, projectId: string): HubTask | undefined {
|
|
116
|
+
const task = tasks.find((candidate) => candidate.id === taskId);
|
|
117
|
+
if (!task || task.projectId !== projectId) return undefined;
|
|
118
|
+
return task;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildTarget(client: Client, project: Project | undefined, task: HubTask | undefined): WorkTarget {
|
|
85
122
|
return {
|
|
86
123
|
clientId: client.id,
|
|
87
124
|
clientCode: client.code,
|
|
@@ -93,6 +130,7 @@ function buildTarget(client: Client, project: Project | undefined): WorkTarget {
|
|
|
93
130
|
projectName: project.name,
|
|
94
131
|
}
|
|
95
132
|
: {}),
|
|
133
|
+
...(task !== undefined ? { hubTaskId: task.id, hubTaskTitle: task.title } : {}),
|
|
96
134
|
};
|
|
97
135
|
}
|
|
98
136
|
|
|
@@ -101,13 +139,17 @@ function buildTarget(client: Client, project: Project | undefined): WorkTarget {
|
|
|
101
139
|
* file) into a target, or `undefined` when the client id does not resolve
|
|
102
140
|
* to a usable client. A projectId that does not resolve to a usable
|
|
103
141
|
* project of that client is dropped — the client-only target is still
|
|
104
|
-
* returned — rather than failing the whole candidate.
|
|
142
|
+
* returned — rather than failing the whole candidate. A `hubTaskId` is
|
|
143
|
+
* kept only when it resolves to a task of the resolved project; a target
|
|
144
|
+
* with no project never carries one.
|
|
105
145
|
*/
|
|
106
|
-
function resolveCandidate(candidate: WorkTargetCandidate, clients: Client[], projects: Project[]): WorkTarget | undefined {
|
|
146
|
+
function resolveCandidate(candidate: WorkTargetCandidate, clients: Client[], projects: Project[], tasks: HubTask[]): WorkTarget | undefined {
|
|
107
147
|
const client = findUsableClient(clients, candidate.clientId);
|
|
108
148
|
if (!client) return undefined;
|
|
109
149
|
const project = candidate.projectId !== undefined ? findUsableProject(projects, candidate.projectId, client.id) : undefined;
|
|
110
|
-
|
|
150
|
+
const task =
|
|
151
|
+
project !== undefined && candidate.hubTaskId !== undefined ? findUsableTask(tasks, candidate.hubTaskId, project.id) : undefined;
|
|
152
|
+
return buildTarget(client, project, task);
|
|
111
153
|
}
|
|
112
154
|
|
|
113
155
|
/** The longest-matching active project whose `repoPaths` contains `cwd`, exactly or as an ancestor directory. */
|
|
@@ -139,10 +181,11 @@ function isCwdWithin(cwd: string, repoPath: string): boolean {
|
|
|
139
181
|
/** Which source (if any) {@link resolveWorkTarget} would use, computed independently so callers can display it. */
|
|
140
182
|
export function resolveWorkTargetSource(input: ResolveWorkTargetInput): WorkTargetSourceName | undefined {
|
|
141
183
|
const { session } = input;
|
|
184
|
+
const tasks = input.tasks ?? [];
|
|
142
185
|
if (session === "skipped") return undefined;
|
|
143
|
-
if (session !== undefined && resolveCandidate(session, input.clients, input.projects) !== undefined) return "session";
|
|
186
|
+
if (session !== undefined && resolveCandidate(session, input.clients, input.projects, tasks) !== undefined) return "session";
|
|
144
187
|
|
|
145
|
-
if (input.project !== undefined && resolveCandidate(input.project, input.clients, input.projects) !== undefined) return "project";
|
|
188
|
+
if (input.project !== undefined && resolveCandidate(input.project, input.clients, input.projects, tasks) !== undefined) return "project";
|
|
146
189
|
|
|
147
190
|
const matched = matchByRepoPath(input.cwd, input.projects);
|
|
148
191
|
if (matched && findUsableClient(input.clients, matched.clientId)) return "repoPaths";
|
|
@@ -158,28 +201,34 @@ export function resolveWorkTargetSource(input: ResolveWorkTargetInput): WorkTarg
|
|
|
158
201
|
*/
|
|
159
202
|
export function resolveWorkTarget(input: ResolveWorkTargetInput): WorkTarget | undefined {
|
|
160
203
|
const { session } = input;
|
|
204
|
+
const tasks = input.tasks ?? [];
|
|
161
205
|
if (session === "skipped") return undefined;
|
|
162
206
|
|
|
163
207
|
if (session !== undefined) {
|
|
164
|
-
const resolved = resolveCandidate(session, input.clients, input.projects);
|
|
208
|
+
const resolved = resolveCandidate(session, input.clients, input.projects, tasks);
|
|
165
209
|
if (resolved) return resolved;
|
|
166
210
|
}
|
|
167
211
|
|
|
168
212
|
if (input.project !== undefined) {
|
|
169
|
-
const resolved = resolveCandidate(input.project, input.clients, input.projects);
|
|
213
|
+
const resolved = resolveCandidate(input.project, input.clients, input.projects, tasks);
|
|
170
214
|
if (resolved) return resolved;
|
|
171
215
|
}
|
|
172
216
|
|
|
173
217
|
const matched = matchByRepoPath(input.cwd, input.projects);
|
|
174
218
|
if (matched) {
|
|
175
219
|
const client = findUsableClient(input.clients, matched.clientId);
|
|
176
|
-
if (client) return buildTarget(client, matched);
|
|
220
|
+
if (client) return buildTarget(client, matched, undefined);
|
|
177
221
|
}
|
|
178
222
|
|
|
179
223
|
return undefined;
|
|
180
224
|
}
|
|
181
225
|
|
|
182
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* `<clientName>`, `<clientName> · <projectName>`, or with a linked hub task
|
|
228
|
+
* appended as `... › <hubTaskTitle>` — the display label used by the
|
|
229
|
+
* status bar and the "remember" prompt.
|
|
230
|
+
*/
|
|
183
231
|
export function formatWorkTargetLabel(target: WorkTarget): string {
|
|
184
|
-
|
|
232
|
+
const base = target.projectName ? `${target.clientName} · ${target.projectName}` : target.clientName;
|
|
233
|
+
return target.hubTaskTitle ? `${base} › ${target.hubTaskTitle}` : base;
|
|
185
234
|
}
|
package/src/extension.ts
CHANGED
|
@@ -184,6 +184,18 @@ export default function kankaku(pi: ExtensionAPI): void {
|
|
|
184
184
|
let hubConfigError: string | undefined;
|
|
185
185
|
let sync: SyncCommandDeps | undefined;
|
|
186
186
|
let autoSyncEnabled: boolean | undefined;
|
|
187
|
+
let hubUrl: string | undefined;
|
|
188
|
+
|
|
189
|
+
// Resolved once, here (never on a hot path): see README "Hub
|
|
190
|
+
// (PocketBase)" > "Agent and measurement quality" and the panel's `about`
|
|
191
|
+
// screen (`adapters/panel/screens/about.ts`). Both degrade to `undefined`
|
|
192
|
+
// on any failure rather than guessing. Unlike the sync sink below (which
|
|
193
|
+
// needs them only when the hub is configured), the `about` screen shows
|
|
194
|
+
// these regardless — so they are resolved unconditionally here.
|
|
195
|
+
const agentVersion = resolveAgentVersion();
|
|
196
|
+
// extension.ts lives at <package root>/src/extension.ts.
|
|
197
|
+
const pluginPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
198
|
+
const pluginVersion = resolvePluginVersion(pluginPackageRoot);
|
|
187
199
|
|
|
188
200
|
// `homeDir` is passed as a reference, never invoked here: any failure
|
|
189
201
|
// resolving it (no HOME, a sandbox) must not fail extension load for
|
|
@@ -194,6 +206,7 @@ export default function kankaku(pi: ExtensionAPI): void {
|
|
|
194
206
|
|
|
195
207
|
if (hub.credentials) {
|
|
196
208
|
const credentials = hub.credentials;
|
|
209
|
+
hubUrl = credentials.url;
|
|
197
210
|
const client = new PocketBaseClient({ url: credentials.url, email: credentials.email, password: credentials.password });
|
|
198
211
|
// Same defensive resolution as above; falls back to the OS temp dir
|
|
199
212
|
// when no home directory is available so an env-only hub configuration
|
|
@@ -226,20 +239,13 @@ export default function kankaku(pi: ExtensionAPI): void {
|
|
|
226
239
|
const catalogRef = catalog;
|
|
227
240
|
const machineName = machine;
|
|
228
241
|
|
|
229
|
-
// Resolved once, here (never on a hot path): see README "Hub
|
|
230
|
-
// (PocketBase)" > "Agent and measurement quality". Both degrade to
|
|
231
|
-
// `undefined` on any failure rather than guessing.
|
|
232
|
-
const agentVersion = resolveAgentVersion();
|
|
233
|
-
// extension.ts lives at <package root>/src/extension.ts.
|
|
234
|
-
const pluginPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
235
|
-
const pluginVersion = resolvePluginVersion(pluginPackageRoot);
|
|
236
|
-
|
|
237
242
|
const runOnce = (options?: { full?: boolean; trigger?: SyncTrigger }) => {
|
|
238
243
|
const snapshot = catalogRef.read();
|
|
239
244
|
const sink = new PocketBaseSink({
|
|
240
245
|
client,
|
|
241
246
|
clients: snapshot?.clients ?? [],
|
|
242
247
|
projects: snapshot?.projects ?? [],
|
|
248
|
+
tasks: snapshot?.tasks ?? [],
|
|
243
249
|
machine: machineName,
|
|
244
250
|
promptMode: syncConfig.promptMode,
|
|
245
251
|
syncRecords: syncConfig.syncRecords,
|
|
@@ -308,6 +314,11 @@ export default function kankaku(pi: ExtensionAPI): void {
|
|
|
308
314
|
...(hubConfigError !== undefined ? { hubConfigError } : {}),
|
|
309
315
|
...(sync !== undefined ? { sync } : {}),
|
|
310
316
|
...(autoSyncEnabled !== undefined ? { autoSyncEnabled } : {}),
|
|
317
|
+
config,
|
|
318
|
+
kankakuDir: resolvedDir,
|
|
319
|
+
...(agentVersion !== undefined ? { agentVersion } : {}),
|
|
320
|
+
...(pluginVersion !== undefined ? { pluginVersion } : {}),
|
|
321
|
+
...(hubUrl !== undefined ? { hubUrl } : {}),
|
|
311
322
|
...(roleOverride !== undefined ? { roleOverride } : {}),
|
|
312
323
|
...(childMarkerPresent ? { childMarkerPresent } : {}),
|
|
313
324
|
...(profile !== undefined ? { profile } : {}),
|
package/src/ports/catalog.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { Client, Project } from "../domain/work-target.ts";
|
|
1
|
+
import type { Client, HubTask, Project } from "../domain/work-target.ts";
|
|
2
2
|
|
|
3
|
-
/** A cached read of the hub's clients/projects, plus when and against which hub URL it was fetched. */
|
|
3
|
+
/** A cached read of the hub's clients/projects/tasks, plus when and against which hub URL it was fetched. */
|
|
4
4
|
export interface CatalogSnapshot {
|
|
5
5
|
fetchedAt: number;
|
|
6
6
|
url: string;
|
|
7
7
|
clients: Client[];
|
|
8
8
|
projects: Project[];
|
|
9
|
+
/** Optional because a cache written by an older build (before hub task linking) never had this field; a missing value reads as "no tasks known yet." */
|
|
10
|
+
tasks?: HubTask[];
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
/**
|