@kontextmind/kxm 0.7.40 → 0.7.43

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 (177) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +41 -7
  3. package/docs/README.md +5 -3
  4. package/docs/agent-skills.md +1 -1
  5. package/docs/architecture.md +1 -1
  6. package/docs/configuration.md +28 -5
  7. package/docs/{vnext → contracts}/README.md +7 -7
  8. package/docs/{vnext → contracts}/architecture.md +2 -2
  9. package/docs/{vnext → contracts}/migration.md +25 -25
  10. package/docs/{vnext → contracts}/routing.md +2 -2
  11. package/docs/{vnext → contracts}/synchronization.md +1 -1
  12. package/docs/{vnext → contracts}/validation.md +7 -7
  13. package/docs/getting-started.md +1 -1
  14. package/docs/kb/qa-authentik-authentication.md +47 -0
  15. package/docs/kb/qa-extension-install-and-hub-bootstrap.md +68 -0
  16. package/docs/kb/qa-hub-on-a-public-host.md +31 -0
  17. package/docs/kb/qa-sqlite-vs-duckdb.md +18 -0
  18. package/docs/kb/qa-what-the-hub-stores.md +47 -0
  19. package/docs/packages.md +91 -0
  20. package/docs/templates/architecture.md +1 -1
  21. package/docs/test-matrix.md +6 -6
  22. package/docs/tui-components.md +131 -0
  23. package/examples/README.md +2 -2
  24. package/examples/{vnext → project}/.kxm/agents/critic-3.yaml +1 -1
  25. package/examples/{vnext → project}/README.md +4 -4
  26. package/package.json +18 -8
  27. package/packages/core/tui/CHANGELOG.md +19 -0
  28. package/packages/core/tui/LICENSE +21 -0
  29. package/packages/core/tui/README.md +45 -0
  30. package/packages/core/tui/dist/index.js +1439 -0
  31. package/packages/core/tui/src/adapters/pi.ts +73 -0
  32. package/packages/core/tui/src/adapters/terminal.ts +106 -0
  33. package/packages/core/tui/src/exports/index.ts +30 -0
  34. package/packages/core/tui/src/services/registry.ts +274 -0
  35. package/packages/core/tui/src/tui/keys.ts +130 -0
  36. package/packages/core/tui/src/tui/layout.ts +117 -0
  37. package/packages/core/tui/src/tui/panel.ts +435 -0
  38. package/packages/core/tui/src/tui/panelComponent.ts +208 -0
  39. package/packages/core/tui/src/tui/render.ts +309 -0
  40. package/packages/core/tui/src/tui/theme.ts +145 -0
  41. package/packages/core/tui/src/types/surface.ts +407 -0
  42. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  43. package/plugins/kxm/dist/cli.js +681 -553
  44. package/plugins/kxm/dist/extension.js +18 -18
  45. package/plugins/kxm/dist/mcp-server.js +1 -1
  46. package/plugins/kxm/dist/{vnext-runtime-supervisor.js → runtime-supervisor.js} +553 -553
  47. package/plugins/kxm/dist/runtime.js +608 -608
  48. package/plugins/kxm/dist/server.js +13 -13
  49. package/plugins/kxm/package.json +2 -2
  50. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +2 -2
  51. package/plugins/kxm/skills/kxm-runs/SKILL.md +3 -3
  52. package/plugins/kxm/skills/kxm-session/SKILL.md +1 -1
  53. package/plugins/kxm/skills/kxm-workflow/SKILL.md +1 -1
  54. package/plugins/kxm/src/autocomplete.ts +4 -4
  55. package/plugins/kxm/src/{vnext-bindings.ts → bindings.ts} +48 -48
  56. package/plugins/kxm/src/cli/hub.ts +3 -3
  57. package/plugins/kxm/src/cli/{vnext.ts → project.ts} +106 -106
  58. package/plugins/kxm/src/cli/roles.ts +9 -9
  59. package/plugins/kxm/src/cli/system.ts +2 -2
  60. package/plugins/kxm/src/cli/tasks.ts +8 -8
  61. package/plugins/kxm/src/cli/workflows.ts +8 -8
  62. package/plugins/kxm/src/cli.ts +47 -47
  63. package/plugins/kxm/src/config.ts +69 -14
  64. package/plugins/kxm/src/database.ts +4 -4
  65. package/plugins/kxm/src/{vnext-engine-artifacts.ts → engine-artifacts.ts} +7 -7
  66. package/plugins/kxm/src/{vnext-engine-command.ts → engine-command.ts} +40 -40
  67. package/plugins/kxm/src/{vnext-engine-compile.ts → engine-compile.ts} +64 -64
  68. package/plugins/kxm/src/{vnext-engine-evidence.ts → engine-evidence.ts} +26 -26
  69. package/plugins/kxm/src/{vnext-engine-fold.ts → engine-fold.ts} +99 -99
  70. package/plugins/kxm/src/{vnext-engine-gate-records.ts → engine-gate-records.ts} +105 -105
  71. package/plugins/kxm/src/{vnext-engine-plan.ts → engine-plan.ts} +69 -69
  72. package/plugins/kxm/src/{vnext-engine.ts → engine.ts} +502 -502
  73. package/plugins/kxm/src/gate-hash.ts +10 -0
  74. package/plugins/kxm/src/{vnext-harness.ts → harness.ts} +1 -1
  75. package/plugins/kxm/src/hub.ts +1 -1
  76. package/plugins/kxm/src/init-guide-setup.ts +3 -3
  77. package/plugins/kxm/src/{vnext-init.ts → init.ts} +93 -93
  78. package/plugins/kxm/src/kxm-install-kind.ts +1 -1
  79. package/plugins/kxm/src/kxm-update-config.ts +2 -2
  80. package/plugins/kxm/src/local-snapshot.ts +23 -23
  81. package/plugins/kxm/src/mcp-server.ts +1 -1
  82. package/plugins/kxm/src/{vnext-migrate.ts → migrate.ts} +123 -123
  83. package/plugins/kxm/src/model-inventory.ts +1 -1
  84. package/plugins/kxm/src/{vnext-oneshot-evidence.ts → oneshot-evidence.ts} +2 -2
  85. package/plugins/kxm/src/{vnext-oneshot-process.ts → oneshot-process.ts} +4 -4
  86. package/plugins/kxm/src/{vnext-oneshot-producer.ts → oneshot-producer.ts} +24 -24
  87. package/plugins/kxm/src/{vnext-permission.ts → permission.ts} +86 -86
  88. package/plugins/kxm/src/{vnext-pi-producer.ts → pi-producer.ts} +13 -13
  89. package/plugins/kxm/src/{vnext-config.ts → project-config.ts} +177 -177
  90. package/plugins/kxm/src/{vnext-repair.ts → repair.ts} +159 -159
  91. package/plugins/kxm/src/restricted-yaml.d.mts +3 -3
  92. package/plugins/kxm/src/restricted-yaml.mjs +4 -4
  93. package/plugins/kxm/src/{vnext-runtime-owner.ts → runtime-owner.ts} +35 -35
  94. package/plugins/kxm/src/{vnext-runtime.ts → runtime-service.ts} +136 -136
  95. package/plugins/kxm/src/{vnext-runtime-store.ts → runtime-store.ts} +145 -145
  96. package/plugins/kxm/src/{vnext-runtime-supervisor.ts → runtime-supervisor.ts} +80 -80
  97. package/plugins/kxm/src/runtime.ts +6 -6
  98. package/plugins/kxm/src/session-work.ts +3 -3
  99. package/plugins/kxm/src/studio-layout.ts +6 -6
  100. package/plugins/kxm/src/{vnext-template.ts → template.ts} +33 -33
  101. package/plugins/kxm/src/tui.ts +10 -24
  102. package/schemas/{vnext/README.md → README.md} +3 -3
  103. package/schemas/{vnext/agent.schema.json → agent.schema.json} +1 -1
  104. package/schemas/{vnext/assignment-result.schema.json → assignment-result.schema.json} +1 -1
  105. package/schemas/{vnext/backup-manifest.schema.json → backup-manifest.schema.json} +1 -1
  106. package/schemas/{vnext/candidate.schema.json → candidate.schema.json} +1 -1
  107. package/schemas/{vnext/common.schema.json → common.schema.json} +2 -2
  108. package/schemas/{vnext/context-candidate.schema.json → context-candidate.schema.json} +1 -1
  109. package/schemas/{vnext/context-packet.schema.json → context-packet.schema.json} +1 -1
  110. package/schemas/{vnext/delivery-manifest.schema.json → delivery-manifest.schema.json} +1 -1
  111. package/schemas/{vnext/drive-receipt.schema.json → drive-receipt.schema.json} +2 -2
  112. package/schemas/{vnext/environment.schema.json → environment.schema.json} +1 -1
  113. package/schemas/{vnext/gate-registry.schema.json → gate-registry.schema.json} +1 -1
  114. package/schemas/{vnext/handoff-manifest.schema.json → handoff-manifest.schema.json} +1 -1
  115. package/schemas/{vnext/init-operation.schema.json → init-operation.schema.json} +1 -1
  116. package/schemas/{vnext/local-repository-bindings.schema.json → local-repository-bindings.schema.json} +1 -1
  117. package/schemas/{vnext/memory-record.schema.json → memory-record.schema.json} +1 -1
  118. package/schemas/{vnext/migration-decision.schema.json → migration-decision.schema.json} +1 -1
  119. package/schemas/{vnext/migration-plan.schema.json → migration-plan.schema.json} +1 -1
  120. package/schemas/{vnext/migration-receipt.schema.json → migration-receipt.schema.json} +1 -1
  121. package/schemas/{vnext/model.schema.json → model.schema.json} +1 -1
  122. package/schemas/{vnext/modes.schema.json → modes.schema.json} +1 -1
  123. package/schemas/{vnext/permission-diff.schema.json → permission-diff.schema.json} +1 -1
  124. package/schemas/policy-draft/README.md +2 -2
  125. package/schemas/{vnext/prices.schema.json → prices.schema.json} +1 -1
  126. package/schemas/{vnext/project.schema.json → project.schema.json} +1 -1
  127. package/schemas/{vnext/repository.schema.json → repository.schema.json} +1 -1
  128. package/schemas/{vnext/role.schema.json → role.schema.json} +1 -1
  129. package/schemas/{vnext/run-event.schema.json → run-event.schema.json} +1 -1
  130. package/schemas/{vnext/session-brief.schema.json → session-brief.schema.json} +2 -2
  131. package/schemas/{vnext/sync-event.schema.json → sync-event.schema.json} +1 -1
  132. package/schemas/{vnext/template-provenance.schema.json → template-provenance.schema.json} +1 -1
  133. package/schemas/{vnext/workflow.schema.json → workflow.schema.json} +1 -1
  134. package/scripts/build-package.mjs +59 -0
  135. package/scripts/build-runtime.mjs +3 -2
  136. package/scripts/check-generated.mjs +2 -1
  137. package/scripts/check-versions.mjs +12 -1
  138. package/scripts/docker-install-smoke.mjs +268 -0
  139. package/scripts/emit-codex-artifacts.mjs +1 -1
  140. package/scripts/kxm-bump-version.mjs +53 -3
  141. package/scripts/kxm-runtime-supervisor.mjs +3 -3
  142. package/scripts/package-surfaces.mjs +39 -0
  143. package/plugins/kxm/src/vnext-gate-hash.ts +0 -10
  144. /package/docs/{vnext → contracts}/effects-and-recovery.md +0 -0
  145. /package/docs/{vnext → contracts}/lifecycles.md +0 -0
  146. /package/docs/{vnext → contracts}/terminology.md +0 -0
  147. /package/examples/{vnext → project}/.kxm/agents/coordinator.yaml +0 -0
  148. /package/examples/{vnext → project}/.kxm/agents/critic-1.yaml +0 -0
  149. /package/examples/{vnext → project}/.kxm/agents/critic-2.yaml +0 -0
  150. /package/examples/{vnext → project}/.kxm/agents/implementer.yaml +0 -0
  151. /package/examples/{vnext → project}/.kxm/agents/planner.yaml +0 -0
  152. /package/examples/{vnext → project}/.kxm/agents/reproducer.yaml +0 -0
  153. /package/examples/{vnext → project}/.kxm/agents/reviewer.yaml +0 -0
  154. /package/examples/{vnext → project}/.kxm/gates.yaml +0 -0
  155. /package/examples/{vnext → project}/.kxm/models/critic-claude.yaml +0 -0
  156. /package/examples/{vnext → project}/.kxm/models/critic-gemini.yaml +0 -0
  157. /package/examples/{vnext → project}/.kxm/models/critic-grok.yaml +0 -0
  158. /package/examples/{vnext → project}/.kxm/models/implementation.yaml +0 -0
  159. /package/examples/{vnext → project}/.kxm/models/primary.yaml +0 -0
  160. /package/examples/{vnext → project}/.kxm/prices.yaml +0 -0
  161. /package/examples/{vnext → project}/.kxm/project/env.yaml +0 -0
  162. /package/examples/{vnext → project}/.kxm/project.yaml +0 -0
  163. /package/examples/{vnext → project}/.kxm/repo/repo.yaml +0 -0
  164. /package/examples/{vnext → project}/.kxm/workflows/default.yaml +0 -0
  165. /package/examples/{vnext → project}/.kxm/workflows/fix.yaml +0 -0
  166. /package/examples/{vnext → project}/.kxm/workflows/improve.yaml +0 -0
  167. /package/examples/{vnext → project}/records/assignment-result-recorded.json +0 -0
  168. /package/examples/{vnext → project}/records/assignment-result.json +0 -0
  169. /package/examples/{vnext → project}/records/context-candidate.json +0 -0
  170. /package/examples/{vnext → project}/records/delivery-manifest.json +0 -0
  171. /package/examples/{vnext → project}/records/effect-uncertainty-resolved-sync.json +0 -0
  172. /package/examples/{vnext → project}/records/effect-uncertainty-resolved.json +0 -0
  173. /package/examples/{vnext → project}/records/run-created.json +0 -0
  174. /package/examples/{vnext → project}/records/sync-event.json +0 -0
  175. /package/examples/{vnext → project}/repositories/api/.kxm/repo/env.yaml +0 -0
  176. /package/examples/{vnext → project}/repositories/api/.kxm/repo/repo.yaml +0 -0
  177. /package/examples/{vnext → project}/repositories/web/.kxm/repo/repo.yaml +0 -0
@@ -0,0 +1,1439 @@
1
+ // packages/core/tui/src/types/surface.ts
2
+ var KXM_TUI_CONTRACT_SCHEMA = "kxm.tui-contract.v1";
3
+ var KXM_TUI_FIELD_KINDS = ["text", "enum", "choice", "info"];
4
+ var KXM_TUI_STATUSES = ["ready", "available", "blocked"];
5
+ var KXM_TUI_STEP_STATES = ["pending", "satisfied", "running", "done", "failed"];
6
+ var KXM_TUI_NOTICE_LEVELS = ["info", "error"];
7
+ var KXM_TUI_SET_ACTION = "set";
8
+ var KXM_TUI_CLEAR_ACTION = "clear";
9
+ var KXM_TUI_ABORT_ACTION = "abort";
10
+ var KXM_TUI_LIMITS = Object.freeze({
11
+ sourceMax: 128,
12
+ idMax: 128,
13
+ choiceIdMax: 256,
14
+ labelMax: 48,
15
+ choiceLabelMax: 96,
16
+ detailMax: 240,
17
+ shortDetailMax: 96,
18
+ stepDetailMax: 160,
19
+ statusTextMax: 240,
20
+ valueMax: 4096,
21
+ keyPathMax: 128,
22
+ outputLineMax: 512,
23
+ outputMaxLines: 64,
24
+ choicesMax: 128,
25
+ actionsMax: 4,
26
+ stepsMax: 24,
27
+ fieldsMax: 64,
28
+ sectionsMax: 32,
29
+ orderMax: 1e3
30
+ });
31
+ var ID_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/;
32
+ var SOURCE_PATTERN = /^[A-Za-z0-9@][A-Za-z0-9@/._:-]*$/;
33
+ var KEY_PATH_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.[\]-]*$/;
34
+ var ACTION_KEY_PATTERN = /^[A-Za-z0-9]$/;
35
+ function issue(path, code, message) {
36
+ return { path, code, message };
37
+ }
38
+ function isPlainObject(value) {
39
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40
+ }
41
+ function boundedText(issues, path, value, max, required) {
42
+ if (value === void 0) {
43
+ if (required) issues.push(issue(path, "required", `${path} is required`));
44
+ return;
45
+ }
46
+ if (typeof value !== "string" || value.length === 0) {
47
+ issues.push(issue(path, "invalid_string", `${path} must be a non-empty string`));
48
+ return;
49
+ }
50
+ if (value.length > max) {
51
+ issues.push(issue(path, "too_long", `${path} exceeds ${max} characters`));
52
+ return;
53
+ }
54
+ if (/[\u0000-\u0008\u000a-\u001f\u007f]/u.test(value)) {
55
+ issues.push(issue(path, "control_character", `${path} must not contain control characters`));
56
+ }
57
+ }
58
+ function optionalEnum(issues, path, value, allowed) {
59
+ if (value === void 0) return;
60
+ if (typeof value !== "string" || !allowed.includes(value)) {
61
+ issues.push(issue(path, "invalid_choice", `${path} must be one of ${allowed.join(", ")}`));
62
+ }
63
+ }
64
+ function ratioField(issues, path, value) {
65
+ if (value === void 0) return;
66
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
67
+ issues.push(issue(path, "invalid_ratio", `${path} must be a finite number between 0 and 1`));
68
+ }
69
+ }
70
+ function validateChoice(issues, path, raw) {
71
+ if (!isPlainObject(raw)) {
72
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
73
+ return;
74
+ }
75
+ boundedText(issues, `${path}.id`, raw.id, KXM_TUI_LIMITS.choiceIdMax, true);
76
+ if (typeof raw.id === "string" && /[\s]/u.test(raw.id)) {
77
+ issues.push(issue(`${path}.id`, "invalid_choice_id", `${path}.id must not contain whitespace`));
78
+ }
79
+ boundedText(issues, `${path}.label`, raw.label, KXM_TUI_LIMITS.choiceLabelMax, true);
80
+ boundedText(issues, `${path}.detail`, raw.detail, KXM_TUI_LIMITS.shortDetailMax, false);
81
+ boundedText(issues, `${path}.group`, raw.group, KXM_TUI_LIMITS.shortDetailMax, false);
82
+ boundedText(issues, `${path}.statusText`, raw.statusText, KXM_TUI_LIMITS.shortDetailMax, false);
83
+ boundedText(issues, `${path}.action`, raw.action, KXM_TUI_LIMITS.idMax, false);
84
+ optionalEnum(issues, `${path}.status`, raw.status, KXM_TUI_STATUSES);
85
+ }
86
+ function validateAction(issues, path, raw) {
87
+ if (!isPlainObject(raw)) {
88
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
89
+ return;
90
+ }
91
+ if (typeof raw.key !== "string" || !ACTION_KEY_PATTERN.test(raw.key)) {
92
+ issues.push(issue(`${path}.key`, "invalid_action_key", `${path}.key must be a single alphanumeric key`));
93
+ }
94
+ boundedText(issues, `${path}.label`, raw.label, KXM_TUI_LIMITS.shortDetailMax, true);
95
+ boundedText(issues, `${path}.action`, raw.action, KXM_TUI_LIMITS.idMax, true);
96
+ }
97
+ function validateStep(issues, path, raw) {
98
+ if (!isPlainObject(raw)) {
99
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
100
+ return;
101
+ }
102
+ boundedText(issues, `${path}.label`, raw.label, KXM_TUI_LIMITS.shortDetailMax, true);
103
+ boundedText(issues, `${path}.detail`, raw.detail, KXM_TUI_LIMITS.stepDetailMax, false);
104
+ optionalEnum(issues, `${path}.state`, raw.state, KXM_TUI_STEP_STATES);
105
+ if (raw.state === void 0) issues.push(issue(`${path}.state`, "required", `${path}.state is required`));
106
+ }
107
+ function validateField(issues, path, raw) {
108
+ if (!isPlainObject(raw)) {
109
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
110
+ return;
111
+ }
112
+ boundedText(issues, `${path}.id`, raw.id, KXM_TUI_LIMITS.idMax, true);
113
+ if (typeof raw.id === "string" && !ID_PATTERN.test(raw.id)) {
114
+ issues.push(issue(`${path}.id`, "invalid_id", `${path}.id must match ${ID_PATTERN}`));
115
+ }
116
+ boundedText(issues, `${path}.label`, raw.label, KXM_TUI_LIMITS.labelMax, true);
117
+ if (raw.kind === void 0) {
118
+ issues.push(issue(`${path}.kind`, "required", `${path}.kind is required`));
119
+ } else {
120
+ optionalEnum(issues, `${path}.kind`, raw.kind, KXM_TUI_FIELD_KINDS);
121
+ }
122
+ boundedText(issues, `${path}.value`, raw.value, KXM_TUI_LIMITS.valueMax, false);
123
+ boundedText(issues, `${path}.placeholder`, raw.placeholder, KXM_TUI_LIMITS.shortDetailMax, false);
124
+ boundedText(issues, `${path}.detail`, raw.detail, KXM_TUI_LIMITS.detailMax, false);
125
+ boundedText(issues, `${path}.statusText`, raw.statusText, KXM_TUI_LIMITS.statusTextMax, false);
126
+ if (raw.updatedAt !== void 0) {
127
+ boundedText(issues, `${path}.updatedAt`, raw.updatedAt, 64, false);
128
+ if (typeof raw.updatedAt === "string" && !Number.isFinite(Date.parse(raw.updatedAt))) {
129
+ issues.push(issue(`${path}.updatedAt`, "invalid_timestamp", `${path}.updatedAt must be an ISO timestamp`));
130
+ }
131
+ }
132
+ optionalEnum(issues, `${path}.status`, raw.status, KXM_TUI_STATUSES);
133
+ if (raw.keyPath !== void 0) {
134
+ boundedText(issues, `${path}.keyPath`, raw.keyPath, KXM_TUI_LIMITS.keyPathMax, false);
135
+ if (typeof raw.keyPath === "string" && !KEY_PATH_PATTERN.test(raw.keyPath)) {
136
+ issues.push(issue(`${path}.keyPath`, "invalid_key_path", `${path}.keyPath must be a dotted config key`));
137
+ }
138
+ }
139
+ for (const listKey of ["busy", "awaitingInput"]) {
140
+ if (raw[listKey] !== void 0 && typeof raw[listKey] !== "boolean") {
141
+ issues.push(issue(`${path}.${listKey}`, "invalid_boolean", `${path}.${listKey} must be a boolean`));
142
+ }
143
+ }
144
+ if (raw.progress !== void 0) {
145
+ if (!isPlainObject(raw.progress)) {
146
+ issues.push(issue(`${path}.progress`, "invalid_object", `${path}.progress must be an object`));
147
+ } else {
148
+ boundedText(issues, `${path}.progress.label`, raw.progress.label, KXM_TUI_LIMITS.shortDetailMax, true);
149
+ ratioField(issues, `${path}.progress.ratio`, raw.progress.ratio);
150
+ }
151
+ }
152
+ for (const [listKey, max, check] of [
153
+ ["choices", KXM_TUI_LIMITS.choicesMax, validateChoice],
154
+ ["actions", KXM_TUI_LIMITS.actionsMax, validateAction],
155
+ ["steps", KXM_TUI_LIMITS.stepsMax, validateStep]
156
+ ]) {
157
+ const list = raw[listKey];
158
+ if (list === void 0) continue;
159
+ if (!Array.isArray(list) || list.length > max) {
160
+ issues.push(issue(`${path}.${listKey}`, "invalid_list", `${path}.${listKey} must be an array of at most ${max}`));
161
+ continue;
162
+ }
163
+ list.forEach((entry, index) => check(issues, `${path}.${listKey}[${index}]`, entry));
164
+ }
165
+ const output = raw.output;
166
+ if (output !== void 0) {
167
+ if (!Array.isArray(output) || output.length > KXM_TUI_LIMITS.outputMaxLines) {
168
+ issues.push(issue(`${path}.output`, "invalid_list", `${path}.output must be an array of at most ${KXM_TUI_LIMITS.outputMaxLines} lines`));
169
+ } else {
170
+ output.forEach((line, index) => {
171
+ if (typeof line !== "string") issues.push(issue(`${path}.output[${index}]`, "invalid_string", "output lines must be strings"));
172
+ else if (line.length > KXM_TUI_LIMITS.outputLineMax) issues.push(issue(`${path}.output[${index}]`, "too_long", "output line is too long"));
173
+ });
174
+ }
175
+ }
176
+ }
177
+ function validateSection(issues, path, raw) {
178
+ if (!isPlainObject(raw)) {
179
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
180
+ return;
181
+ }
182
+ boundedText(issues, `${path}.id`, raw.id, KXM_TUI_LIMITS.idMax, true);
183
+ if (typeof raw.id === "string" && !ID_PATTERN.test(raw.id)) {
184
+ issues.push(issue(`${path}.id`, "invalid_id", `${path}.id must match ${ID_PATTERN}`));
185
+ }
186
+ boundedText(issues, `${path}.title`, raw.title, KXM_TUI_LIMITS.labelMax, true);
187
+ boundedText(issues, `${path}.detail`, raw.detail, KXM_TUI_LIMITS.shortDetailMax, false);
188
+ boundedText(issues, `${path}.notice`, raw.notice, KXM_TUI_LIMITS.detailMax, false);
189
+ optionalEnum(issues, `${path}.noticeLevel`, raw.noticeLevel, KXM_TUI_NOTICE_LEVELS);
190
+ if (typeof raw.order !== "number" || !Number.isInteger(raw.order) || raw.order < 0 || raw.order > KXM_TUI_LIMITS.orderMax) {
191
+ issues.push(issue(`${path}.order`, "invalid_order", `${path}.order must be an integer between 0 and ${KXM_TUI_LIMITS.orderMax}`));
192
+ }
193
+ if (!Array.isArray(raw.fields) || raw.fields.length > KXM_TUI_LIMITS.fieldsMax) {
194
+ issues.push(issue(`${path}.fields`, "invalid_list", `${path}.fields must be an array of at most ${KXM_TUI_LIMITS.fieldsMax}`));
195
+ return;
196
+ }
197
+ const seen = /* @__PURE__ */ new Set();
198
+ raw.fields.forEach((field, index) => {
199
+ const fieldPath = `${path}.fields[${index}]`;
200
+ validateField(issues, fieldPath, field);
201
+ if (isPlainObject(field) && typeof field.id === "string") {
202
+ if (seen.has(field.id)) issues.push(issue(`${fieldPath}.id`, "duplicate_id", `${field.id} is published twice in ${path}`));
203
+ seen.add(field.id);
204
+ }
205
+ });
206
+ }
207
+ function validateKxmTuiSurface(sections) {
208
+ if (!Array.isArray(sections)) return [issue("sections", "invalid_list", "sections must be an array")];
209
+ if (sections.length > KXM_TUI_LIMITS.sectionsMax) {
210
+ return [issue("sections", "too_many", `sections must number at most ${KXM_TUI_LIMITS.sectionsMax}`)];
211
+ }
212
+ const issues = [];
213
+ const seenSources = /* @__PURE__ */ new Map();
214
+ sections.forEach((section, index) => {
215
+ const path = `sections[${index}]`;
216
+ validateSection(issues, path, section);
217
+ if (!isPlainObject(section)) return;
218
+ boundedText(issues, `${path}.source`, section.source, KXM_TUI_LIMITS.sourceMax, true);
219
+ if (typeof section.source === "string" && !SOURCE_PATTERN.test(section.source)) {
220
+ issues.push(issue(`${path}.source`, "invalid_source", `${path}.source must be an addressable owner id`));
221
+ }
222
+ if (typeof section.source === "string" && typeof section.id === "string") {
223
+ const ids = seenSources.get(section.source) ?? /* @__PURE__ */ new Set();
224
+ seenSources.set(section.source, ids);
225
+ if (ids.has(section.id)) issues.push(issue(`${path}.id`, "duplicate_section", `${section.source}/${section.id} is published twice`));
226
+ ids.add(section.id);
227
+ }
228
+ });
229
+ return issues;
230
+ }
231
+ function assertKxmTuiSurface(sections) {
232
+ const issues = validateKxmTuiSurface(sections);
233
+ if (issues.length > 0) {
234
+ const first = issues[0];
235
+ throw new Error(`kxm tui surface refused (${issues.length} issue(s)): ${first.path} ${first.code}: ${first.message}`);
236
+ }
237
+ return sections.slice().sort((a, b) => sectionOrder(a) - sectionOrder(b));
238
+ }
239
+ function sectionOrder(value) {
240
+ return isPlainObject(value) && typeof value.order === "number" ? value.order : Number.MAX_SAFE_INTEGER;
241
+ }
242
+ function isSelectableField(field) {
243
+ return field !== void 0 && field.kind !== "info";
244
+ }
245
+ function fieldIsEditable(field) {
246
+ if (!isSelectableField(field)) return false;
247
+ const editable = field;
248
+ return editable.kind === "text" || editable.kind === "enum" || editable.kind === "choice" && (editable.choices?.length ?? 0) > 0;
249
+ }
250
+ function groupedKxmTuiChoices(field) {
251
+ const choices = field.choices ?? [];
252
+ if (choices.length === 0) return [];
253
+ if (choices.every((choice) => choice.group === void 0)) return [{ group: "", choices }];
254
+ const groups = [];
255
+ const index = /* @__PURE__ */ new Map();
256
+ for (const choice of choices) {
257
+ const group = choice.group ?? "";
258
+ let bucket = index.get(group);
259
+ if (!bucket) {
260
+ bucket = [];
261
+ index.set(group, bucket);
262
+ groups.push({ group, choices: bucket });
263
+ }
264
+ bucket.push(choice);
265
+ }
266
+ return groups;
267
+ }
268
+
269
+ // packages/core/tui/src/tui/keys.ts
270
+ import { decodeKittyPrintable, parseKey } from "@earendil-works/pi-tui";
271
+ var KXM_TUI_INPUT_MAX_LENGTH = 4096;
272
+ var CONTROL_BYTES = /[\u0000-\u0008\u000a-\u001f\u007f\u0080-\u009f]/u;
273
+ var NAMED_KEYS = Object.freeze({
274
+ up: "up",
275
+ down: "down",
276
+ left: "left",
277
+ right: "right",
278
+ home: "home",
279
+ end: "end",
280
+ pageup: "pageUp",
281
+ pagedown: "pageDown",
282
+ enter: "enter",
283
+ return: "enter",
284
+ escape: "escape",
285
+ esc: "escape",
286
+ tab: "tab",
287
+ backspace: "backspace",
288
+ delete: "delete"
289
+ });
290
+ var CONTROL_BYTES_GLOBAL = /[\u0000-\u0008\u000a-\u001f\u007f\u0080-\u009f]/gu;
291
+ function normalizeInputText(value) {
292
+ return value.replace(CONTROL_BYTES_GLOBAL, " ").replace(/ {2,}/gu, " ").trim().slice(0, KXM_TUI_INPUT_MAX_LENGTH);
293
+ }
294
+ function isControlInput(data) {
295
+ for (let index = 0; index < data.length; index += 1) {
296
+ const code = data.charCodeAt(index);
297
+ if (code < 32 || code === 127) return true;
298
+ }
299
+ return false;
300
+ }
301
+ function decodeKxmTuiInput(data) {
302
+ if (typeof data !== "string" || data.length === 0) return { kind: "ignored" };
303
+ const named = parseKey(data);
304
+ if (named === "ctrl+c") return { kind: "ctrlC" };
305
+ if (named) {
306
+ const kind = NAMED_KEYS[named.toLowerCase()];
307
+ if (kind) return { kind };
308
+ if (named === "space") return { kind: "text", text: " " };
309
+ if (Array.from(named).length === 1 && !CONTROL_BYTES.test(named)) {
310
+ return { kind: "text", text: named };
311
+ }
312
+ return { kind: "ignored" };
313
+ }
314
+ const kitty = decodeKittyPrintable(data);
315
+ if (kitty) {
316
+ const text = normalizeInputText(kitty);
317
+ return text ? { kind: "text", text } : { kind: "ignored" };
318
+ }
319
+ if (data.startsWith("\x1B")) return { kind: "ignored" };
320
+ const pasted = normalizeInputText(data);
321
+ return pasted ? { kind: "text", text: pasted } : { kind: "ignored" };
322
+ }
323
+ function deleteBackward(text) {
324
+ const units = Array.from(text);
325
+ units.pop();
326
+ return units.join("");
327
+ }
328
+ function insertAt(text, caret, insert, maxLength = KXM_TUI_INPUT_MAX_LENGTH) {
329
+ const units = Array.from(text);
330
+ const at = Math.max(0, Math.min(caret, units.length));
331
+ const added = Array.from(insert);
332
+ const merged = [...units.slice(0, at), ...added, ...units.slice(at)].join("");
333
+ const next = merged.length > maxLength ? merged.slice(0, maxLength) : merged;
334
+ return { text: next, caret: Math.min(next.length, at + added.length) };
335
+ }
336
+
337
+ // packages/core/tui/src/tui/layout.ts
338
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
339
+ var KXM_TUI_ELLIPSIS = "\u2026";
340
+ function fitText(text, width) {
341
+ if (width <= 0) return "";
342
+ if (visibleWidth(text) <= width) return text;
343
+ if (!text.includes("\x1B")) {
344
+ let out = "";
345
+ for (const unit of Array.from(text)) {
346
+ if (visibleWidth(out) + visibleWidth(unit) > width - KXM_TUI_ELLIPSIS.length) break;
347
+ out += unit;
348
+ }
349
+ return out + KXM_TUI_ELLIPSIS;
350
+ }
351
+ return truncateToWidth(text, width, KXM_TUI_ELLIPSIS);
352
+ }
353
+ function alignRight(left, right, width) {
354
+ if (width <= 0) return "";
355
+ const detail = fitText(right, Math.max(0, width - 2));
356
+ const detailWidth = visibleWidth(detail);
357
+ const leftWidth = Math.max(1, width - detailWidth - 1);
358
+ const fitted = fitText(left, leftWidth);
359
+ const filler = Math.max(1, width - visibleWidth(fitted) - detailWidth);
360
+ return `${fitted}${" ".repeat(filler)}${detail}`;
361
+ }
362
+ function padRight(text, width) {
363
+ const missing = width - visibleWidth(text);
364
+ return missing > 0 ? text + " ".repeat(missing) : text;
365
+ }
366
+ function wrapPlain(text, width) {
367
+ if (width <= 0) return [];
368
+ const lines = [];
369
+ let current = "";
370
+ for (const word of text.split(/\s+/u).filter(Boolean)) {
371
+ if (!current.length) current = word;
372
+ else if (visibleWidth(current) + 1 + visibleWidth(word) <= width) current = `${current} ${word}`;
373
+ else {
374
+ lines.push(current);
375
+ current = word;
376
+ }
377
+ }
378
+ if (current.length) lines.push(current);
379
+ return lines.length ? lines : [];
380
+ }
381
+ function formatAge(iso, nowMs) {
382
+ if (!iso) return "\u2014";
383
+ const at = Date.parse(iso);
384
+ if (!Number.isFinite(at)) return "\u2014";
385
+ const seconds = Math.floor((nowMs - at) / 1e3);
386
+ if (seconds < 0) return "\u2014";
387
+ if (seconds < 60) return `${seconds}s`;
388
+ const minutes = Math.floor(seconds / 60);
389
+ if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
390
+ const hours = Math.floor(minutes / 60);
391
+ if (hours < 24) return `${hours}h${String(minutes % 60).padStart(2, "0")}m`;
392
+ const days = Math.floor(hours / 24);
393
+ return `${days}d${String(hours % 24).padStart(2, "0")}h`;
394
+ }
395
+ function progressBar(ratio, width = 10) {
396
+ const cells = Math.max(1, width);
397
+ if (ratio === void 0 || !Number.isFinite(ratio)) return "\u2591".repeat(cells);
398
+ const filled = Math.round(Math.max(0, Math.min(1, ratio)) * cells);
399
+ return "\u2588".repeat(filled) + "\u2591".repeat(Math.max(0, cells - filled));
400
+ }
401
+ var KXM_TUI_MARKS = Object.freeze({
402
+ pending: "\u25CB",
403
+ active: "\u25B6",
404
+ waiting: "\u29D7",
405
+ passed: "\u2714",
406
+ failed: "\u2716",
407
+ cancelled: "\u2298",
408
+ blocked: "\u2717",
409
+ ready: "\u25CF",
410
+ available: "\u25D0",
411
+ cursor: "\u25B8",
412
+ blankCursor: " "
413
+ });
414
+
415
+ // packages/core/tui/src/tui/theme.ts
416
+ var SGR = Object.freeze({
417
+ text: "37",
418
+ accent: "1;36",
419
+ muted: "2",
420
+ dim: "2",
421
+ success: "1;32",
422
+ error: "1;31",
423
+ warning: "1;33",
424
+ border: "90"
425
+ });
426
+ function createPlainKxmTuiTheme() {
427
+ const passthrough = (text) => text;
428
+ return {
429
+ colored: false,
430
+ fg: (_token, text) => text,
431
+ accent: passthrough,
432
+ success: passthrough,
433
+ error: passthrough,
434
+ warning: passthrough,
435
+ muted: passthrough,
436
+ dim: passthrough,
437
+ label: passthrough,
438
+ value: passthrough,
439
+ key: passthrough,
440
+ cursor: passthrough,
441
+ headerBg: passthrough,
442
+ panelBg: passthrough
443
+ };
444
+ }
445
+ function createKxmTuiAnsiTheme(color) {
446
+ if (!color) return createPlainKxmTuiTheme();
447
+ const fg = (token, text) => `\x1B[${SGR[token]}m${text}\x1B[0m`;
448
+ return {
449
+ colored: true,
450
+ fg,
451
+ accent: (text) => fg("accent", text),
452
+ success: (text) => fg("success", text),
453
+ error: (text) => fg("error", text),
454
+ warning: (text) => fg("warning", text),
455
+ muted: (text) => fg("muted", text),
456
+ dim: (text) => fg("dim", text),
457
+ label: (text) => fg("text", text),
458
+ value: (text) => `\x1B[1m${text}\x1B[0m`,
459
+ key: (text) => fg("muted", text),
460
+ cursor: (text) => fg("accent", text),
461
+ headerBg: (text) => `\x1B[1;97;44m${text}\x1B[0m`,
462
+ panelBg: (text) => `\x1B[48;5;236m${text}\x1B[0m`
463
+ };
464
+ }
465
+ function createDefaultKxmTuiTheme(env = process.env) {
466
+ return createKxmTuiAnsiTheme(!(env.NO_COLOR || env.KXM_TUI_NO_COLOR));
467
+ }
468
+ function createKxmTuiThemeFromPi(theme, colored = true) {
469
+ if (!colored) return createPlainKxmTuiTheme();
470
+ const fg = (token, text) => theme.fg(token === "dim" ? "muted" : token, text);
471
+ return {
472
+ colored: true,
473
+ fg,
474
+ accent: (text) => fg("accent", text),
475
+ success: (text) => fg("success", text),
476
+ error: (text) => fg("error", text),
477
+ warning: (text) => fg("warning", text),
478
+ muted: (text) => fg("muted", text),
479
+ dim: (text) => fg("dim", text),
480
+ label: (text) => fg("text", text),
481
+ value: (text) => fg("accent", text),
482
+ key: (text) => fg("muted", text),
483
+ cursor: (text) => fg("accent", text),
484
+ headerBg: (text) => theme.bg("selectedBg", text),
485
+ panelBg: (text) => theme.bg("selectedBg", text)
486
+ };
487
+ }
488
+
489
+ // packages/core/tui/src/tui/panel.ts
490
+ var PANE_KEYS = /* @__PURE__ */ new Set(["j", "k", "h", "l"]);
491
+ function initialKxmTuiPanelState(sections) {
492
+ return {
493
+ pane: "sections",
494
+ mode: "browse",
495
+ sectionIndex: 0,
496
+ fieldIndex: firstSelectableFieldIndex(sections[0]?.fields ?? []),
497
+ choiceIndex: 0,
498
+ draft: "",
499
+ caret: 0,
500
+ filter: "",
501
+ filtering: false,
502
+ help: false,
503
+ pending: []
504
+ };
505
+ }
506
+ function pendingKey(invocation) {
507
+ return `${invocation.source}\0${invocation.sectionId}\0${invocation.fieldId}`;
508
+ }
509
+ function firstSelectableFieldIndex(fields) {
510
+ const index = fields.findIndex(isSelectableField);
511
+ return index < 0 ? 0 : index;
512
+ }
513
+ function visibleChoices(field, filter) {
514
+ const choices = field?.choices ?? [];
515
+ const needle = filter.trim().toLowerCase();
516
+ if (!needle) return [...choices];
517
+ return choices.filter(
518
+ (choice) => `${choice.group ?? ""} ${choice.label} ${choice.id} ${choice.detail ?? ""}`.toLowerCase().includes(needle)
519
+ );
520
+ }
521
+ function currentSection(sections, state) {
522
+ return sections[state.sectionIndex];
523
+ }
524
+ function currentField(sections, state) {
525
+ return currentSection(sections, state)?.fields[state.fieldIndex];
526
+ }
527
+ function reconcileKxmTuiPanelState(state, sections) {
528
+ const sectionIndex = Math.min(state.sectionIndex, Math.max(0, sections.length - 1));
529
+ const fields = sections[sectionIndex]?.fields ?? [];
530
+ let fieldIndex = Math.min(state.fieldIndex, Math.max(0, fields.length - 1));
531
+ if (!isSelectableField(fields[fieldIndex])) fieldIndex = firstSelectableFieldIndex(fields);
532
+ const field = fields[fieldIndex];
533
+ const keepsEdit = state.mode === "edit" && (field?.statusText !== void 0 || field?.awaitingInput === true);
534
+ return {
535
+ ...state,
536
+ sectionIndex,
537
+ fieldIndex,
538
+ mode: keepsEdit ? "edit" : "browse",
539
+ choiceIndex: 0,
540
+ pending: []
541
+ };
542
+ }
543
+ function focusedFieldIsEditable(sections, state) {
544
+ return fieldIsEditable(currentField(sections, state));
545
+ }
546
+ function reduceKxmTuiInput(state, sections, input) {
547
+ return reduceDecoded(state, sections, decodeKxmTuiInput(input));
548
+ }
549
+ function invokeEffect(state, section, field, action, value) {
550
+ const invocation = {
551
+ source: section.source,
552
+ sectionId: section.id,
553
+ fieldId: field.id,
554
+ action,
555
+ ...value === void 0 ? {} : { value }
556
+ };
557
+ const key = pendingKey(invocation);
558
+ return {
559
+ state: { ...state, pending: state.pending.includes(key) ? state.pending : [...state.pending, key] },
560
+ effects: [{ type: "invoke", invocation }]
561
+ };
562
+ }
563
+ function moveSection(state, sections, delta) {
564
+ if (sections.length === 0) return state;
565
+ const next = Math.max(0, Math.min(state.sectionIndex + delta, sections.length - 1));
566
+ if (next === state.sectionIndex) return state;
567
+ return {
568
+ ...state,
569
+ sectionIndex: next,
570
+ fieldIndex: firstSelectableFieldIndex(sections[next]?.fields ?? []),
571
+ mode: "browse",
572
+ choiceIndex: 0,
573
+ draft: "",
574
+ caret: 0,
575
+ filter: "",
576
+ filtering: false
577
+ };
578
+ }
579
+ function moveField(state, fields, delta) {
580
+ const step = delta < 0 ? -1 : 1;
581
+ let next = state.fieldIndex + step;
582
+ while (next >= 0 && next < fields.length && !isSelectableField(fields[next])) next += step;
583
+ if (next < 0 || next >= fields.length) return state;
584
+ return {
585
+ ...state,
586
+ fieldIndex: next,
587
+ mode: "browse",
588
+ choiceIndex: 0,
589
+ draft: "",
590
+ caret: 0,
591
+ filter: "",
592
+ filtering: false
593
+ };
594
+ }
595
+ function moveChoice(state, count, delta) {
596
+ if (count === 0) return state;
597
+ return { ...state, choiceIndex: Math.max(0, Math.min(state.choiceIndex + delta, count - 1)) };
598
+ }
599
+ function choiceAt(field, state) {
600
+ const choices = visibleChoices(field, state.filter);
601
+ return choices[Math.max(0, Math.min(state.choiceIndex, choices.length - 1))];
602
+ }
603
+ function enterField(state, section, field) {
604
+ if (!field) return { state, effects: [] };
605
+ if (field.kind === "choice") {
606
+ if ((field.choices?.length ?? 0) === 0) {
607
+ return { state, effects: [{ type: "notice", message: `${field.label} has nothing to offer yet` }] };
608
+ }
609
+ return { state: { ...state, mode: "choices", choiceIndex: 0 }, effects: [] };
610
+ }
611
+ if (field.kind === "enum") {
612
+ const choices = field.choices ?? [];
613
+ if (choices.length === 0) {
614
+ return { state, effects: [{ type: "notice", message: `${field.label} has no values published` }] };
615
+ }
616
+ const current = choices.findIndex((choice) => choice.id === field.value);
617
+ const next = choices[(current + 1) % choices.length];
618
+ if (next.status === "blocked") {
619
+ return { state, effects: [{ type: "notice", message: next.statusText ?? `${next.label} is unavailable` }] };
620
+ }
621
+ return invokeEffect(state, section, field, next.action ?? KXM_TUI_SET_ACTION, next.id);
622
+ }
623
+ if (field.kind !== "text") return { state, effects: [] };
624
+ return { state: { ...state, mode: "edit", draft: field.value ?? "", caret: (field.value ?? "").length }, effects: [] };
625
+ }
626
+ function reduceDecoded(state, sections, decoded) {
627
+ const section = sections[state.sectionIndex];
628
+ const field = currentField(sections, state);
629
+ if (decoded.kind === "ctrlC") {
630
+ if (field?.busy && section) {
631
+ return invokeEffect(state, section, field, KXM_TUI_ABORT_ACTION);
632
+ }
633
+ return { state, effects: [{ type: "quit" }] };
634
+ }
635
+ if (!section) return { state, effects: [{ type: "quit" }] };
636
+ if (state.mode === "edit") return reduceEdit(state, section, field, decoded);
637
+ if (state.mode === "choices") return reduceChoices(state, section, field, decoded);
638
+ return reduceBrowse(state, sections, section, field, decoded);
639
+ }
640
+ function caretSource(text, caret) {
641
+ return Array.from(text).slice(0, caret).join("").length;
642
+ }
643
+ function reduceEdit(state, section, field, decoded) {
644
+ if (!field) return { state: { ...state, mode: "browse" }, effects: [] };
645
+ switch (decoded.kind) {
646
+ case "escape":
647
+ return { state: { ...state, mode: "browse", draft: "", caret: 0 }, effects: [] };
648
+ case "enter": {
649
+ const value = normalizeInputText(state.draft).trim();
650
+ if (!value) return { state: { ...state, mode: "browse", draft: "", caret: 0 }, effects: [] };
651
+ const step = invokeEffect(state, section, field, KXM_TUI_SET_ACTION, value);
652
+ return { state: { ...step.state, mode: "browse", draft: "", caret: 0 }, effects: step.effects };
653
+ }
654
+ case "backspace": {
655
+ if (state.caret === 0) return { state, effects: [] };
656
+ const cut = caretSource(state.draft, state.caret);
657
+ const before = deleteBackward(state.draft.slice(0, cut));
658
+ const draft = `${before}${state.draft.slice(cut)}`;
659
+ return { state: { ...state, draft, caret: Array.from(before).length }, effects: [] };
660
+ }
661
+ case "delete": {
662
+ const units = Array.from(state.draft);
663
+ const text = [...units.slice(0, state.caret), ...units.slice(state.caret + 1)].join("");
664
+ return { state: { ...state, draft: text }, effects: [] };
665
+ }
666
+ case "left":
667
+ return { state: { ...state, caret: Math.max(0, state.caret - 1) }, effects: [] };
668
+ case "right":
669
+ return { state: { ...state, caret: Math.min(Array.from(state.draft).length, state.caret + 1) }, effects: [] };
670
+ case "home":
671
+ return { state: { ...state, caret: 0 }, effects: [] };
672
+ case "end":
673
+ return { state: { ...state, caret: Array.from(state.draft).length }, effects: [] };
674
+ case "text": {
675
+ const text = decoded.text ?? "";
676
+ if (!text) return { state, effects: [] };
677
+ const inserted = insertAt(state.draft, state.caret, text);
678
+ return { state: { ...state, draft: inserted.text, caret: inserted.caret }, effects: [] };
679
+ }
680
+ default:
681
+ return { state, effects: [] };
682
+ }
683
+ }
684
+ function reduceChoices(state, section, field, decoded) {
685
+ if (!field) return { state: { ...state, mode: "browse" }, effects: [] };
686
+ const choices = visibleChoices(field, state.filter);
687
+ switch (decoded.kind) {
688
+ case "escape":
689
+ return { state: { ...state, mode: "browse", filter: "", filtering: false, choiceIndex: 0 }, effects: [] };
690
+ case "up":
691
+ return { state: moveChoice(state, choices.length, -1), effects: [] };
692
+ case "down":
693
+ return { state: moveChoice(state, choices.length, 1), effects: [] };
694
+ case "pageUp":
695
+ return { state: moveChoice(state, choices.length, -8), effects: [] };
696
+ case "pageDown":
697
+ return { state: moveChoice(state, choices.length, 8), effects: [] };
698
+ case "enter": {
699
+ const choice = choiceAt(field, state);
700
+ if (!choice) return { state, effects: [] };
701
+ if (choice.status === "blocked") {
702
+ return { state, effects: [{ type: "notice", message: choice.statusText ?? `${choice.label} is unavailable` }] };
703
+ }
704
+ const clearing = (choice.action ?? KXM_TUI_SET_ACTION) === KXM_TUI_CLEAR_ACTION;
705
+ const step = invokeEffect(
706
+ state,
707
+ section,
708
+ field,
709
+ choice.action ?? KXM_TUI_SET_ACTION,
710
+ clearing ? void 0 : choice.id
711
+ );
712
+ return { state: { ...step.state, mode: "browse", filter: "", filtering: false }, effects: step.effects };
713
+ }
714
+ case "backspace": {
715
+ if (state.filter.length === 0) return { state: { ...state, filtering: false }, effects: [] };
716
+ return { state: { ...state, filter: deleteBackward(state.filter), choiceIndex: 0 }, effects: [] };
717
+ }
718
+ case "text": {
719
+ const text = normalizeInputText(decoded.text ?? "");
720
+ if (!text) return { state, effects: [] };
721
+ return { state: { ...state, filtering: true, filter: `${state.filter}${text}`, choiceIndex: 0 }, effects: [] };
722
+ }
723
+ default:
724
+ return { state, effects: [] };
725
+ }
726
+ }
727
+ function reduceBrowse(state, sections, section, field, decoded) {
728
+ if (state.help && (decoded.kind === "escape" || decoded.kind === "enter")) {
729
+ return { state: { ...state, help: false }, effects: [] };
730
+ }
731
+ if (state.help) return { state, effects: [] };
732
+ if (decoded.kind === "text") {
733
+ const key = decoded.text ?? "";
734
+ if (key === "q") return { state, effects: [{ type: "quit" }] };
735
+ if (key === "h" || key === "?") return { state: { ...state, help: true }, effects: [] };
736
+ const action = field?.actions?.find((candidate) => candidate.key === key);
737
+ if (action && field) return invokeEffect(state, section, field, action.action);
738
+ if ((key === "x" || key === "d") && field && field.value !== void 0 && field.kind !== "info") {
739
+ return invokeEffect(state, section, field, KXM_TUI_CLEAR_ACTION);
740
+ }
741
+ if (PANE_KEYS.has(key)) {
742
+ if (key === "h") return { state: { ...state, pane: "sections" }, effects: [] };
743
+ if (key === "l") return { state: { ...state, pane: "fields" }, effects: [] };
744
+ const delta = key === "k" ? -1 : 1;
745
+ return {
746
+ state: state.pane === "fields" ? moveField(state, section.fields, delta) : moveSection(state, sections, delta),
747
+ effects: []
748
+ };
749
+ }
750
+ return { state, effects: [] };
751
+ }
752
+ switch (decoded.kind) {
753
+ case "escape":
754
+ return state.pane === "fields" ? { state: { ...state, pane: "sections", mode: "browse" }, effects: [] } : { state, effects: [{ type: "quit" }] };
755
+ case "tab":
756
+ return { state: { ...state, pane: state.pane === "fields" ? "sections" : "fields" }, effects: [] };
757
+ case "right":
758
+ return { state: { ...state, pane: "fields" }, effects: [] };
759
+ case "left":
760
+ return { state: { ...state, pane: "sections" }, effects: [] };
761
+ case "up":
762
+ return { state: state.pane === "fields" ? moveField(state, section.fields, -1) : moveSection(state, sections, -1), effects: [] };
763
+ case "down":
764
+ return { state: state.pane === "fields" ? moveField(state, section.fields, 1) : moveSection(state, sections, 1), effects: [] };
765
+ case "pageUp":
766
+ return { state: moveSection(state, sections, -1), effects: [] };
767
+ case "pageDown":
768
+ return { state: moveSection(state, sections, 1), effects: [] };
769
+ case "enter":
770
+ return state.pane === "fields" ? enterField(state, section, field) : { state: { ...state, pane: "fields" }, effects: [] };
771
+ default:
772
+ return { state, effects: [] };
773
+ }
774
+ }
775
+
776
+ // packages/core/tui/src/tui/render.ts
777
+ import { stripTerminalSequences } from "@earendil-works/pi-tui";
778
+ var SECTION_PANE_RATIO = 1 / 3;
779
+ var MIN_PANE_WIDTH = 16;
780
+ var PROGRESS_WIDTH = 20;
781
+ var OUTPUT_TAIL = 6;
782
+ var SPLIT_MIN_WIDTH = 76;
783
+ function statusMark(status, theme, fallback) {
784
+ switch (status) {
785
+ case "ready":
786
+ return theme.fg("success", KXM_TUI_MARKS.ready);
787
+ case "available":
788
+ return theme.fg("accent", KXM_TUI_MARKS.available);
789
+ case "blocked":
790
+ return theme.fg("error", KXM_TUI_MARKS.blocked);
791
+ default:
792
+ return theme.dim(fallback);
793
+ }
794
+ }
795
+ function cursorMark(active, theme) {
796
+ return active ? theme.cursor(KXM_TUI_MARKS.cursor) : KXM_TUI_MARKS.blankCursor;
797
+ }
798
+ function paneSplit(width) {
799
+ const third = Math.max(MIN_PANE_WIDTH, Math.floor(width * SECTION_PANE_RATIO));
800
+ const sections = Math.min(third, Math.max(MIN_PANE_WIDTH, Math.floor(width / 2)));
801
+ return { sections, fields: Math.max(MIN_PANE_WIDTH, width - sections - 1) };
802
+ }
803
+ function fieldIsPending(state, section, field) {
804
+ return state.pending.includes(pendingKey({ source: section.source, sectionId: section.id, fieldId: field.id }));
805
+ }
806
+ function sectionRow(section, focused, pane, theme, width) {
807
+ const cursor = cursorMark(focused && pane === "sections", theme);
808
+ const flag = section.notice ? theme.fg(section.noticeLevel === "error" ? "error" : "warning", "!") : " ";
809
+ const count = section.fields.length;
810
+ const unset = section.fields.filter((field) => field.kind !== "info" && field.value === void 0).length;
811
+ const right = unset > 0 ? `${unset} unset / ${count}` : `${count} field${count === 1 ? "" : "s"}`;
812
+ return fitText(alignRight(`${cursor} ${flag} ${section.title}`, right, width), width);
813
+ }
814
+ function choiceSuffix(choice) {
815
+ const parts = [];
816
+ if (choice.detail) parts.push(choice.detail);
817
+ if (choice.statusText) parts.push(choice.statusText);
818
+ return parts.join(" \xB7 ");
819
+ }
820
+ function choiceRows(field, state, theme, width) {
821
+ const shown = visibleChoices(field, state.filter);
822
+ const lines = [];
823
+ if (state.filtering) {
824
+ lines.push(fitText(` ${theme.dim(`filter: ${state.filter || "(type to filter \xB7 esc clears)"}`)}`, width));
825
+ }
826
+ if (shown.length === 0) {
827
+ lines.push(fitText(` ${theme.dim("no match")}`, width));
828
+ return lines;
829
+ }
830
+ let position = -1;
831
+ for (const group of groupedKxmTuiChoices(field)) {
832
+ const filtered = shown.filter((choice) => (choice.group ?? "") === group.group);
833
+ if (filtered.length === 0) continue;
834
+ if (group.group) lines.push(fitText(` ${theme.key(group.group.toUpperCase())}`, width));
835
+ for (const choice of filtered) {
836
+ position += 1;
837
+ lines.push(
838
+ fitText(
839
+ alignRight(
840
+ `${cursorMark(position === state.choiceIndex, theme)} ${statusMark(choice.status, theme, KXM_TUI_MARKS.pending)} ${choice.label}`,
841
+ choiceSuffix(choice),
842
+ width
843
+ ),
844
+ width
845
+ )
846
+ );
847
+ }
848
+ }
849
+ return lines;
850
+ }
851
+ var STEP_MARKS = {
852
+ pending: "\xB7",
853
+ satisfied: KXM_TUI_MARKS.passed,
854
+ running: KXM_TUI_MARKS.active,
855
+ done: KXM_TUI_MARKS.passed,
856
+ failed: KXM_TUI_MARKS.failed
857
+ };
858
+ function stepColor(state, theme) {
859
+ switch (state) {
860
+ case "running":
861
+ return (text) => theme.fg("accent", text);
862
+ case "done":
863
+ return (text) => theme.fg("success", text);
864
+ case "failed":
865
+ return (text) => theme.fg("error", text);
866
+ default:
867
+ return theme.dim;
868
+ }
869
+ }
870
+ function stepRows(steps, theme, width) {
871
+ return steps.map((step) => {
872
+ const paint = stepColor(step.state, theme);
873
+ return fitText(alignRight(` ${paint(STEP_MARKS[step.state])} ${step.label}`, step.detail ?? "", width), width);
874
+ });
875
+ }
876
+ function progressRow(field, theme, width) {
877
+ const progress = field.progress;
878
+ if (!progress) return "";
879
+ const ratio = progress.ratio;
880
+ const right = ratio === void 0 ? "working" : `${Math.round(ratio * 100)}%`;
881
+ return fitText(alignRight(` ${progress.label}`, `${progressBar(ratio, PROGRESS_WIDTH)} ${right}`, width), width);
882
+ }
883
+ function focusedFieldDetail(field, state, theme, width) {
884
+ const lines = [];
885
+ if (field.keyPath) lines.push(fitText(` ${theme.dim(`key: ${field.keyPath}`)}`, width));
886
+ for (const line of wrapPlain(field.detail ?? "", width - 3)) {
887
+ if (line) lines.push(fitText(` ${theme.dim(line)}`, width));
888
+ }
889
+ for (const line of wrapPlain(field.statusText ?? "", width - 3)) {
890
+ if (line) lines.push(fitText(` ${theme.fg("error", line)}`, width));
891
+ }
892
+ const progress = progressRow(field, theme, width);
893
+ if (progress) lines.push(progress);
894
+ if (field.steps?.length) lines.push(...stepRows(field.steps, theme, width));
895
+ if (field.output?.length) {
896
+ lines.push(...field.output.slice(-OUTPUT_TAIL).map((line) => fitText(` ${theme.dim(line)}`, width)));
897
+ }
898
+ if (field.actions?.length) {
899
+ lines.push(fitText(` ${theme.key(field.actions.map((action) => `${action.key} ${action.label}`).join(" \xB7 "))}`, width));
900
+ }
901
+ if (state.mode === "choices") lines.push(...choiceRows(field, state, theme, width));
902
+ if (state.mode === "edit" || field.awaitingInput) {
903
+ const draft = state.mode === "edit" ? `${state.draft.slice(0, state.caret)}|${state.draft.slice(state.caret)}` : "|";
904
+ lines.push(fitText(` ${theme.dim("\u203A")} ${draft}`, width));
905
+ }
906
+ return lines;
907
+ }
908
+ function fieldRows(section, state, theme, width, nowMs) {
909
+ const lines = [];
910
+ section.fields.forEach((field, index) => {
911
+ const focused = state.pane === "fields" && state.fieldIndex === index;
912
+ if (field.kind === "info") {
913
+ lines.push(fitText(` ${theme.dim(field.label)} ${theme.dim(field.value ?? field.placeholder ?? "")}`, width));
914
+ return;
915
+ }
916
+ const pending = fieldIsPending(state, section, field);
917
+ const mark = statusMark(field.status, theme, pending ? "\u25CC" : KXM_TUI_MARKS.pending);
918
+ const value = pending && field.value === void 0 ? theme.dim("saving\u2026") : field.value;
919
+ const shown = value ?? theme.dim(field.placeholder ?? "not set");
920
+ const age = field.updatedAt ? theme.dim(` (${formatAge(field.updatedAt, nowMs)})`) : "";
921
+ lines.push(fitText(`${cursorMark(focused, theme)} ${mark} ${alignRight(field.label, `${shown}${age}`, width - 4)}`, width));
922
+ if (focused) lines.push(...focusedFieldDetail(field, state, theme, width));
923
+ });
924
+ return lines;
925
+ }
926
+ function noticeRows(section, theme, width) {
927
+ if (!section?.notice) return [];
928
+ const token = section.noticeLevel === "error" ? "error" : "warning";
929
+ return wrapPlain(section.notice, width - 2).map((line) => fitText(theme.fg(token, line), width));
930
+ }
931
+ function singlePaneLines(sections, state, theme, width, nowMs) {
932
+ const section = sections[state.sectionIndex];
933
+ if (state.pane === "fields" && section) {
934
+ return [
935
+ fitText(theme.headerBg(alignRight(` ${section.title}`, section.source, width)), width),
936
+ ...noticeRows(section, theme, width),
937
+ ...fieldRows(section, state, theme, width, nowMs)
938
+ ];
939
+ }
940
+ return [
941
+ fitText(theme.headerBg(alignRight(" SECTIONS", `${sections.length}`, width)), width),
942
+ ...sections.map((candidate, index) => sectionRow(candidate, index === state.sectionIndex, state.pane, theme, width))
943
+ ];
944
+ }
945
+ function twoPaneLines(sections, state, theme, width, nowMs) {
946
+ const split = paneSplit(width);
947
+ const left = [fitText(theme.key(" SECTIONS"), split.sections)];
948
+ sections.forEach(
949
+ (candidate, index) => left.push(sectionRow(candidate, index === state.sectionIndex, state.pane, theme, split.sections))
950
+ );
951
+ const right = [];
952
+ const section = sections[state.sectionIndex];
953
+ if (section) {
954
+ right.push(fitText(alignRight(` ${section.title}`, section.source, split.fields), split.fields));
955
+ if (section.detail) right.push(fitText(` ${theme.dim(section.detail)}`, split.fields));
956
+ right.push(...noticeRows(section, theme, split.fields));
957
+ right.push(...fieldRows(section, state, theme, split.fields, nowMs));
958
+ } else {
959
+ right.push(fitText(theme.dim("no surface is published"), split.fields));
960
+ }
961
+ const rows = [];
962
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
963
+ rows.push(fitText(`${padRight(left[index] ?? "", split.sections)} ${right[index] ?? ""}`, width));
964
+ }
965
+ return rows;
966
+ }
967
+ function renderKxmTuiPanel(sections, state, theme, options, width = 120) {
968
+ const nowMs = options.nowMs ?? Date.now();
969
+ const title = `${theme.cursor(options.title)}${options.breadcrumb ? ` ${theme.dim(options.breadcrumb)}` : ""}`;
970
+ const header = [fitText(title, width)];
971
+ if (state.help) {
972
+ const help = (options.helpLines ?? ["no help published"]).flatMap((line) => wrapPlain(line, Math.max(1, width - 2)));
973
+ return [...header, ...help.map((line) => fitText(line, width)), fitText(theme.dim("esc close help"), width)];
974
+ }
975
+ const body = width < SPLIT_MIN_WIDTH ? singlePaneLines(sections, state, theme, width, nowMs) : twoPaneLines(sections, state, theme, width, nowMs);
976
+ const footer = options.footer ? [fitText(theme.dim(options.footer), width)] : [];
977
+ return [...header, ...body, ...footer];
978
+ }
979
+ function renderKxmTuiPanelText(sections, state, options, width = 120) {
980
+ return `${renderKxmTuiPanel(sections, state, createPlainKxmTuiTheme(), options, width).map((line) => stripTerminalSequences(line)).join("\n")}
981
+ `;
982
+ }
983
+
984
+ // packages/core/tui/src/tui/panelComponent.ts
985
+ import { Box, HStack, ScrollView, Text, TruncatedText, VStack, Key, matchesKey } from "@earendil-works/pi-tui";
986
+ var KXM_TUI_PANEL_HELP = Object.freeze([
987
+ "\u2191\u2193 move \xB7 \u2190\u2192 or Tab panes \xB7 PgUp/PgDn section \xB7 enter edit or open list",
988
+ "type to filter a list \xB7 x clears a value \xB7 esc backs out \xB7 q quit \xB7 h help"
989
+ ]);
990
+ function rowsComponent() {
991
+ const stack = new VStack([], { gap: 0 });
992
+ const scroll = new ScrollView(stack, { primary: true, overscroll: "contain", scrollbar: "auto" });
993
+ const fill = (rows) => {
994
+ stack.clear();
995
+ for (const row of rows) stack.addChild(new TruncatedText(row, 0, 0));
996
+ };
997
+ return { stack, scroll, fill };
998
+ }
999
+ var KxmTuiPanelComponent = class {
1000
+ root = new VStack([], { gap: 0 });
1001
+ options;
1002
+ body = rowsComponent();
1003
+ sections;
1004
+ state;
1005
+ notice;
1006
+ unsubscribe;
1007
+ constructor(options) {
1008
+ this.options = options;
1009
+ this.sections = options.registry.getSections();
1010
+ this.state = initialKxmTuiPanelState(this.sections);
1011
+ this.unsubscribe = options.registry.subscribe(() => this.refresh());
1012
+ this.rebuild();
1013
+ }
1014
+ getState() {
1015
+ return this.state;
1016
+ }
1017
+ getSections() {
1018
+ return this.sections;
1019
+ }
1020
+ /** Point the cursor at a field, e.g. after opening a named section. */
1021
+ focus(sectionId, fieldId) {
1022
+ const sectionIndex = this.sections.findIndex((section) => section.id === sectionId);
1023
+ if (sectionIndex < 0) return;
1024
+ const fields = this.sections[sectionIndex]?.fields ?? [];
1025
+ const fieldIndex = fieldId ? Math.max(0, fields.findIndex((field) => field.id === fieldId)) : this.state.fieldIndex;
1026
+ this.state = { ...this.state, pane: "fields", sectionIndex, fieldIndex };
1027
+ this.rebuild();
1028
+ this.options.requestRender();
1029
+ }
1030
+ setStatus(message) {
1031
+ this.notice = message;
1032
+ this.rebuild();
1033
+ this.options.requestRender();
1034
+ }
1035
+ refresh() {
1036
+ const next = this.options.registry.getSections();
1037
+ this.sections = next;
1038
+ this.state = reconcileKxmTuiPanelState(this.state, next);
1039
+ this.rebuild();
1040
+ this.options.requestRender();
1041
+ }
1042
+ handleInput(data) {
1043
+ if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.ctrl("u"))) {
1044
+ this.body.scroll.scrollBy(-(this.viewport() - 2));
1045
+ this.options.requestRender();
1046
+ return;
1047
+ }
1048
+ if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("d"))) {
1049
+ this.body.scroll.scrollBy(this.viewport() - 2);
1050
+ this.options.requestRender();
1051
+ return;
1052
+ }
1053
+ const step = reduceKxmTuiInput(this.state, this.sections, data);
1054
+ this.state = step.state;
1055
+ for (const effect of step.effects) this.run(effect);
1056
+ this.rebuild();
1057
+ this.options.requestRender();
1058
+ }
1059
+ viewport() {
1060
+ const height = this.body.scroll.viewportHeight;
1061
+ return height > 3 ? height : 10;
1062
+ }
1063
+ run(effect) {
1064
+ if (effect.type === "quit") {
1065
+ this.options.onQuit();
1066
+ return;
1067
+ }
1068
+ if (effect.type === "notice") {
1069
+ this.notice = effect.message;
1070
+ return;
1071
+ }
1072
+ this.notice = void 0;
1073
+ void this.options.registry.invoke(effect.invocation).then((result) => {
1074
+ if (!result.ok) this.setStatus(result.message ?? "the owner refused that change");
1075
+ else if (this.notice === void 0) this.setStatus(void 0);
1076
+ });
1077
+ }
1078
+ footer() {
1079
+ const status = this.notice ?? this.options.statusText?.();
1080
+ const base = "\u2191\u2193 \xB7 \u2190\u2192 panes \xB7 enter edit \xB7 x clear \xB7 esc back \xB7 h help \xB7 q quit";
1081
+ return status ? `${base} ${status}` : base;
1082
+ }
1083
+ rebuild() {
1084
+ const theme = this.options.theme;
1085
+ const width = this.options.getWidth?.() ?? 120;
1086
+ const title = new Box(1, 0, theme.headerBg);
1087
+ title.addChild(new HStack([
1088
+ { component: new Text(theme.cursor(this.options.title), 0, 0), basis: 12, shrink: 1, minSize: 6 },
1089
+ {
1090
+ component: new Text(
1091
+ this.options.breadcrumb ? theme.dim(fitText(this.options.breadcrumb, Math.max(4, width - 16))) : "",
1092
+ 0,
1093
+ 0
1094
+ ),
1095
+ grow: 1,
1096
+ shrink: 1,
1097
+ minSize: 8
1098
+ }
1099
+ ], { gap: 2 }));
1100
+ this.body.fill(
1101
+ renderKxmTuiPanel(this.sections, this.state, theme, {
1102
+ title: this.options.title,
1103
+ ...this.options.breadcrumb ? { breadcrumb: this.options.breadcrumb } : {},
1104
+ helpLines: this.options.helpLines ?? KXM_TUI_PANEL_HELP,
1105
+ footer: this.footer()
1106
+ }, width)
1107
+ );
1108
+ const panel = new Box(0, 0, theme.panelBg);
1109
+ panel.addChild(this.body.scroll);
1110
+ this.root.clear();
1111
+ this.root.addChild(title, { basis: "auto", shrink: 0 });
1112
+ this.root.addChild(panel, { basis: "auto", grow: 1, shrink: 1, minSize: 1 });
1113
+ }
1114
+ handleMouse(event) {
1115
+ return this.root.handleMouse?.(event);
1116
+ }
1117
+ invalidate() {
1118
+ this.root.invalidate();
1119
+ }
1120
+ render(width) {
1121
+ return this.root.render(width);
1122
+ }
1123
+ dispose() {
1124
+ this.unsubscribe();
1125
+ this.unsubscribe = () => void 0;
1126
+ }
1127
+ };
1128
+ function createKxmTuiPanelComponent(options) {
1129
+ return new KxmTuiPanelComponent(options);
1130
+ }
1131
+
1132
+ // packages/core/tui/src/services/registry.ts
1133
+ function describe(error) {
1134
+ if (error instanceof Error && error.message) return error.message.split(/\r?\n/u)[0].slice(0, KXM_TUI_LIMITS.detailMax);
1135
+ return String(error).split(/\r?\n/u)[0].slice(0, KXM_TUI_LIMITS.detailMax);
1136
+ }
1137
+ function normalizeSource(source) {
1138
+ if (!/^[A-Za-z0-9@][A-Za-z0-9@/._:-]*$/u.test(source) || source.length > KXM_TUI_LIMITS.sourceMax) {
1139
+ throw new Error(`invalid kxm tui surface source: ${source.slice(0, 64)}`);
1140
+ }
1141
+ return source;
1142
+ }
1143
+ function createKxmTuiRegistry(options = {}) {
1144
+ const contributions = /* @__PURE__ */ new Map();
1145
+ const notices = /* @__PURE__ */ new Map();
1146
+ const listeners = /* @__PURE__ */ new Set();
1147
+ const published = /* @__PURE__ */ new Map();
1148
+ const issues = /* @__PURE__ */ new Map();
1149
+ let generation = 0;
1150
+ let disposed = false;
1151
+ let epoch = 0;
1152
+ const report = (error, context) => {
1153
+ if (options.onInternalError) options.onInternalError(error, context);
1154
+ };
1155
+ const emit = (event) => {
1156
+ for (const listener of Array.from(listeners)) {
1157
+ try {
1158
+ listener(event);
1159
+ } catch (error) {
1160
+ report(error, "listener");
1161
+ }
1162
+ }
1163
+ };
1164
+ function readOwner(source, force) {
1165
+ if (!force) {
1166
+ const cached = published.get(source);
1167
+ if (cached) return cached;
1168
+ }
1169
+ const contribution = contributions.get(source);
1170
+ if (!contribution) {
1171
+ published.set(source, []);
1172
+ return [];
1173
+ }
1174
+ let sections;
1175
+ try {
1176
+ const raw = contribution.listSections();
1177
+ const ownerIssues = validateKxmTuiSurface(raw);
1178
+ if (ownerIssues.length > 0) {
1179
+ issues.set(source, ownerIssues);
1180
+ sections = raw.filter((section) => Boolean(section) && typeof section === "object").map((section) => ({
1181
+ ...section,
1182
+ notice: section.notice ?? `published surface was refused: ${ownerIssues[0].code} at ${ownerIssues[0].path}`,
1183
+ noticeLevel: "error"
1184
+ }));
1185
+ } else {
1186
+ issues.delete(source);
1187
+ sections = raw;
1188
+ }
1189
+ } catch (error) {
1190
+ issues.set(source, [{ path: source, code: "list_sections_failed", message: describe(error) }]);
1191
+ report(error, `listSections:${source}`);
1192
+ sections = [{ source, id: "error", title: source, order: 900, notice: describe(error), noticeLevel: "error", fields: [] }];
1193
+ }
1194
+ const pending = notices.get(source);
1195
+ const withNotice = pending !== void 0 && sections.length > 0 ? sections.map((section, index) => index === 0 ? { ...section, notice: pending, noticeLevel: "error" } : section) : sections;
1196
+ published.set(source, withNotice);
1197
+ return withNotice;
1198
+ }
1199
+ function merged() {
1200
+ const all = [];
1201
+ for (const source of contributions.keys()) all.push(...readOwner(source, false));
1202
+ return all.slice().sort((a, b) => a.order - b.order || a.source.localeCompare(b.source) || a.title.localeCompare(b.title));
1203
+ }
1204
+ return {
1205
+ register(contribution) {
1206
+ if (disposed) throw new Error("kxm tui registry is disposed");
1207
+ const source = normalizeSource(contribution.source);
1208
+ notices.delete(source);
1209
+ published.delete(source);
1210
+ issues.delete(source);
1211
+ contributions.set(source, { ...contribution, source });
1212
+ emit({ reason: "publish", source });
1213
+ },
1214
+ unregister(source) {
1215
+ contributions.delete(source);
1216
+ published.delete(source);
1217
+ issues.delete(source);
1218
+ notices.delete(source);
1219
+ emit({ reason: "publish", source });
1220
+ },
1221
+ getSections() {
1222
+ return merged();
1223
+ },
1224
+ getIssues() {
1225
+ return [...issues.values()].flat();
1226
+ },
1227
+ async invoke(invocation) {
1228
+ if (disposed) return { ok: false, message: "kxm tui registry is disposed" };
1229
+ const contribution = contributions.get(invocation.source);
1230
+ if (!contribution) {
1231
+ emit({ reason: "refused", source: invocation.source, message: "unknown owner" });
1232
+ return { ok: false, message: `${invocation.source} does not own a surface here` };
1233
+ }
1234
+ const handler = contribution.handlers[invocation.action];
1235
+ if (!handler) {
1236
+ const message = `${invocation.source} does not accept "${invocation.action}"`;
1237
+ emit({ reason: "refused", source: invocation.source, message });
1238
+ return { ok: false, message };
1239
+ }
1240
+ const currentEpoch = epoch;
1241
+ try {
1242
+ await handler({
1243
+ sectionId: invocation.sectionId,
1244
+ fieldId: invocation.fieldId,
1245
+ action: invocation.action,
1246
+ ...invocation.value === void 0 ? {} : { value: invocation.value }
1247
+ });
1248
+ } catch (error) {
1249
+ const failure = contribution.describeError?.(error, invocation.action) ?? describe(error);
1250
+ notices.set(invocation.source, failure);
1251
+ emit({ reason: "failed", source: invocation.source, message: failure });
1252
+ readOwner(invocation.source, true);
1253
+ generation += 1;
1254
+ return { ok: false, message: failure };
1255
+ }
1256
+ if (currentEpoch !== epoch) {
1257
+ readOwner(invocation.source, true);
1258
+ return { ok: false, message: "stale write result was discarded" };
1259
+ }
1260
+ notices.delete(invocation.source);
1261
+ readOwner(invocation.source, true);
1262
+ generation += 1;
1263
+ emit({ reason: "publish", source: invocation.source });
1264
+ return { ok: true };
1265
+ },
1266
+ subscribe(listener) {
1267
+ listeners.add(listener);
1268
+ return () => listeners.delete(listener);
1269
+ },
1270
+ getGeneration() {
1271
+ return generation;
1272
+ },
1273
+ hasSource(source) {
1274
+ return contributions.has(source);
1275
+ },
1276
+ listSources() {
1277
+ return [...contributions.keys()];
1278
+ },
1279
+ dispose() {
1280
+ if (disposed) return;
1281
+ disposed = true;
1282
+ epoch += 1;
1283
+ listeners.clear();
1284
+ contributions.clear();
1285
+ published.clear();
1286
+ issues.clear();
1287
+ notices.clear();
1288
+ }
1289
+ };
1290
+ }
1291
+ function checkedKxmTuiSections(sections) {
1292
+ return assertKxmTuiSurface(sections);
1293
+ }
1294
+
1295
+ // packages/core/tui/src/adapters/terminal.ts
1296
+ import { ProcessTerminal, TuiAltScreen, stripTerminalSequences as stripTerminalSequences2 } from "@earendil-works/pi-tui";
1297
+ function renderKxmTuiPanelFrame(input) {
1298
+ const theme = input.theme ?? createPlainKxmTuiTheme();
1299
+ const lines = renderKxmTuiPanel(
1300
+ input.registry.getSections(),
1301
+ initialKxmTuiPanelState(input.registry.getSections()),
1302
+ theme,
1303
+ { title: input.title, footer: input.footer ?? "non-interactive frame", nowMs: input.nowMs ?? Date.now() },
1304
+ input.width ?? 100
1305
+ );
1306
+ return `${lines.map((line) => stripTerminalSequences2(line)).join("\n")}
1307
+ `;
1308
+ }
1309
+ async function runKxmTuiPanel(input) {
1310
+ const theme = input.theme ?? createDefaultKxmTuiTheme();
1311
+ const tty = input.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
1312
+ if (!tty) {
1313
+ input.stdout(renderKxmTuiPanelFrame({
1314
+ registry: input.registry,
1315
+ title: input.title,
1316
+ theme,
1317
+ ...input.now === void 0 ? {} : { nowMs: input.now() },
1318
+ ...input.breadcrumb ? { footer: input.breadcrumb } : {}
1319
+ }));
1320
+ return 0;
1321
+ }
1322
+ const terminal = input.terminal ?? new ProcessTerminal();
1323
+ const tui = new TuiAltScreen(terminal, false, void 0, { mouse: true });
1324
+ let timer;
1325
+ let stopped = false;
1326
+ let finished = () => void 0;
1327
+ const done = new Promise((resolve) => {
1328
+ finished = resolve;
1329
+ });
1330
+ const stop = () => {
1331
+ if (stopped) return;
1332
+ stopped = true;
1333
+ if (timer) clearInterval(timer);
1334
+ input.abort?.removeEventListener("abort", stop);
1335
+ panel.dispose();
1336
+ tui.stop();
1337
+ finished();
1338
+ };
1339
+ const panel = new KxmTuiPanelComponent({
1340
+ registry: input.registry,
1341
+ theme,
1342
+ title: input.title,
1343
+ ...input.breadcrumb ? { breadcrumb: input.breadcrumb } : {},
1344
+ ...input.helpLines ? { helpLines: input.helpLines } : {},
1345
+ requestRender: () => tui.requestRender(),
1346
+ onQuit: stop,
1347
+ getWidth: () => terminal.columns
1348
+ });
1349
+ input.abort?.addEventListener("abort", stop, { once: true });
1350
+ tui.setLayoutRoot(panel.root);
1351
+ tui.setFocus(panel);
1352
+ tui.start();
1353
+ timer = setInterval(() => panel.refresh(), input.pollMs ?? 2e3);
1354
+ timer.unref?.();
1355
+ await done;
1356
+ return stopped && input.abort?.aborted ? 130 : 0;
1357
+ }
1358
+
1359
+ // packages/core/tui/src/adapters/pi.ts
1360
+ function createKxmTuiPiPanel(options) {
1361
+ let closed = false;
1362
+ const panel = new KxmTuiPanelComponent({
1363
+ registry: options.registry,
1364
+ theme: createKxmTuiThemeFromPi(options.theme),
1365
+ title: options.title,
1366
+ ...options.breadcrumb === void 0 ? {} : { breadcrumb: options.breadcrumb },
1367
+ ...options.helpLines === void 0 ? {} : { helpLines: options.helpLines },
1368
+ requestRender: () => options.tui.requestRender(),
1369
+ onQuit: () => {
1370
+ if (closed) return;
1371
+ closed = true;
1372
+ panel.dispose();
1373
+ options.done?.(true);
1374
+ },
1375
+ ...options.width === void 0 ? {} : { getWidth: options.width }
1376
+ });
1377
+ return {
1378
+ render: (width) => panel.render(width),
1379
+ handleInput: (data) => {
1380
+ if (closed) return;
1381
+ panel.handleInput(data);
1382
+ options.tui.requestRender();
1383
+ },
1384
+ invalidate: () => panel.invalidate()
1385
+ };
1386
+ }
1387
+ export {
1388
+ KXM_TUI_ABORT_ACTION,
1389
+ KXM_TUI_CLEAR_ACTION,
1390
+ KXM_TUI_CONTRACT_SCHEMA,
1391
+ KXM_TUI_ELLIPSIS,
1392
+ KXM_TUI_FIELD_KINDS,
1393
+ KXM_TUI_INPUT_MAX_LENGTH,
1394
+ KXM_TUI_LIMITS,
1395
+ KXM_TUI_MARKS,
1396
+ KXM_TUI_NOTICE_LEVELS,
1397
+ KXM_TUI_PANEL_HELP,
1398
+ KXM_TUI_SET_ACTION,
1399
+ KXM_TUI_STATUSES,
1400
+ KXM_TUI_STEP_STATES,
1401
+ KxmTuiPanelComponent,
1402
+ alignRight,
1403
+ assertKxmTuiSurface,
1404
+ checkedKxmTuiSections,
1405
+ createDefaultKxmTuiTheme,
1406
+ createKxmTuiAnsiTheme,
1407
+ createKxmTuiPanelComponent,
1408
+ createKxmTuiPiPanel,
1409
+ createKxmTuiRegistry,
1410
+ createKxmTuiThemeFromPi,
1411
+ createPlainKxmTuiTheme,
1412
+ currentField,
1413
+ currentSection,
1414
+ decodeKxmTuiInput,
1415
+ deleteBackward,
1416
+ fieldIsEditable,
1417
+ firstSelectableFieldIndex,
1418
+ fitText,
1419
+ focusedFieldIsEditable,
1420
+ formatAge,
1421
+ groupedKxmTuiChoices,
1422
+ initialKxmTuiPanelState,
1423
+ insertAt,
1424
+ isControlInput,
1425
+ isSelectableField,
1426
+ normalizeInputText,
1427
+ padRight,
1428
+ pendingKey,
1429
+ progressBar,
1430
+ reconcileKxmTuiPanelState,
1431
+ reduceKxmTuiInput,
1432
+ renderKxmTuiPanel,
1433
+ renderKxmTuiPanelFrame,
1434
+ renderKxmTuiPanelText,
1435
+ runKxmTuiPanel,
1436
+ validateKxmTuiSurface,
1437
+ visibleChoices,
1438
+ wrapPlain
1439
+ };