@xp266/dshtui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/bin/dshtui.js +65 -0
  2. package/cordis.patch.yml +81 -0
  3. package/lib/contract/index.d.mts +2 -0
  4. package/lib/contract/index.mjs +5 -0
  5. package/lib/dialog.d.mts +156 -0
  6. package/lib/dialog.mjs +3 -0
  7. package/lib/index-DqnaSXqP.d.mts +823 -0
  8. package/lib/index.d.mts +17 -0
  9. package/lib/index.mjs +11404 -0
  10. package/lib/prod-react-Dzof4qJx.mjs +4 -0
  11. package/lib/surface-DdxzIgIY.mjs +3126 -0
  12. package/lib/vendor.d.mts +3 -0
  13. package/lib/vendor.mjs +4 -0
  14. package/package.json +128 -0
  15. package/src/apply-theme.ts +8 -0
  16. package/src/boot-log.ts +35 -0
  17. package/src/chat/blocks.ts +41 -0
  18. package/src/chat/bridge.ts +1076 -0
  19. package/src/chat/builtin-tool-views.ts +53 -0
  20. package/src/chat/chat-face.ts +16 -0
  21. package/src/chat/chat-nodes.ts +53 -0
  22. package/src/chat/efforts.ts +4 -0
  23. package/src/chat/interactions.ts +329 -0
  24. package/src/chat/models.ts +263 -0
  25. package/src/chat/partial-json.ts +125 -0
  26. package/src/chat/presets.ts +26 -0
  27. package/src/chat/question-view.ts +106 -0
  28. package/src/chat/retry-status.ts +33 -0
  29. package/src/chat/session-groups.ts +34 -0
  30. package/src/chat/session-list.ts +311 -0
  31. package/src/chat/store.ts +636 -0
  32. package/src/chat/todo-view.ts +66 -0
  33. package/src/chat/tool-view.ts +311 -0
  34. package/src/chat/tool-views.ts +114 -0
  35. package/src/contract/index.ts +704 -0
  36. package/src/contract/upstream.ts +165 -0
  37. package/src/core/caret-nonce.ts +19 -0
  38. package/src/core/composer-layout.ts +65 -0
  39. package/src/core/edit.ts +152 -0
  40. package/src/core/field-view.ts +52 -0
  41. package/src/core/fields.ts +185 -0
  42. package/src/core/metrics.ts +26 -0
  43. package/src/core/paste.ts +149 -0
  44. package/src/core/segments.ts +112 -0
  45. package/src/core/text.ts +290 -0
  46. package/src/dialog.ts +11 -0
  47. package/src/env.ts +38 -0
  48. package/src/harness-home.ts +22 -0
  49. package/src/hot-theme.ts +56 -0
  50. package/src/index.tsx +185 -0
  51. package/src/kernel/registry.ts +102 -0
  52. package/src/kernel/surface.ts +33 -0
  53. package/src/log.ts +249 -0
  54. package/src/model/message.ts +89 -0
  55. package/src/model/selection.ts +63 -0
  56. package/src/performance-guard.ts +28 -0
  57. package/src/runtime/prod-react.ts +10 -0
  58. package/src/terminal/background.ts +66 -0
  59. package/src/terminal/capabilities.ts +54 -0
  60. package/src/terminal/clipboard-backends.ts +69 -0
  61. package/src/terminal/clipboard.ts +161 -0
  62. package/src/terminal/cursor-shape.ts +13 -0
  63. package/src/terminal/glyphs.ts +44 -0
  64. package/src/terminal/mouse.ts +154 -0
  65. package/src/terminal/probe.ts +61 -0
  66. package/src/terminal/screen.ts +349 -0
  67. package/src/theme-settings.ts +31 -0
  68. package/src/theme.ts +319 -0
  69. package/src/ui/app.tsx +454 -0
  70. package/src/ui/chrome/caps.tsx +9 -0
  71. package/src/ui/chrome/hint-service.ts +87 -0
  72. package/src/ui/chrome/input-status.ts +27 -0
  73. package/src/ui/chrome/key-gate.tsx +29 -0
  74. package/src/ui/chrome/status-bar.tsx +147 -0
  75. package/src/ui/contributions.ts +47 -0
  76. package/src/ui/dialog/defaults-dialog.tsx +152 -0
  77. package/src/ui/dialog/dialog-item.tsx +38 -0
  78. package/src/ui/dialog/dialog.tsx +386 -0
  79. package/src/ui/dialog/effort-dialog.tsx +45 -0
  80. package/src/ui/dialog/geometry.ts +131 -0
  81. package/src/ui/dialog/items.ts +124 -0
  82. package/src/ui/dialog/list-dialog.tsx +63 -0
  83. package/src/ui/dialog/models-dialog.tsx +203 -0
  84. package/src/ui/dialog/presets-dialog.tsx +46 -0
  85. package/src/ui/dialog/providers-dialog.tsx +313 -0
  86. package/src/ui/dialog/sessions-dialog.tsx +163 -0
  87. package/src/ui/dialog/sizes.ts +7 -0
  88. package/src/ui/dialog/status-lines.ts +11 -0
  89. package/src/ui/dialog/todo-dialog.tsx +29 -0
  90. package/src/ui/dialog/use-dialog-input.ts +186 -0
  91. package/src/ui/extension-point.ts +188 -0
  92. package/src/ui/home-logo.ts +132 -0
  93. package/src/ui/hooks/use-async-action.ts +25 -0
  94. package/src/ui/hooks/use-async-list.ts +71 -0
  95. package/src/ui/hooks/use-caret.ts +24 -0
  96. package/src/ui/hooks/use-chat-events.ts +153 -0
  97. package/src/ui/hooks/use-mouse-selection.ts +352 -0
  98. package/src/ui/hooks/use-scroll.ts +49 -0
  99. package/src/ui/hooks/use-terminal-size.ts +20 -0
  100. package/src/ui/input/commands.ts +118 -0
  101. package/src/ui/input/composer-bus.ts +14 -0
  102. package/src/ui/input/composer-fields.ts +111 -0
  103. package/src/ui/input/composer-keys.ts +21 -0
  104. package/src/ui/input/composer-paste.ts +20 -0
  105. package/src/ui/input/input-bar.tsx +279 -0
  106. package/src/ui/input/use-composer.ts +495 -0
  107. package/src/ui/key-arbiter.ts +37 -0
  108. package/src/ui/keymap.ts +15 -0
  109. package/src/ui/layout-service.ts +77 -0
  110. package/src/ui/message/layout.ts +763 -0
  111. package/src/ui/message/md/block.ts +247 -0
  112. package/src/ui/message/md/engine.ts +430 -0
  113. package/src/ui/message/md/extensions.ts +58 -0
  114. package/src/ui/message/md/highlight.ts +626 -0
  115. package/src/ui/message/md/index.ts +14 -0
  116. package/src/ui/message/md/inline.ts +97 -0
  117. package/src/ui/message/md/palette.ts +87 -0
  118. package/src/ui/message/message-list.tsx +69 -0
  119. package/src/ui/message/message-row.tsx +137 -0
  120. package/src/ui/message/message-views.ts +16 -0
  121. package/src/ui/message/renderers.ts +39 -0
  122. package/src/ui/message/tool-diff.ts +188 -0
  123. package/src/ui/message/warmup.ts +41 -0
  124. package/src/ui/message/wave.ts +98 -0
  125. package/src/ui/overlay.ts +22 -0
  126. package/src/ui/panels/approval-panel.tsx +193 -0
  127. package/src/ui/panels/exports.ts +3 -0
  128. package/src/ui/panels/question-model.ts +424 -0
  129. package/src/ui/panels/question-panel.tsx +154 -0
  130. package/src/ui/panels/surface.tsx +106 -0
  131. package/src/ui/panels-builtin.tsx +86 -0
  132. package/src/ui/pointer/builtins.ts +281 -0
  133. package/src/ui/pointer/registry.ts +25 -0
  134. package/src/ui/region.tsx +31 -0
  135. package/src/ui/selection/service.ts +92 -0
  136. package/src/ui/selection-registry.ts +88 -0
  137. package/src/ui/selection.tsx +148 -0
  138. package/src/ui/spinner-tick.tsx +24 -0
  139. package/src/ui/use-available-windows.ts +31 -0
  140. package/src/ui/widgets/actions.tsx +84 -0
  141. package/src/ui/widgets/button.tsx +57 -0
  142. package/src/ui/widgets/checkbox.tsx +58 -0
  143. package/src/ui/widgets/header.tsx +36 -0
  144. package/src/ui/widgets/index.ts +8 -0
  145. package/src/ui/widgets/input.tsx +76 -0
  146. package/src/ui/widgets/registry.ts +32 -0
  147. package/src/ui/widgets/search.tsx +59 -0
  148. package/src/ui/widgets/select.tsx +97 -0
  149. package/src/ui/widgets/static.tsx +37 -0
  150. package/src/ui/widgets/types.ts +1 -0
  151. package/src/ui/window-services-bridge.ts +71 -0
  152. package/src/ui/window-services.ts +20 -0
  153. package/src/ui/windows/models-window.tsx +15 -0
  154. package/src/ui/windows/providers-window.tsx +14 -0
  155. package/src/ui/windows/service-window.tsx +18 -0
  156. package/src/ui/windows/sessions-window.tsx +25 -0
  157. package/src/ui/windows-builtin.tsx +94 -0
  158. package/src/ui/windows.ts +18 -0
  159. package/src/vendor.ts +12 -0
@@ -0,0 +1,3126 @@
1
+ import chalk from "chalk";
2
+ import { appendFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { Box, Text, useCursor, useInput, usePaste, useStdout } from "ink";
6
+ import { createContext, createElement, memo, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import stringWidth from "string-width";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ //#region src/harness-home.ts
10
+ const DSH_HOME_DIR_NAME = ".dsh";
11
+ const DSH_HOME_ENV = "DSH_HOME";
12
+ function expandHomePath(path) {
13
+ if (path === "~") return homedir();
14
+ if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
15
+ return path;
16
+ }
17
+ /**
18
+ * Resolve the DeepSeek Harness home the same way the harness does:
19
+ * `$DSH_HOME` when set (blank counts as unset), otherwise `~/.dsh`.
20
+ * Mirrors @deepseek-ai/dsh-home-paths without depending on it.
21
+ */
22
+ function resolveDshHome(env = process.env) {
23
+ const fromEnv = env[DSH_HOME_ENV];
24
+ const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), DSH_HOME_DIR_NAME);
25
+ return resolve(expandHomePath(selected));
26
+ }
27
+ //#endregion
28
+ //#region src/log.ts
29
+ const LOG_LEVELS = [
30
+ "debug",
31
+ "info",
32
+ "warn",
33
+ "error"
34
+ ];
35
+ const state = {
36
+ stderrEnabled: false,
37
+ fileEnabled: false,
38
+ minLevel: "info",
39
+ maxFileBytes: 5242880
40
+ };
41
+ const sinks = /* @__PURE__ */ new Set();
42
+ let fileSink;
43
+ function levelRank(level) {
44
+ return LOG_LEVELS.indexOf(level);
45
+ }
46
+ function fieldsText(fields) {
47
+ return fields === void 0 ? "" : ` ${JSON.stringify(fields)}`;
48
+ }
49
+ function formatLine(entry) {
50
+ return `${(/* @__PURE__ */ new Date()).toISOString()} [${entry.level}] ${entry.tag} ${entry.message}${fieldsText(entry.fields)}\n`;
51
+ }
52
+ function formatCrash(report) {
53
+ return [
54
+ `=== dshtui crash: ${report.kind} ===`,
55
+ `time: ${(/* @__PURE__ */ new Date()).toISOString()}`,
56
+ `node: ${process.version}`,
57
+ `message: ${report.message}${fieldsText(report.fields)}`,
58
+ report.stack === void 0 ? "" : `stack:\n${report.stack}`,
59
+ report.cause === void 0 ? "" : `cause: ${report.cause}`,
60
+ ""
61
+ ].join("\n");
62
+ }
63
+ function writeStderr(line) {
64
+ if (!state.stderrEnabled) return;
65
+ try {
66
+ process.stderr.write(line);
67
+ } catch {}
68
+ }
69
+ function resolveLogFile(dir) {
70
+ return join(dir ?? join(resolveDshHome(), "logs"), "dshtui.log");
71
+ }
72
+ function rotateIfNeeded(logFile) {
73
+ let size;
74
+ try {
75
+ size = statSync(logFile).size;
76
+ } catch {
77
+ return;
78
+ }
79
+ if (size < state.maxFileBytes) return;
80
+ try {
81
+ unlinkSync(`${logFile}.1`);
82
+ } catch {}
83
+ try {
84
+ renameSync(logFile, `${logFile}.1`);
85
+ } catch {}
86
+ }
87
+ function fileSinkFor(logFile) {
88
+ return {
89
+ line(entry) {
90
+ if (!state.fileEnabled) return;
91
+ try {
92
+ rotateIfNeeded(logFile);
93
+ try {
94
+ chmodSync(logFile, 384);
95
+ } catch {}
96
+ appendFileSync(logFile, formatLine(entry), { mode: 384 });
97
+ } catch {}
98
+ if (entry.level === "warn" || entry.level === "error") writeStderr(`[dshtui] ${entry.message}${fieldsText(entry.fields)}\n`);
99
+ },
100
+ crash(report) {
101
+ if (!state.fileEnabled) return;
102
+ const line = formatCrash(report);
103
+ try {
104
+ rotateIfNeeded(logFile);
105
+ try {
106
+ chmodSync(logFile, 384);
107
+ } catch {}
108
+ appendFileSync(logFile, line, { mode: 384 });
109
+ } catch {}
110
+ writeStderr(line);
111
+ }
112
+ };
113
+ }
114
+ function removeFileSink() {
115
+ if (fileSink === void 0) return;
116
+ sinks.delete(fileSink);
117
+ fileSink = void 0;
118
+ }
119
+ function configureLogs(options = {}) {
120
+ if (options.stderr !== void 0) state.stderrEnabled = options.stderr;
121
+ if (options.level !== void 0) state.minLevel = options.level;
122
+ if (options.maxFileBytes !== void 0) state.maxFileBytes = Math.max(1, options.maxFileBytes);
123
+ state.fileEnabled = options.file ?? state.fileEnabled;
124
+ removeFileSink();
125
+ if (state.fileEnabled) {
126
+ const logFile = resolveLogFile(options.dir);
127
+ try {
128
+ mkdirSync(dirname(logFile), {
129
+ recursive: true,
130
+ mode: 448
131
+ });
132
+ fileSink = fileSinkFor(logFile);
133
+ sinks.add(fileSink);
134
+ } catch {
135
+ state.fileEnabled = false;
136
+ }
137
+ }
138
+ }
139
+ function dispatch(entry) {
140
+ if (levelRank(entry.level) < levelRank(state.minLevel)) return;
141
+ for (const sink of sinks) try {
142
+ sink.line(entry);
143
+ } catch {}
144
+ }
145
+ function dispatchCrash(report) {
146
+ for (const sink of sinks) try {
147
+ sink.crash(report);
148
+ } catch {}
149
+ }
150
+ function log(level, tag, message, fields) {
151
+ dispatch({
152
+ level,
153
+ tag,
154
+ message,
155
+ fields
156
+ });
157
+ }
158
+ function warn(tag, message, fields) {
159
+ log("warn", tag, message, fields);
160
+ }
161
+ function error(tag, message, fields) {
162
+ log("error", tag, message, fields);
163
+ }
164
+ function stringifyError(cause) {
165
+ if (cause instanceof Error) return cause.message;
166
+ return String(cause);
167
+ }
168
+ function writeCrashReport(report) {
169
+ dispatchCrash(report);
170
+ }
171
+ function installCrashHandlers(options = {}) {
172
+ const onUncaught = (cause) => {
173
+ writeCrashReport({
174
+ kind: "uncaughtException",
175
+ message: stringifyError(cause),
176
+ stack: cause instanceof Error ? cause.stack : void 0
177
+ });
178
+ if (options.exit === true) process.exit(1);
179
+ };
180
+ const onRejected = (cause) => {
181
+ writeCrashReport({
182
+ kind: "unhandledRejection",
183
+ message: stringifyError(cause),
184
+ stack: cause instanceof Error ? cause.stack : void 0
185
+ });
186
+ if (options.exit === true) process.exit(1);
187
+ };
188
+ process.on("uncaughtException", onUncaught);
189
+ process.on("unhandledRejection", onRejected);
190
+ return () => {
191
+ process.off("uncaughtException", onUncaught);
192
+ process.off("unhandledRejection", onRejected);
193
+ };
194
+ }
195
+ //#endregion
196
+ //#region src/env.ts
197
+ /**
198
+ Every dshtui environment variable, parsed once at import.
199
+ */
200
+ function parseLogLevel(value) {
201
+ return LOG_LEVELS.includes(value ?? "") ? value : void 0;
202
+ }
203
+ function parseBytes(value) {
204
+ if (value === void 0 || value.trim() === "") return void 0;
205
+ const parsed = Number(value);
206
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
207
+ }
208
+ const env = {
209
+ /** Force color level: 0 none, 1 ansi16, 2 ansi256, 3 truecolor. */
210
+ color: process.env.DSH_TUI_COLOR,
211
+ /** Render every glyph as plain ASCII. */
212
+ ascii: process.env.DSH_TUI_ASCII === "1",
213
+ /** Force background mode: 'dark' or 'light'. */
214
+ background: process.env.DSH_TUI_BG,
215
+ /** Reload theme.ts on every save (path relative to cwd, default src/theme.ts). */
216
+ hotTheme: process.env.DSH_TUI_HOT_THEME === "1",
217
+ themePath: process.env.DSH_TUI_THEME_PATH,
218
+ /** Mirror warnings and errors to stderr; only the exact value '1' enables it. */
219
+ debug: process.env.DSH_TUI_DEBUG === "1",
220
+ /** Write diagnostics under the harness home logs directory (default: on). */
221
+ logFile: process.env.DSH_TUI_LOG_FILE !== "0",
222
+ /** Override the harness home base directory used for logs. */
223
+ logDir: process.env.DSH_TUI_LOG_DIR,
224
+ /** Minimum level written to the log file: debug | info | warn | error. */
225
+ logLevel: parseLogLevel(process.env.DSH_TUI_LOG_LEVEL),
226
+ /** Rotation cap for the log file in bytes. */
227
+ logMaxBytes: parseBytes(process.env.DSH_TUI_LOG_MAX_BYTES),
228
+ /** Exit the host process on an uncaught crash (default: record only). */
229
+ crashExit: process.env.DSH_TUI_CRASH_EXIT === "1"
230
+ };
231
+ //#endregion
232
+ //#region src/terminal/capabilities.ts
233
+ function parseForcedColorLevel(value) {
234
+ if (value === "0" || value === "1" || value === "2" || value === "3") return Number(value);
235
+ if (value === "truecolor") return 3;
236
+ if (value === "none" || value === "false") return 0;
237
+ }
238
+ function detectColorLevel() {
239
+ const forced = parseForcedColorLevel(env.color);
240
+ if (forced !== void 0) return forced;
241
+ if (process.env.NO_COLOR !== void 0) return 0;
242
+ const forceColor = process.env.FORCE_COLOR;
243
+ if (forceColor !== void 0) {
244
+ if (forceColor === "false" || forceColor === "0") return 0;
245
+ if (forceColor === "true" || forceColor === "") return 1;
246
+ const parsed = Number(forceColor);
247
+ if (parsed === 1 || parsed === 2 || parsed === 3) return parsed;
248
+ }
249
+ const term = process.env.TERM ?? "";
250
+ if (term === "dumb") return 0;
251
+ const colorterm = process.env.COLORTERM ?? "";
252
+ if (colorterm === "truecolor" || colorterm === "24bit") return 3;
253
+ if (term.includes("truecolor")) return 3;
254
+ if (process.env.WT_SESSION !== void 0) return 3;
255
+ if (term.includes("256color")) return 2;
256
+ if (term !== "" || process.env.TERM_PROGRAM !== void 0) return 1;
257
+ return 0;
258
+ }
259
+ function detectUnicode() {
260
+ if (env.ascii) return false;
261
+ if ((process.env.TERM ?? "") === "dumb") return false;
262
+ const locale = process.env.LC_ALL ?? process.env.LANG ?? "";
263
+ if (locale === "" || /utf-?8/i.test(locale)) return true;
264
+ const base = locale.split(".")[0] ?? locale;
265
+ if (base === "C" || base === "POSIX") return false;
266
+ return !locale.includes(".");
267
+ }
268
+ let colorLevel = detectColorLevel();
269
+ function setColorLevel(level) {
270
+ colorLevel = level;
271
+ chalk.level = level;
272
+ }
273
+ chalk.level = colorLevel;
274
+ const unicode = detectUnicode();
275
+ //#endregion
276
+ //#region src/kernel/registry.ts
277
+ /**
278
+ * Keyed contribution registry with layered override semantics: registering
279
+ * an existing key displaces the previous layer, and disposing a layer
280
+ * restores the nearest live layer below it, so an overriding plugin that
281
+ * unmounts hands the slot back to whatever it replaced.
282
+ *
283
+ * `get(key)` resolves to the live layer with the lowest order (the one that
284
+ * would win a dispatch loop), not merely the most recently registered one, so
285
+ * single-key lookups agree with the iteration order plugins observe.
286
+ */
287
+ function keyedRegistry(compareKeys = (a, b) => a < b ? -1 : a > b ? 1 : 0) {
288
+ const map = /* @__PURE__ */ new Map();
289
+ const listeners = /* @__PURE__ */ new Set();
290
+ function notify() {
291
+ for (const listener of [...listeners]) try {
292
+ listener();
293
+ } catch {}
294
+ }
295
+ function winningLayer(key) {
296
+ let best;
297
+ let layer = map.get(key);
298
+ while (layer !== void 0) {
299
+ if (!layer.dead && (best === void 0 || layer.order < best.order)) best = layer;
300
+ layer = layer.prev;
301
+ }
302
+ return best;
303
+ }
304
+ return {
305
+ register(key, value, options) {
306
+ const prev = map.get(key);
307
+ const layer = {
308
+ key,
309
+ order: options?.order ?? 100,
310
+ value,
311
+ prev,
312
+ dead: false
313
+ };
314
+ map.set(key, layer);
315
+ notify();
316
+ return () => {
317
+ if (layer.dead) return;
318
+ layer.dead = true;
319
+ if (map.get(key) !== layer) return;
320
+ let restore = layer.prev;
321
+ while (restore !== void 0 && restore.dead) restore = restore.prev;
322
+ if (restore === void 0) map.delete(key);
323
+ else map.set(key, restore);
324
+ notify();
325
+ };
326
+ },
327
+ entries() {
328
+ return [...map.values()].map((layer) => ({
329
+ key: layer.key,
330
+ order: layer.order,
331
+ value: layer.value
332
+ })).sort((a, b) => a.order - b.order || compareKeys(a.key, b.key));
333
+ },
334
+ values() {
335
+ return this.entries().map((entry) => entry.value);
336
+ },
337
+ get(key) {
338
+ return winningLayer(key)?.value;
339
+ },
340
+ has(key) {
341
+ return map.has(key);
342
+ },
343
+ subscribe(listener) {
344
+ listeners.add(listener);
345
+ return () => {
346
+ listeners.delete(listener);
347
+ };
348
+ }
349
+ };
350
+ }
351
+ //#endregion
352
+ //#region src/theme.ts
353
+ const paletteContributions = keyedRegistry();
354
+ function registerPalette(contribution) {
355
+ const dispose = paletteContributions.register(contribution.id, contribution, { order: contribution.order });
356
+ materialize(currentMode);
357
+ return () => {
358
+ dispose();
359
+ materialize(currentMode);
360
+ };
361
+ }
362
+ function subscribePalettes(listener) {
363
+ return paletteContributions.subscribe(listener);
364
+ }
365
+ const DARK_LADDER = {
366
+ base: "#0d0d0d",
367
+ sunken: "#1a1a1a",
368
+ surface: "#262626",
369
+ raised: "#333333",
370
+ line: "#686868",
371
+ text: "#9a9a9a",
372
+ ink: "#cccccc"
373
+ };
374
+ const LIGHT_LADDER = {
375
+ base: "#ffffff",
376
+ sunken: "#f2f2f2",
377
+ surface: "#e6e6e6",
378
+ raised: "#d9d9d9",
379
+ line: "#999999",
380
+ text: "#595959",
381
+ ink: "#1a1a1a"
382
+ };
383
+ const DARK_HUES = {
384
+ accent: "#ffae00",
385
+ info: "#4da0e8",
386
+ success: "#4caf50",
387
+ error: "#ff6753",
388
+ added: "#5d9e50",
389
+ removed: "#d25044"
390
+ };
391
+ const LIGHT_HUES = {
392
+ accent: "#a86800",
393
+ info: "#2f6fd0",
394
+ success: "#2e8b3d",
395
+ error: "#d44a3a",
396
+ added: "#0a8a2c",
397
+ removed: "#c22318"
398
+ };
399
+ const DARK_CODE = {
400
+ comment: "#5d9e50",
401
+ string: "#d78f6e",
402
+ number: "#82c46e",
403
+ keyword: "#c883c8",
404
+ fn: "#d8d89e",
405
+ type: "#52a898",
406
+ variable: "#4d9fd6",
407
+ fallback: "#a3b56a"
408
+ };
409
+ const LIGHT_CODE = {
410
+ comment: "#4a8a3d",
411
+ string: "#a05f42",
412
+ number: "#3f9a63",
413
+ keyword: "#8f4a88",
414
+ fn: "#7a7a3d",
415
+ type: "#2a7a6e",
416
+ variable: "#3a76a8",
417
+ fallback: "#647a34"
418
+ };
419
+ /** Mix two hex colors channel-wise; t=0 keeps the hue, t=1 keeps the gray. */
420
+ function tint(hue, gray, t) {
421
+ const channel = (hex, at) => parseInt(hex.slice(at, at + 2), 16);
422
+ const mix = (at) => {
423
+ return Math.round(channel(hue, at) * (1 - t) + channel(gray, at) * t).toString(16).padStart(2, "0");
424
+ };
425
+ return `#${mix(1)}${mix(3)}${mix(5)}`;
426
+ }
427
+ const THINK_DESATURATE = .55;
428
+ function buildPalette(ladder, hues, code, mode) {
429
+ const think = (hue) => tint(hue, ladder.text, mode === "dark" ? THINK_DESATURATE : .35);
430
+ const thinkCode = Object.fromEntries(Object.entries(code).map(([key, hue]) => [key, think(hue)]));
431
+ return {
432
+ ink: ladder.ink,
433
+ userBubbleBackground: ladder.surface,
434
+ aiBubbleBackground: ladder.sunken,
435
+ permissionBackground: ladder.sunken,
436
+ workspaceWriteText: hues.info,
437
+ dangerFullAccessText: hues.accent,
438
+ readOnlyText: hues.success,
439
+ dialogBackground: ladder.base,
440
+ dialogInputBackground: ladder.raised,
441
+ dialogHintText: ladder.text,
442
+ carouselCurrentBg: ladder.raised,
443
+ carouselButtonBg: ladder.sunken,
444
+ carouselButtonPressedBg: ladder.surface,
445
+ carouselSelectedText: hues.accent,
446
+ sectionHeader: hues.accent,
447
+ panelQuestionText: ladder.ink,
448
+ panelKeyText: ladder.ink,
449
+ modelText: ladder.ink,
450
+ effortText: hues.accent,
451
+ statusSeparator: ladder.text,
452
+ presetText: ladder.text,
453
+ cwdText: ladder.text,
454
+ statsText: ladder.text,
455
+ errorText: hues.error,
456
+ success: hues.success,
457
+ warning: hues.accent,
458
+ specialFieldText: mode === "dark" ? "#1a1a1a" : "#3d2800",
459
+ specialFieldBackground: hues.accent,
460
+ toolLabel: mode === "dark" ? "#2fc0e0" : "#0092b8",
461
+ toolBodyText: ladder.text,
462
+ diffAdded: hues.added,
463
+ diffRemoved: hues.removed,
464
+ diffAddedBackground: mode === "dark" ? "#384751" : "#e1f1e5",
465
+ diffRemovedBackground: mode === "dark" ? "#4f312c" : "#f5e8e5",
466
+ scrollTrackBackground: ladder.sunken,
467
+ scrollThumbBackground: ladder.line,
468
+ selectionBg: "#0066ff",
469
+ selectionFg: "#cccccc",
470
+ mdLink: hues.info,
471
+ mdInlineCode: hues.added,
472
+ mdQuoteBar: ladder.line,
473
+ mdHr: ladder.line,
474
+ mdList: hues.accent,
475
+ mdTaskDone: hues.added,
476
+ mdTaskTodo: ladder.line,
477
+ mdH1: mode === "dark" ? "#e0b568" : "#8a6d2f",
478
+ mdH3: mode === "dark" ? "#64b5d6" : "#2f6fa8",
479
+ mdCodePlain: ladder.ink,
480
+ mdCodeFallback: code.fallback,
481
+ codeComment: code.comment,
482
+ codeString: code.string,
483
+ codeNumber: code.number,
484
+ codeKeyword: code.keyword,
485
+ codeFunction: code.fn,
486
+ codeType: code.type,
487
+ codeVariable: code.variable,
488
+ codeConstant: code.variable,
489
+ codeOperator: ladder.ink,
490
+ thinkLink: think(hues.info),
491
+ thinkInlineCode: think(hues.added),
492
+ thinkHr: tint(ladder.line, ladder.base, mode === "dark" ? .35 : .15),
493
+ thinkList: ladder.text,
494
+ thinkTaskDone: think(hues.added),
495
+ thinkTaskTodo: ladder.line,
496
+ thinkH1: ladder.text,
497
+ thinkH3: ladder.text,
498
+ thinkCodePlain: ladder.text,
499
+ thinkCodeFallback: thinkCode.fallback,
500
+ thinkCodeComment: thinkCode.comment,
501
+ thinkCodeString: thinkCode.string,
502
+ thinkCodeNumber: thinkCode.number,
503
+ thinkCodeKeyword: thinkCode.keyword,
504
+ thinkCodeFunction: thinkCode.fn,
505
+ thinkCodeType: thinkCode.type,
506
+ thinkCodeVariable: thinkCode.variable,
507
+ thinkCodeConstant: thinkCode.variable,
508
+ homeLogoTop: mode === "dark" ? "#2181ff" : "#0381ff",
509
+ homeLogoBottom: mode === "dark" ? "#3a9aa0" : "#97c4ff"
510
+ };
511
+ }
512
+ const darkPalette = buildPalette(DARK_LADDER, DARK_HUES, DARK_CODE, "dark");
513
+ const palettes = {
514
+ dark: darkPalette,
515
+ light: buildPalette(LIGHT_LADDER, LIGHT_HUES, LIGHT_CODE, "light")
516
+ };
517
+ const COLORS = { ...darkPalette };
518
+ let currentMode = "dark";
519
+ function themeMode() {
520
+ return currentMode;
521
+ }
522
+ function permissionModes() {
523
+ return {
524
+ "workspace-write": {
525
+ color: COLORS.permissionBackground,
526
+ textColor: COLORS.workspaceWriteText,
527
+ name: "Workspace Write"
528
+ },
529
+ "danger-full-access": {
530
+ color: COLORS.permissionBackground,
531
+ textColor: COLORS.dangerFullAccessText,
532
+ name: "Full access"
533
+ },
534
+ "read-only": {
535
+ color: COLORS.permissionBackground,
536
+ textColor: COLORS.readOnlyText,
537
+ name: "Read Only"
538
+ }
539
+ };
540
+ }
541
+ function effectivePalette(mode) {
542
+ const merged = { ...palettes[mode] };
543
+ for (const contribution of paletteContributions.values()) {
544
+ if (contribution.mode !== void 0 && contribution.mode !== "both" && contribution.mode !== mode) continue;
545
+ Object.assign(merged, contribution.colors);
546
+ }
547
+ return merged;
548
+ }
549
+ function materialize(mode) {
550
+ Object.assign(COLORS, effectivePalette(mode));
551
+ Object.assign(PERMISSION_MODES, permissionModes());
552
+ }
553
+ /**
554
+ * Read a palette color by key, including keys contributed by plugins that
555
+ * are not part of the built-in Theme type. Falls back to the raw key when
556
+ * nothing defines it, so custom semantic names remain visible.
557
+ */
558
+ function paletteColor(key) {
559
+ return COLORS[key] ?? key;
560
+ }
561
+ function setThemeMode(mode) {
562
+ currentMode = mode;
563
+ materialize(mode);
564
+ }
565
+ function replacePalettes(next) {
566
+ Object.assign(palettes.dark, next.dark);
567
+ Object.assign(palettes.light, next.light);
568
+ materialize(currentMode);
569
+ }
570
+ const PERMISSION_MODES = permissionModes();
571
+ materialize(currentMode);
572
+ function permissionModeInfo(mode) {
573
+ return PERMISSION_MODES[mode] ?? {
574
+ color: COLORS.permissionBackground,
575
+ textColor: COLORS.workspaceWriteText,
576
+ name: mode
577
+ };
578
+ }
579
+ //#endregion
580
+ //#region src/core/fields.ts
581
+ const FIELD_CHAR_BASE = 57344;
582
+ const FIELD_CHAR_END = 63743;
583
+ const FIELD_CHAR_COUNT = 6400;
584
+ const slots = /* @__PURE__ */ new Map();
585
+ const kindDefs = /* @__PURE__ */ new Map();
586
+ const labelWidthCache = /* @__PURE__ */ new Map();
587
+ let cursor = 0;
588
+ function specialFieldStyle() {
589
+ return {
590
+ color: COLORS.specialFieldText,
591
+ background: COLORS.specialFieldBackground,
592
+ bold: true
593
+ };
594
+ }
595
+ function registerFieldKind(def) {
596
+ kindDefs.set(def.kind, def);
597
+ return () => {
598
+ if (kindDefs.get(def.kind) === def) kindDefs.delete(def.kind);
599
+ };
600
+ }
601
+ function fieldStyleOf(slot) {
602
+ const def = kindDefs.get(slot.kind);
603
+ if (def !== void 0) return def.style();
604
+ return specialFieldStyle();
605
+ }
606
+ function fieldExpandOf(kind) {
607
+ return kindDefs.get(kind)?.expand;
608
+ }
609
+ function specialFieldFactory() {
610
+ return {
611
+ create: (input) => allocateField(input.kind, input.label, input.owner ?? "composer"),
612
+ release: releaseField,
613
+ pin: pinField,
614
+ isFieldChar,
615
+ labelOf: (char) => fieldSlotOf(char)?.label
616
+ };
617
+ }
618
+ function isFieldCode(code) {
619
+ return code >= FIELD_CHAR_BASE && code <= FIELD_CHAR_END;
620
+ }
621
+ function hasFieldChar(text) {
622
+ for (let i = 0; i < text.length; i++) if (isFieldCode(text.charCodeAt(i))) return true;
623
+ return false;
624
+ }
625
+ function isFieldChar(char) {
626
+ return char.length === 1 && isFieldCode(char.charCodeAt(0));
627
+ }
628
+ function fieldCodeWidth(code) {
629
+ const slot = slots.get(code - FIELD_CHAR_BASE);
630
+ if (slot === void 0) return 1;
631
+ return labelWidth(slot.label);
632
+ }
633
+ function labelWidth(label) {
634
+ const hit = labelWidthCache.get(label);
635
+ if (hit !== void 0) return hit;
636
+ const width = stringWidth(label);
637
+ if (labelWidthCache.size >= 4096) labelWidthCache.clear();
638
+ labelWidthCache.set(label, width);
639
+ return width;
640
+ }
641
+ function allocateField(kind, label, owner) {
642
+ for (let i = 0; i < FIELD_CHAR_COUNT; i++) {
643
+ const index = (cursor + i) % FIELD_CHAR_COUNT;
644
+ if (slots.has(index)) continue;
645
+ cursor = (index + 1) % FIELD_CHAR_COUNT;
646
+ slots.set(index, {
647
+ kind,
648
+ label,
649
+ owner,
650
+ pins: 0
651
+ });
652
+ return String.fromCharCode(FIELD_CHAR_BASE + index);
653
+ }
654
+ return null;
655
+ }
656
+ function fieldSlotOf(char) {
657
+ if (char.length !== 1) return void 0;
658
+ const code = char.charCodeAt(0);
659
+ if (!isFieldCode(code)) return void 0;
660
+ return slots.get(code - FIELD_CHAR_BASE);
661
+ }
662
+ function releaseField(char) {
663
+ if (char.length !== 1) return;
664
+ const index = char.charCodeAt(0) - FIELD_CHAR_BASE;
665
+ const slot = slots.get(index);
666
+ if (slot === void 0 || slot.pins > 0) return;
667
+ slots.delete(index);
668
+ }
669
+ /**
670
+ * Hold a field slot against the automatic reclaim scans (composer edits and
671
+ * message-list rebuilds drop every slot whose char is absent from the live
672
+ * text). Callers that create chips outside the composer value — custom
673
+ * windows, overlays, background tasks — must pin them and call the returned
674
+ * disposer when done.
675
+ */
676
+ function pinField(char) {
677
+ if (char.length !== 1) return void 0;
678
+ const slot = slots.get(char.charCodeAt(0) - FIELD_CHAR_BASE);
679
+ if (slot === void 0) return void 0;
680
+ slot.pins += 1;
681
+ return () => {
682
+ slot.pins -= 1;
683
+ };
684
+ }
685
+ function hasFieldSlots(owner) {
686
+ for (const slot of slots.values()) if (slot.owner === owner) return true;
687
+ return false;
688
+ }
689
+ function releaseUnreferenced(owner, keepText) {
690
+ for (const [index, slot] of slots) {
691
+ if (slot.owner !== owner || slot.pins > 0) continue;
692
+ const char = String.fromCharCode(FIELD_CHAR_BASE + index);
693
+ if (!keepText.includes(char)) slots.delete(index);
694
+ }
695
+ }
696
+ function releaseFields(owner) {
697
+ for (const [index, slot] of slots) if (slot.owner === owner && slot.pins === 0) slots.delete(index);
698
+ }
699
+ function imageChipLabel(count) {
700
+ return `[${count} images]`;
701
+ }
702
+ function linesChipLabel(count) {
703
+ return `[${count} lines]`;
704
+ }
705
+ function charactersChipLabel(count) {
706
+ return `[${count} characters]`;
707
+ }
708
+ //#endregion
709
+ //#region src/core/text.ts
710
+ const WIDTH_CACHE_LIMIT = 8192;
711
+ function cachedWidth(cache, text, compute) {
712
+ const hit = cache.get(text);
713
+ if (hit !== void 0) return hit;
714
+ const width = compute();
715
+ if (cache.size >= WIDTH_CACHE_LIMIT) cache.clear();
716
+ cache.set(text, width);
717
+ return width;
718
+ }
719
+ const widthCache = /* @__PURE__ */ new Map();
720
+ function textWidth(text) {
721
+ if (isAsciiPrintable(text)) return text.length;
722
+ const hit = widthCache.get(text);
723
+ if (hit !== void 0) return hit;
724
+ let total = 0;
725
+ let run = "";
726
+ for (let i = 0; i < text.length; i++) {
727
+ const code = text.charCodeAt(i);
728
+ if (isFieldCode(code)) {
729
+ if (run !== "") {
730
+ total += stringWidth(run);
731
+ run = "";
732
+ }
733
+ total += fieldCodeWidth(code);
734
+ continue;
735
+ }
736
+ run += text[i];
737
+ }
738
+ if (run !== "") total += stringWidth(run);
739
+ if (widthCache.size >= WIDTH_CACHE_LIMIT) widthCache.clear();
740
+ widthCache.set(text, total);
741
+ return total;
742
+ }
743
+ function isAsciiPrintable(text) {
744
+ for (let i = 0; i < text.length; i++) {
745
+ const code = text.charCodeAt(i);
746
+ if (code < 32 || code > 126) return false;
747
+ }
748
+ return true;
749
+ }
750
+ const charWidthCache = /* @__PURE__ */ new Map();
751
+ const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
752
+ function segmentGraphemes(text) {
753
+ return graphemeSegmenter.segment(text);
754
+ }
755
+ function charWidth(cluster) {
756
+ if (cluster.length === 1) {
757
+ const code = cluster.charCodeAt(0);
758
+ if (code >= 32 && code <= 126) return 1;
759
+ if (isFieldCode(code)) return fieldCodeWidth(code);
760
+ }
761
+ return cachedWidth(charWidthCache, cluster, () => stringWidth(cluster));
762
+ }
763
+ const NO_START_CHARS = /* @__PURE__ */ new Set("!%,.:;?)]}'\"。,、:;!?)]}》〉」』】〕〗〙〛…—~·%°′″‰");
764
+ const NO_END_CHARS = /* @__PURE__ */ new Set("([{'\"$#@`([{《〈「『【〔〖$¥");
765
+ const BREAK_DELIM_CHARS = /* @__PURE__ */ new Set("-/\\,.;:!?)]}");
766
+ function matchesAny(cluster, set) {
767
+ for (const ch of cluster) if (set.has(ch)) return true;
768
+ return false;
769
+ }
770
+ const ASCII_NO_START = /* @__PURE__ */ new Set();
771
+ for (const ch of "!%,.:;?)]}'\"") ASCII_NO_START.add(ch.charCodeAt(0));
772
+ const ASCII_NO_END = /* @__PURE__ */ new Set();
773
+ for (const ch of "([{'\"$#@`") ASCII_NO_END.add(ch.charCodeAt(0));
774
+ function canBreakBefore(prev, cur) {
775
+ if (prev.length === 1 && cur.length === 1) {
776
+ const pc = prev.charCodeAt(0);
777
+ const cc = cur.charCodeAt(0);
778
+ if (pc >= 32 && pc < 127 && cc >= 32 && cc < 127) {
779
+ if (ASCII_NO_START.has(cc) || ASCII_NO_END.has(pc)) return false;
780
+ if (pc === 32 || BREAK_DELIM_CHARS.has(prev)) return true;
781
+ return false;
782
+ }
783
+ }
784
+ if (matchesAny(cur, NO_START_CHARS) || matchesAny(prev, NO_END_CHARS)) return false;
785
+ if (charWidth(prev) >= 2 || charWidth(cur) >= 2) return true;
786
+ if (prev === " " || prev === " ") return true;
787
+ if (BREAK_DELIM_CHARS.has(prev)) return true;
788
+ return false;
789
+ }
790
+ function computeWrapStarts(clusters, width) {
791
+ const total = clusters.length;
792
+ const starts = [0];
793
+ let start = 0;
794
+ let lineW = 0;
795
+ let lastBreak = -1;
796
+ for (let i = 0; i < total; i++) {
797
+ const w = charWidth(clusters[i]);
798
+ if (lineW + w > width && i > start) {
799
+ let j;
800
+ if (lastBreak > start) j = lastBreak;
801
+ else {
802
+ j = i;
803
+ while (j > start + 1 && matchesAny(clusters[j], NO_START_CHARS)) j--;
804
+ while (j > start + 1 && matchesAny(clusters[j - 1], NO_END_CHARS)) j--;
805
+ }
806
+ starts.push(j);
807
+ lineW = 0;
808
+ for (let k = j; k < i; k++) lineW += charWidth(clusters[k]);
809
+ start = j;
810
+ lastBreak = -1;
811
+ }
812
+ const next = i + 1;
813
+ if (next < total && canBreakBefore(clusters[i], clusters[next])) lastBreak = next;
814
+ lineW += w;
815
+ }
816
+ return starts;
817
+ }
818
+ function pushWrapped(line, width, out) {
819
+ if (line.length === 0) {
820
+ out.push("");
821
+ return;
822
+ }
823
+ const clusters = [];
824
+ for (const { segment } of segmentGraphemes(line)) clusters.push(segment);
825
+ const starts = computeWrapStarts(clusters, width);
826
+ for (let r = 0; r < starts.length; r++) {
827
+ const from = starts[r];
828
+ const to = r + 1 < starts.length ? starts[r + 1] : clusters.length;
829
+ out.push(clusters.slice(from, to).join(""));
830
+ }
831
+ }
832
+ /**
833
+ * Single normalization applied before every wrap pass. Terminal rendering
834
+ * expands tabs and a lone carriage return rewinds the cursor, so both would
835
+ * make the visually rendered line wider than the width computation assumed
836
+ * and every later row would land on the wrong column. Both are replaced
837
+ * with spaces / newlines here so wrap math and rendering always agree.
838
+ */
839
+ function normalizeWrapText(text) {
840
+ if (!text.includes("\r") && !text.includes(" ")) return text;
841
+ return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
842
+ }
843
+ function wrapLines(text, width) {
844
+ if (width <= 0) return [""];
845
+ const cacheKey = `${width}\u0000${text}`;
846
+ const hit = wrapCache.get(cacheKey);
847
+ if (hit !== void 0) return hit.lines;
848
+ const lines = [];
849
+ for (const rawLine of normalizeWrapText(text).split("\n")) pushWrapped(rawLine, width, lines);
850
+ evictWrapCacheIfNeeded();
851
+ wrapCache.set(cacheKey, {
852
+ lines,
853
+ bytes: text.length
854
+ });
855
+ return lines;
856
+ }
857
+ const WRAP_CACHE_MAX_BYTES = 4194304;
858
+ const wrapCache = /* @__PURE__ */ new Map();
859
+ let wrapCacheBytes = 0;
860
+ function evictWrapCacheIfNeeded() {
861
+ while (wrapCache.size > 0 && wrapCacheBytes >= WRAP_CACHE_MAX_BYTES) {
862
+ const oldest = wrapCache.keys().next();
863
+ if (oldest.done) return;
864
+ const entry = wrapCache.get(oldest.value);
865
+ if (entry !== void 0) wrapCacheBytes -= entry.bytes;
866
+ wrapCache.delete(oldest.value);
867
+ }
868
+ }
869
+ function pushWrappedOffsets(line, width, base, out) {
870
+ if (line.length === 0) {
871
+ out.push({
872
+ start: base,
873
+ end: base
874
+ });
875
+ return;
876
+ }
877
+ const clusters = [];
878
+ const offsets = [];
879
+ for (const { segment, index } of segmentGraphemes(line)) {
880
+ clusters.push(segment);
881
+ offsets.push(index);
882
+ }
883
+ const starts = computeWrapStarts(clusters, width);
884
+ for (let r = 0; r < starts.length; r++) {
885
+ const from = starts[r];
886
+ const to = r + 1 < starts.length ? offsets[starts[r + 1]] : line.length;
887
+ out.push({
888
+ start: base + offsets[from],
889
+ end: base + to
890
+ });
891
+ }
892
+ }
893
+ function lineBreaks(text, width) {
894
+ const breaks = [];
895
+ let start = 0;
896
+ for (const rawLine of text.split("\n")) {
897
+ pushWrappedOffsets(rawLine, width, start, breaks);
898
+ start += rawLine.length + 1;
899
+ }
900
+ return breaks;
901
+ }
902
+ function locToPoint(text, width, index) {
903
+ const breaks = lineBreaks(text, width);
904
+ for (let row = 0; row < breaks.length; row++) {
905
+ const { start, end } = breaks[row];
906
+ if (index < end || index === end && row < breaks.length - 1 && breaks[row + 1].start > end) return {
907
+ row,
908
+ col: textWidth(text.slice(start, index))
909
+ };
910
+ }
911
+ const last = breaks[breaks.length - 1];
912
+ return {
913
+ row: breaks.length - 1,
914
+ col: textWidth(text.slice(last.start, last.end))
915
+ };
916
+ }
917
+ function colToCharIndex(line, col) {
918
+ let width = 0;
919
+ for (const { segment, index } of segmentGraphemes(line)) {
920
+ if (width >= col) return index;
921
+ width += charWidth(segment);
922
+ }
923
+ return line.length;
924
+ }
925
+ function caretScrollStart(text, caret, width) {
926
+ const clamped = Math.min(Math.max(0, caret), text.length);
927
+ const caretCol = textWidth(text.slice(0, clamped));
928
+ if (caretCol <= width) return 0;
929
+ const minPrefix = caretCol - width;
930
+ let prefix = 0;
931
+ for (const { segment, index } of segmentGraphemes(text)) {
932
+ if (index >= clamped) break;
933
+ if (prefix >= minPrefix) return index;
934
+ prefix += charWidth(segment);
935
+ }
936
+ return clamped;
937
+ }
938
+ function padToWidth(text, width) {
939
+ const used = textWidth(text);
940
+ return text + " ".repeat(Math.max(0, width - used));
941
+ }
942
+ function truncate(text, width) {
943
+ let used = 0;
944
+ for (const { segment, index } of segmentGraphemes(text)) {
945
+ const w = charWidth(segment);
946
+ if (used + w > width) return text.slice(0, index);
947
+ used += w;
948
+ }
949
+ return text;
950
+ }
951
+ function errorText(cause) {
952
+ return cause instanceof Error ? cause.message : String(cause);
953
+ }
954
+ //#endregion
955
+ //#region src/model/selection.ts
956
+ function selectedRange(sel, row) {
957
+ const top = Math.min(sel.anchorRow, sel.focusRow);
958
+ const bottom = Math.max(sel.anchorRow, sel.focusRow);
959
+ if (row < top || row > bottom) return null;
960
+ if (sel.anchorRow === sel.focusRow) return {
961
+ start: Math.min(sel.anchorCol, sel.focusCol),
962
+ end: Math.max(sel.anchorCol, sel.focusCol)
963
+ };
964
+ const upward = sel.focusRow < sel.anchorRow;
965
+ if (row === sel.anchorRow) return upward ? {
966
+ start: 0,
967
+ end: sel.anchorCol
968
+ } : {
969
+ start: sel.anchorCol,
970
+ end: Infinity
971
+ };
972
+ if (row === sel.focusRow) return upward ? {
973
+ start: sel.focusCol,
974
+ end: Infinity
975
+ } : {
976
+ start: 0,
977
+ end: sel.focusCol
978
+ };
979
+ return {
980
+ start: 0,
981
+ end: Infinity
982
+ };
983
+ }
984
+ function toScreenSelection(selection, scrollTop, messageHeight) {
985
+ if (selection === null) return null;
986
+ if (!selection.inMessage) return selection;
987
+ const rawAnchor = selection.anchorRow - scrollTop;
988
+ const rawFocus = selection.focusRow - scrollTop;
989
+ const visible = (row) => row >= 0 && row < messageHeight;
990
+ if (!visible(rawAnchor) && !visible(rawFocus)) return null;
991
+ const clampRow = (row) => row < 0 ? 0 : row >= messageHeight ? messageHeight - 1 : row;
992
+ const snapCol = (raw, col) => raw < 0 ? 0 : raw >= messageHeight ? Infinity : col;
993
+ return {
994
+ anchorRow: clampRow(rawAnchor),
995
+ anchorCol: snapCol(rawAnchor, selection.anchorCol),
996
+ focusRow: clampRow(rawFocus),
997
+ focusCol: snapCol(rawFocus, selection.focusCol),
998
+ inMessage: true
999
+ };
1000
+ }
1001
+ function clampFocusRow(inMessage, eventY, scrollTop, messageHeight, rows, dialogOpen) {
1002
+ if (inMessage) return Math.min(eventY + scrollTop, scrollTop + messageHeight - 1);
1003
+ if (dialogOpen) return Math.max(0, Math.min(eventY, rows - 1));
1004
+ return Math.max(messageHeight, eventY);
1005
+ }
1006
+ //#endregion
1007
+ //#region src/ui/selection-registry.ts
1008
+ const rows = /* @__PURE__ */ new Map();
1009
+ function registerRowPiece(id, y, piece) {
1010
+ let pieces = rows.get(y);
1011
+ if (piece === null) {
1012
+ if (pieces !== void 0) {
1013
+ pieces.delete(id);
1014
+ if (pieces.size === 0) rows.delete(y);
1015
+ }
1016
+ return;
1017
+ }
1018
+ if (pieces === void 0) {
1019
+ pieces = /* @__PURE__ */ new Map();
1020
+ rows.set(y, pieces);
1021
+ }
1022
+ pieces.set(id, piece);
1023
+ }
1024
+ function rowPieces(y) {
1025
+ const pieces = rows.get(y);
1026
+ if (pieces === void 0) return [];
1027
+ return [...pieces.values()].sort((a, b) => a.col - b.col);
1028
+ }
1029
+ function visiblePieces(y) {
1030
+ const pieces = rowPieces(y);
1031
+ const chrome = pieces.filter((piece) => piece.layer !== "message");
1032
+ return chrome.length > 0 ? chrome : pieces;
1033
+ }
1034
+ function envelopeOf(selection) {
1035
+ return {
1036
+ start: Math.min(selection.anchorCol, selection.focusCol),
1037
+ end: Math.max(selection.anchorCol, selection.focusCol)
1038
+ };
1039
+ }
1040
+ function envelopeOverlaps(selection, col, width) {
1041
+ const env = envelopeOf(selection);
1042
+ return env.end > col && env.start < col + width;
1043
+ }
1044
+ function sliceByColumns(text, startCol, endCol) {
1045
+ const startIndex = colToCharIndex(text, Math.max(0, startCol));
1046
+ const endIndex = colToCharIndex(text, Math.max(0, endCol));
1047
+ return text.slice(startIndex, endIndex);
1048
+ }
1049
+ function chromeSelectionText(selection) {
1050
+ const top = Math.min(selection.anchorRow, selection.focusRow);
1051
+ const bottom = Math.max(selection.anchorRow, selection.focusRow);
1052
+ const env = envelopeOf(selection);
1053
+ const lines = [];
1054
+ for (let row = top; row <= bottom; row++) {
1055
+ const range = selectedRange(selection, row);
1056
+ if (range === null) continue;
1057
+ let line = "";
1058
+ let prevEnd = -1;
1059
+ for (const piece of visiblePieces(row)) {
1060
+ const width = textWidth(piece.text);
1061
+ if (!(env.end > piece.col && env.start < piece.col + width)) continue;
1062
+ const start = Math.max(range.start, piece.col);
1063
+ const end = Math.min(range.end, piece.col + width);
1064
+ if (start >= end) continue;
1065
+ const text = sliceByColumns(piece.text, start - piece.col, end - piece.col);
1066
+ if (text === "") continue;
1067
+ if (line !== "") line += prevEnd === piece.col ? "" : " ";
1068
+ line += text;
1069
+ prevEnd = piece.col + width;
1070
+ }
1071
+ lines.push(line);
1072
+ }
1073
+ return lines.join("\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "");
1074
+ }
1075
+ //#endregion
1076
+ //#region src/core/segments.ts
1077
+ function wrapSegments(segments, width) {
1078
+ if (width <= 0) return [[]];
1079
+ const rows = [];
1080
+ let clusters = [];
1081
+ let styles = [];
1082
+ const flushLine = () => {
1083
+ const starts = computeWrapStarts(clusters, width);
1084
+ for (let r = 0; r < starts.length; r++) {
1085
+ const from = starts[r];
1086
+ const to = r + 1 < starts.length ? starts[r + 1] : clusters.length;
1087
+ if (from >= to) {
1088
+ rows.push([]);
1089
+ continue;
1090
+ }
1091
+ const row = [];
1092
+ let runText = "";
1093
+ let runStyle = styles[from];
1094
+ for (let k = from; k < to; k++) {
1095
+ const style = styles[k];
1096
+ if (k > from && !sameStyle(runStyle, style)) {
1097
+ row.push({
1098
+ text: runText,
1099
+ style: runStyle
1100
+ });
1101
+ runText = "";
1102
+ runStyle = style;
1103
+ }
1104
+ runText += clusters[k];
1105
+ }
1106
+ row.push({
1107
+ text: runText,
1108
+ style: runStyle
1109
+ });
1110
+ rows.push(row);
1111
+ }
1112
+ clusters = [];
1113
+ styles = [];
1114
+ };
1115
+ for (const seg of segments) {
1116
+ const parts = normalizeWrapText(seg.text).split("\n");
1117
+ for (let p = 0; p < parts.length; p++) {
1118
+ if (p > 0) flushLine();
1119
+ appendClusters(parts[p], seg.style, clusters, styles);
1120
+ }
1121
+ }
1122
+ flushLine();
1123
+ return rows;
1124
+ }
1125
+ function sameStyle(a, b) {
1126
+ return a.color === b.color && a.background === b.background && a.bold === b.bold && a.italic === b.italic && a.strike === b.strike && a.underline === b.underline;
1127
+ }
1128
+ function appendClusters(text, style, clusters, styles) {
1129
+ let ascii = true;
1130
+ for (let i = 0; i < text.length; i++) {
1131
+ const code = text.charCodeAt(i);
1132
+ if (code < 32 || code > 126) {
1133
+ ascii = false;
1134
+ break;
1135
+ }
1136
+ }
1137
+ if (ascii) {
1138
+ for (let i = 0; i < text.length; i++) {
1139
+ clusters.push(text[i]);
1140
+ styles.push(style);
1141
+ }
1142
+ return;
1143
+ }
1144
+ for (const { segment } of segmentGraphemes(text)) {
1145
+ clusters.push(segment);
1146
+ styles.push(style);
1147
+ }
1148
+ }
1149
+ function mergeRuns(segments) {
1150
+ if (segments.length === 0) return segments;
1151
+ const out = [];
1152
+ for (const seg of segments) {
1153
+ const last = out[out.length - 1];
1154
+ if (last !== void 0 && sameStyle(last.style, seg.style)) last.text += seg.text;
1155
+ else out.push({
1156
+ text: seg.text,
1157
+ style: seg.style
1158
+ });
1159
+ }
1160
+ return out;
1161
+ }
1162
+ function segmentsKey(segments) {
1163
+ let key = "";
1164
+ for (const seg of segments) {
1165
+ const flags = `${seg.style.bold ? "b" : ""}${seg.style.italic ? "i" : ""}${seg.style.strike ? "s" : ""}${seg.style.underline ? "u" : ""}${seg.style.background ? "g" : ""}`;
1166
+ key += `${seg.text.length}:${seg.text}\x1f${seg.style.color ?? ""}\x1e${seg.style.background ?? ""}\x1e${flags}\x1d`;
1167
+ }
1168
+ return key;
1169
+ }
1170
+ const glyphs = {
1171
+ spinnerFrames: unicode ? [
1172
+ "⠋",
1173
+ "⠙",
1174
+ "⠹",
1175
+ "⠸",
1176
+ "⠼",
1177
+ "⠴",
1178
+ "⠦",
1179
+ "⠧",
1180
+ "⠇",
1181
+ "⠏"
1182
+ ] : [
1183
+ "|",
1184
+ "/",
1185
+ "-",
1186
+ "\\"
1187
+ ],
1188
+ bullets: unicode ? [
1189
+ "•",
1190
+ "◦",
1191
+ "▪"
1192
+ ] : [
1193
+ "*",
1194
+ "o",
1195
+ "-"
1196
+ ],
1197
+ horizontal: unicode ? "─" : "-",
1198
+ tableVertical: unicode ? "│" : "|",
1199
+ tableBorders: {
1200
+ top: unicode ? [
1201
+ "┌",
1202
+ "┬",
1203
+ "┐"
1204
+ ] : [
1205
+ "+",
1206
+ "-",
1207
+ "+"
1208
+ ],
1209
+ middle: unicode ? [
1210
+ "├",
1211
+ "┼",
1212
+ "┤"
1213
+ ] : [
1214
+ "+",
1215
+ "-",
1216
+ "+"
1217
+ ],
1218
+ bottom: unicode ? [
1219
+ "└",
1220
+ "┴",
1221
+ "┘"
1222
+ ] : [
1223
+ "+",
1224
+ "-",
1225
+ "+"
1226
+ ]
1227
+ },
1228
+ quoteBar: unicode ? "▌" : "|",
1229
+ taskChecked: unicode ? "☑" : "[x]",
1230
+ taskUnchecked: unicode ? "☐" : "[ ]",
1231
+ headerExpanded: unicode ? "↓" : "v",
1232
+ headerCollapsed: "-",
1233
+ treeBranch: unicode ? "└" : "+",
1234
+ separator: unicode ? "·" : "-",
1235
+ tokenArrow: unicode ? "→" : "->",
1236
+ ellipsis: unicode ? "…" : "...",
1237
+ focusMarker: unicode ? "❯" : ">",
1238
+ tick: unicode ? "✓" : "x",
1239
+ checkboxOn: unicode ? "[✓]" : "[x]",
1240
+ checkboxOff: "[ ]",
1241
+ pageFlip: unicode ? "⇄" : "<->",
1242
+ wrapArrows: unicode ? "⇅" : "^v",
1243
+ carouselLeft: unicode ? "◀" : "<",
1244
+ carouselRight: unicode ? "▶" : ">",
1245
+ todoPending: "[ ]",
1246
+ todoInProgress: unicode ? "[●]" : "[o]",
1247
+ todoDone: unicode ? "[√]" : "[x]",
1248
+ halfBlockCaps: unicode,
1249
+ blockCapTop: "▄",
1250
+ blockCapBottom: "▀"
1251
+ };
1252
+ function inputFrameTop(rows, realRows) {
1253
+ return rows - 4 - realRows;
1254
+ }
1255
+ function inputStatusRow(rows) {
1256
+ return rows - 4 + 1;
1257
+ }
1258
+ function hintBlockTop(rows, inputHeight, visibleCount) {
1259
+ return Math.max(0, rows - inputHeight - 1 - visibleCount);
1260
+ }
1261
+ //#endregion
1262
+ //#region src/ui/layout-service.ts
1263
+ function inputContentTop(rows, inputHeight) {
1264
+ return rows - inputHeight;
1265
+ }
1266
+ /** Inclusive bottom row of the input's editable content area. */
1267
+ function inputContentBottom(rows, inputHeight) {
1268
+ return inputContentTop(rows, inputHeight) + inputHeight - 4;
1269
+ }
1270
+ function inputContentContains(y, rows, inputHeight) {
1271
+ return y >= inputContentTop(rows, inputHeight) && y <= inputContentBottom(rows, inputHeight);
1272
+ }
1273
+ /** Inclusive row span of the active interaction panel (message bottom .. above the status line). */
1274
+ function panelSpan(geometry) {
1275
+ return {
1276
+ top: geometry.messageHeight,
1277
+ bottom: geometry.rows - 2
1278
+ };
1279
+ }
1280
+ function panelContains(y, geometry) {
1281
+ const span = panelSpan(geometry);
1282
+ return y >= span.top && y <= span.bottom;
1283
+ }
1284
+ /** Anchor row where a panel of `bodyRows` lines must start to sit above the status line. */
1285
+ function panelAnchorRow(rows, bodyRows) {
1286
+ return rows - 2 - bodyRows;
1287
+ }
1288
+ function hintSpan(rows, inputHeight, visibleCount, dialogOpen) {
1289
+ if (dialogOpen || visibleCount <= 0) return null;
1290
+ return {
1291
+ top: hintBlockTop(rows, inputHeight, visibleCount),
1292
+ bottom: rows - inputHeight - 1 - 1
1293
+ };
1294
+ }
1295
+ function scrollbarColumn(columns) {
1296
+ return columns - 3;
1297
+ }
1298
+ function messageBackgroundWidth(width) {
1299
+ return width - 2 - 3 - 1;
1300
+ }
1301
+ const RegionContext = createContext({
1302
+ x: 0,
1303
+ y: 0
1304
+ });
1305
+ function useOrigin() {
1306
+ return useContext(RegionContext);
1307
+ }
1308
+ function translate(parent, x, y) {
1309
+ return {
1310
+ x: parent.x + x,
1311
+ y: parent.y + y
1312
+ };
1313
+ }
1314
+ function Region({ x = 0, y = 0, children }) {
1315
+ const parent = useOrigin();
1316
+ const value = useMemo(() => translate(parent, x, y), [
1317
+ parent.x,
1318
+ parent.y,
1319
+ x,
1320
+ y
1321
+ ]);
1322
+ return /* @__PURE__ */ jsx(RegionContext.Provider, {
1323
+ value,
1324
+ children
1325
+ });
1326
+ }
1327
+ //#endregion
1328
+ //#region src/ui/selection.tsx
1329
+ const SelectionContext = createContext(null);
1330
+ const SelectableContent = memo(function SelectableContent({ content, segments, color, bold, inverse, backgroundColor, sliceStart, sliceEnd }) {
1331
+ if (sliceStart >= 0 && sliceEnd > sliceStart) {
1332
+ const highlight = /* @__PURE__ */ jsx(Text, {
1333
+ backgroundColor: COLORS.selectionBg,
1334
+ color: COLORS.selectionFg,
1335
+ children: content.slice(sliceStart, sliceEnd)
1336
+ });
1337
+ if (segments !== void 0) return /* @__PURE__ */ jsxs(Text, {
1338
+ backgroundColor,
1339
+ children: [
1340
+ renderSegments(segments, 0, sliceStart, color ?? COLORS.ink),
1341
+ highlight,
1342
+ renderSegments(segments, sliceEnd, content.length, color ?? COLORS.ink)
1343
+ ]
1344
+ });
1345
+ return /* @__PURE__ */ jsxs(Text, {
1346
+ backgroundColor,
1347
+ inverse,
1348
+ color: color ?? COLORS.ink,
1349
+ bold,
1350
+ children: [
1351
+ content.slice(0, sliceStart),
1352
+ highlight,
1353
+ content.slice(sliceEnd)
1354
+ ]
1355
+ });
1356
+ }
1357
+ if (segments !== void 0) return /* @__PURE__ */ jsx(Text, {
1358
+ backgroundColor,
1359
+ children: renderSegments(segments, 0, content.length, color ?? COLORS.ink)
1360
+ });
1361
+ return /* @__PURE__ */ jsx(Text, {
1362
+ backgroundColor,
1363
+ inverse,
1364
+ color: color ?? COLORS.ink,
1365
+ bold,
1366
+ children: content
1367
+ });
1368
+ }, (prev, next) => prev.content === next.content && prev.segments === next.segments && prev.color === next.color && prev.bold === next.bold && prev.inverse === next.inverse && prev.backgroundColor === next.backgroundColor && prev.sliceStart === next.sliceStart && prev.sliceEnd === next.sliceEnd);
1369
+ const SelectableText = function SelectableText({ y, col, text, segments, color, bold = false, inverse = false, backgroundColor, messageLayer = false, flow = false }) {
1370
+ const selection = useContext(SelectionContext);
1371
+ const origin = useOrigin();
1372
+ const absY = origin.y + y;
1373
+ const absCol = origin.x + col;
1374
+ const content = text ?? segments?.map((segment) => segment.text).join("") ?? "";
1375
+ const pieceId = useRef({});
1376
+ useLayoutEffect(() => {
1377
+ registerRowPiece(pieceId.current, absY, {
1378
+ col: absCol,
1379
+ text: content,
1380
+ ...messageLayer ? { layer: "message" } : {}
1381
+ });
1382
+ return () => registerRowPiece(pieceId.current, absY, null);
1383
+ }, [
1384
+ absY,
1385
+ absCol,
1386
+ content,
1387
+ messageLayer
1388
+ ]);
1389
+ let sliceStart = -1;
1390
+ let sliceEnd = -1;
1391
+ if (selection !== null) {
1392
+ const range = selectedRange(selection, absY);
1393
+ if (range !== null && (flow || envelopeOverlaps(selection, absCol, textWidth(content)))) {
1394
+ const lineWidth = textWidth(content);
1395
+ const start = Math.max(range.start, absCol);
1396
+ const end = Math.min(range.end, absCol + lineWidth);
1397
+ if (start < end) {
1398
+ sliceStart = colToCharIndex(content, start - absCol);
1399
+ sliceEnd = colToCharIndex(content, end - absCol);
1400
+ }
1401
+ }
1402
+ }
1403
+ return /* @__PURE__ */ jsx(SelectableContent, {
1404
+ content,
1405
+ segments,
1406
+ color,
1407
+ bold,
1408
+ inverse,
1409
+ backgroundColor,
1410
+ sliceStart,
1411
+ sliceEnd
1412
+ });
1413
+ };
1414
+ function renderSegments(segments, start, end, baseColor) {
1415
+ const out = [];
1416
+ let offset = 0;
1417
+ for (const segment of segments) {
1418
+ const segmentStart = offset;
1419
+ const segmentEnd = offset + segment.text.length;
1420
+ offset = segmentEnd;
1421
+ if (segmentEnd <= start || segmentStart >= end) continue;
1422
+ const from = Math.max(0, start - segmentStart);
1423
+ const to = Math.min(segment.text.length, end - segmentStart);
1424
+ if (from >= to) continue;
1425
+ out.push(/* @__PURE__ */ jsx(Text, {
1426
+ color: segment.style.color ?? baseColor,
1427
+ bold: segment.style.bold,
1428
+ italic: segment.style.italic,
1429
+ underline: segment.style.underline,
1430
+ strikethrough: segment.style.strike,
1431
+ backgroundColor: segment.style.background,
1432
+ children: segment.text.slice(from, to)
1433
+ }, out.length));
1434
+ }
1435
+ return out;
1436
+ }
1437
+ //#endregion
1438
+ //#region src/terminal/mouse.ts
1439
+ const MOUSE_ENABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000h\x1B[?1002h\x1B[?1006h";
1440
+ const MOUSE_DISABLE_SEQUENCES = "\x1B[?1003l\x1B[?1000l\x1B[?1002l\x1B[?1006l";
1441
+ function writeMouseEnable() {
1442
+ process.stdout.write(MOUSE_ENABLE_SEQUENCES);
1443
+ }
1444
+ function writeMouseDisable() {
1445
+ process.stdout.write(MOUSE_DISABLE_SEQUENCES);
1446
+ }
1447
+ const MAX_BUFFER = 64;
1448
+ const SGR_PREFIX = "\x1B[<";
1449
+ function partialPrefixLength(buffer) {
1450
+ for (let keep = Math.min(2, buffer.length); keep > 0; keep--) if (SGR_PREFIX.startsWith(buffer.slice(-keep))) return keep;
1451
+ return 0;
1452
+ }
1453
+ function createMouseParser(onEvent) {
1454
+ let buffer = "";
1455
+ const pressedButtons = /* @__PURE__ */ new Set();
1456
+ function parseSequenceAt(str, offset) {
1457
+ if (!str.startsWith("\x1B[<", offset)) return null;
1458
+ let index = offset + 3;
1459
+ const values = [];
1460
+ let current = 0;
1461
+ let hasDigit = false;
1462
+ let terminator = "";
1463
+ for (; index < str.length; index++) {
1464
+ const ch = str[index];
1465
+ if (ch >= "0" && ch <= "9") {
1466
+ current = current * 10 + (ch.charCodeAt(0) - 48);
1467
+ hasDigit = true;
1468
+ } else if (ch === ";" && hasDigit) {
1469
+ values.push(current);
1470
+ current = 0;
1471
+ hasDigit = false;
1472
+ } else if (ch === "M" || ch === "m") {
1473
+ if (!hasDigit) return null;
1474
+ values.push(current);
1475
+ terminator = ch;
1476
+ break;
1477
+ } else return null;
1478
+ }
1479
+ if (terminator === "") return null;
1480
+ if (values.length !== 3) return null;
1481
+ return {
1482
+ event: decodeSgrEvent(values[0], values[1], values[2], terminator, pressedButtons),
1483
+ consumed: index - offset + 1
1484
+ };
1485
+ }
1486
+ return { feed(chunk) {
1487
+ buffer += chunk;
1488
+ while (true) {
1489
+ const start = buffer.indexOf("\x1B[<");
1490
+ if (start === -1) {
1491
+ const keep = partialPrefixLength(buffer);
1492
+ buffer = keep === 0 ? "" : buffer.slice(-keep);
1493
+ return;
1494
+ }
1495
+ if (start > 0) {
1496
+ buffer = buffer.slice(start);
1497
+ continue;
1498
+ }
1499
+ if (buffer.length > MAX_BUFFER) {
1500
+ buffer = "";
1501
+ return;
1502
+ }
1503
+ const parsed = parseSequenceAt(buffer, 0);
1504
+ if (parsed === null) return;
1505
+ const { event, consumed } = parsed;
1506
+ buffer = buffer.slice(consumed);
1507
+ onEvent(event);
1508
+ }
1509
+ } };
1510
+ }
1511
+ function decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease, pressed) {
1512
+ const button = rawButtonCode & 3;
1513
+ const isScroll = (rawButtonCode & 64) !== 0;
1514
+ const isMotion = (rawButtonCode & 32) !== 0;
1515
+ const modifiers = {
1516
+ shift: (rawButtonCode & 4) !== 0,
1517
+ alt: (rawButtonCode & 8) !== 0,
1518
+ ctrl: (rawButtonCode & 16) !== 0
1519
+ };
1520
+ let type;
1521
+ let scrollDirection;
1522
+ if (isScroll) {
1523
+ type = "scroll";
1524
+ scrollDirection = button === 0 ? "up" : "down";
1525
+ } else if (isMotion) type = pressed.size > 0 ? "drag" : "move";
1526
+ else if (pressRelease === "M") {
1527
+ type = "down";
1528
+ if (button !== 3) pressed.add(button);
1529
+ } else {
1530
+ type = "up";
1531
+ if (button === 3) pressed.clear();
1532
+ else pressed.delete(button);
1533
+ }
1534
+ return {
1535
+ type,
1536
+ button: button === 3 ? 0 : button,
1537
+ x: wireX - 1,
1538
+ y: wireY - 1,
1539
+ modifiers,
1540
+ ...scrollDirection === void 0 ? {} : { scrollDirection }
1541
+ };
1542
+ }
1543
+ function isMouseResidue(input) {
1544
+ return input.includes("\x1B") || /^\[<\d+(;\d+)*[Mm]$/.test(input);
1545
+ }
1546
+ function createMouseController(onEvent) {
1547
+ const parser = createMouseParser(onEvent);
1548
+ const onData = (chunk) => parser.feed(chunk.toString("latin1"));
1549
+ return {
1550
+ enable() {
1551
+ writeMouseEnable();
1552
+ process.stdin.on("data", onData);
1553
+ },
1554
+ disable() {
1555
+ writeMouseDisable();
1556
+ process.stdin.off("data", onData);
1557
+ }
1558
+ };
1559
+ }
1560
+ //#endregion
1561
+ //#region src/ui/key-arbiter.ts
1562
+ /**
1563
+ * Key-event suppression for ink's broadcast-based useInput.
1564
+ *
1565
+ * ink delivers every stdin chunk to every useInput subscriber, and delivery
1566
+ * order follows subscription order: KeymapGate (chrome key contributions,
1567
+ * mounted as the first child) -> message list -> composer -> panels ->
1568
+ * dialog -> the App shell. A participant that must win over the later ones
1569
+ * marks the current event consumed; later participants skip it.
1570
+ *
1571
+ * The consumed flag is scoped to one stdin chunk. KeymapGate bumps the
1572
+ * generation from its own stdin data listener, so even several synchronous
1573
+ * writes (as tests do) each get a fresh generation and no flag leaks across
1574
+ * events.
1575
+ */
1576
+ let generation = 0;
1577
+ let consumedAtGeneration = -1;
1578
+ /** Start a new key-event generation; called once per stdin chunk. */
1579
+ function beginKeyEvent() {
1580
+ generation += 1;
1581
+ }
1582
+ /** Chrome key contributions consumed the event; later participants must skip it. */
1583
+ function markKeymapConsumed() {
1584
+ consumedAtGeneration = generation;
1585
+ }
1586
+ /** True when the chrome key chain consumed the current key event. */
1587
+ function isKeyConsumed() {
1588
+ return consumedAtGeneration === generation;
1589
+ }
1590
+ //#endregion
1591
+ //#region src/ui/hooks/use-caret.ts
1592
+ /**
1593
+ * Ink 7.1.1 rests the hardware cursor one row above where frames without a
1594
+ * trailing newline end; this layout fills the viewport, so request one row
1595
+ * lower.
1596
+ */
1597
+ function useCaret() {
1598
+ const { setCursorPosition } = useCursor();
1599
+ return { setCursorPosition: useCallback((position) => {
1600
+ if (position === void 0) {
1601
+ setCursorPosition(void 0);
1602
+ return;
1603
+ }
1604
+ setCursorPosition({
1605
+ x: position.x,
1606
+ y: position.y + 1
1607
+ });
1608
+ }, [setCursorPosition]) };
1609
+ }
1610
+ //#endregion
1611
+ //#region src/core/caret-nonce.ts
1612
+ /**
1613
+ * Ink 7.1.1 places the caret one row high on the rewrite path; every caret
1614
+ * move changes these frame bytes so the diff renderer stays on the rewrite
1615
+ * path that useCaret() compensates for. The variant span exceeds any
1616
+ * terminal width, so no frame follows the misplaced cursor-only path.
1617
+ */
1618
+ const NONCE_SPAN = 240;
1619
+ function caretNonceColor(cursor) {
1620
+ return `ansi256(${16 + cursor % NONCE_SPAN})`;
1621
+ }
1622
+ function caretNonceText(cursor) {
1623
+ return Math.floor(cursor / NONCE_SPAN) % 2 === 0 ? " " : "\xA0";
1624
+ }
1625
+ function caretNonceBold(cursor) {
1626
+ return Math.floor(cursor / 480) % 2 === 0;
1627
+ }
1628
+ //#endregion
1629
+ //#region src/terminal/cursor-shape.ts
1630
+ const SEQUENCES = {
1631
+ beam: "\x1B[1 q",
1632
+ block: "\x1B[2 q",
1633
+ reset: "\x1B[0 q",
1634
+ hide: "\x1B[?25l",
1635
+ show: "\x1B[?25h"
1636
+ };
1637
+ function writeCursorShape(shape) {
1638
+ process.stdout.write(SEQUENCES[shape]);
1639
+ }
1640
+ //#endregion
1641
+ //#region src/ui/chrome/caps.tsx
1642
+ function CapText({ background, top, width }) {
1643
+ if (glyphs.halfBlockCaps) return /* @__PURE__ */ jsx(Text, {
1644
+ color: background,
1645
+ children: (top ? glyphs.blockCapTop : glyphs.blockCapBottom).repeat(width)
1646
+ });
1647
+ return /* @__PURE__ */ jsx(Text, {
1648
+ backgroundColor: background,
1649
+ children: " ".repeat(width)
1650
+ });
1651
+ }
1652
+ //#endregion
1653
+ //#region src/core/edit.ts
1654
+ function clamp(state, cursor) {
1655
+ return {
1656
+ value: state.value,
1657
+ cursor: Math.max(0, Math.min(cursor, state.value.length))
1658
+ };
1659
+ }
1660
+ function spanAtOrBefore(value, cursor) {
1661
+ for (const segment of segmentGraphemes(value)) {
1662
+ const end = segment.index + segment.segment.length;
1663
+ if (end >= cursor) return {
1664
+ start: segment.index,
1665
+ end
1666
+ };
1667
+ }
1668
+ }
1669
+ function spanAtOrAfter(value, cursor) {
1670
+ for (const segment of segmentGraphemes(value)) if (segment.index >= cursor) return {
1671
+ start: segment.index,
1672
+ end: segment.index + segment.segment.length
1673
+ };
1674
+ }
1675
+ function editInsert(state, text) {
1676
+ if (text === "") return clamp(state, state.cursor);
1677
+ const cursor = Math.max(0, Math.min(state.cursor, state.value.length));
1678
+ return {
1679
+ value: state.value.slice(0, cursor) + text + state.value.slice(cursor),
1680
+ cursor: cursor + text.length
1681
+ };
1682
+ }
1683
+ function editBackspace(state) {
1684
+ if (state.cursor <= 0) return null;
1685
+ const span = spanAtOrBefore(state.value, state.cursor);
1686
+ if (span === void 0) return null;
1687
+ return {
1688
+ value: state.value.slice(0, span.start) + state.value.slice(span.end),
1689
+ cursor: span.start
1690
+ };
1691
+ }
1692
+ function editDelete(state) {
1693
+ if (state.cursor >= state.value.length) return null;
1694
+ const span = spanAtOrAfter(state.value, state.cursor);
1695
+ if (span === void 0) return null;
1696
+ return {
1697
+ value: state.value.slice(0, state.cursor) + state.value.slice(span.end),
1698
+ cursor: state.cursor
1699
+ };
1700
+ }
1701
+ function editCursorLeft(state) {
1702
+ if (state.cursor <= 0) return state;
1703
+ const span = spanAtOrBefore(state.value, state.cursor);
1704
+ return clamp(state, span === void 0 ? state.cursor - 1 : span.start);
1705
+ }
1706
+ function editCursorRight(state) {
1707
+ if (state.cursor >= state.value.length) return state;
1708
+ const span = spanAtOrAfter(state.value, state.cursor);
1709
+ return clamp(state, span === void 0 ? state.cursor + 1 : span.end);
1710
+ }
1711
+ const SEPARATOR_RANGES = [
1712
+ [8208, 8231],
1713
+ [12288, 12351],
1714
+ [65281, 65295],
1715
+ [65306, 65312],
1716
+ [65339, 65344],
1717
+ [65371, 65381],
1718
+ [65504, 65508]
1719
+ ];
1720
+ function isAsciiWordChar(code) {
1721
+ return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 95;
1722
+ }
1723
+ function isLineBreakAt(value, index) {
1724
+ const code = value.charCodeAt(index);
1725
+ return code === 10 || code === 13;
1726
+ }
1727
+ function isSeparatorAt(value, index) {
1728
+ const code = value.charCodeAt(index);
1729
+ if (code === 10 || code === 13) return true;
1730
+ if (isAsciiWordChar(code)) return false;
1731
+ if (code >= 128) {
1732
+ for (const [start, end] of SEPARATOR_RANGES) if (code >= start && code <= end) return true;
1733
+ return false;
1734
+ }
1735
+ if (code === 32) return true;
1736
+ return code >= 33 && code <= 126;
1737
+ }
1738
+ function wordLeftBoundary(value, cursor) {
1739
+ let i = cursor - 1;
1740
+ while (i >= 0 && !isLineBreakAt(value, i) && isSeparatorAt(value, i)) i--;
1741
+ while (i >= 0 && !isSeparatorAt(value, i)) i--;
1742
+ return i + 1;
1743
+ }
1744
+ function wordRightBoundary(value, cursor) {
1745
+ if (cursor >= value.length) return cursor;
1746
+ if (!isSeparatorAt(value, cursor)) {
1747
+ let i = cursor;
1748
+ while (i < value.length && !isSeparatorAt(value, i)) i++;
1749
+ return i;
1750
+ }
1751
+ let i = cursor;
1752
+ while (i < value.length && !isLineBreakAt(value, i) && isSeparatorAt(value, i)) i++;
1753
+ while (i < value.length && !isSeparatorAt(value, i)) i++;
1754
+ return i;
1755
+ }
1756
+ function editCursorWordLeft(state) {
1757
+ const boundary = wordLeftBoundary(state.value, state.cursor);
1758
+ return boundary >= state.cursor ? state : {
1759
+ value: state.value,
1760
+ cursor: boundary
1761
+ };
1762
+ }
1763
+ function editCursorWordRight(state) {
1764
+ const boundary = wordRightBoundary(state.value, state.cursor);
1765
+ return boundary <= state.cursor ? state : {
1766
+ value: state.value,
1767
+ cursor: boundary
1768
+ };
1769
+ }
1770
+ function editDeleteWordLeft(state) {
1771
+ const boundary = wordLeftBoundary(state.value, state.cursor);
1772
+ if (boundary >= state.cursor) return null;
1773
+ return {
1774
+ value: state.value.slice(0, boundary) + state.value.slice(state.cursor),
1775
+ cursor: boundary
1776
+ };
1777
+ }
1778
+ function editDeleteWordRight(state) {
1779
+ const boundary = wordRightBoundary(state.value, state.cursor);
1780
+ if (boundary <= state.cursor) return null;
1781
+ return {
1782
+ value: state.value.slice(0, state.cursor) + state.value.slice(boundary),
1783
+ cursor: state.cursor
1784
+ };
1785
+ }
1786
+ //#endregion
1787
+ //#region src/ui/widgets/registry.ts
1788
+ const widgets = keyedRegistry();
1789
+ function fallbackWidget(type) {
1790
+ return {
1791
+ height: () => 1,
1792
+ paintWidth: () => 24,
1793
+ render: ({ item }) => {
1794
+ const label = item.label;
1795
+ const text = typeof label === "string" && label !== "" ? `${type}: ${label}` : `${type}: (unknown item)`;
1796
+ return createElement(Text, { dimColor: true }, text);
1797
+ }
1798
+ };
1799
+ }
1800
+ function registerWidget(type, def, options) {
1801
+ return widgets.register(type, def, options);
1802
+ }
1803
+ function widgetOf(type) {
1804
+ return widgets.get(type) ?? fallbackWidget(type);
1805
+ }
1806
+ //#endregion
1807
+ //#region src/ui/dialog/items.ts
1808
+ function isSelectableRow(row) {
1809
+ if (row === void 0) return false;
1810
+ return row.items.some((item) => widgetOf(item.type).selectable === true);
1811
+ }
1812
+ function selectableSpan(row) {
1813
+ if (row === void 0) return 0;
1814
+ let stops = 0;
1815
+ for (const item of row.items) stops += widgetOf(item.type).stops?.(item) ?? 1;
1816
+ return Math.max(0, stops - 1);
1817
+ }
1818
+ function snapRow(rows, row) {
1819
+ const count = rows.length;
1820
+ if (count === 0) return 0;
1821
+ const index = Math.min(Math.max(row, 0), count - 1);
1822
+ if (isSelectableRow(rows[index])) return index;
1823
+ for (let down = index + 1; down < count; down++) if (isSelectableRow(rows[down])) return down;
1824
+ for (let up = index - 1; up >= 0; up--) if (isSelectableRow(rows[up])) return up;
1825
+ return index;
1826
+ }
1827
+ function stepRow(rows, from, delta) {
1828
+ let row = from + delta;
1829
+ while (row >= 0 && row < rows.length && !isSelectableRow(rows[row])) row += delta;
1830
+ return row >= 0 && row < rows.length ? row : from;
1831
+ }
1832
+ function focusedItem(row, col) {
1833
+ if (row === void 0) return void 0;
1834
+ const only = row.items.length === 1 ? row.items[0] : void 0;
1835
+ if (only !== void 0 && widgetOf(only.type).fullRowFocus === true) return only;
1836
+ return row.items[col];
1837
+ }
1838
+ function moveFocus(rows, focus, key) {
1839
+ switch (key) {
1840
+ case "up": return {
1841
+ row: stepRow(rows, focus.row, -1),
1842
+ col: 0
1843
+ };
1844
+ case "down": return {
1845
+ row: stepRow(rows, focus.row, 1),
1846
+ col: 0
1847
+ };
1848
+ case "left": return {
1849
+ row: focus.row,
1850
+ col: Math.max(0, focus.col - 1)
1851
+ };
1852
+ case "right": {
1853
+ const max = selectableSpan(rows[focus.row]);
1854
+ return {
1855
+ row: focus.row,
1856
+ col: Math.max(0, Math.min(max, focus.col + 1))
1857
+ };
1858
+ }
1859
+ }
1860
+ }
1861
+ function clampFocus(rows, focus, minRow, maxRow) {
1862
+ const row = snapRow(rows, Math.min(Math.max(focus.row, minRow), maxRow));
1863
+ return {
1864
+ row,
1865
+ col: Math.min(focus.col, selectableSpan(rows[row]))
1866
+ };
1867
+ }
1868
+ function asTextItem(item) {
1869
+ return widgetOf(item.type).editable === true ? item : null;
1870
+ }
1871
+ function filterRowsWithHeaders(rows, query, searchRight) {
1872
+ const q = query.trim().toLowerCase();
1873
+ if (q === "") return rows;
1874
+ const matches = (item) => {
1875
+ const texts = widgetOf(item.type).searchTexts?.(item, searchRight);
1876
+ if (texts === void 0) return false;
1877
+ return texts.some((text) => text.toLowerCase().includes(q));
1878
+ };
1879
+ const out = [];
1880
+ let pendingHeader;
1881
+ for (const row of rows) {
1882
+ if (!isSelectableRow(row)) {
1883
+ pendingHeader = row;
1884
+ continue;
1885
+ }
1886
+ if (!row.items.some(matches)) continue;
1887
+ if (pendingHeader !== void 0) {
1888
+ out.push(pendingHeader);
1889
+ pendingHeader = void 0;
1890
+ }
1891
+ out.push(row);
1892
+ }
1893
+ const first = out[0];
1894
+ if (first !== void 0 && first.items.length === 1 && first.items[0]?.type === "header") out[0] = { items: [{
1895
+ ...first.items[0],
1896
+ leadingBlank: false
1897
+ }] };
1898
+ return out;
1899
+ }
1900
+ const SELECT_BLOCK_WIDTH_RATIO = .6;
1901
+ const ACTION_EDGE_RATIO = .2;
1902
+ const FOOTER_MAX_LINES_PER_ENTRY = 2;
1903
+ function wrapFooter(footer, width) {
1904
+ if (footer === void 0) return [];
1905
+ const rows = [];
1906
+ for (const line of footer) {
1907
+ if (line.text === "") continue;
1908
+ const wrapped = wrapLines(line.text, width);
1909
+ const capped = wrapped.slice(0, FOOTER_MAX_LINES_PER_ENTRY);
1910
+ if (wrapped.length > FOOTER_MAX_LINES_PER_ENTRY) capped[1] = truncate(capped[1] ?? "", Math.max(1, width - glyphs.ellipsis.length)) + glyphs.ellipsis;
1911
+ for (const text of capped) rows.push({
1912
+ text,
1913
+ color: line.color
1914
+ });
1915
+ }
1916
+ return rows;
1917
+ }
1918
+ function wrapStatusLines(lines, width) {
1919
+ if (lines === void 0) return [];
1920
+ const rows = [];
1921
+ for (const line of lines) {
1922
+ if (line.text === "") continue;
1923
+ for (const text of wrapLines(line.text, width)) rows.push({
1924
+ text,
1925
+ color: line.color
1926
+ });
1927
+ }
1928
+ return rows;
1929
+ }
1930
+ function rowHeight(row, width) {
1931
+ let height = 1;
1932
+ for (const item of row.items) {
1933
+ const itemHeight = widgetOf(item.type).height(item, width);
1934
+ if (itemHeight > height) height = itemHeight;
1935
+ }
1936
+ return height;
1937
+ }
1938
+ function rowTopOffset(rows, rowIndex, width) {
1939
+ let offset = 0;
1940
+ for (let i = 0; i < rowIndex; i++) offset += rowHeight(rows[i], width);
1941
+ return offset;
1942
+ }
1943
+ function rowBlockSpan(rows, rowIndex, width) {
1944
+ const top = rowTopOffset(rows, rowIndex, width);
1945
+ let blockTop = top;
1946
+ let bottom = top + rowHeight(rows[rowIndex] ?? { items: [] }, width) - 1;
1947
+ for (let i = rowIndex - 1; i >= 0 && !isSelectableRow(rows[i]); i--) blockTop -= rowHeight(rows[i], width);
1948
+ for (let i = rowIndex + 1; i < rows.length && !isSelectableRow(rows[i]); i++) bottom += rowHeight(rows[i], width);
1949
+ return {
1950
+ top: blockTop,
1951
+ bottom
1952
+ };
1953
+ }
1954
+ function adjustScroll(rows, focus, scrollTop, contentHeight, width) {
1955
+ const { top, bottom } = rowBlockSpan(rows, focus.row, width);
1956
+ if (top < scrollTop) return top;
1957
+ if (bottom >= scrollTop + contentHeight) return Math.max(0, bottom - contentHeight + 1);
1958
+ return scrollTop;
1959
+ }
1960
+ function listCenterScroll(rows, focusRow, viewportHeight, width, direction) {
1961
+ const total = rows.reduce((sum, row) => sum + rowHeight(row, width), 0);
1962
+ const maxScroll = Math.max(0, total - viewportHeight);
1963
+ const anchor = direction === "down" ? Math.floor(viewportHeight / 2) : Math.floor((viewportHeight - 1) / 2);
1964
+ const top = rowTopOffset(rows, focusRow, width);
1965
+ return Math.max(0, Math.min(top - anchor, maxScroll));
1966
+ }
1967
+ function hitRowIndex(y, top, titleLines, rows, scrollTop = 0, width = Number.POSITIVE_INFINITY) {
1968
+ const localY = y - top - 1 - titleLines + scrollTop;
1969
+ let offset = 0;
1970
+ for (let i = 0; i < rows.length; i++) {
1971
+ const height = rowHeight(rows[i], width);
1972
+ if (localY >= offset && localY < offset + height) return i;
1973
+ offset += height;
1974
+ }
1975
+ return null;
1976
+ }
1977
+ function selectBlock(contentWidth, value) {
1978
+ const blockWidth = Math.max(3, Math.floor(contentWidth * SELECT_BLOCK_WIDTH_RATIO));
1979
+ const innerWidth = Math.max(1, blockWidth - 6);
1980
+ const text = truncate(value || "(none)", innerWidth);
1981
+ const padding = innerWidth - textWidth(text);
1982
+ const fullPadding = blockWidth - textWidth(text);
1983
+ return {
1984
+ blockWidth,
1985
+ blockStart: contentWidth - blockWidth,
1986
+ text,
1987
+ leftPad: Math.floor(padding / 2),
1988
+ fullLeftPad: Math.floor(fullPadding / 2),
1989
+ cursorX: 3 + Math.floor(padding / 2) + textWidth(text)
1990
+ };
1991
+ }
1992
+ function actionPositions(contentWidth, confirmLabel, cancelLabel) {
1993
+ const edge = Math.floor(contentWidth * ACTION_EDGE_RATIO);
1994
+ const confirmX = Math.min(edge, Math.max(0, contentWidth - textWidth(confirmLabel)));
1995
+ return {
1996
+ confirmX,
1997
+ cancelX: Math.max(confirmX + textWidth(confirmLabel), contentWidth - edge - textWidth(cancelLabel))
1998
+ };
1999
+ }
2000
+ //#endregion
2001
+ //#region src/ui/widgets/header.tsx
2002
+ registerWidget("header", {
2003
+ selectable: false,
2004
+ height(item) {
2005
+ return item.leadingBlank === true ? 2 : 1;
2006
+ },
2007
+ paintWidth(item) {
2008
+ return textWidth(item.label);
2009
+ },
2010
+ onEnter() {
2011
+ return true;
2012
+ },
2013
+ render({ item, y, x, clip }) {
2014
+ const lead = item.leadingBlank === true && clip === 0 ? 1 : 0;
2015
+ const label = /* @__PURE__ */ jsx(SelectableText, {
2016
+ y: y + lead,
2017
+ col: x,
2018
+ text: item.label,
2019
+ color: COLORS.sectionHeader,
2020
+ bold: true
2021
+ });
2022
+ if (lead === 0) return label;
2023
+ return /* @__PURE__ */ jsxs(Box, {
2024
+ flexDirection: "column",
2025
+ children: [/* @__PURE__ */ jsx(Box, {
2026
+ height: 1,
2027
+ children: /* @__PURE__ */ jsx(Text, { children: " " })
2028
+ }), label]
2029
+ });
2030
+ }
2031
+ }, { order: 500 });
2032
+ //#endregion
2033
+ //#region src/ui/widgets/actions.tsx
2034
+ function paddedLabel(label) {
2035
+ return ` ${label} `;
2036
+ }
2037
+ function hitActionZone(item, width, localX) {
2038
+ const positions = actionPositions(width, paddedLabel(item.confirmLabel), paddedLabel(item.cancelLabel));
2039
+ if (localX >= positions.confirmX && localX < positions.confirmX + textWidth(paddedLabel(item.confirmLabel))) return "confirm";
2040
+ if (localX >= positions.cancelX && localX < positions.cancelX + textWidth(paddedLabel(item.cancelLabel))) return "cancel";
2041
+ return null;
2042
+ }
2043
+ registerWidget("actions", {
2044
+ selectable: true,
2045
+ fullRowFocus: true,
2046
+ stops: () => 2,
2047
+ height() {
2048
+ return 1;
2049
+ },
2050
+ paintWidth() {
2051
+ return 0;
2052
+ },
2053
+ render({ item, focused, width, y, x, subCol }) {
2054
+ const confirmLabel = paddedLabel(item.confirmLabel);
2055
+ const cancelLabel = paddedLabel(item.cancelLabel);
2056
+ const positions = actionPositions(width, confirmLabel, cancelLabel);
2057
+ const mid = positions.cancelX - (positions.confirmX + textWidth(confirmLabel));
2058
+ return /* @__PURE__ */ jsxs(Box, {
2059
+ flexDirection: "row",
2060
+ children: [
2061
+ /* @__PURE__ */ jsx(Text, { children: " ".repeat(positions.confirmX) }),
2062
+ /* @__PURE__ */ jsx(SelectableText, {
2063
+ y,
2064
+ col: x + positions.confirmX,
2065
+ text: confirmLabel,
2066
+ inverse: focused && subCol === 0
2067
+ }),
2068
+ /* @__PURE__ */ jsx(Text, { children: " ".repeat(Math.max(0, mid)) }),
2069
+ /* @__PURE__ */ jsx(SelectableText, {
2070
+ y,
2071
+ col: x + positions.cancelX,
2072
+ text: cancelLabel,
2073
+ inverse: focused && subCol === 1
2074
+ })
2075
+ ]
2076
+ });
2077
+ },
2078
+ onLeftRight(item, direction, api) {
2079
+ api.navigate(direction === 1 ? "right" : "left");
2080
+ return true;
2081
+ },
2082
+ onEnter(item, api) {
2083
+ if (api.subCol === 1) item.onCancel();
2084
+ else item.onConfirm();
2085
+ return true;
2086
+ },
2087
+ onClick(item, hit, actions) {
2088
+ if (hit.localY !== 0) {
2089
+ actions.focus(0);
2090
+ return true;
2091
+ }
2092
+ const zone = hitActionZone(item, hit.width, hit.localX);
2093
+ if (zone === "confirm") {
2094
+ actions.focus(0);
2095
+ item.onConfirm();
2096
+ return true;
2097
+ }
2098
+ if (zone === "cancel") {
2099
+ actions.focus(1);
2100
+ item.onCancel();
2101
+ return true;
2102
+ }
2103
+ actions.focus(0);
2104
+ return true;
2105
+ }
2106
+ }, { order: 500 });
2107
+ //#endregion
2108
+ //#region src/ui/dialog/sizes.ts
2109
+ const DIALOG_MAX_HEIGHT = .6;
2110
+ //#endregion
2111
+ //#region src/ui/widgets/input.tsx
2112
+ function scrollStart(lines, caretRow) {
2113
+ return Math.min(Math.max(caretRow - 2, 0), Math.max(0, lines - 3));
2114
+ }
2115
+ registerWidget("input", {
2116
+ selectable: true,
2117
+ editable: true,
2118
+ height(item, width) {
2119
+ return 2 + Math.min(wrapLines(item.value, width).length, 3);
2120
+ },
2121
+ paintWidth(item) {
2122
+ return textWidth(item.label);
2123
+ },
2124
+ caret(item, cursor, width) {
2125
+ const point = locToPoint(item.value, width, cursor);
2126
+ const lines = wrapLines(item.value, width).length;
2127
+ return {
2128
+ dy: 1 + point.row - scrollStart(lines, point.row),
2129
+ dx: point.col
2130
+ };
2131
+ },
2132
+ render({ item, focused, cursor, width, y, x, clip }) {
2133
+ const lines = wrapLines(item.value, width);
2134
+ const shown = Math.min(lines.length, 3);
2135
+ const point = cursor === void 0 ? null : locToPoint(item.value, width, cursor);
2136
+ const first = point === null ? 0 : scrollStart(lines.length, point.row);
2137
+ const parts = [];
2138
+ if (clip === 0) parts.push(/* @__PURE__ */ jsx(Box, {
2139
+ height: 1,
2140
+ children: /* @__PURE__ */ jsx(SelectableText, {
2141
+ y,
2142
+ col: x,
2143
+ text: item.label,
2144
+ color: focused ? void 0 : COLORS.dialogHintText
2145
+ })
2146
+ }, "label"));
2147
+ for (let index = 0; index < shown; index++) {
2148
+ if (1 + index < clip) continue;
2149
+ const currentY = y + 1 + index - clip;
2150
+ parts.push(/* @__PURE__ */ jsx(Box, {
2151
+ width,
2152
+ height: 1,
2153
+ backgroundColor: COLORS.dialogInputBackground,
2154
+ children: /* @__PURE__ */ jsx(SelectableText, {
2155
+ y: currentY,
2156
+ col: x,
2157
+ text: lines[first + index]
2158
+ })
2159
+ }, index));
2160
+ }
2161
+ parts.push(/* @__PURE__ */ jsx(Box, {
2162
+ height: 1,
2163
+ children: /* @__PURE__ */ jsx(Text, {
2164
+ color: focused && cursor !== void 0 ? caretNonceColor(cursor) : void 0,
2165
+ bold: focused && cursor !== void 0 ? caretNonceBold(cursor) : void 0,
2166
+ children: focused && cursor !== void 0 ? caretNonceText(cursor) : " "
2167
+ })
2168
+ }, "pad"));
2169
+ return /* @__PURE__ */ jsx(Box, {
2170
+ flexDirection: "column",
2171
+ children: parts
2172
+ });
2173
+ },
2174
+ onLeftRight(item, direction, api) {
2175
+ if (direction === -1 && api.cursor > 0) api.setCursor(api.cursor - 1);
2176
+ if (direction === 1 && api.cursor < item.value.length) api.setCursor(api.cursor + 1);
2177
+ return true;
2178
+ },
2179
+ onEnter(item) {
2180
+ if (item.onEnter !== void 0) {
2181
+ item.onEnter();
2182
+ return true;
2183
+ }
2184
+ return false;
2185
+ }
2186
+ }, { order: 500 });
2187
+ //#endregion
2188
+ //#region src/ui/widgets/select.tsx
2189
+ function carouselHit(item, width, localX) {
2190
+ if (item.options.length <= 1) return null;
2191
+ const block = selectBlock(width, item.value);
2192
+ if (localX >= block.blockStart && localX < block.blockStart + 3) return -1;
2193
+ if (localX >= block.blockStart + block.blockWidth - 3 && localX < block.blockStart + block.blockWidth) return 1;
2194
+ return null;
2195
+ }
2196
+ function paintSelect(paint) {
2197
+ const { item, width, y, x } = paint;
2198
+ const block = selectBlock(width, item.value);
2199
+ const hasArrows = item.options.length > 1;
2200
+ const pad = hasArrows ? block.leftPad : block.fullLeftPad;
2201
+ const inner = block.blockWidth - (hasArrows ? 6 : 0);
2202
+ const valueCol = x + block.blockStart + (hasArrows ? 3 : 0);
2203
+ const valueText = " ".repeat(pad) + block.text + " ".repeat(Math.max(0, inner - pad - textWidth(block.text)));
2204
+ const carousel = /* @__PURE__ */ jsxs(Box, {
2205
+ width,
2206
+ justifyContent: "space-between",
2207
+ children: [/* @__PURE__ */ jsx(SelectableText, {
2208
+ y,
2209
+ col: x,
2210
+ text: `${item.label}:`
2211
+ }), /* @__PURE__ */ jsxs(Box, {
2212
+ width: block.blockWidth,
2213
+ flexDirection: "row",
2214
+ children: [
2215
+ hasArrows && /* @__PURE__ */ jsx(Text, {
2216
+ color: COLORS.ink,
2217
+ backgroundColor: paint.pressed === "left" ? COLORS.carouselButtonPressedBg : COLORS.carouselButtonBg,
2218
+ children: ` ${glyphs.carouselLeft} `
2219
+ }),
2220
+ /* @__PURE__ */ jsx(SelectableText, {
2221
+ y,
2222
+ col: valueCol,
2223
+ text: valueText,
2224
+ color: paint.focused ? COLORS.carouselSelectedText : COLORS.ink,
2225
+ backgroundColor: COLORS.carouselCurrentBg
2226
+ }),
2227
+ hasArrows && /* @__PURE__ */ jsx(Text, {
2228
+ color: COLORS.ink,
2229
+ backgroundColor: paint.pressed === "right" ? COLORS.carouselButtonPressedBg : COLORS.carouselButtonBg,
2230
+ children: ` ${glyphs.carouselRight} `
2231
+ })
2232
+ ]
2233
+ })]
2234
+ });
2235
+ if (!item.spaced) return carousel;
2236
+ return /* @__PURE__ */ jsxs(Box, {
2237
+ flexDirection: "column",
2238
+ children: [paint.clip === 0 && carousel, /* @__PURE__ */ jsx(Box, {
2239
+ height: 1,
2240
+ children: /* @__PURE__ */ jsx(Text, { children: " " })
2241
+ })]
2242
+ });
2243
+ }
2244
+ function cycle(item, direction) {
2245
+ const index = Math.max(0, item.options.indexOf(item.value));
2246
+ const length = item.options.length;
2247
+ const next = item.options[(index + direction + length) % length];
2248
+ if (next !== void 0) item.onChange(next);
2249
+ }
2250
+ registerWidget("select", {
2251
+ selectable: true,
2252
+ height(item) {
2253
+ return item.spaced === true ? 2 : 1;
2254
+ },
2255
+ paintWidth(item) {
2256
+ return textWidth(item.label);
2257
+ },
2258
+ render: paintSelect,
2259
+ onLeftRight(item, direction) {
2260
+ if (item.options.length <= 1) return false;
2261
+ cycle(item, direction);
2262
+ return true;
2263
+ },
2264
+ onEnter(item, api) {
2265
+ if (item.onEnter !== void 0) {
2266
+ item.onEnter();
2267
+ return true;
2268
+ }
2269
+ api.navigate("down");
2270
+ return true;
2271
+ },
2272
+ onClick(item, hit, actions) {
2273
+ if (hit.localY !== 0) return false;
2274
+ const direction = carouselHit(item, hit.width, hit.localX);
2275
+ if (direction === null) return false;
2276
+ cycle(item, direction);
2277
+ actions.flash(direction === -1 ? "left" : "right");
2278
+ return true;
2279
+ }
2280
+ }, { order: 500 });
2281
+ //#endregion
2282
+ //#region src/ui/widgets/button.tsx
2283
+ function paintButton(item, paint) {
2284
+ const { width, y, x, focused } = paint;
2285
+ if (item.right !== void 0) {
2286
+ const right = truncate(item.right, Math.floor(width / 2));
2287
+ const leftWidth = Math.max(1, width - textWidth(right));
2288
+ const label = padToWidth(truncate(item.label, leftWidth), leftWidth);
2289
+ if (focused) return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(SelectableText, {
2290
+ y,
2291
+ col: x,
2292
+ text: label,
2293
+ inverse: true,
2294
+ color: item.rightColor
2295
+ }), /* @__PURE__ */ jsx(SelectableText, {
2296
+ y,
2297
+ col: x + leftWidth,
2298
+ text: right,
2299
+ inverse: true,
2300
+ color: item.rightColor
2301
+ })] });
2302
+ return /* @__PURE__ */ jsxs(Box, {
2303
+ width,
2304
+ justifyContent: "space-between",
2305
+ children: [/* @__PURE__ */ jsx(SelectableText, {
2306
+ y,
2307
+ col: x,
2308
+ text: item.label
2309
+ }), /* @__PURE__ */ jsx(SelectableText, {
2310
+ y,
2311
+ col: x + width - textWidth(right),
2312
+ text: right,
2313
+ color: item.rightColor ?? COLORS.dialogHintText
2314
+ })]
2315
+ });
2316
+ }
2317
+ if (focused) return /* @__PURE__ */ jsx(SelectableText, {
2318
+ y,
2319
+ col: x,
2320
+ text: padToWidth(truncate(item.label, width), width),
2321
+ inverse: true
2322
+ });
2323
+ return /* @__PURE__ */ jsx(SelectableText, {
2324
+ y,
2325
+ col: x,
2326
+ text: item.label
2327
+ });
2328
+ }
2329
+ registerWidget("button", {
2330
+ selectable: true,
2331
+ height() {
2332
+ return 1;
2333
+ },
2334
+ paintWidth(item) {
2335
+ return textWidth(item.label + (item.right ?? ""));
2336
+ },
2337
+ searchTexts(item, searchRight) {
2338
+ return searchRight && item.right !== void 0 ? [item.label, item.right] : [item.label];
2339
+ },
2340
+ render(paint) {
2341
+ return paintButton(paint.item, paint);
2342
+ },
2343
+ onEnter(item) {
2344
+ item.onPress();
2345
+ return true;
2346
+ },
2347
+ activate(item) {
2348
+ item.onPress();
2349
+ }
2350
+ }, { order: 500 });
2351
+ //#endregion
2352
+ //#region src/ui/widgets/checkbox.tsx
2353
+ function paintCheckbox(item, paint) {
2354
+ const { width, y, x, focused } = paint;
2355
+ const label = padToWidth(truncate(item.label, width - 2), width - 2);
2356
+ const check = item.checked ? ` ${glyphs.tick}` : " ";
2357
+ if (focused) return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(SelectableText, {
2358
+ y,
2359
+ col: x,
2360
+ text: label,
2361
+ inverse: true
2362
+ }), /* @__PURE__ */ jsx(SelectableText, {
2363
+ y,
2364
+ col: x + width - 2,
2365
+ text: check,
2366
+ inverse: true
2367
+ })] });
2368
+ return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(SelectableText, {
2369
+ y,
2370
+ col: x,
2371
+ text: label
2372
+ }), item.checked && /* @__PURE__ */ jsx(SelectableText, {
2373
+ y,
2374
+ col: x + width - 2,
2375
+ text: ` ${glyphs.tick}`,
2376
+ color: COLORS.success
2377
+ })] });
2378
+ }
2379
+ registerWidget("checkbox", {
2380
+ selectable: true,
2381
+ height() {
2382
+ return 1;
2383
+ },
2384
+ paintWidth(item) {
2385
+ return textWidth(item.label);
2386
+ },
2387
+ searchTexts(item) {
2388
+ return [item.label];
2389
+ },
2390
+ render(paint) {
2391
+ return paintCheckbox(paint.item, paint);
2392
+ },
2393
+ onEnter(item) {
2394
+ item.onConfirm();
2395
+ return true;
2396
+ },
2397
+ onSpace(item) {
2398
+ item.onToggle();
2399
+ },
2400
+ activate(item) {
2401
+ item.onConfirm();
2402
+ }
2403
+ }, { order: 500 });
2404
+ //#endregion
2405
+ //#region src/ui/widgets/search.tsx
2406
+ function searchTextOf(item) {
2407
+ return item.value === "" ? "Search" : item.value;
2408
+ }
2409
+ registerWidget("search", {
2410
+ selectable: true,
2411
+ editable: true,
2412
+ height() {
2413
+ return 2;
2414
+ },
2415
+ paintWidth(item) {
2416
+ return textWidth(searchTextOf(item));
2417
+ },
2418
+ onLeftRight(item, direction, api) {
2419
+ const next = Math.max(0, Math.min(item.value.length, api.cursor + direction));
2420
+ if (next === api.cursor) return false;
2421
+ api.setCursor(next);
2422
+ return true;
2423
+ },
2424
+ render({ item, cursor, width, y, x, clip }) {
2425
+ const isEmpty = item.value === "";
2426
+ const start = caretScrollStart(item.value, cursor ?? item.value.length, width);
2427
+ const text = isEmpty ? "Search" : truncate(item.value.slice(start), width);
2428
+ if (clip !== 0) return /* @__PURE__ */ jsx(Box, {
2429
+ flexDirection: "column",
2430
+ children: /* @__PURE__ */ jsx(Box, {
2431
+ height: 1,
2432
+ children: /* @__PURE__ */ jsx(Text, { children: " " })
2433
+ })
2434
+ });
2435
+ return /* @__PURE__ */ jsxs(Box, {
2436
+ flexDirection: "column",
2437
+ children: [/* @__PURE__ */ jsx(Box, {
2438
+ width,
2439
+ backgroundColor: COLORS.dialogInputBackground,
2440
+ children: /* @__PURE__ */ jsx(SelectableText, {
2441
+ y,
2442
+ col: x,
2443
+ text: padToWidth(text, width),
2444
+ color: isEmpty ? COLORS.dialogHintText : void 0
2445
+ })
2446
+ }), /* @__PURE__ */ jsx(Box, {
2447
+ height: 1,
2448
+ children: /* @__PURE__ */ jsx(Text, {
2449
+ color: cursor !== void 0 ? caretNonceColor(cursor) : void 0,
2450
+ bold: cursor !== void 0 ? caretNonceBold(cursor) : void 0,
2451
+ children: cursor !== void 0 ? caretNonceText(cursor) : " "
2452
+ })
2453
+ })]
2454
+ });
2455
+ }
2456
+ }, { order: 500 });
2457
+ //#endregion
2458
+ //#region src/ui/widgets/static.tsx
2459
+ registerWidget("static", {
2460
+ selectable: false,
2461
+ height(item, width) {
2462
+ return Math.max(1, wrapLines(item.label, Math.max(4, width)).length);
2463
+ },
2464
+ paintWidth(item) {
2465
+ return textWidth(item.label);
2466
+ },
2467
+ render({ item, width, y, x }) {
2468
+ const lines = item.label === "" ? [""] : wrapLines(item.label, Math.max(4, width));
2469
+ if (lines.length === 1) return /* @__PURE__ */ jsx(SelectableText, {
2470
+ y,
2471
+ col: x,
2472
+ text: item.label === "" ? " " : item.label,
2473
+ color: item.label === "" ? void 0 : COLORS.toolBodyText
2474
+ });
2475
+ return /* @__PURE__ */ jsx(Box, {
2476
+ flexDirection: "column",
2477
+ children: lines.map((line, index) => /* @__PURE__ */ jsx(SelectableText, {
2478
+ y: y + index,
2479
+ col: x,
2480
+ text: line === "" ? " " : line,
2481
+ color: line === "" ? void 0 : COLORS.toolBodyText
2482
+ }, index))
2483
+ });
2484
+ }
2485
+ }, { order: 500 });
2486
+ //#endregion
2487
+ //#region src/ui/dialog/dialog-item.tsx
2488
+ function renderRow(row, focused, contentWidth, baseY, left, pressed = null, subCol = 0, clip = 0, cursor) {
2489
+ let col = left;
2490
+ return /* @__PURE__ */ jsx(Box, {
2491
+ flexDirection: "row",
2492
+ children: row.items.map((item, index) => {
2493
+ const def = widgetOf(item.type);
2494
+ const next = def.render({
2495
+ item,
2496
+ focused,
2497
+ width: contentWidth,
2498
+ y: baseY,
2499
+ x: col,
2500
+ pressed,
2501
+ subCol,
2502
+ clip,
2503
+ cursor: focused ? cursor : void 0
2504
+ });
2505
+ col += def.paintWidth(item) + 1;
2506
+ return /* @__PURE__ */ jsxs(Box, {
2507
+ flexDirection: "row",
2508
+ children: [index > 0 && /* @__PURE__ */ jsx(Text, { children: " " }), next]
2509
+ }, index);
2510
+ })
2511
+ });
2512
+ }
2513
+ //#endregion
2514
+ //#region src/ui/dialog/use-dialog-input.ts
2515
+ function applyNavigation(live, direction, search, setters, centerScroll = false) {
2516
+ const next = moveFocus(live.rows, live.focus, direction);
2517
+ const clamped = clampFocus(live.rows, next, live.focusMinRow, live.focusMaxRow);
2518
+ setters.setFocus(clamped);
2519
+ const contentRow = Math.max(0, clamped.row - (search ? 1 : 0));
2520
+ setters.setScrollTop(centerScroll && (direction === "up" || direction === "down") ? listCenterScroll(live.contentRows, contentRow, live.viewportHeight, live.contentWidth, direction) : adjustScroll(live.contentRows, {
2521
+ row: contentRow,
2522
+ col: clamped.col
2523
+ }, live.scrollTop, live.viewportHeight, live.contentWidth));
2524
+ }
2525
+ function arrowFromRaw(input) {
2526
+ const match = /^(?:\[|O)(?:1;)?\d*(?::\d+)?([ABCD])$/.exec(input);
2527
+ if (match === null) return null;
2528
+ const letter = match[1];
2529
+ return letter === "A" ? "up" : letter === "B" ? "down" : letter === "C" ? "right" : "left";
2530
+ }
2531
+ function useDialogInput(options) {
2532
+ const { live, search, closeGuarded, onClose, onCtrlA, onCtrlD, onCtrlE, onActivity, requestSearch, setFocus, setScrollTop, setCursor } = options;
2533
+ const setters = {
2534
+ setFocus,
2535
+ setScrollTop
2536
+ };
2537
+ const centerScroll = options.centerScroll === true;
2538
+ usePaste((text) => {
2539
+ onActivity?.();
2540
+ const state = live.current;
2541
+ const current = focusedItem(state.rows[state.focus.row], state.focus.col);
2542
+ const textItem = current === void 0 ? null : asTextItem(current);
2543
+ if (textItem === null) return;
2544
+ const normalized = text.replace(/\r\n?/g, "\n");
2545
+ if (normalized === "") return;
2546
+ const next = editInsert({
2547
+ value: textItem.value,
2548
+ cursor: state.cursor
2549
+ }, normalized);
2550
+ textItem.onChange(next.value);
2551
+ state.value = next.value;
2552
+ state.cursor = next.cursor;
2553
+ setCursor(next.cursor);
2554
+ });
2555
+ useInput((input, key) => {
2556
+ if (isKeyConsumed()) return;
2557
+ const state = live.current;
2558
+ const liveCurrent = focusedItem(state.rows[state.focus.row], state.focus.col);
2559
+ const rawArrow = arrowFromRaw(input);
2560
+ const isUp = key.upArrow || rawArrow === "up";
2561
+ const isDown = key.downArrow || rawArrow === "down";
2562
+ const isLeft = key.leftArrow || rawArrow === "left";
2563
+ const isRight = key.rightArrow || rawArrow === "right";
2564
+ if (key.ctrl && input === "a" && onCtrlA !== void 0 && onCtrlA(liveCurrent)) return;
2565
+ if (key.ctrl && input === "d" && onCtrlD !== void 0 && onCtrlD(liveCurrent)) return;
2566
+ if (key.ctrl && input === "e" && onCtrlE !== void 0 && onCtrlE(liveCurrent)) return;
2567
+ onActivity?.();
2568
+ if (key.escape || key.ctrl && input === "c") {
2569
+ if (!(key.ctrl && closeGuarded)) onClose();
2570
+ return;
2571
+ }
2572
+ const def = liveCurrent === void 0 ? void 0 : widgetOf(liveCurrent.type);
2573
+ const keyApi = {
2574
+ cursor: state.cursor,
2575
+ subCol: state.focus.col,
2576
+ setCursor(next) {
2577
+ state.cursor = next;
2578
+ setCursor(next);
2579
+ },
2580
+ navigate(direction) {
2581
+ applyNavigation(state, direction, search, setters, centerScroll);
2582
+ }
2583
+ };
2584
+ if ((isLeft || isRight) && liveCurrent !== void 0 && def !== void 0) {
2585
+ if (def.onLeftRight?.(liveCurrent, isRight ? 1 : -1, keyApi) === true) return;
2586
+ }
2587
+ if (isUp || isDown) {
2588
+ applyNavigation(state, isUp ? "up" : "down", search, setters, centerScroll);
2589
+ return;
2590
+ }
2591
+ if (key.return) {
2592
+ if (liveCurrent === void 0 || def === void 0) return;
2593
+ if (!(def.onEnter?.(liveCurrent, keyApi) ?? false)) applyNavigation(state, "down", search, setters, centerScroll);
2594
+ return;
2595
+ }
2596
+ if (input === " " && liveCurrent !== void 0 && def?.onSpace !== void 0) {
2597
+ def.onSpace(liveCurrent);
2598
+ return;
2599
+ }
2600
+ const editingFormInput = liveCurrent !== void 0 && liveCurrent.type !== "search" && widgetOf(liveCurrent.type).editable === true;
2601
+ if (search && !editingFormInput) {
2602
+ const v = state.searchValue;
2603
+ const caret = state.focus.row === 0 ? Math.max(0, Math.min(state.cursor, v.length)) : v.length;
2604
+ if (isLeft || isRight) {
2605
+ const next = Math.max(0, Math.min(v.length, caret + (isRight ? 1 : -1)));
2606
+ if (next !== caret) {
2607
+ state.cursor = next;
2608
+ setCursor(next);
2609
+ }
2610
+ return;
2611
+ }
2612
+ const applyEdit = (next) => {
2613
+ if (next === null) return false;
2614
+ requestSearch(next.value);
2615
+ state.cursor = next.cursor;
2616
+ setCursor(next.cursor);
2617
+ return true;
2618
+ };
2619
+ if (key.backspace) {
2620
+ applyEdit(editBackspace({
2621
+ value: v,
2622
+ cursor: caret
2623
+ }));
2624
+ return;
2625
+ }
2626
+ if (key.delete) {
2627
+ applyEdit(editDelete({
2628
+ value: v,
2629
+ cursor: caret
2630
+ }));
2631
+ return;
2632
+ }
2633
+ if (input && !key.ctrl && !key.meta && !isMouseResidue(input)) {
2634
+ applyEdit(editInsert({
2635
+ value: v,
2636
+ cursor: caret
2637
+ }, input));
2638
+ return;
2639
+ }
2640
+ }
2641
+ if (def?.editable === true && liveCurrent !== void 0) {
2642
+ const textItem = asTextItem(liveCurrent);
2643
+ if (textItem === null) return;
2644
+ const apply = (next, trackCursor) => {
2645
+ if (next === null) return false;
2646
+ state.value = next.value;
2647
+ textItem.onChange(next.value);
2648
+ if (trackCursor) {
2649
+ state.cursor = next.cursor;
2650
+ setCursor(next.cursor);
2651
+ }
2652
+ return true;
2653
+ };
2654
+ if (key.backspace) apply(editBackspace({
2655
+ value: state.value,
2656
+ cursor: state.cursor
2657
+ }), true);
2658
+ else if (key.delete) apply(editDelete({
2659
+ value: state.value,
2660
+ cursor: state.cursor
2661
+ }), false);
2662
+ else if (input && !key.ctrl && !key.meta && !isMouseResidue(input)) apply(editInsert({
2663
+ value: state.value,
2664
+ cursor: state.cursor
2665
+ }, input), true);
2666
+ }
2667
+ });
2668
+ }
2669
+ //#endregion
2670
+ //#region src/ui/dialog/dialog.tsx
2671
+ const CloseGuardContext = createContext(false);
2672
+ function Dialog({ width, maxHeight, title, rows, footer, errors, onClose, search = false, searchRight = false, centerScroll = false, onCtrlA, onCtrlD, onCtrlE, onActivity, ref }) {
2673
+ const { stdout } = useStdout();
2674
+ const { setCursorPosition } = useCaret();
2675
+ const closeGuarded = useContext(CloseGuardContext);
2676
+ const columns = stdout?.columns ?? 80;
2677
+ const totalRows = stdout?.rows ?? 24;
2678
+ const [scrollTop, setScrollTop] = useState(0);
2679
+ const [cursor, setCursor] = useState(0);
2680
+ const cursorSyncRef = useRef({
2681
+ row: -1,
2682
+ col: -1,
2683
+ type: void 0
2684
+ });
2685
+ const [searchValue, setSearchValue] = useState("");
2686
+ const [carouselPress, setCarouselPress] = useState(null);
2687
+ const carouselPressTimer = useRef(void 0);
2688
+ const [errorScrollTop, setErrorScrollTop] = useState(0);
2689
+ const handleSearch = (value) => {
2690
+ setSearchValue(value);
2691
+ setScrollTop(0);
2692
+ };
2693
+ const searchRow = { items: [{
2694
+ type: "search",
2695
+ value: searchValue,
2696
+ onChange: handleSearch
2697
+ }] };
2698
+ const filteredRows = search ? filterRowsWithHeaders(rows, searchValue, searchRight) : rows;
2699
+ const displayRows = search ? [searchRow, ...filteredRows] : rows;
2700
+ const contentRows = search ? filteredRows : rows;
2701
+ const [focus, setFocus] = useState(() => ({
2702
+ row: snapRow(search ? [searchRow, ...rows] : rows, search ? 1 : 0),
2703
+ col: 0
2704
+ }));
2705
+ const widthCells = width <= 1 ? Math.floor(columns * width) : width;
2706
+ const windowWidth = Math.min(Math.max(1, widthCells), columns);
2707
+ const contentWidth = Math.max(1, windowWidth - 4);
2708
+ const fixedHeight = search ? rowHeight(searchRow, contentWidth) : 0;
2709
+ const focusMinRow = search ? 1 : 0;
2710
+ const focusMaxRow = Math.max(0, displayRows.length - 1);
2711
+ useEffect(() => {
2712
+ setFocus((current) => {
2713
+ const next = clampFocus(displayRows, current, focusMinRow, focusMaxRow);
2714
+ return next.row === current.row && next.col === current.col ? current : next;
2715
+ });
2716
+ }, [
2717
+ displayRows,
2718
+ focusMinRow,
2719
+ focusMaxRow
2720
+ ]);
2721
+ const errorKey = (errors ?? []).map((line) => line.text).join("\n");
2722
+ useEffect(() => {
2723
+ setErrorScrollTop(0);
2724
+ }, [errorKey]);
2725
+ const titleLines = title === void 0 ? 0 : 2;
2726
+ const footerRows = wrapFooter(footer, contentWidth);
2727
+ const extraHeight = footerRows.length > 0 ? 1 + footerRows.length : 0;
2728
+ const desired = displayRows.reduce((sum, row) => sum + rowHeight(row, contentWidth), 0) + titleLines + extraHeight + 2;
2729
+ const maxRows = Math.max(1, maxHeight <= 1 ? Math.floor(totalRows * maxHeight) : maxHeight);
2730
+ const windowHeight = Math.min(desired, maxRows, totalRows);
2731
+ const contentHeight = Math.max(1, windowHeight - 2 - titleLines - extraHeight);
2732
+ const viewportHeight = Math.max(1, contentHeight - fixedHeight);
2733
+ const top = Math.max(0, Math.floor((totalRows - windowHeight) / 2));
2734
+ const left = Math.max(0, Math.floor((columns - windowWidth) / 2));
2735
+ const frameLeft = 2;
2736
+ const errorRows = wrapStatusLines(errors, contentWidth);
2737
+ const availableBelow = Math.max(0, totalRows - (top + windowHeight));
2738
+ const errorVisible = Math.max(0, Math.min(3, errorRows.length, availableBelow - 1));
2739
+ const maxErrorScroll = Math.max(0, errorRows.length - errorVisible);
2740
+ const errorScroll = Math.min(errorScrollTop, maxErrorScroll);
2741
+ const headerBottom = 1 + titleLines;
2742
+ const contentTop = headerBottom + fixedHeight;
2743
+ const safeFocus = {
2744
+ row: Math.min(Math.max(focus.row, 0), focusMaxRow),
2745
+ col: Math.min(Math.max(focus.col, 0), selectableSpan(displayRows[focus.row]))
2746
+ };
2747
+ const contentRowsHeight = contentRows.reduce((sum, row) => sum + rowHeight(row, contentWidth), 0);
2748
+ const maxScroll = Math.max(0, contentRowsHeight - viewportHeight);
2749
+ const scroll = adjustScroll(contentRows, {
2750
+ row: Math.max(0, safeFocus.row - (search ? 1 : 0)),
2751
+ col: 0
2752
+ }, Math.min(scrollTop, maxScroll), viewportHeight, contentWidth);
2753
+ const live = useRef({
2754
+ rows: displayRows,
2755
+ contentRows,
2756
+ focus,
2757
+ scrollTop: scroll,
2758
+ cursor,
2759
+ value: "",
2760
+ searchValue,
2761
+ viewportHeight,
2762
+ focusMinRow,
2763
+ focusMaxRow,
2764
+ contentWidth,
2765
+ top,
2766
+ windowHeight
2767
+ });
2768
+ live.current.rows = displayRows;
2769
+ live.current.contentRows = contentRows;
2770
+ live.current.focus = focus;
2771
+ live.current.scrollTop = scroll;
2772
+ live.current.searchValue = searchValue;
2773
+ live.current.viewportHeight = viewportHeight;
2774
+ live.current.focusMinRow = focusMinRow;
2775
+ live.current.focusMaxRow = focusMaxRow;
2776
+ live.current.contentWidth = contentWidth;
2777
+ live.current.top = top;
2778
+ live.current.windowHeight = windowHeight;
2779
+ const errorLive = useRef({
2780
+ top: 0,
2781
+ height: 0,
2782
+ maxScroll: 0
2783
+ });
2784
+ errorLive.current = {
2785
+ top: top + windowHeight,
2786
+ height: errorVisible + 1,
2787
+ maxScroll: maxErrorScroll
2788
+ };
2789
+ const current = focusedItem(displayRows[safeFocus.row], safeFocus.col);
2790
+ const currentText = current === void 0 ? null : asTextItem(current);
2791
+ live.current.value = currentText?.value ?? "";
2792
+ const effectiveCursor = cursorSyncRef.current.row === safeFocus.row && cursorSyncRef.current.col === safeFocus.col && cursorSyncRef.current.type === current?.type || currentText === null || currentText.type === "search" ? cursor : currentText.value.length;
2793
+ live.current.cursor = effectiveCursor;
2794
+ useEffect(() => {
2795
+ cursorSyncRef.current = {
2796
+ row: safeFocus.row,
2797
+ col: safeFocus.col,
2798
+ type: current?.type
2799
+ };
2800
+ if (currentText !== null && currentText.type !== "search") setCursor(currentText.value.length);
2801
+ }, [
2802
+ current?.type,
2803
+ safeFocus.row,
2804
+ safeFocus.col,
2805
+ displayRows.length
2806
+ ]);
2807
+ const editingFormInput = current !== void 0 && current.type !== "search" && asTextItem(current) !== null;
2808
+ if (search && !editingFormInput) {
2809
+ const caret = Math.min(Math.max(0, cursor), searchValue.length);
2810
+ const start = caretScrollStart(searchValue, caret, contentWidth);
2811
+ setCursorPosition({
2812
+ x: left + frameLeft + textWidth(searchValue.slice(start, caret)),
2813
+ y: top + headerBottom
2814
+ });
2815
+ } else {
2816
+ const caretOffset = current === void 0 ? void 0 : widgetOf(current.type).caret?.(current, effectiveCursor, contentWidth);
2817
+ if (caretOffset === void 0) setCursorPosition(void 0);
2818
+ else {
2819
+ const rowOffset = rowTopOffset(contentRows, Math.max(0, safeFocus.row - (search ? 1 : 0)), contentWidth);
2820
+ setCursorPosition({
2821
+ x: left + frameLeft + caretOffset.dx,
2822
+ y: top + contentTop + rowOffset + caretOffset.dy - scroll
2823
+ });
2824
+ }
2825
+ }
2826
+ useEffect(() => {
2827
+ if (search && !editingFormInput) {
2828
+ writeCursorShape("beam");
2829
+ return;
2830
+ }
2831
+ writeCursorShape(current !== void 0 && widgetOf(current.type).caret !== void 0 ? "beam" : "block");
2832
+ }, [
2833
+ current?.type,
2834
+ safeFocus.row,
2835
+ safeFocus.col
2836
+ ]);
2837
+ useEffect(() => {
2838
+ return () => {
2839
+ writeCursorShape("reset");
2840
+ clearTimeout(carouselPressTimer.current);
2841
+ };
2842
+ }, []);
2843
+ useDialogInput({
2844
+ live,
2845
+ search,
2846
+ closeGuarded,
2847
+ centerScroll,
2848
+ onClose,
2849
+ onCtrlA,
2850
+ onCtrlD,
2851
+ onCtrlE,
2852
+ onActivity,
2853
+ requestSearch: handleSearch,
2854
+ setFocus,
2855
+ setScrollTop,
2856
+ setCursor
2857
+ });
2858
+ const flashCarousel = (row, side) => {
2859
+ setCarouselPress({
2860
+ row,
2861
+ side
2862
+ });
2863
+ clearTimeout(carouselPressTimer.current);
2864
+ carouselPressTimer.current = setTimeout(() => setCarouselPress(null), 120);
2865
+ };
2866
+ useImperativeHandle(ref, () => ({
2867
+ wheelAt(y, dir) {
2868
+ onActivity?.();
2869
+ if (y < live.current.top || y >= live.current.top + live.current.windowHeight) {
2870
+ const error = errorLive.current;
2871
+ if (error.height > 1 && y >= error.top && y < error.top + error.height) {
2872
+ setErrorScrollTop((current) => Math.max(0, Math.min(current + dir, error.maxScroll)));
2873
+ return true;
2874
+ }
2875
+ return false;
2876
+ }
2877
+ applyNavigation(live.current, dir === -1 ? "up" : "down", search, {
2878
+ setFocus,
2879
+ setScrollTop
2880
+ }, centerScroll);
2881
+ return true;
2882
+ },
2883
+ clickAt(y, x) {
2884
+ onActivity?.();
2885
+ if (y < top || y >= top + windowHeight || x < left || x >= left + windowWidth) return;
2886
+ if (search) {
2887
+ const searchTop = top + headerBottom;
2888
+ if (y >= searchTop && y < searchTop + fixedHeight) {
2889
+ const clickCaret = safeFocus.row === 0 ? Math.min(cursor, searchValue.length) : searchValue.length;
2890
+ const start = caretScrollStart(searchValue, clickCaret, contentWidth);
2891
+ const visible = truncate(searchValue.slice(start), contentWidth);
2892
+ const col = Math.max(0, Math.min(x - (left + frameLeft), textWidth(visible)));
2893
+ setFocus({
2894
+ row: 0,
2895
+ col: 0
2896
+ });
2897
+ setCursor(Math.min(searchValue.length, start + colToCharIndex(visible, col)));
2898
+ return;
2899
+ }
2900
+ }
2901
+ const rowIndex = hitRowIndex(y, top + fixedHeight, titleLines, contentRows, scroll, contentWidth);
2902
+ if (rowIndex === null) return;
2903
+ const rowSpec = contentRows[rowIndex];
2904
+ if (!isSelectableRow(rowSpec)) return;
2905
+ const displayRow = Math.min(Math.max(rowIndex + (search ? 1 : 0), focusMinRow), focusMaxRow);
2906
+ const rowOffset = rowTopOffset(contentRows, rowIndex, contentWidth);
2907
+ const hit = {
2908
+ localX: x - (left + frameLeft),
2909
+ localY: y - (top + contentTop + rowOffset - scroll),
2910
+ width: contentWidth
2911
+ };
2912
+ const clickActions = {
2913
+ focus: (col) => setFocus({
2914
+ row: displayRow,
2915
+ col
2916
+ }),
2917
+ flash: (side) => flashCarousel(rowIndex, side)
2918
+ };
2919
+ for (const item of rowSpec?.items ?? []) if (widgetOf(item.type).onClick?.(item, hit, clickActions) === true) return;
2920
+ if (safeFocus.row === displayRow && rowSpec !== void 0) {
2921
+ const first = rowSpec.items[0];
2922
+ if (first !== void 0) {
2923
+ const activate = widgetOf(first.type).activate;
2924
+ if (activate !== void 0) {
2925
+ activate(first);
2926
+ return;
2927
+ }
2928
+ }
2929
+ }
2930
+ setFocus({
2931
+ row: displayRow,
2932
+ col: 0
2933
+ });
2934
+ const contentRow = Math.max(0, Math.min(rowIndex, contentRows.length - 1));
2935
+ setScrollTop(adjustScroll(contentRows, {
2936
+ row: contentRow,
2937
+ col: 0
2938
+ }, scroll, viewportHeight, contentWidth));
2939
+ }
2940
+ }));
2941
+ const visibleRows = [];
2942
+ let offset = 0;
2943
+ for (let i = 0; i < contentRows.length; i++) {
2944
+ const height = rowHeight(contentRows[i], contentWidth);
2945
+ if (offset + height <= scroll) {
2946
+ offset += height;
2947
+ continue;
2948
+ }
2949
+ if (offset >= scroll + viewportHeight) break;
2950
+ const clip = Math.max(0, scroll - offset);
2951
+ const rel = Math.max(0, offset - scroll);
2952
+ const baseY = contentTop + rel;
2953
+ const focused = safeFocus.row === i + (search ? 1 : 0);
2954
+ visibleRows.push(/* @__PURE__ */ jsx(Box, {
2955
+ position: "absolute",
2956
+ top: rel,
2957
+ left: 0,
2958
+ width: contentWidth,
2959
+ children: renderRow(contentRows[i], focused, contentWidth, baseY, frameLeft, carouselPress?.row === i ? carouselPress.side : null, focused ? safeFocus.col : 0, clip, focused ? effectiveCursor : void 0)
2960
+ }, `row-${i}`));
2961
+ offset += height;
2962
+ }
2963
+ return /* @__PURE__ */ jsxs(Region, {
2964
+ x: left,
2965
+ y: top,
2966
+ children: [/* @__PURE__ */ jsxs(Box, {
2967
+ position: "absolute",
2968
+ top,
2969
+ left,
2970
+ width: windowWidth,
2971
+ height: windowHeight,
2972
+ flexDirection: "column",
2973
+ backgroundColor: COLORS.dialogBackground,
2974
+ paddingLeft: 2,
2975
+ paddingRight: 2,
2976
+ paddingTop: 1,
2977
+ paddingBottom: 1,
2978
+ children: [
2979
+ title !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Box, {
2980
+ justifyContent: "center",
2981
+ children: /* @__PURE__ */ jsx(SelectableText, {
2982
+ y: 1,
2983
+ col: frameLeft + Math.max(0, Math.floor((contentWidth - textWidth(title)) / 2)),
2984
+ text: title
2985
+ })
2986
+ }), /* @__PURE__ */ jsx(Box, {
2987
+ height: 1,
2988
+ children: /* @__PURE__ */ jsx(Text, { children: " " })
2989
+ })] }),
2990
+ /* @__PURE__ */ jsxs(Box, {
2991
+ flexDirection: "column",
2992
+ children: [search && /* @__PURE__ */ jsx(Box, {
2993
+ flexDirection: "column",
2994
+ children: renderRow(searchRow, safeFocus.row === 0, contentWidth, headerBottom, frameLeft, null, 0, 0, cursor)
2995
+ }), /* @__PURE__ */ jsx(Box, {
2996
+ flexDirection: "column",
2997
+ height: viewportHeight,
2998
+ overflow: "hidden",
2999
+ children: visibleRows
3000
+ })]
3001
+ }),
3002
+ footerRows.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Box, {
3003
+ height: 1,
3004
+ children: /* @__PURE__ */ jsx(Text, { children: " " })
3005
+ }), footerRows.map((line, index) => /* @__PURE__ */ jsx(SelectableText, {
3006
+ y: windowHeight - footerRows.length + index - 1,
3007
+ col: frameLeft,
3008
+ text: line.text,
3009
+ color: line.color
3010
+ }, index))] })
3011
+ ]
3012
+ }), errorVisible > 0 && /* @__PURE__ */ jsx(Box, {
3013
+ position: "absolute",
3014
+ top: top + windowHeight,
3015
+ left,
3016
+ width: windowWidth,
3017
+ height: errorVisible + 1,
3018
+ flexDirection: "column",
3019
+ backgroundColor: COLORS.dialogBackground,
3020
+ paddingLeft: 2,
3021
+ paddingRight: 2,
3022
+ paddingBottom: 1,
3023
+ children: errorRows.slice(errorScroll, errorScroll + errorVisible).map((line, index) => /* @__PURE__ */ jsx(SelectableText, {
3024
+ y: windowHeight + index,
3025
+ col: frameLeft,
3026
+ text: line.text,
3027
+ color: line.color
3028
+ }, errorScroll + index))
3029
+ })]
3030
+ });
3031
+ }
3032
+ //#endregion
3033
+ //#region src/ui/panels/surface.tsx
3034
+ /**
3035
+ * Build a key-hint row: keys in the panel key color, action words in the
3036
+ * muted description color ("enter select esc close").
3037
+ */
3038
+ function panelLegend(hints, options = {}) {
3039
+ const keyStyle = { color: options.keyColor ?? COLORS.panelKeyText };
3040
+ const descriptionStyle = { color: options.descriptionColor ?? COLORS.toolBodyText };
3041
+ const parts = [];
3042
+ if (options.prefix !== void 0 && options.prefix !== "") parts.push({
3043
+ text: options.prefix,
3044
+ style: descriptionStyle
3045
+ });
3046
+ for (const [index, hint] of hints.entries()) {
3047
+ if (index > 0 || parts.length > 0) parts.push({
3048
+ text: " ",
3049
+ style: descriptionStyle
3050
+ });
3051
+ parts.push({
3052
+ text: hint.key,
3053
+ style: keyStyle
3054
+ });
3055
+ parts.push({
3056
+ text: " ",
3057
+ style: descriptionStyle
3058
+ });
3059
+ parts.push({
3060
+ text: hint.description,
3061
+ style: descriptionStyle
3062
+ });
3063
+ }
3064
+ return mergeRuns(parts);
3065
+ }
3066
+ /**
3067
+ * Shared panel chrome: a floating block anchored above the status line with
3068
+ * half-block caps, the panel background, and per-row text or interactive
3069
+ * content. Builtin panels and plugin panels built through dshtui/dialog
3070
+ * share this shell so every interaction panel looks the same.
3071
+ *
3072
+ * Selection pieces span the row text only, never the pill padding, so
3073
+ * dragging over blank panel area selects nothing (same rule as messages).
3074
+ */
3075
+ function PanelSurface({ columns, rows, body, bodyStart, background, blockWidth }) {
3076
+ return /* @__PURE__ */ jsx(Region, { children: /* @__PURE__ */ jsxs(Box, {
3077
+ position: "absolute",
3078
+ top: 0,
3079
+ left: 0,
3080
+ width: columns,
3081
+ height: rows,
3082
+ children: [
3083
+ /* @__PURE__ */ jsx(Box, {
3084
+ position: "absolute",
3085
+ top: bodyStart - 1,
3086
+ left: 2,
3087
+ width: blockWidth,
3088
+ children: /* @__PURE__ */ jsx(CapText, {
3089
+ background,
3090
+ top: true,
3091
+ width: blockWidth
3092
+ })
3093
+ }),
3094
+ body.map((row, index) => {
3095
+ const isEmpty = row.segments.every((segment) => segment.text.trim() === "");
3096
+ return /* @__PURE__ */ jsx(Box, {
3097
+ position: "absolute",
3098
+ top: bodyStart + index,
3099
+ left: 2,
3100
+ width: blockWidth,
3101
+ paddingLeft: 2,
3102
+ paddingRight: 2,
3103
+ backgroundColor: background,
3104
+ children: row.content !== void 0 ? row.content : isEmpty ? /* @__PURE__ */ jsx(Text, { children: " " }) : /* @__PURE__ */ jsx(SelectableText, {
3105
+ y: bodyStart + index,
3106
+ col: 4,
3107
+ segments: row.segments
3108
+ })
3109
+ }, `panel-row-${index}`);
3110
+ }),
3111
+ /* @__PURE__ */ jsx(Box, {
3112
+ position: "absolute",
3113
+ top: bodyStart + body.length,
3114
+ left: 2,
3115
+ width: blockWidth,
3116
+ children: /* @__PURE__ */ jsx(CapText, {
3117
+ background,
3118
+ top: false,
3119
+ width: blockWidth
3120
+ })
3121
+ })
3122
+ ]
3123
+ }) });
3124
+ }
3125
+ //#endregion
3126
+ export { errorText as $, SelectableText as A, setColorLevel as At, inputFrameTop as B, caretNonceText as C, registerPalette as Ct, markKeymapConsumed as D, themeMode as Dt, isKeyConsumed as E, subscribePalettes as Et, messageBackgroundWidth as F, warn as Ft, wrapSegments as G, glyphs as H, panelAnchorRow as I, clampFocusRow as J, chromeSelectionText as K, panelContains as L, Region as M, configureLogs as Mt, hintSpan as N, error as Nt, createMouseController as O, keyedRegistry as Ot, inputContentContains as P, installCrashHandlers as Pt, colToCharIndex as Q, scrollbarColumn as R, caretNonceColor as S, permissionModeInfo as St, beginKeyEvent as T, setThemeMode as Tt, mergeRuns as U, inputStatusRow as V, segmentsKey as W, toScreenSelection as X, selectedRange as Y, charWidth as Z, editDeleteWordRight as _, releaseFields as _t, DIALOG_MAX_HEIGHT as a, wrapLines as at, writeCursorShape as b, COLORS as bt, asTextItem as c, fieldExpandOf as ct, editCursorLeft as d, hasFieldChar as dt, lineBreaks as et, editCursorRight as f, hasFieldSlots as ft, editDeleteWordLeft as g, registerFieldKind as gt, editDelete as h, linesChipLabel as ht, Dialog as i, truncate as it, SelectionContext as j, env as jt, isMouseResidue as k, colorLevel as kt, registerWidget as l, fieldSlotOf as lt, editCursorWordRight as m, isFieldChar as mt, panelLegend as n, segmentGraphemes as nt, rowHeight as o, allocateField as ot, editCursorWordLeft as p, imageChipLabel as pt, sliceByColumns as q, CloseGuardContext as r, textWidth as rt, wrapStatusLines as s, charactersChipLabel as st, PanelSurface as t, locToPoint as tt, editBackspace as u, fieldStyleOf as ut, editInsert as v, releaseUnreferenced as vt, useCaret as w, replacePalettes as wt, caretNonceBold as x, paletteColor as xt, CapText as y, specialFieldFactory as yt, hintBlockTop as z };