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,89 @@
1
+ /**
2
+ * The `/kankaku` panel's `doctor` screen (see odd/tasks/kankaku-panel.md):
3
+ * renders `buildDoctorLines`' output (the exact lines `/kankaku doctor`
4
+ * shows) in a bounded body, with `pin`/`refresh` actions.
5
+ */
6
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import type { Component, SettingsListTheme, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
8
+ import { SettingsList } from "@earendil-works/pi-tui";
9
+ import { buildDoctorLines } from "../../kankaku-command.ts";
10
+ import type { KankakuCommandDeps, KankakuReportData } from "../../kankaku-command.ts";
11
+ import type { PanelHost, PanelScreenFactory } from "../kankaku-panel.ts";
12
+ import { actionItem } from "../panel-items.ts";
13
+ import { renderBoundedLines } from "../panel-lines.ts";
14
+ import { buildSettingsListTheme } from "../panel-theme.ts";
15
+
16
+ export interface DoctorScreenDeps {
17
+ commandDeps: KankakuCommandDeps;
18
+ ctx: ExtensionContext;
19
+ pinReport: (report: KankakuReportData) => void;
20
+ }
21
+
22
+ const MAX_VISIBLE = 4;
23
+ const MAX_BODY_LINES = 14;
24
+
25
+ class DoctorScreenComponent implements Component {
26
+ readonly searchable = false;
27
+ private readonly deps: DoctorScreenDeps;
28
+ private readonly host: PanelHost;
29
+ private readonly settingsTheme: SettingsListTheme;
30
+ private lines: string[];
31
+ private list: SettingsList;
32
+
33
+ constructor(deps: DoctorScreenDeps, host: PanelHost) {
34
+ this.deps = deps;
35
+ this.host = host;
36
+ this.settingsTheme = buildSettingsListTheme(host.theme);
37
+ this.lines = buildDoctorLines(deps.commandDeps, deps.ctx);
38
+ this.list = this.buildList();
39
+ }
40
+
41
+ private buildList(): SettingsList {
42
+ const items = [
43
+ actionItem(this.settingsTheme, {
44
+ id: "pin",
45
+ label: "Pin to chat",
46
+ run: () => {
47
+ this.deps.pinReport({ title: "doctor", lines: this.lines });
48
+ return ["pinned to the chat transcript"];
49
+ },
50
+ onOpen: () => this.host.setBodyCapturesEscape(true),
51
+ onClose: () => this.host.setBodyCapturesEscape(false),
52
+ onDone: () => this.host.requestRender(),
53
+ }),
54
+ actionItem(this.settingsTheme, {
55
+ id: "refresh",
56
+ label: "Refresh",
57
+ run: () => {
58
+ this.lines = buildDoctorLines(this.deps.commandDeps, this.deps.ctx);
59
+ return ["refreshed"];
60
+ },
61
+ onOpen: () => this.host.setBodyCapturesEscape(true),
62
+ onClose: () => this.host.setBodyCapturesEscape(false),
63
+ onDone: () => this.host.requestRender(),
64
+ }),
65
+ ];
66
+ return new SettingsList(items, MAX_VISIBLE, this.settingsTheme, () => {}, () => this.host.back(), { enableSearch: false });
67
+ }
68
+
69
+ invalidate(): void {
70
+ this.list.invalidate();
71
+ }
72
+
73
+ render(width: number): string[] {
74
+ return [...this.list.render(width), "", ...renderBoundedLines(this.lines, MAX_BODY_LINES)];
75
+ }
76
+
77
+ handleInput(data: string): void {
78
+ this.list.handleInput(data);
79
+ }
80
+
81
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
82
+ return this.list.handleMouse?.(event);
83
+ }
84
+ }
85
+
86
+ /** `openKankakuPanel`'s `screens.doctor` factory; see {@link DoctorScreenDeps}. */
87
+ export function createDoctorScreen(deps: DoctorScreenDeps): PanelScreenFactory {
88
+ return (host: PanelHost) => new DoctorScreenComponent(deps, host);
89
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The `/kankaku` panel's `export` screen (see odd/tasks/kankaku-panel.md
3
+ * P4): `Format`/`Range` rows plus a `Write file` action that computes the
4
+ * export content through the exact same `buildExportContent`
5
+ * (`adapters/report-views.ts`) `/kankaku export` uses, so the panel and the
6
+ * subcommand can never drift. `Pin to chat` pins a short note of the last
7
+ * write (or the current selection, before anything has been written).
8
+ */
9
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import type { Component, SettingItem, SettingsListTheme, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
11
+ import { SettingsList } from "@earendil-works/pi-tui";
12
+ import type { KankakuReportData } from "../../kankaku-command.ts";
13
+ import { buildExportContent } from "../../report-views.ts";
14
+ import type { WorkLog } from "../../../ports/work-log.ts";
15
+ import type { PanelHost, PanelScreenFactory } from "../kankaku-panel.ts";
16
+ import { actionItem } from "../panel-items.ts";
17
+ import { buildSettingsListTheme } from "../panel-theme.ts";
18
+
19
+ export interface ExportScreenDeps {
20
+ log: WorkLog;
21
+ /**
22
+ * Write an export file (name, content) under the kankaku dir and return
23
+ * its absolute path — the same dep `/kankaku export` uses. When absent,
24
+ * `Write file` reports the exact message the subcommand gives today
25
+ * ("export is not configured").
26
+ */
27
+ writeExportFile?: (name: string, content: string) => string;
28
+ ctx: ExtensionContext;
29
+ pinReport: (report: KankakuReportData) => void;
30
+ }
31
+
32
+ type ExportFormat = "csv" | "json";
33
+ type ExportRange = "today" | "all";
34
+
35
+ const FORMAT_VALUES: ExportFormat[] = ["csv", "json"];
36
+ const RANGE_VALUES: ExportRange[] = ["today", "all"];
37
+ const MAX_VISIBLE = 4;
38
+ const EXPORT_NOT_CONFIGURED = "export is not configured";
39
+
40
+ class ExportScreenComponent implements Component {
41
+ readonly searchable = false;
42
+ private readonly deps: ExportScreenDeps;
43
+ private readonly host: PanelHost;
44
+ private readonly settingsTheme: SettingsListTheme;
45
+ private format: ExportFormat = "csv";
46
+ private range: ExportRange = "today";
47
+ private lastNote = "nothing written yet";
48
+ private list: SettingsList;
49
+
50
+ constructor(deps: ExportScreenDeps, host: PanelHost) {
51
+ this.deps = deps;
52
+ this.host = host;
53
+ this.settingsTheme = buildSettingsListTheme(host.theme);
54
+ this.list = this.buildList();
55
+ }
56
+
57
+ private buildList(): SettingsList {
58
+ const items: SettingItem[] = [
59
+ { id: "format", label: "Format", currentValue: this.format, values: FORMAT_VALUES },
60
+ { id: "range", label: "Range", currentValue: this.range, values: RANGE_VALUES },
61
+ actionItem(this.settingsTheme, {
62
+ id: "write",
63
+ label: "Write file",
64
+ run: () => {
65
+ if (!this.deps.writeExportFile) {
66
+ throw new Error(EXPORT_NOT_CONFIGURED);
67
+ }
68
+ const { name, content } = buildExportContent(this.deps.log.readAll(), { format: this.format, all: this.range === "all" });
69
+ const path = this.deps.writeExportFile(name, content);
70
+ this.lastNote = `wrote ${path}`;
71
+ return [this.lastNote];
72
+ },
73
+ onOpen: () => this.host.setBodyCapturesEscape(true),
74
+ onClose: () => this.host.setBodyCapturesEscape(false),
75
+ onDone: () => this.host.requestRender(),
76
+ }),
77
+ actionItem(this.settingsTheme, {
78
+ id: "pin",
79
+ label: "Pin to chat",
80
+ run: () => {
81
+ this.deps.pinReport({ title: "export", lines: [this.lastNote] });
82
+ return ["pinned to the chat transcript"];
83
+ },
84
+ onOpen: () => this.host.setBodyCapturesEscape(true),
85
+ onClose: () => this.host.setBodyCapturesEscape(false),
86
+ onDone: () => this.host.requestRender(),
87
+ }),
88
+ ];
89
+ return new SettingsList(items, MAX_VISIBLE, this.settingsTheme, (id, value) => this.onChange(id, value), () => this.host.back(), {
90
+ enableSearch: false,
91
+ });
92
+ }
93
+
94
+ private onChange(id: string, value: string): void {
95
+ if (id === "format") this.format = value as ExportFormat;
96
+ else if (id === "range") this.range = value as ExportRange;
97
+
98
+ this.list = this.buildList();
99
+ this.list.selectItem(id);
100
+ this.host.requestRender();
101
+ }
102
+
103
+ invalidate(): void {
104
+ this.list.invalidate();
105
+ }
106
+
107
+ render(width: number): string[] {
108
+ return this.list.render(width);
109
+ }
110
+
111
+ handleInput(data: string): void {
112
+ this.list.handleInput(data);
113
+ }
114
+
115
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
116
+ return this.list.handleMouse?.(event);
117
+ }
118
+ }
119
+
120
+ /** `openKankakuPanel`'s `screens.export` factory; see {@link ExportScreenDeps}. */
121
+ export function createExportScreen(deps: ExportScreenDeps): PanelScreenFactory {
122
+ return (host: PanelHost) => new ExportScreenComponent(deps, host);
123
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The `/kankaku` panel's `report` screen (see odd/tasks/kankaku-panel.md):
3
+ * `view`/`range`/`scope` rows drive the same five report builders
4
+ * (`adapters/report-views.ts`) the `/kankaku` subcommands use, so this
5
+ * screen can never drift from `tasks`/`sessions`/`clients`/`projects`/the
6
+ * plain summary. A `pin` action appends the current view's report as a
7
+ * durable chat entry, exactly like every subcommand already does.
8
+ */
9
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import type { Component, SettingItem, SettingsListTheme, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
11
+ import { SettingsList } from "@earendil-works/pi-tui";
12
+ import type { KankakuReportData } from "../../kankaku-command.ts";
13
+ import { buildClientsView, buildProjectsView, buildSessionsView, buildSummaryView, buildTasksView } from "../../report-views.ts";
14
+ import type { WorkLog } from "../../../ports/work-log.ts";
15
+ import type { PanelHost, PanelScreenFactory } from "../kankaku-panel.ts";
16
+ import { renderBoundedLines } from "../panel-lines.ts";
17
+ import { actionItem } from "../panel-items.ts";
18
+ import { buildSettingsListTheme } from "../panel-theme.ts";
19
+
20
+ export interface ReportScreenDeps {
21
+ log: WorkLog;
22
+ ctx: ExtensionContext;
23
+ pi: ExtensionAPI;
24
+ /** Resolves this session's id, e.g. `() => ctx.sessionManager.getSessionId()`. Used by the `tasks` view's `scope` row. */
25
+ sessionId?: () => string | undefined;
26
+ pinReport: (report: KankakuReportData) => void;
27
+ }
28
+
29
+ type ReportView = "summary" | "tasks" | "sessions" | "clients" | "projects";
30
+ type RangeValue = "today" | "all";
31
+ type ScopeValue = "session" | "all sessions";
32
+
33
+ const VIEW_VALUES: ReportView[] = ["summary", "tasks", "sessions", "clients", "projects"];
34
+ const RANGE_VALUES: RangeValue[] = ["today", "all"];
35
+ const SCOPE_VALUES: ScopeValue[] = ["session", "all sessions"];
36
+ const MAX_VISIBLE = 6;
37
+ const MAX_BODY_LINES = 14;
38
+ const NOT_APPLICABLE = "n/a";
39
+
40
+ class ReportScreenComponent implements Component {
41
+ readonly searchable = false;
42
+ private readonly deps: ReportScreenDeps;
43
+ private readonly host: PanelHost;
44
+ private readonly settingsTheme: SettingsListTheme;
45
+ private view: ReportView = "summary";
46
+ private range: RangeValue = "today";
47
+ private scope: ScopeValue = "session";
48
+ private report: KankakuReportData;
49
+ private list: SettingsList;
50
+
51
+ constructor(deps: ReportScreenDeps, host: PanelHost) {
52
+ this.deps = deps;
53
+ this.host = host;
54
+ this.settingsTheme = buildSettingsListTheme(host.theme);
55
+ this.report = this.computeReport();
56
+ this.list = this.buildList();
57
+ }
58
+
59
+ private computeReport(): KankakuReportData {
60
+ const records = this.deps.log.readAll();
61
+ switch (this.view) {
62
+ case "tasks":
63
+ return buildTasksView(records, { all: this.scope === "all sessions", sessionId: this.deps.sessionId?.() });
64
+ case "sessions":
65
+ return buildSessionsView(records, { all: this.range === "all" });
66
+ case "clients":
67
+ return buildClientsView(records, { all: this.range === "all" });
68
+ case "projects":
69
+ return buildProjectsView(records, { all: this.range === "all" });
70
+ case "summary":
71
+ default:
72
+ return buildSummaryView(records, { all: this.range === "all" });
73
+ }
74
+ }
75
+
76
+ private buildRangeItem(): SettingItem {
77
+ if (this.view === "tasks") {
78
+ return { id: "range", label: "Range", currentValue: NOT_APPLICABLE, description: "the tasks view uses scope, not range" };
79
+ }
80
+ return { id: "range", label: "Range", currentValue: this.range, values: RANGE_VALUES };
81
+ }
82
+
83
+ private buildScopeItem(): SettingItem {
84
+ if (this.view !== "tasks") {
85
+ return { id: "scope", label: "Scope", currentValue: NOT_APPLICABLE, description: "applies only to the tasks view" };
86
+ }
87
+ return { id: "scope", label: "Scope", currentValue: this.scope, values: SCOPE_VALUES };
88
+ }
89
+
90
+ private buildList(): SettingsList {
91
+ const items: SettingItem[] = [
92
+ { id: "view", label: "View", currentValue: this.view, values: VIEW_VALUES },
93
+ this.buildRangeItem(),
94
+ this.buildScopeItem(),
95
+ actionItem(this.settingsTheme, {
96
+ id: "pin",
97
+ label: "Pin to chat",
98
+ run: () => {
99
+ this.deps.pinReport(this.report);
100
+ return ["pinned to the chat transcript"];
101
+ },
102
+ onOpen: () => this.host.setBodyCapturesEscape(true),
103
+ onClose: () => this.host.setBodyCapturesEscape(false),
104
+ onDone: () => this.host.requestRender(),
105
+ }),
106
+ ];
107
+ return new SettingsList(items, MAX_VISIBLE, this.settingsTheme, (id, value) => this.onChange(id, value), () => this.host.back(), {
108
+ enableSearch: false,
109
+ });
110
+ }
111
+
112
+ private onChange(id: string, value: string): void {
113
+ if (id === "view") this.view = value as ReportView;
114
+ else if (id === "range") this.range = value as RangeValue;
115
+ else if (id === "scope") this.scope = value as ScopeValue;
116
+
117
+ this.report = this.computeReport();
118
+ this.list = this.buildList();
119
+ this.list.selectItem(id);
120
+ this.host.requestRender();
121
+ }
122
+
123
+ invalidate(): void {
124
+ this.list.invalidate();
125
+ }
126
+
127
+ render(width: number): string[] {
128
+ return [...this.list.render(width), "", ...renderBoundedLines(this.report.lines, MAX_BODY_LINES)];
129
+ }
130
+
131
+ handleInput(data: string): void {
132
+ this.list.handleInput(data);
133
+ }
134
+
135
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
136
+ return this.list.handleMouse?.(event);
137
+ }
138
+ }
139
+
140
+ /** `openKankakuPanel`'s `screens.report` factory; see {@link ReportScreenDeps}. */
141
+ export function createReportScreen(deps: ReportScreenDeps): PanelScreenFactory {
142
+ return (host: PanelHost) => new ReportScreenComponent(deps, host);
143
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The `/kankaku` panel's `sync` screen (see odd/tasks/kankaku-panel.md P4):
3
+ * status lines (`hub-actions.ts#buildSyncStatusLines`) plus `Sync now`,
4
+ * `Sync all`, `Backfill` and `Refresh catalog` actions, all sharing the
5
+ * exact line-building the `/kankaku sync`/`backfill`/`catalog refresh`
6
+ * subcommands use (`adapters/hub-actions.ts`), so this screen can never
7
+ * drift from them. `Pin to chat` pins the current status lines, exactly
8
+ * like `/kankaku sync status` already does.
9
+ */
10
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ import type { Component, SettingItem, SettingsListTheme, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
12
+ import { SettingsList } from "@earendil-works/pi-tui";
13
+ import type { SyncCommandDeps, KankakuReportData } from "../../kankaku-command.ts";
14
+ import { buildSyncStatusLines, formatBackfillLines, formatCatalogRefreshLines, formatSyncSummaryLines } from "../../hub-actions.ts";
15
+ import type { Catalog } from "../../../ports/catalog.ts";
16
+ import type { PanelHost, PanelScreenFactory } from "../kankaku-panel.ts";
17
+ import { actionItem } from "../panel-items.ts";
18
+ import { renderBoundedLines } from "../panel-lines.ts";
19
+ import { buildSettingsListTheme } from "../panel-theme.ts";
20
+
21
+ export interface SyncScreenDeps {
22
+ sync: SyncCommandDeps;
23
+ /** Present only when the hub is configured; the `Refresh catalog` row is omitted entirely without one. */
24
+ catalog?: Catalog;
25
+ ctx: ExtensionContext;
26
+ refreshIdleStatus: (ctx: ExtensionContext) => void;
27
+ pinReport: (report: KankakuReportData) => void;
28
+ }
29
+
30
+ const MAX_VISIBLE = 5;
31
+ const MAX_BODY_LINES = 14;
32
+ const STATUS_TITLE = "sync status";
33
+
34
+ class SyncScreenComponent implements Component {
35
+ readonly searchable = false;
36
+ private readonly deps: SyncScreenDeps;
37
+ private readonly host: PanelHost;
38
+ private readonly settingsTheme: SettingsListTheme;
39
+ private statusLines: string[];
40
+ private list: SettingsList;
41
+
42
+ constructor(deps: SyncScreenDeps, host: PanelHost) {
43
+ this.deps = deps;
44
+ this.host = host;
45
+ this.settingsTheme = buildSettingsListTheme(host.theme);
46
+ this.statusLines = buildSyncStatusLines(this.deps.sync.status());
47
+ this.list = this.buildList();
48
+ }
49
+
50
+ /** Refreshed after every action, per the feature doc: recompute the status body, refresh the idle status line, and re-render. */
51
+ private refreshStatus(): void {
52
+ this.statusLines = buildSyncStatusLines(this.deps.sync.status());
53
+ this.deps.refreshIdleStatus(this.deps.ctx);
54
+ this.host.requestRender();
55
+ }
56
+
57
+ private buildList(): SettingsList {
58
+ const items: SettingItem[] = [
59
+ actionItem(this.settingsTheme, {
60
+ id: "sync-now",
61
+ label: "Sync now",
62
+ run: async () => formatSyncSummaryLines(await this.deps.sync.run({})),
63
+ onOpen: () => this.host.setBodyCapturesEscape(true),
64
+ onClose: () => this.host.setBodyCapturesEscape(false),
65
+ onDone: () => this.refreshStatus(),
66
+ }),
67
+ actionItem(this.settingsTheme, {
68
+ id: "sync-all",
69
+ label: "Sync all",
70
+ run: async () => formatSyncSummaryLines(await this.deps.sync.run({ full: true })),
71
+ onOpen: () => this.host.setBodyCapturesEscape(true),
72
+ onClose: () => this.host.setBodyCapturesEscape(false),
73
+ onDone: () => this.refreshStatus(),
74
+ }),
75
+ actionItem(this.settingsTheme, {
76
+ id: "backfill",
77
+ label: "Backfill",
78
+ run: async () => formatBackfillLines(await this.deps.sync.run({ full: true })),
79
+ onOpen: () => this.host.setBodyCapturesEscape(true),
80
+ onClose: () => this.host.setBodyCapturesEscape(false),
81
+ onDone: () => this.refreshStatus(),
82
+ }),
83
+ ];
84
+
85
+ const catalog = this.deps.catalog;
86
+ if (catalog) {
87
+ items.push(
88
+ actionItem(this.settingsTheme, {
89
+ id: "catalog-refresh",
90
+ label: "Refresh catalog",
91
+ run: async () => formatCatalogRefreshLines(await catalog.refresh()),
92
+ onOpen: () => this.host.setBodyCapturesEscape(true),
93
+ onClose: () => this.host.setBodyCapturesEscape(false),
94
+ onDone: () => this.refreshStatus(),
95
+ }),
96
+ );
97
+ }
98
+
99
+ items.push(
100
+ actionItem(this.settingsTheme, {
101
+ id: "pin",
102
+ label: "Pin to chat",
103
+ run: () => {
104
+ this.deps.pinReport({ title: STATUS_TITLE, lines: this.statusLines });
105
+ return ["pinned to the chat transcript"];
106
+ },
107
+ onOpen: () => this.host.setBodyCapturesEscape(true),
108
+ onClose: () => this.host.setBodyCapturesEscape(false),
109
+ onDone: () => this.host.requestRender(),
110
+ }),
111
+ );
112
+
113
+ return new SettingsList(items, MAX_VISIBLE, this.settingsTheme, () => {}, () => this.host.back(), { enableSearch: false });
114
+ }
115
+
116
+ invalidate(): void {
117
+ this.list.invalidate();
118
+ }
119
+
120
+ render(width: number): string[] {
121
+ return [...this.list.render(width), "", ...renderBoundedLines(this.statusLines, MAX_BODY_LINES)];
122
+ }
123
+
124
+ handleInput(data: string): void {
125
+ this.list.handleInput(data);
126
+ }
127
+
128
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
129
+ return this.list.handleMouse?.(event);
130
+ }
131
+ }
132
+
133
+ /** `openKankakuPanel`'s `screens.sync` factory; see {@link SyncScreenDeps}. */
134
+ export function createSyncScreen(deps: SyncScreenDeps): PanelScreenFactory {
135
+ return (host: PanelHost) => new SyncScreenComponent(deps, host);
136
+ }