@causyne/agent 0.0.0-stage → 1.10.1

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 (274) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +31 -2
  3. package/SECURITY.md +24 -0
  4. package/bin/causyne.js +13 -0
  5. package/docs/ARCHITECTURE.md +63 -0
  6. package/docs/PROVENANCE.md +29 -0
  7. package/docs/SECURITY_REVIEW.md +89 -0
  8. package/docs/THIRD_PARTY_NOTICES.md +49 -0
  9. package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
  10. package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
  11. package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
  12. package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
  13. package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
  14. package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
  15. package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
  16. package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
  17. package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
  18. package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
  19. package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
  20. package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
  21. package/native-skills/android/play-store-release/SKILL.md +43 -0
  22. package/native-skills/android/room-and-persistence/SKILL.md +40 -0
  23. package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
  24. package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
  25. package/native-skills/backend/caching-strategy/SKILL.md +44 -0
  26. package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
  27. package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
  28. package/native-skills/backend/rest-api-design/SKILL.md +40 -0
  29. package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
  30. package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
  31. package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
  32. package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
  33. package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
  34. package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
  35. package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
  36. package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
  37. package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
  38. package/native-skills/data/metric-definition/SKILL.md +42 -0
  39. package/native-skills/data/notebook-to-production/SKILL.md +43 -0
  40. package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
  41. package/native-skills/database/connection-pooling/SKILL.md +42 -0
  42. package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
  43. package/native-skills/database/query-optimisation/SKILL.md +38 -0
  44. package/native-skills/database/safe-migrations/SKILL.md +47 -0
  45. package/native-skills/database/schema-design/SKILL.md +38 -0
  46. package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
  47. package/native-skills/design/colour-system-construction/SKILL.md +38 -0
  48. package/native-skills/design/design-direction-brief/SKILL.md +41 -0
  49. package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
  50. package/native-skills/design/responsive-strategy/SKILL.md +39 -0
  51. package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
  52. package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
  53. package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
  54. package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
  55. package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
  56. package/native-skills/devops/incident-triage/SKILL.md +38 -0
  57. package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
  58. package/native-skills/devops/observability-baseline/SKILL.md +38 -0
  59. package/native-skills/devops/secret-management/SKILL.md +40 -0
  60. package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
  61. package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
  62. package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
  63. package/native-skills/git/branch-strategy/SKILL.md +39 -0
  64. package/native-skills/git/commit-hygiene/SKILL.md +41 -0
  65. package/native-skills/git/history-recovery/SKILL.md +44 -0
  66. package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
  67. package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
  68. package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
  69. package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
  70. package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
  71. package/native-skills/mobile/deep-linking/SKILL.md +39 -0
  72. package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
  73. package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
  74. package/native-skills/mobile/push-notifications/SKILL.md +38 -0
  75. package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
  76. package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
  77. package/native-skills/performance/load-testing/SKILL.md +42 -0
  78. package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
  79. package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
  80. package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
  81. package/native-skills/python/async-correctness/SKILL.md +40 -0
  82. package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
  83. package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
  84. package/native-skills/python/environment-isolation/SKILL.md +41 -0
  85. package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
  86. package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
  87. package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
  88. package/native-skills/security/authorization-model/SKILL.md +42 -0
  89. package/native-skills/security/crypto-usage/SKILL.md +48 -0
  90. package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
  91. package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
  92. package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
  93. package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
  94. package/native-skills/security/threat-modelling/SKILL.md +46 -0
  95. package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
  96. package/native-skills/testing/property-based-testing/SKILL.md +46 -0
  97. package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
  98. package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
  99. package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
  100. package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
  101. package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
  102. package/native-skills/web/accessible-components/SKILL.md +45 -0
  103. package/native-skills/web/api-client-resilience/SKILL.md +39 -0
  104. package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
  105. package/native-skills/web/core-web-vitals/SKILL.md +40 -0
  106. package/native-skills/web/css-layout-systems/SKILL.md +41 -0
  107. package/native-skills/web/form-validation-ux/SKILL.md +39 -0
  108. package/native-skills/web/react-state-architecture/SKILL.md +41 -0
  109. package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
  110. package/package.json +68 -4
  111. package/src/agentLoop.js +2458 -0
  112. package/src/appChannel.js +109 -0
  113. package/src/approvalGrants.js +66 -0
  114. package/src/auditLog.js +52 -0
  115. package/src/autoUpdate.js +12 -0
  116. package/src/capabilities.js +36 -0
  117. package/src/checkpoints.js +1199 -0
  118. package/src/cliMode.js +638 -0
  119. package/src/commands.js +805 -0
  120. package/src/compiledSkills.js +540 -0
  121. package/src/config.js +130 -0
  122. package/src/contextManager.js +498 -0
  123. package/src/cortex/autoCondenser.js +192 -0
  124. package/src/cortex/provisioner.js +27 -0
  125. package/src/cortex/vectorDb.js +447 -0
  126. package/src/designPalettes.js +99 -0
  127. package/src/devMode.js +6 -0
  128. package/src/embodimentSession.js +180 -0
  129. package/src/entrypoint.js +194 -0
  130. package/src/execSafe.js +21 -0
  131. package/src/gitSnapshot.js +378 -0
  132. package/src/mcp/jsonRpc.js +165 -0
  133. package/src/mcp/mcpClient.js +136 -0
  134. package/src/mcp/mcpRegistry.js +207 -0
  135. package/src/memory.js +62 -0
  136. package/src/memoryManager.js +124 -0
  137. package/src/modelCatalogue.js +132 -0
  138. package/src/modelIds.js +42 -0
  139. package/src/modelRouter.js +145 -0
  140. package/src/outputStore.js +246 -0
  141. package/src/perception.js +124 -0
  142. package/src/planCache.js +437 -0
  143. package/src/procSignal.js +32 -0
  144. package/src/providers/anthropic.js +408 -0
  145. package/src/providers/base.js +328 -0
  146. package/src/providers/dashscope-byok.js +86 -0
  147. package/src/providers/deepseek.js +80 -0
  148. package/src/providers/factory.js +37 -0
  149. package/src/providers/gemini.js +346 -0
  150. package/src/providers/moonshot.js +73 -0
  151. package/src/providers/muse.js +169 -0
  152. package/src/providers/ollama.js +104 -0
  153. package/src/providers/openai-compatible.js +84 -0
  154. package/src/providers/openai.js +20 -0
  155. package/src/providers/qwen-byok.js +76 -0
  156. package/src/providers/xai.js +73 -0
  157. package/src/provisioning/capabilityConsent.js +24 -0
  158. package/src/provisioning/capabilityProvisioner.js +271 -0
  159. package/src/provisioning/capabilityTools.js +29 -0
  160. package/src/reporting.js +373 -0
  161. package/src/sandbox.js +529 -0
  162. package/src/sessionStore.js +279 -0
  163. package/src/skillsManager.js +288 -0
  164. package/src/slopDetector.js +307 -0
  165. package/src/taskBuffer.js +45 -0
  166. package/src/taskShape.js +241 -0
  167. package/src/textDiff.js +150 -0
  168. package/src/tokenTracker.js +560 -0
  169. package/src/tool-scripts/ax_snapshot.applescript +39 -0
  170. package/src/tool-scripts/ax_snapshot.ps1 +10 -0
  171. package/src/tool-scripts/desktop_act.applescript +58 -0
  172. package/src/tool-scripts/resolve_automator.py +119 -0
  173. package/src/toolPacks.js +524 -0
  174. package/src/tools/archive.js +63 -0
  175. package/src/tools/astryxOracle.js +78 -0
  176. package/src/tools/axSnapshot.js +165 -0
  177. package/src/tools/blenderAutomator.js +349 -0
  178. package/src/tools/browserAutomator.js +252 -0
  179. package/src/tools/checkSystemSpecs.js +64 -0
  180. package/src/tools/clipboard.js +36 -0
  181. package/src/tools/codeSearchAst.js +101 -0
  182. package/src/tools/codeSearcher.js +230 -0
  183. package/src/tools/codeTools.js +262 -0
  184. package/src/tools/codebaseSearch.js +75 -0
  185. package/src/tools/createSkill.js +129 -0
  186. package/src/tools/deferredMcpSearch.js +246 -0
  187. package/src/tools/desktopAct.js +208 -0
  188. package/src/tools/desktopOpen.js +34 -0
  189. package/src/tools/dynamicLoader.js +44 -0
  190. package/src/tools/executeProceduralMedia.js +93 -0
  191. package/src/tools/fsDelete.js +51 -0
  192. package/src/tools/fsEdit.js +105 -0
  193. package/src/tools/fsFind.js +128 -0
  194. package/src/tools/fsList.js +77 -0
  195. package/src/tools/fsMove.js +57 -0
  196. package/src/tools/fsRead.js +87 -0
  197. package/src/tools/fsSearch.js +198 -0
  198. package/src/tools/fsWrite.js +41 -0
  199. package/src/tools/gameAiBehaviorTree.js +208 -0
  200. package/src/tools/gameAnimationRig.js +194 -0
  201. package/src/tools/gameAssetFetcher.js +372 -0
  202. package/src/tools/gameAssetHarvester.js +210 -0
  203. package/src/tools/gameAudioSynth.js +267 -0
  204. package/src/tools/gameBuilder.js +886 -0
  205. package/src/tools/gameCanvasAudit.js +101 -0
  206. package/src/tools/gameCinematicSequencer.js +151 -0
  207. package/src/tools/gameDataForge.js +124 -0
  208. package/src/tools/gameDialogueTreeGen.js +166 -0
  209. package/src/tools/gameEconomyBalancer.js +161 -0
  210. package/src/tools/gameEntitySpawner.js +207 -0
  211. package/src/tools/gameLoreForge.js +153 -0
  212. package/src/tools/gameQuestArcBuilder.js +152 -0
  213. package/src/tools/gameSceneBuilder.js +158 -0
  214. package/src/tools/gameShaderForge.js +181 -0
  215. package/src/tools/gameTilemapGenerator.js +164 -0
  216. package/src/tools/gameUiHudBuilder.js +292 -0
  217. package/src/tools/gameVfxEmitterGen.js +108 -0
  218. package/src/tools/gameWebRuntimeInspector.js +91 -0
  219. package/src/tools/generateMediaViaApi.js +358 -0
  220. package/src/tools/gitExec.js +54 -0
  221. package/src/tools/godotAutomator.js +437 -0
  222. package/src/tools/manageLocalAiDependencies.js +220 -0
  223. package/src/tools/memoryWrite.js +99 -0
  224. package/src/tools/multiEdit.js +107 -0
  225. package/src/tools/palmierAutomator.js +104 -0
  226. package/src/tools/pdfReader.js +112 -0
  227. package/src/tools/persistentShell.js +148 -0
  228. package/src/tools/printer.js +316 -0
  229. package/src/tools/procTerm.js +91 -0
  230. package/src/tools/processList.js +52 -0
  231. package/src/tools/readNativeSkill.js +39 -0
  232. package/src/tools/registry.js +277 -0
  233. package/src/tools/resolveAutomator.js +102 -0
  234. package/src/tools/screenshot.js +75 -0
  235. package/src/tools/subagentOrchestrator.js +305 -0
  236. package/src/tools/sysRun.js +125 -0
  237. package/src/tools/todoWrite.js +75 -0
  238. package/src/tools/toolDispatch.js +127 -0
  239. package/src/tools/toolForge.js +124 -0
  240. package/src/tools/updatePlan.js +88 -0
  241. package/src/tools/viewImage.js +82 -0
  242. package/src/tools/visionInspector.js +154 -0
  243. package/src/tools/webFetch.js +74 -0
  244. package/src/tools/webSearch.js +82 -0
  245. package/src/tui/ask.js +164 -0
  246. package/src/tui/banner.js +158 -0
  247. package/src/tui/block.js +152 -0
  248. package/src/tui/chatRenderer.js +953 -0
  249. package/src/tui/completion.js +424 -0
  250. package/src/tui/dialogs.js +606 -0
  251. package/src/tui/diff.js +296 -0
  252. package/src/tui/history.js +135 -0
  253. package/src/tui/home.js +217 -0
  254. package/src/tui/invocation.js +105 -0
  255. package/src/tui/keys.js +443 -0
  256. package/src/tui/lineEditor.js +256 -0
  257. package/src/tui/overlay.js +256 -0
  258. package/src/tui/prompt.js +1258 -0
  259. package/src/tui/selectList.js +335 -0
  260. package/src/tui/stage.js +330 -0
  261. package/src/tui/stageSurface.js +609 -0
  262. package/src/tui/statusLine.js +254 -0
  263. package/src/tui/text.js +345 -0
  264. package/src/tui/theme.js +393 -0
  265. package/src/tui/toolDisplay.js +176 -0
  266. package/src/tui/transcript.js +235 -0
  267. package/src/tui/viewport.js +337 -0
  268. package/src/uncertainty.js +271 -0
  269. package/src/userSkills.js +265 -0
  270. package/src/utils/cli_ui.js +0 -0
  271. package/src/utils/inquirer.js +12 -0
  272. package/src/utils/paths.js +37 -0
  273. package/src/version.js +38 -0
  274. package/src/vfeTracker.js +220 -0
@@ -0,0 +1,256 @@
1
+ // A multi-line text buffer with a cursor, and the arithmetic to draw it.
2
+ //
3
+ // The chat prompt was a single `inquirer` line: no way to write a paragraph, no
4
+ // way to fix the start of a sentence without deleting the end of it. This is the
5
+ // buffer that replaces it.
6
+ //
7
+ // The buffer holds CODE POINTS, not UTF-16 units. Storing a JavaScript string
8
+ // and moving the cursor by index splits an emoji in half and leaves the terminal
9
+ // printing a replacement character — the cursor must move by what the user sees
10
+ // as one character.
11
+ //
12
+ // Drawing is not done here. render() returns the lines and where the cursor
13
+ // landed in them; putting bytes on the terminal belongs to prompt.js.
14
+ //
15
+ // Pure Node. No dependency, no shell.
16
+
17
+ import { visibleWidth } from './text.js';
18
+
19
+ /** Split into code points, so one emoji is one editable unit. */
20
+ function toCodePoints(text) {
21
+ return Array.from(String(text == null ? '' : text));
22
+ }
23
+
24
+ // A "word" for word-wise movement. Deliberately generous: an identifier, a
25
+ // path segment and a flag should each move as one unit.
26
+ const WORD_CHARACTER = /[\p{L}\p{N}_-]/u;
27
+
28
+ export class LineEditor {
29
+ constructor(initial = '') {
30
+ this.setText(initial);
31
+ this.killRing = '';
32
+ }
33
+
34
+ // -------------------------------------------------------------------------
35
+ // Content
36
+ // -------------------------------------------------------------------------
37
+
38
+ setText(text) {
39
+ this.lines = String(text == null ? '' : text).split('\n').map(toCodePoints);
40
+ if (this.lines.length === 0) this.lines = [[]];
41
+ this.line = this.lines.length - 1;
42
+ this.col = this.lines[this.line].length;
43
+ }
44
+
45
+ getText() {
46
+ return this.lines.map((line) => line.join('')).join('\n');
47
+ }
48
+
49
+ get isEmpty() {
50
+ return this.lines.length === 1 && this.lines[0].length === 0;
51
+ }
52
+
53
+ clear() {
54
+ this.lines = [[]];
55
+ this.line = 0;
56
+ this.col = 0;
57
+ }
58
+
59
+ /** Insert text at the cursor. Embedded newlines split the line, as a paste does. */
60
+ insert(text) {
61
+ const parts = String(text).split('\n');
62
+ const current = this.lines[this.line];
63
+ const before = current.slice(0, this.col);
64
+ const after = current.slice(this.col);
65
+
66
+ if (parts.length === 1) {
67
+ this.lines[this.line] = before.concat(toCodePoints(parts[0]), after);
68
+ this.col += toCodePoints(parts[0]).length;
69
+ return;
70
+ }
71
+
72
+ const inserted = parts.map(toCodePoints);
73
+ const first = before.concat(inserted[0]);
74
+ const last = inserted[inserted.length - 1].concat(after);
75
+ const middle = inserted.slice(1, -1);
76
+
77
+ this.lines.splice(this.line, 1, first, ...middle, last);
78
+ this.line += parts.length - 1;
79
+ this.col = inserted[inserted.length - 1].length;
80
+ }
81
+
82
+ newline() {
83
+ const current = this.lines[this.line];
84
+ const before = current.slice(0, this.col);
85
+ const after = current.slice(this.col);
86
+ this.lines.splice(this.line, 1, before, after);
87
+ this.line += 1;
88
+ this.col = 0;
89
+ }
90
+
91
+ backspace() {
92
+ if (this.col > 0) {
93
+ this.lines[this.line].splice(this.col - 1, 1);
94
+ this.col -= 1;
95
+ return;
96
+ }
97
+ if (this.line === 0) return;
98
+ // Joining lines: the cursor lands where the two met.
99
+ const removed = this.lines.splice(this.line, 1)[0];
100
+ this.line -= 1;
101
+ this.col = this.lines[this.line].length;
102
+ this.lines[this.line] = this.lines[this.line].concat(removed);
103
+ }
104
+
105
+ deleteForward() {
106
+ const current = this.lines[this.line];
107
+ if (this.col < current.length) {
108
+ current.splice(this.col, 1);
109
+ return;
110
+ }
111
+ if (this.line >= this.lines.length - 1) return;
112
+ const next = this.lines.splice(this.line + 1, 1)[0];
113
+ this.lines[this.line] = current.concat(next);
114
+ }
115
+
116
+ // -------------------------------------------------------------------------
117
+ // Movement
118
+ // -------------------------------------------------------------------------
119
+
120
+ moveLeft() {
121
+ if (this.col > 0) { this.col -= 1; return; }
122
+ if (this.line > 0) { this.line -= 1; this.col = this.lines[this.line].length; }
123
+ }
124
+
125
+ moveRight() {
126
+ if (this.col < this.lines[this.line].length) { this.col += 1; return; }
127
+ if (this.line < this.lines.length - 1) { this.line += 1; this.col = 0; }
128
+ }
129
+
130
+ moveUp() {
131
+ if (this.line === 0) return false;
132
+ this.line -= 1;
133
+ this.col = Math.min(this.col, this.lines[this.line].length);
134
+ return true;
135
+ }
136
+
137
+ moveDown() {
138
+ if (this.line >= this.lines.length - 1) return false;
139
+ this.line += 1;
140
+ this.col = Math.min(this.col, this.lines[this.line].length);
141
+ return true;
142
+ }
143
+
144
+ moveHome() { this.col = 0; }
145
+ moveEnd() { this.col = this.lines[this.line].length; }
146
+
147
+ moveWordLeft() {
148
+ const line = this.lines[this.line];
149
+ if (this.col === 0) { this.moveLeft(); return; }
150
+ let index = this.col;
151
+ while (index > 0 && !WORD_CHARACTER.test(line[index - 1])) index -= 1;
152
+ while (index > 0 && WORD_CHARACTER.test(line[index - 1])) index -= 1;
153
+ this.col = index;
154
+ }
155
+
156
+ moveWordRight() {
157
+ const line = this.lines[this.line];
158
+ if (this.col === line.length) { this.moveRight(); return; }
159
+ let index = this.col;
160
+ while (index < line.length && !WORD_CHARACTER.test(line[index])) index += 1;
161
+ while (index < line.length && WORD_CHARACTER.test(line[index])) index += 1;
162
+ this.col = index;
163
+ }
164
+
165
+ // -------------------------------------------------------------------------
166
+ // Kill and yank — the readline bindings a terminal user already has muscle
167
+ // memory for. Killed text goes to a ring so Ctrl+Y can put it back.
168
+ // -------------------------------------------------------------------------
169
+
170
+ killToEnd() {
171
+ const line = this.lines[this.line];
172
+ this.killRing = line.slice(this.col).join('');
173
+ this.lines[this.line] = line.slice(0, this.col);
174
+ }
175
+
176
+ killToStart() {
177
+ const line = this.lines[this.line];
178
+ this.killRing = line.slice(0, this.col).join('');
179
+ this.lines[this.line] = line.slice(this.col);
180
+ this.col = 0;
181
+ }
182
+
183
+ killWordLeft() {
184
+ const start = this.col;
185
+ this.moveWordLeft();
186
+ const line = this.lines[this.line];
187
+ this.killRing = line.slice(this.col, start).join('');
188
+ line.splice(this.col, start - this.col);
189
+ }
190
+
191
+ yank() {
192
+ if (this.killRing) this.insert(this.killRing);
193
+ }
194
+
195
+ // -------------------------------------------------------------------------
196
+ // Layout
197
+ // -------------------------------------------------------------------------
198
+
199
+ /**
200
+ * Lay the buffer out for a terminal `width` columns wide.
201
+ *
202
+ * Returns the display rows plus where the cursor ended up in them. A logical
203
+ * line longer than the terminal soft-wraps onto several rows, so display row
204
+ * and logical line are not the same number — mixing them up is what makes a
205
+ * redraw leave debris on screen.
206
+ */
207
+ render(width, continuation = '') {
208
+ const usable = Math.max(8, width);
209
+ const rows = [];
210
+ let cursorRow = 0;
211
+ let cursorCol = 0;
212
+
213
+ for (let index = 0; index < this.lines.length; index++) {
214
+ const prefix = index === 0 ? '' : continuation;
215
+ const prefixWidth = visibleWidth(prefix);
216
+ const budget = Math.max(1, usable - prefixWidth);
217
+ const line = this.lines[index];
218
+
219
+ let row = prefix;
220
+ let used = 0;
221
+ let placed = false;
222
+
223
+ for (let position = 0; position <= line.length; position++) {
224
+ if (index === this.line && position === this.col) {
225
+ cursorRow = rows.length;
226
+ cursorCol = prefixWidth + used;
227
+ placed = true;
228
+ }
229
+ if (position === line.length) break;
230
+
231
+ const character = line[position];
232
+ const charWidth = visibleWidth(character);
233
+ if (used + charWidth > budget) {
234
+ rows.push(row);
235
+ row = prefix;
236
+ used = 0;
237
+ // Re-place the cursor if it sat exactly at the wrap point.
238
+ if (placed && cursorRow === rows.length - 1 && cursorCol === prefixWidth + budget) {
239
+ cursorRow = rows.length;
240
+ cursorCol = prefixWidth;
241
+ }
242
+ }
243
+ row += character;
244
+ used += charWidth;
245
+ }
246
+
247
+ rows.push(row);
248
+ if (index === this.line && !placed) {
249
+ cursorRow = rows.length - 1;
250
+ cursorCol = prefixWidth + used;
251
+ }
252
+ }
253
+
254
+ return { rows, cursorRow, cursorCol };
255
+ }
256
+ }
@@ -0,0 +1,256 @@
1
+ // Modal layer: the thing that makes this an application rather than a log.
2
+ //
3
+ // opencode keeps a dialog STACK and pushes a "modal" entry onto a keybinding
4
+ // mode stack while any dialog is open (ui/dialog.tsx, keymap.tsx). That detail is
5
+ // the whole design. Without a mode stack, a dialog is just a box that happens to
6
+ // be painted on top, and keys leak through to the prompt underneath — you type
7
+ // to filter the palette and your text also lands in the message box. With one,
8
+ // the top layer owns the keyboard and pops cleanly.
9
+ //
10
+ // The backdrop is dimmed rather than blacked out. opencode paints an alpha fill
11
+ // over the screen; a cell grid cannot blend, but it can add the SGR dim
12
+ // attribute to every cell behind the panel, which reads the same way and keeps
13
+ // the conversation legible underneath.
14
+ //
15
+ // Pure Node over ./screen.js, ./theme.js and ./text.js. No dependency, no shell.
16
+
17
+ import { paint, bold, dim, GLYPHS, HAS_COLOR, HAS_UNICODE } from './theme.js';
18
+ import { truncate, visibleWidth } from './text.js';
19
+
20
+ // Box-drawing set, degrading with the rest of the glyphs.
21
+ export const FRAME = HAS_UNICODE
22
+ ? { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' }
23
+ : { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' };
24
+
25
+ /**
26
+ * Add the SGR dim attribute to every cell in a region.
27
+ *
28
+ * Operates on the grid AFTER the page beneath has been painted, which is why the
29
+ * conversation stays readable behind a dialog instead of being erased.
30
+ */
31
+ export function dimBackdrop(screen, viewport, hole) {
32
+ if (!HAS_COLOR) return;
33
+ for (let row = viewport.y; row < viewport.y + viewport.height; row++) {
34
+ if (row < 0 || row >= screen.height) continue;
35
+ for (let column = viewport.x; column < viewport.x + viewport.width; column++) {
36
+ if (column < 0 || column >= screen.width) continue;
37
+ if (hole
38
+ && row >= hole.y && row < hole.y + hole.height
39
+ && column >= hole.x && column < hole.x + hole.width) continue;
40
+
41
+ const cell = screen.cells[row * screen.width + column];
42
+ if (cell.ch === ' ' && !cell.style) continue;
43
+ const parts = cell.style ? cell.style.split(';') : [];
44
+ if (!parts.includes('2')) parts.push('2');
45
+ cell.style = parts.join(';');
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Draw a framed panel and return the rect available INSIDE it.
52
+ *
53
+ * The frame is painted onto its own blank ground first, so a dialog never shows
54
+ * the transcript bleeding through the gaps between its rows.
55
+ */
56
+ export function drawPanel(screen, rect, { title = '', footer = '' } = {}) {
57
+ if (rect.width < 4 || rect.height < 3) return { x: rect.x, y: rect.y, width: 0, height: 0 };
58
+
59
+ screen.fill(rect.x, rect.y, rect.width, rect.height, '');
60
+
61
+ const inner = rect.width - 2;
62
+ const line = FRAME.h.repeat(inner);
63
+
64
+ // Title sits in the top border, the way every terminal dialog has since curses.
65
+ let top = FRAME.tl + line + FRAME.tr;
66
+ if (title) {
67
+ const label = ` ${truncate(title, Math.max(0, inner - 4), GLYPHS.ellipsis)} `;
68
+ const width = visibleWidth(label);
69
+ top = FRAME.tl + FRAME.h + label + FRAME.h.repeat(Math.max(0, inner - width - 1)) + FRAME.tr;
70
+ }
71
+
72
+ let bottom = FRAME.bl + line + FRAME.br;
73
+ if (footer) {
74
+ const label = ` ${truncate(footer, Math.max(0, inner - 4), GLYPHS.ellipsis)} `;
75
+ const width = visibleWidth(label);
76
+ bottom = FRAME.bl + FRAME.h.repeat(Math.max(0, inner - width - 1)) + label + FRAME.h + FRAME.br;
77
+ }
78
+
79
+ screen.put(rect.x, rect.y, paint('rule', top), rect.width);
80
+ screen.put(rect.x, rect.y + rect.height - 1, paint('rule', bottom), rect.width);
81
+
82
+ for (let row = rect.y + 1; row < rect.y + rect.height - 1; row++) {
83
+ screen.put(rect.x, row, paint('rule', FRAME.v), 1);
84
+ screen.put(rect.x + rect.width - 1, row, paint('rule', FRAME.v), 1);
85
+ }
86
+
87
+ return {
88
+ x: rect.x + 2,
89
+ y: rect.y + 1,
90
+ width: Math.max(0, rect.width - 4),
91
+ height: Math.max(0, rect.height - 2)
92
+ };
93
+ }
94
+
95
+ /**
96
+ * A layer on the stack.
97
+ *
98
+ * Subclasses implement `body(width, height)` returning styled rows, and may
99
+ * implement `key(event)` returning 'close' | 'handled' | null. Returning null
100
+ * means "not mine" — but the stack still does NOT forward it downward, because a
101
+ * modal that leaks keys is the bug this class exists to prevent. null only means
102
+ * the layer chose to ignore it.
103
+ */
104
+ export class Layer {
105
+ constructor(options = {}) {
106
+ this.title = options.title || '';
107
+ this.footer = options.footer || '';
108
+ this.width = options.width || 64;
109
+ this.height = options.height || 16;
110
+ this.onClose = options.onClose || null;
111
+ this.onSelect = options.onSelect || null;
112
+ this.cursor = null;
113
+ }
114
+
115
+ /**
116
+ * Rows this layer would like, given its content.
117
+ *
118
+ * A dialog padded out to a fixed height with blank rows looks unfinished —
119
+ * it is the single clearest tell between a prototype and a shipped tool. Every
120
+ * layer reports what it actually needs and the frame shrinks to it.
121
+ */
122
+ preferredHeight() { return this.height; }
123
+
124
+ /** Panel rect, centred and clamped to the viewport. */
125
+ rect(viewport) {
126
+ const width = Math.min(this.width, Math.max(20, viewport.width - 4));
127
+ const wanted = Math.max(5, Math.min(this.preferredHeight(), this.height));
128
+ const height = Math.min(wanted, Math.max(5, viewport.height - 4));
129
+ return {
130
+ x: viewport.x + Math.floor((viewport.width - width) / 2),
131
+ // A quarter down reads as centred; dead centre sits visually too low.
132
+ y: viewport.y + Math.max(1, Math.floor((viewport.height - height) / 4)),
133
+ width,
134
+ height
135
+ };
136
+ }
137
+
138
+ body() { return []; }
139
+ key() { return null; }
140
+ }
141
+
142
+ export class OverlayStack {
143
+ constructor() {
144
+ this.layers = [];
145
+ }
146
+
147
+ get top() { return this.layers.length > 0 ? this.layers[this.layers.length - 1] : null; }
148
+ get active() { return this.layers.length > 0; }
149
+ get depth() { return this.layers.length; }
150
+
151
+ push(layer) {
152
+ this.layers.push(layer);
153
+ return layer;
154
+ }
155
+
156
+ /** Pop exactly one layer. Nested dialogs unwind one esc at a time. */
157
+ pop() {
158
+ const layer = this.layers.pop();
159
+ if (layer && layer.onClose) layer.onClose();
160
+ return layer || null;
161
+ }
162
+
163
+ clear() {
164
+ while (this.layers.length > 0) this.pop();
165
+ }
166
+
167
+ /**
168
+ * Route a key.
169
+ *
170
+ * Returns true when a modal consumed it — which is ALWAYS, while one is open.
171
+ * The caller uses that to decide whether the prompt beneath ever sees the key,
172
+ * and the answer while a dialog is up must be no.
173
+ */
174
+ handleKey(key) {
175
+ const layer = this.top;
176
+ if (!layer) return false;
177
+
178
+ const result = layer.key(key);
179
+ if (result === 'close') this.pop();
180
+ return true;
181
+ }
182
+
183
+ /** Paint every layer bottom to top, dimming what is behind the topmost. */
184
+ paint(screen, viewport) {
185
+ for (let index = 0; index < this.layers.length; index++) {
186
+ const layer = this.layers[index];
187
+ const rect = layer.rect(viewport);
188
+
189
+ dimBackdrop(screen, viewport, rect);
190
+ const inner = drawPanel(screen, rect, { title: layer.title, footer: layer.footer });
191
+ if (inner.height <= 0) continue;
192
+
193
+ const rows = layer.body(inner.width, inner.height) || [];
194
+ for (let row = 0; row < Math.min(rows.length, inner.height); row++) {
195
+ screen.put(inner.x, inner.y + row, rows[row], inner.width);
196
+ }
197
+
198
+ // Only the topmost layer may own the caret.
199
+ if (index === this.layers.length - 1 && layer.cursor) {
200
+ screen.setCursor(inner.x + layer.cursor.x, inner.y + layer.cursor.y);
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ /** Shared footer text so every dialog teaches the same keys. */
207
+ /**
208
+ * A layer as framed rows, for a normal-buffer live region.
209
+ *
210
+ * drawPanel() paints absolutely into a cell grid, which only exists inside an
211
+ * alternate-buffer app — and chat does not have one any more (UX-01). This is the
212
+ * same panel expressed as styled `string[]`, which is all a live region can
213
+ * append. It is the shape openclaw draws its pickers in: extra lines in the
214
+ * composed frame, not a floating window.
215
+ *
216
+ * There is deliberately no backdrop dim: a line already committed to the user's
217
+ * scrollback cannot be restyled, and pretending otherwise would corrupt it.
218
+ */
219
+ export function panelRows(layer, width, maxHeight) {
220
+ const frameWidth = Math.max(8, width);
221
+ const inner = frameWidth - 4; // two borders, one pad each side
222
+ const bodyHeight = Math.max(1, Math.min(layer.preferredHeight(), maxHeight) - 2);
223
+ const body = layer.body(inner, bodyHeight);
224
+
225
+ const line = FRAME.h.repeat(frameWidth - 2);
226
+
227
+ let top = FRAME.tl + line + FRAME.tr;
228
+ if (layer.title) {
229
+ const label = ` ${truncate(layer.title, Math.max(0, inner - 4), GLYPHS.ellipsis)} `;
230
+ top = FRAME.tl + FRAME.h + label + FRAME.h.repeat(Math.max(0, frameWidth - 3 - visibleWidth(label))) + FRAME.tr;
231
+ }
232
+
233
+ let bottom = FRAME.bl + line + FRAME.br;
234
+ if (layer.footer) {
235
+ const label = ` ${truncate(layer.footer, Math.max(0, inner - 4), GLYPHS.ellipsis)} `;
236
+ bottom = FRAME.bl + FRAME.h.repeat(Math.max(0, frameWidth - 3 - visibleWidth(label))) + label + FRAME.h + FRAME.br;
237
+ }
238
+
239
+ const edge = paint('rule', FRAME.v);
240
+ const rows = [paint('rule', top)];
241
+ for (let index = 0; index < bodyHeight; index++) {
242
+ const content = truncate(body[index] || '', inner, GLYPHS.ellipsis);
243
+ const pad = ' '.repeat(Math.max(0, inner - visibleWidth(content)));
244
+ rows.push(`${edge} ${content}${pad} ${edge}`);
245
+ }
246
+ rows.push(paint('rule', bottom));
247
+ return rows;
248
+ }
249
+
250
+ export function dialogHint(extra = '') {
251
+ const parts = ['↑↓ move', 'enter select', 'esc close'];
252
+ if (extra) parts.push(extra);
253
+ return parts.join(` ${GLYPHS.dot} `);
254
+ }
255
+
256
+ export { bold, dim };