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.
Files changed (44) hide show
  1. package/README.md +103 -14
  2. package/dist/adapters/cached-catalog.d.ts +8 -2
  3. package/dist/adapters/cached-catalog.js +13 -3
  4. package/dist/adapters/pocketbase-catalog.d.ts +7 -5
  5. package/dist/adapters/pocketbase-catalog.js +22 -5
  6. package/dist/adapters/pocketbase-sink.d.ts +2 -1
  7. package/dist/adapters/pocketbase-sink.js +2 -1
  8. package/dist/domain/hub-entry.d.ts +12 -3
  9. package/dist/domain/hub-entry.js +14 -4
  10. package/dist/domain/task-view.d.ts +4 -0
  11. package/dist/domain/task-view.js +2 -0
  12. package/dist/domain/work-record.d.ts +18 -0
  13. package/dist/domain/work-record.js +18 -0
  14. package/dist/domain/work-target.d.ts +30 -1
  15. package/dist/domain/work-target.js +33 -11
  16. package/dist/ports/catalog.d.ts +4 -2
  17. package/package.json +1 -1
  18. package/src/adapters/cached-catalog.ts +22 -6
  19. package/src/adapters/hub-actions.ts +83 -0
  20. package/src/adapters/kankaku-command.ts +236 -244
  21. package/src/adapters/panel/kankaku-panel.ts +274 -0
  22. package/src/adapters/panel/panel-items.ts +77 -0
  23. package/src/adapters/panel/panel-lines.ts +13 -0
  24. package/src/adapters/panel/panel-theme.ts +32 -0
  25. package/src/adapters/panel/screens/about.ts +69 -0
  26. package/src/adapters/panel/screens/doctor.ts +85 -0
  27. package/src/adapters/panel/screens/export.ts +119 -0
  28. package/src/adapters/panel/screens/report.ts +141 -0
  29. package/src/adapters/panel/screens/sync.ts +126 -0
  30. package/src/adapters/panel/screens/target.ts +367 -0
  31. package/src/adapters/pi-tracker.ts +110 -6
  32. package/src/adapters/pocketbase-catalog.ts +36 -7
  33. package/src/adapters/pocketbase-sink.ts +15 -3
  34. package/src/adapters/report-views.ts +98 -0
  35. package/src/adapters/report.ts +66 -16
  36. package/src/adapters/session-target.ts +111 -4
  37. package/src/adapters/target-picker.ts +46 -1
  38. package/src/domain/hub-entry.ts +20 -5
  39. package/src/domain/panel-model.ts +267 -0
  40. package/src/domain/task-view.ts +6 -0
  41. package/src/domain/work-record.ts +23 -0
  42. package/src/domain/work-target.ts +60 -11
  43. package/src/extension.ts +19 -8
  44. 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
- function buildTarget(client: Client, project: Project | undefined): WorkTarget {
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
- return buildTarget(client, project);
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
- /** `<clientName>` or `<clientName> · <projectName>` — the display label used by the status bar and the "remember" prompt. */
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
- return target.projectName ? `${target.clientName} · ${target.projectName}` : target.clientName;
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 } : {}),
@@ -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
  /**