kankaku-pi 1.0.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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1438 -0
  3. package/dist/adapters/cached-catalog.d.ts +42 -0
  4. package/dist/adapters/cached-catalog.js +121 -0
  5. package/dist/adapters/export-writer.d.ts +13 -0
  6. package/dist/adapters/export-writer.js +28 -0
  7. package/dist/adapters/file-modes.d.ts +20 -0
  8. package/dist/adapters/file-modes.js +34 -0
  9. package/dist/adapters/hub-actions.d.ts +35 -0
  10. package/dist/adapters/hub-actions.js +70 -0
  11. package/dist/adapters/hub-credentials.d.ts +35 -0
  12. package/dist/adapters/hub-credentials.js +58 -0
  13. package/dist/adapters/jsonl-work-log.d.ts +20 -0
  14. package/dist/adapters/jsonl-work-log.js +62 -0
  15. package/dist/adapters/kankaku-dir.d.ts +38 -0
  16. package/dist/adapters/kankaku-dir.js +85 -0
  17. package/dist/adapters/lazy-jsonl-work-log.d.ts +17 -0
  18. package/dist/adapters/lazy-jsonl-work-log.js +31 -0
  19. package/dist/adapters/pocketbase-catalog.d.ts +16 -0
  20. package/dist/adapters/pocketbase-catalog.js +56 -0
  21. package/dist/adapters/pocketbase-client.d.ts +81 -0
  22. package/dist/adapters/pocketbase-client.js +148 -0
  23. package/dist/adapters/pocketbase-sink.d.ts +53 -0
  24. package/dist/adapters/pocketbase-sink.js +181 -0
  25. package/dist/adapters/project-config.d.ts +42 -0
  26. package/dist/adapters/project-config.js +108 -0
  27. package/dist/adapters/report-data.d.ts +12 -0
  28. package/dist/adapters/report-data.js +8 -0
  29. package/dist/adapters/report-views.d.ts +45 -0
  30. package/dist/adapters/report-views.js +73 -0
  31. package/dist/adapters/report.d.ts +112 -0
  32. package/dist/adapters/report.js +236 -0
  33. package/dist/adapters/sync-runner.d.ts +114 -0
  34. package/dist/adapters/sync-runner.js +273 -0
  35. package/dist/adapters/sync-state-store.d.ts +62 -0
  36. package/dist/adapters/sync-state-store.js +188 -0
  37. package/dist/config.d.ts +168 -0
  38. package/dist/config.js +392 -0
  39. package/dist/domain/ancestry-match.d.ts +49 -0
  40. package/dist/domain/ancestry-match.js +82 -0
  41. package/dist/domain/client-label.d.ts +28 -0
  42. package/dist/domain/client-label.js +44 -0
  43. package/dist/domain/day.d.ts +2 -0
  44. package/dist/domain/day.js +8 -0
  45. package/dist/domain/export.d.ts +38 -0
  46. package/dist/domain/export.js +68 -0
  47. package/dist/domain/hub-entry.d.ts +234 -0
  48. package/dist/domain/hub-entry.js +265 -0
  49. package/dist/domain/index.d.ts +19 -0
  50. package/dist/domain/index.js +19 -0
  51. package/dist/domain/intervals.d.ts +17 -0
  52. package/dist/domain/intervals.js +43 -0
  53. package/dist/domain/registry-health.d.ts +49 -0
  54. package/dist/domain/registry-health.js +58 -0
  55. package/dist/domain/segment-rule.d.ts +10 -0
  56. package/dist/domain/segment-rule.js +1 -0
  57. package/dist/domain/subagent-profile.d.ts +278 -0
  58. package/dist/domain/subagent-profile.js +418 -0
  59. package/dist/domain/sync-plan.d.ts +151 -0
  60. package/dist/domain/sync-plan.js +196 -0
  61. package/dist/domain/task-view.d.ts +117 -0
  62. package/dist/domain/task-view.js +428 -0
  63. package/dist/domain/work-record.d.ts +236 -0
  64. package/dist/domain/work-record.js +91 -0
  65. package/dist/domain/work-target.d.ts +101 -0
  66. package/dist/domain/work-target.js +149 -0
  67. package/dist/domain/work-tracker.d.ts +90 -0
  68. package/dist/domain/work-tracker.js +405 -0
  69. package/dist/hub/index.d.ts +25 -0
  70. package/dist/hub/index.js +25 -0
  71. package/dist/ports/catalog.d.ts +31 -0
  72. package/dist/ports/catalog.js +1 -0
  73. package/dist/ports/clock.d.ts +3 -0
  74. package/dist/ports/clock.js +1 -0
  75. package/dist/ports/index.d.ts +11 -0
  76. package/dist/ports/index.js +1 -0
  77. package/dist/ports/inflight-store.d.ts +15 -0
  78. package/dist/ports/inflight-store.js +1 -0
  79. package/dist/ports/process-registry.d.ts +72 -0
  80. package/dist/ports/process-registry.js +1 -0
  81. package/dist/ports/work-log.d.ts +14 -0
  82. package/dist/ports/work-log.js +1 -0
  83. package/dist/ports/work-sink.d.ts +39 -0
  84. package/dist/ports/work-sink.js +1 -0
  85. package/package.json +66 -0
  86. package/src/adapters/agent-info.ts +86 -0
  87. package/src/adapters/ancestry.ts +260 -0
  88. package/src/adapters/cached-catalog.ts +147 -0
  89. package/src/adapters/export-writer.ts +33 -0
  90. package/src/adapters/file-inflight-store.ts +115 -0
  91. package/src/adapters/file-modes.ts +35 -0
  92. package/src/adapters/hub-actions.ts +82 -0
  93. package/src/adapters/hub-credentials.ts +95 -0
  94. package/src/adapters/jsonl-work-log.ts +67 -0
  95. package/src/adapters/kankaku-command.ts +717 -0
  96. package/src/adapters/kankaku-dir.ts +102 -0
  97. package/src/adapters/lazy-file-inflight-store.ts +43 -0
  98. package/src/adapters/lazy-jsonl-work-log.ts +39 -0
  99. package/src/adapters/machine-process-registry.ts +256 -0
  100. package/src/adapters/panel/kankaku-panel.ts +419 -0
  101. package/src/adapters/panel/panel-items.ts +87 -0
  102. package/src/adapters/panel/panel-lines.ts +13 -0
  103. package/src/adapters/panel/panel-theme.ts +32 -0
  104. package/src/adapters/panel/screens/about.ts +69 -0
  105. package/src/adapters/panel/screens/doctor.ts +89 -0
  106. package/src/adapters/panel/screens/export.ts +123 -0
  107. package/src/adapters/panel/screens/report.ts +143 -0
  108. package/src/adapters/panel/screens/sync.ts +136 -0
  109. package/src/adapters/panel/screens/target.ts +384 -0
  110. package/src/adapters/pi-tracker.ts +753 -0
  111. package/src/adapters/pocketbase-catalog.ts +89 -0
  112. package/src/adapters/pocketbase-client.ts +197 -0
  113. package/src/adapters/pocketbase-sink.ts +236 -0
  114. package/src/adapters/process-identity-memo.ts +102 -0
  115. package/src/adapters/process-identity.ts +162 -0
  116. package/src/adapters/project-config.ts +116 -0
  117. package/src/adapters/report-data.ts +13 -0
  118. package/src/adapters/report-views.ts +98 -0
  119. package/src/adapters/report.ts +335 -0
  120. package/src/adapters/session-client.ts +116 -0
  121. package/src/adapters/session-dir.ts +28 -0
  122. package/src/adapters/session-target.ts +431 -0
  123. package/src/adapters/status-bar.ts +86 -0
  124. package/src/adapters/subagent-startup.ts +66 -0
  125. package/src/adapters/sync-runner.ts +340 -0
  126. package/src/adapters/sync-state-store.ts +227 -0
  127. package/src/adapters/target-picker.ts +127 -0
  128. package/src/config.ts +536 -0
  129. package/src/domain/ancestry-match.ts +84 -0
  130. package/src/domain/client-label.ts +56 -0
  131. package/src/domain/day.ts +8 -0
  132. package/src/domain/export.ts +107 -0
  133. package/src/domain/hub-entry.ts +433 -0
  134. package/src/domain/index.ts +19 -0
  135. package/src/domain/intervals.ts +53 -0
  136. package/src/domain/panel-model.ts +270 -0
  137. package/src/domain/registry-health.ts +87 -0
  138. package/src/domain/segment-rule.ts +10 -0
  139. package/src/domain/subagent-profile.ts +495 -0
  140. package/src/domain/sync-plan.ts +266 -0
  141. package/src/domain/task-view.ts +526 -0
  142. package/src/domain/work-record.ts +320 -0
  143. package/src/domain/work-target.ts +234 -0
  144. package/src/domain/work-tracker.ts +485 -0
  145. package/src/extension.ts +346 -0
  146. package/src/hub/index.ts +25 -0
  147. package/src/ports/catalog.ts +33 -0
  148. package/src/ports/clock.ts +3 -0
  149. package/src/ports/index.ts +11 -0
  150. package/src/ports/inflight-store.ts +16 -0
  151. package/src/ports/process-registry.ts +75 -0
  152. package/src/ports/work-log.ts +15 -0
  153. package/src/ports/work-sink.ts +35 -0
@@ -0,0 +1,384 @@
1
+ /**
2
+ * The `/kankaku` panel's `target` screen (see odd/tasks/kankaku-panel.md):
3
+ * billing client, project, hub task and the legacy label, all editable from
4
+ * one `SettingsList`. A subagent session never resolves its own target
5
+ * (see AGENTS.md "Measurement rules"), so it gets a read-only note instead.
6
+ */
7
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+ import type { Component, SelectItem, SettingItem, SettingsListTheme, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
9
+ import { Input, SelectList, SettingsList, Text } from "@earendil-works/pi-tui";
10
+ import { isValidClient } from "../../../domain/client-label.ts";
11
+ import { buildTargetRows } from "../../../domain/panel-model.ts";
12
+ import type { PanelRow } from "../../../domain/panel-model.ts";
13
+ import type { WorkRole } from "../../../domain/work-record.ts";
14
+ import type { HubTask, WorkTargetSourceName } from "../../../domain/work-target.ts";
15
+ import type { Catalog } from "../../../ports/catalog.ts";
16
+ import type { SessionClient } from "../../session-client.ts";
17
+ import type { SessionTarget } from "../../session-target.ts";
18
+ import type { PanelHost, PanelScreenFactory } from "../kankaku-panel.ts";
19
+ import { actionItem } from "../panel-items.ts";
20
+ import { buildSelectListTheme, buildSettingsListTheme } from "../panel-theme.ts";
21
+
22
+ export interface TargetScreenDeps {
23
+ pi: ExtensionAPI;
24
+ ctx: ExtensionContext;
25
+ role: WorkRole;
26
+ /** Present only when the hub (PocketBase) is configured; see `PiTrackerDeps.sessionTarget`. */
27
+ sessionTarget?: SessionTarget;
28
+ sessionClient: SessionClient;
29
+ /** Present only when the hub is configured. */
30
+ catalog?: Catalog;
31
+ refreshIdleStatus: (ctx: ExtensionContext) => void;
32
+ }
33
+
34
+ const SUBAGENT_NOTE = "Subagent session: the billing target is inherited from the orchestrator and cannot be changed here.";
35
+ const USE_DEFAULTS_VALUE = "__kankaku_use_defaults__";
36
+ const NO_PROJECT_VALUE = "__kankaku_no_project__";
37
+ const NO_TASK_VALUE = "__kankaku_no_task__";
38
+ const MAX_VISIBLE = 10;
39
+
40
+ /** `WorkTargetSourceName` ("session" | "project" | "repoPaths"), formatted for the read-only `source` row ("session" | "config" | "repoPaths"). */
41
+ function formatTargetSource(source: WorkTargetSourceName | undefined): string | undefined {
42
+ if (source === undefined) return undefined;
43
+ return source === "project" ? "config" : source;
44
+ }
45
+
46
+ /** Sort by `name`, ascending; never mutates the input. */
47
+ function sortByName<T extends { name: string }>(items: T[]): T[] {
48
+ return [...items].sort((a, b) => a.name.localeCompare(b.name));
49
+ }
50
+
51
+ /**
52
+ * Build `label -> task` options for the task submenu, sorted by title. When
53
+ * two tasks share the same title, disambiguate every colliding label with
54
+ * the task's `externalRef` when it has one, or its bare id otherwise —
55
+ * mirrors `adapters/target-picker.ts`'s private `labelTaskOptions` (not
56
+ * exported, so this is a small self-contained equivalent rather than a
57
+ * cross-module reach for a private helper).
58
+ */
59
+ function labelTasks(tasks: HubTask[]): Array<{ label: string; task: HubTask }> {
60
+ const sorted = [...tasks].sort((a, b) => a.title.localeCompare(b.title));
61
+ const titleCounts = new Map<string, number>();
62
+ for (const task of sorted) titleCounts.set(task.title, (titleCounts.get(task.title) ?? 0) + 1);
63
+ return sorted.map((task) => {
64
+ const collides = (titleCounts.get(task.title) ?? 0) > 1;
65
+ const label = collides ? `${task.title} (${task.externalRef ?? task.id})` : task.title;
66
+ return { label, task };
67
+ });
68
+ }
69
+
70
+ /** A one-line note shown as a submenu; closes back to the row that opened it on any key, without touching anything. */
71
+ class NoteComponent implements Component {
72
+ private readonly theme: SettingsListTheme;
73
+ private readonly text: string;
74
+ private readonly done: (selectedValue?: string, options?: { navigateTo?: string }) => void;
75
+
76
+ constructor(theme: SettingsListTheme, text: string, done: (selectedValue?: string, options?: { navigateTo?: string }) => void) {
77
+ this.theme = theme;
78
+ this.text = text;
79
+ this.done = done;
80
+ }
81
+
82
+ invalidate(): void {}
83
+
84
+ render(_width: number): string[] {
85
+ return [this.theme.hint(this.text)];
86
+ }
87
+
88
+ handleInput(_data: string): void {
89
+ this.done();
90
+ }
91
+ }
92
+
93
+ /**
94
+ * The legacy-label submenu: a pi-tui `Input` prefilled with the current
95
+ * label. While open, the panel shell must forward `q` as ordinary input
96
+ * (never close-on-q) — `PanelHost.setBodyWantsText` is set on construction
97
+ * and cleared whenever this component closes, on every path.
98
+ */
99
+ class LegacyLabelComponent implements Component {
100
+ private readonly theme: SettingsListTheme;
101
+ private readonly host: PanelHost;
102
+ private readonly onValid: (value: string | undefined) => void;
103
+ private readonly done: (selectedValue?: string, options?: { navigateTo?: string }) => void;
104
+ private readonly input: Input;
105
+ private invalidLabel: string | undefined;
106
+
107
+ constructor(
108
+ theme: SettingsListTheme,
109
+ initialValue: string,
110
+ host: PanelHost,
111
+ onValid: (value: string | undefined) => void,
112
+ done: (selectedValue?: string, options?: { navigateTo?: string }) => void,
113
+ ) {
114
+ this.theme = theme;
115
+ this.host = host;
116
+ this.onValid = onValid;
117
+ this.done = done;
118
+ this.input = new Input();
119
+ this.input.setValue(initialValue);
120
+ this.input.onSubmit = (value) => this.handleSubmit(value);
121
+ this.input.onEscape = () => this.handleEscape();
122
+ this.host.setBodyWantsText(true);
123
+ }
124
+
125
+ private handleSubmit(rawValue: string): void {
126
+ const trimmed = rawValue.trim();
127
+ if (trimmed.length > 0 && !isValidClient(trimmed)) {
128
+ this.invalidLabel = trimmed;
129
+ return;
130
+ }
131
+ this.close();
132
+ this.onValid(trimmed.length > 0 ? trimmed : undefined);
133
+ }
134
+
135
+ private handleEscape(): void {
136
+ this.close();
137
+ }
138
+
139
+ private close(): void {
140
+ this.host.setBodyWantsText(false);
141
+ // No `navigateTo`: the "legacy" row's own `SettingItem.submenu` would
142
+ // otherwise make `SettingsList.closeSubmenu()` reactivate it right back
143
+ // open (its `navigateAfterClose` mechanism reopens the target item's
144
+ // submenu when it has one — see settings-list.js). A successful submit
145
+ // rebuilds the list from scratch via `applyChange`, which already
146
+ // repositions the cursor on "legacy" itself; a cancel needs no
147
+ // rebuild at all — plain `done()` just restores the row selection.
148
+ this.done();
149
+ }
150
+
151
+ invalidate(): void {
152
+ this.input.invalidate();
153
+ }
154
+
155
+ render(width: number): string[] {
156
+ const lines = this.input.render(width);
157
+ if (this.invalidLabel === undefined) return lines;
158
+ return [...lines, this.theme.hint(`error: invalid label "${this.invalidLabel}"`)];
159
+ }
160
+
161
+ handleInput(data: string): void {
162
+ this.invalidLabel = undefined;
163
+ this.input.handleInput(data);
164
+ }
165
+ }
166
+
167
+ class TargetScreenComponent implements Component {
168
+ readonly searchable = false;
169
+ private readonly deps: TargetScreenDeps;
170
+ private readonly host: PanelHost;
171
+ private readonly settingsTheme: SettingsListTheme;
172
+ private list: SettingsList;
173
+
174
+ constructor(deps: TargetScreenDeps, host: PanelHost) {
175
+ this.deps = deps;
176
+ this.host = host;
177
+ this.settingsTheme = buildSettingsListTheme(host.theme);
178
+ this.list = this.buildList();
179
+ }
180
+
181
+ private currentRows(): PanelRow[] {
182
+ const sessionTarget = this.deps.sessionTarget;
183
+ const target = sessionTarget?.effectiveTarget();
184
+ const source = sessionTarget ? formatTargetSource(sessionTarget.effectiveSource()) : undefined;
185
+ const legacyLabel = this.deps.sessionClient.effectiveClient();
186
+ const legacySource = this.deps.sessionClient.effectiveSource();
187
+ return buildTargetRows({
188
+ hubConfigured: sessionTarget !== undefined,
189
+ target,
190
+ source,
191
+ legacyLabel,
192
+ legacySource,
193
+ });
194
+ }
195
+
196
+ private buildList(): SettingsList {
197
+ const items = this.currentRows().map((row) => this.buildItem(row));
198
+ return new SettingsList(
199
+ items,
200
+ MAX_VISIBLE,
201
+ this.settingsTheme,
202
+ () => {},
203
+ () => this.host.back(),
204
+ { enableSearch: false },
205
+ );
206
+ }
207
+
208
+ /** Rebuild the list from fresh state after a mutation, keep the cursor on `navigateToId`, and notify the host — see the feature doc's "After EVERY change" rule. */
209
+ private applyChange(navigateToId: string): void {
210
+ this.list = this.buildList();
211
+ this.list.selectItem(navigateToId);
212
+ this.deps.refreshIdleStatus(this.deps.ctx);
213
+ this.host.requestRender();
214
+ }
215
+
216
+ private buildItem(row: PanelRow): SettingItem {
217
+ switch (row.id) {
218
+ case "client":
219
+ return this.rowItem(row, (done) => this.buildClientSubmenu(done));
220
+ case "project":
221
+ return this.rowItem(row, (done) => this.buildProjectSubmenu(done));
222
+ case "task":
223
+ return this.rowItem(row, (done) => this.buildTaskSubmenu(done));
224
+ case "remember":
225
+ return this.buildRememberItem(row);
226
+ case "legacy":
227
+ return this.rowItem(row, (done) => this.buildLegacySubmenu(done));
228
+ case "source":
229
+ default:
230
+ return this.plainItem(row);
231
+ }
232
+ }
233
+
234
+ private plainItem(row: PanelRow): SettingItem {
235
+ return { id: row.id, label: row.label, currentValue: row.value, ...(row.description !== undefined ? { description: row.description } : {}) };
236
+ }
237
+
238
+ /**
239
+ * Wraps a row's submenu factory so opening it sets
240
+ * `PanelHost.setBodyCapturesEscape(true)` (the shell forwards Escape/←
241
+ * to this screen instead of popping it while the submenu — or the
242
+ * legacy-label `Input`, which also uses this via `buildLegacySubmenu` —
243
+ * is open) and clears it again right before the submenu's own `done` is
244
+ * invoked, on every close path (a pick, "— use defaults —"/"— none —",
245
+ * cancel, or a disabled note closing on any key).
246
+ */
247
+ private rowItem(row: PanelRow, submenu: (done: (selectedValue?: string, options?: { navigateTo?: string }) => void) => Component): SettingItem {
248
+ return {
249
+ ...this.plainItem(row),
250
+ submenu: (_currentValue, done) => {
251
+ this.host.setBodyCapturesEscape(true);
252
+ return submenu((selectedValue, options) => {
253
+ this.host.setBodyCapturesEscape(false);
254
+ done(selectedValue, options);
255
+ });
256
+ },
257
+ };
258
+ }
259
+
260
+ private buildClientSubmenu(done: (selectedValue?: string, options?: { navigateTo?: string }) => void): Component {
261
+ const clients = (this.deps.catalog?.read()?.clients ?? []).filter((client) => client.active && !client.unassigned);
262
+ const items: SelectItem[] = [
263
+ ...sortByName(clients).map((client) => ({ value: client.id, label: `${client.name} (${client.code})` })),
264
+ { value: USE_DEFAULTS_VALUE, label: "— use defaults —" },
265
+ ];
266
+ const list = new SelectList(items, MAX_VISIBLE, buildSelectListTheme(this.host.theme));
267
+ list.onSelect = (item) => {
268
+ if (item.value === USE_DEFAULTS_VALUE) {
269
+ this.deps.sessionTarget?.clear(this.deps.pi);
270
+ } else {
271
+ this.deps.sessionTarget?.setExplicit(this.deps.pi, { clientId: item.value });
272
+ }
273
+ done();
274
+ this.applyChange("client");
275
+ };
276
+ list.onCancel = () => done();
277
+ return list;
278
+ }
279
+
280
+ private buildProjectSubmenu(done: (selectedValue?: string, options?: { navigateTo?: string }) => void): Component {
281
+ const target = this.deps.sessionTarget?.effectiveTarget();
282
+ if (!target) {
283
+ return new NoteComponent(this.settingsTheme, "Pick a client first.", done);
284
+ }
285
+
286
+ const projects = (this.deps.catalog?.read()?.projects ?? []).filter((project) => project.active && project.clientId === target.clientId);
287
+ const items: SelectItem[] = [
288
+ ...sortByName(projects).map((project) => ({ value: project.id, label: project.name })),
289
+ { value: NO_PROJECT_VALUE, label: "(no project)" },
290
+ ];
291
+ const list = new SelectList(items, MAX_VISIBLE, buildSelectListTheme(this.host.theme));
292
+ list.onSelect = (item) => {
293
+ const projectId = item.value === NO_PROJECT_VALUE ? undefined : item.value;
294
+ this.deps.sessionTarget?.setExplicit(this.deps.pi, { clientId: target.clientId, ...(projectId !== undefined ? { projectId } : {}) });
295
+ done();
296
+ this.applyChange("project");
297
+ };
298
+ list.onCancel = () => done();
299
+ return list;
300
+ }
301
+
302
+ private buildTaskSubmenu(done: (selectedValue?: string, options?: { navigateTo?: string }) => void): Component {
303
+ const target = this.deps.sessionTarget?.effectiveTarget();
304
+ if (!target || target.projectId === undefined) {
305
+ return new NoteComponent(this.settingsTheme, "Pick a project first.", done);
306
+ }
307
+
308
+ const tasks = (this.deps.catalog?.read()?.tasks ?? []).filter((task) => task.status !== "done" && task.projectId === target.projectId);
309
+ if (tasks.length === 0) {
310
+ return new NoteComponent(this.settingsTheme, "No open tasks for this project in the hub", done);
311
+ }
312
+
313
+ const options = labelTasks(tasks);
314
+ const items: SelectItem[] = [...options.map((option) => ({ value: option.task.id, label: option.label })), { value: NO_TASK_VALUE, label: "— none —" }];
315
+ const list = new SelectList(items, MAX_VISIBLE, buildSelectListTheme(this.host.theme));
316
+ list.onSelect = (item) => {
317
+ if (item.value === NO_TASK_VALUE) {
318
+ this.deps.sessionTarget?.clearTask(this.deps.pi);
319
+ } else {
320
+ this.deps.sessionTarget?.setTask(this.deps.pi, item.value);
321
+ }
322
+ done();
323
+ this.applyChange("task");
324
+ };
325
+ list.onCancel = () => done();
326
+ return list;
327
+ }
328
+
329
+ private buildRememberItem(row: PanelRow): SettingItem {
330
+ const item = actionItem(this.settingsTheme, {
331
+ id: row.id,
332
+ label: row.label,
333
+ description: row.description,
334
+ run: () => {
335
+ const saved = this.deps.sessionTarget?.rememberTarget() ?? false;
336
+ return [saved ? "saved clientId/projectId to config.json" : "nothing to save"];
337
+ },
338
+ onOpen: () => this.host.setBodyCapturesEscape(true),
339
+ onClose: () => this.host.setBodyCapturesEscape(false),
340
+ onDone: () => this.host.requestRender(),
341
+ });
342
+ return { ...item, currentValue: row.value };
343
+ }
344
+
345
+ private buildLegacySubmenu(done: (selectedValue?: string, options?: { navigateTo?: string }) => void): Component {
346
+ const initial = this.deps.sessionClient.effectiveClient() ?? "";
347
+ return new LegacyLabelComponent(
348
+ this.settingsTheme,
349
+ initial,
350
+ this.host,
351
+ (value) => {
352
+ this.deps.sessionClient.set(this.deps.pi, value);
353
+ this.applyChange("legacy");
354
+ },
355
+ done,
356
+ );
357
+ }
358
+
359
+ invalidate(): void {
360
+ this.list.invalidate();
361
+ }
362
+
363
+ render(width: number): string[] {
364
+ return this.list.render(width);
365
+ }
366
+
367
+ handleInput(data: string): void {
368
+ this.list.handleInput(data);
369
+ }
370
+
371
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
372
+ return this.list.handleMouse?.(event);
373
+ }
374
+ }
375
+
376
+ /** `openKankakuPanel`'s `screens.target` factory; see {@link TargetScreenDeps}. */
377
+ export function createTargetScreen(deps: TargetScreenDeps): PanelScreenFactory {
378
+ return (host: PanelHost) => {
379
+ if (deps.role !== "orchestrator") {
380
+ return new Text(host.theme.fg("muted", SUBAGENT_NOTE));
381
+ }
382
+ return new TargetScreenComponent(deps, host);
383
+ };
384
+ }