@elaraai/e3-ui-cli 1.0.72 → 1.0.74

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 (220) hide show
  1. package/README.md +46 -21
  2. package/bin/e3-ui.mjs +6 -0
  3. package/dist/app/assets/{engine-BKZbmXkC-DmcCFcnb.js → engine-Bm-cIDd_-DwUdGcTZ.js} +1 -1
  4. package/dist/app/assets/{index-BJNWHnRv.js → index-mj-YVVN1.js} +91 -91
  5. package/dist/app/index.html +1 -1
  6. package/dist/cli.js +36 -5
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/tui.d.ts +84 -0
  9. package/dist/commands/tui.d.ts.map +1 -0
  10. package/dist/commands/tui.js +89 -0
  11. package/dist/commands/tui.js.map +1 -0
  12. package/dist/tui/api.d.ts +108 -0
  13. package/dist/tui/api.d.ts.map +1 -0
  14. package/dist/tui/api.js +130 -0
  15. package/dist/tui/api.js.map +1 -0
  16. package/dist/tui/app.d.ts +23 -0
  17. package/dist/tui/app.d.ts.map +1 -0
  18. package/dist/tui/app.js +244 -0
  19. package/dist/tui/app.js.map +1 -0
  20. package/dist/tui/controller.d.ts +116 -0
  21. package/dist/tui/controller.d.ts.map +1 -0
  22. package/dist/tui/controller.js +644 -0
  23. package/dist/tui/controller.js.map +1 -0
  24. package/dist/tui/data/dataflow.d.ts +68 -0
  25. package/dist/tui/data/dataflow.d.ts.map +1 -0
  26. package/dist/tui/data/dataflow.js +137 -0
  27. package/dist/tui/data/dataflow.js.map +1 -0
  28. package/dist/tui/data/dataset.d.ts +80 -0
  29. package/dist/tui/data/dataset.d.ts.map +1 -0
  30. package/dist/tui/data/dataset.js +297 -0
  31. package/dist/tui/data/dataset.js.map +1 -0
  32. package/dist/tui/data/edit-buffer.d.ts +74 -0
  33. package/dist/tui/data/edit-buffer.d.ts.map +1 -0
  34. package/dist/tui/data/edit-buffer.js +120 -0
  35. package/dist/tui/data/edit-buffer.js.map +1 -0
  36. package/dist/tui/data/feeds.d.ts +45 -0
  37. package/dist/tui/data/feeds.d.ts.map +1 -0
  38. package/dist/tui/data/feeds.js +318 -0
  39. package/dist/tui/data/feeds.js.map +1 -0
  40. package/dist/tui/data/logs.d.ts +68 -0
  41. package/dist/tui/data/logs.d.ts.map +1 -0
  42. package/dist/tui/data/logs.js +134 -0
  43. package/dist/tui/data/logs.js.map +1 -0
  44. package/dist/tui/input/commands.d.ts +159 -0
  45. package/dist/tui/input/commands.d.ts.map +1 -0
  46. package/dist/tui/input/commands.js +259 -0
  47. package/dist/tui/input/commands.js.map +1 -0
  48. package/dist/tui/input/completion.d.ts +63 -0
  49. package/dist/tui/input/completion.d.ts.map +1 -0
  50. package/dist/tui/input/completion.js +128 -0
  51. package/dist/tui/input/completion.js.map +1 -0
  52. package/dist/tui/input/keymap.d.ts +153 -0
  53. package/dist/tui/input/keymap.d.ts.map +1 -0
  54. package/dist/tui/input/keymap.js +206 -0
  55. package/dist/tui/input/keymap.js.map +1 -0
  56. package/dist/tui/input/mouse.d.ts +52 -0
  57. package/dist/tui/input/mouse.d.ts.map +1 -0
  58. package/dist/tui/input/mouse.js +55 -0
  59. package/dist/tui/input/mouse.js.map +1 -0
  60. package/dist/tui/lifecycle.d.ts +61 -0
  61. package/dist/tui/lifecycle.d.ts.map +1 -0
  62. package/dist/tui/lifecycle.js +99 -0
  63. package/dist/tui/lifecycle.js.map +1 -0
  64. package/dist/tui/model/catalogue.d.ts +28 -0
  65. package/dist/tui/model/catalogue.d.ts.map +1 -0
  66. package/dist/tui/model/catalogue.js +94 -0
  67. package/dist/tui/model/catalogue.js.map +1 -0
  68. package/dist/tui/model/help.d.ts +37 -0
  69. package/dist/tui/model/help.d.ts.map +1 -0
  70. package/dist/tui/model/help.js +210 -0
  71. package/dist/tui/model/help.js.map +1 -0
  72. package/dist/tui/model/index.d.ts +42 -0
  73. package/dist/tui/model/index.d.ts.map +1 -0
  74. package/dist/tui/model/index.js +50 -0
  75. package/dist/tui/model/index.js.map +1 -0
  76. package/dist/tui/model/status.d.ts +88 -0
  77. package/dist/tui/model/status.d.ts.map +1 -0
  78. package/dist/tui/model/status.js +137 -0
  79. package/dist/tui/model/status.js.map +1 -0
  80. package/dist/tui/model/tree.d.ts +126 -0
  81. package/dist/tui/model/tree.d.ts.map +1 -0
  82. package/dist/tui/model/tree.js +217 -0
  83. package/dist/tui/model/tree.js.map +1 -0
  84. package/dist/tui/model/types.d.ts +21 -0
  85. package/dist/tui/model/types.d.ts.map +1 -0
  86. package/dist/tui/model/types.js +46 -0
  87. package/dist/tui/model/types.js.map +1 -0
  88. package/dist/tui/render/glyphs.d.ts +122 -0
  89. package/dist/tui/render/glyphs.d.ts.map +1 -0
  90. package/dist/tui/render/glyphs.js +122 -0
  91. package/dist/tui/render/glyphs.js.map +1 -0
  92. package/dist/tui/render/layout.d.ts +145 -0
  93. package/dist/tui/render/layout.d.ts.map +1 -0
  94. package/dist/tui/render/layout.js +209 -0
  95. package/dist/tui/render/layout.js.map +1 -0
  96. package/dist/tui/render/text.d.ts +153 -0
  97. package/dist/tui/render/text.d.ts.map +1 -0
  98. package/dist/tui/render/text.js +320 -0
  99. package/dist/tui/render/text.js.map +1 -0
  100. package/dist/tui/render/theme.d.ts +88 -0
  101. package/dist/tui/render/theme.d.ts.map +1 -0
  102. package/dist/tui/render/theme.js +145 -0
  103. package/dist/tui/render/theme.js.map +1 -0
  104. package/dist/tui/session.d.ts +78 -0
  105. package/dist/tui/session.d.ts.map +1 -0
  106. package/dist/tui/session.js +237 -0
  107. package/dist/tui/session.js.map +1 -0
  108. package/dist/tui/state/actions.d.ts +584 -0
  109. package/dist/tui/state/actions.d.ts.map +1 -0
  110. package/dist/tui/state/actions.js +62 -0
  111. package/dist/tui/state/actions.js.map +1 -0
  112. package/dist/tui/state/persist.d.ts +114 -0
  113. package/dist/tui/state/persist.d.ts.map +1 -0
  114. package/dist/tui/state/persist.js +187 -0
  115. package/dist/tui/state/persist.js.map +1 -0
  116. package/dist/tui/state/poll.d.ts +99 -0
  117. package/dist/tui/state/poll.d.ts.map +1 -0
  118. package/dist/tui/state/poll.js +160 -0
  119. package/dist/tui/state/poll.js.map +1 -0
  120. package/dist/tui/state/reducer.d.ts +16 -0
  121. package/dist/tui/state/reducer.d.ts.map +1 -0
  122. package/dist/tui/state/reducer.js +363 -0
  123. package/dist/tui/state/reducer.js.map +1 -0
  124. package/dist/tui/state/store.d.ts +37 -0
  125. package/dist/tui/state/store.d.ts.map +1 -0
  126. package/dist/tui/state/store.js +84 -0
  127. package/dist/tui/state/store.js.map +1 -0
  128. package/dist/tui/suspend.d.ts +17 -0
  129. package/dist/tui/suspend.d.ts.map +1 -0
  130. package/dist/tui/suspend.js +33 -0
  131. package/dist/tui/suspend.js.map +1 -0
  132. package/dist/tui/ui/App.d.ts +44 -0
  133. package/dist/tui/ui/App.d.ts.map +1 -0
  134. package/dist/tui/ui/App.js +105 -0
  135. package/dist/tui/ui/App.js.map +1 -0
  136. package/dist/tui/ui/frame.d.ts +86 -0
  137. package/dist/tui/ui/frame.d.ts.map +1 -0
  138. package/dist/tui/ui/frame.js +42 -0
  139. package/dist/tui/ui/frame.js.map +1 -0
  140. package/dist/tui/ui/lines.d.ts +92 -0
  141. package/dist/tui/ui/lines.d.ts.map +1 -0
  142. package/dist/tui/ui/lines.js +94 -0
  143. package/dist/tui/ui/lines.js.map +1 -0
  144. package/dist/tui/ui/shell/chrome.d.ts +61 -0
  145. package/dist/tui/ui/shell/chrome.d.ts.map +1 -0
  146. package/dist/tui/ui/shell/chrome.js +272 -0
  147. package/dist/tui/ui/shell/chrome.js.map +1 -0
  148. package/dist/tui/ui/shell/widgets.d.ts +116 -0
  149. package/dist/tui/ui/shell/widgets.d.ts.map +1 -0
  150. package/dist/tui/ui/shell/widgets.js +183 -0
  151. package/dist/tui/ui/shell/widgets.js.map +1 -0
  152. package/dist/tui/ui/views/about.d.ts +20 -0
  153. package/dist/tui/ui/views/about.d.ts.map +1 -0
  154. package/dist/tui/ui/views/about.js +40 -0
  155. package/dist/tui/ui/views/about.js.map +1 -0
  156. package/dist/tui/ui/views/all.d.ts +17 -0
  157. package/dist/tui/ui/views/all.d.ts.map +1 -0
  158. package/dist/tui/ui/views/all.js +17 -0
  159. package/dist/tui/ui/views/all.js.map +1 -0
  160. package/dist/tui/ui/views/dashboard.d.ts +211 -0
  161. package/dist/tui/ui/views/dashboard.d.ts.map +1 -0
  162. package/dist/tui/ui/views/dashboard.js +651 -0
  163. package/dist/tui/ui/views/dashboard.js.map +1 -0
  164. package/dist/tui/ui/views/help.d.ts +17 -0
  165. package/dist/tui/ui/views/help.d.ts.map +1 -0
  166. package/dist/tui/ui/views/help.js +65 -0
  167. package/dist/tui/ui/views/help.js.map +1 -0
  168. package/dist/tui/ui/views/index.d.ts +42 -0
  169. package/dist/tui/ui/views/index.d.ts.map +1 -0
  170. package/dist/tui/ui/views/index.js +42 -0
  171. package/dist/tui/ui/views/index.js.map +1 -0
  172. package/dist/tui/ui/views/input.d.ts +6 -0
  173. package/dist/tui/ui/views/input.d.ts.map +1 -0
  174. package/dist/tui/ui/views/input.js +494 -0
  175. package/dist/tui/ui/views/input.js.map +1 -0
  176. package/dist/tui/ui/views/launch.d.ts +16 -0
  177. package/dist/tui/ui/views/launch.d.ts.map +1 -0
  178. package/dist/tui/ui/views/launch.js +43 -0
  179. package/dist/tui/ui/views/launch.js.map +1 -0
  180. package/dist/tui/ui/views/refusal.d.ts +20 -0
  181. package/dist/tui/ui/views/refusal.d.ts.map +1 -0
  182. package/dist/tui/ui/views/refusal.js +47 -0
  183. package/dist/tui/ui/views/refusal.js.map +1 -0
  184. package/dist/tui/ui/views/repos.d.ts +10 -0
  185. package/dist/tui/ui/views/repos.d.ts.map +1 -0
  186. package/dist/tui/ui/views/repos.js +93 -0
  187. package/dist/tui/ui/views/repos.js.map +1 -0
  188. package/dist/tui/ui/views/task.d.ts +17 -0
  189. package/dist/tui/ui/views/task.d.ts.map +1 -0
  190. package/dist/tui/ui/views/task.js +309 -0
  191. package/dist/tui/ui/views/task.js.map +1 -0
  192. package/dist/tui/ui/views/wordmark.d.ts +15 -0
  193. package/dist/tui/ui/views/wordmark.d.ts.map +1 -0
  194. package/dist/tui/ui/views/wordmark.js +50 -0
  195. package/dist/tui/ui/views/wordmark.js.map +1 -0
  196. package/dist/tui/ui/views/workspaces.d.ts +17 -0
  197. package/dist/tui/ui/views/workspaces.d.ts.map +1 -0
  198. package/dist/tui/ui/views/workspaces.js +127 -0
  199. package/dist/tui/ui/views/workspaces.js.map +1 -0
  200. package/dist/tui/ui/widgets/leaf-editor.d.ts +58 -0
  201. package/dist/tui/ui/widgets/leaf-editor.d.ts.map +1 -0
  202. package/dist/tui/ui/widgets/leaf-editor.js +119 -0
  203. package/dist/tui/ui/widgets/leaf-editor.js.map +1 -0
  204. package/dist/tui/ui/widgets/logs.d.ts +99 -0
  205. package/dist/tui/ui/widgets/logs.d.ts.map +1 -0
  206. package/dist/tui/ui/widgets/logs.js +291 -0
  207. package/dist/tui/ui/widgets/logs.js.map +1 -0
  208. package/dist/tui/ui/widgets/reads.d.ts +62 -0
  209. package/dist/tui/ui/widgets/reads.d.ts.map +1 -0
  210. package/dist/tui/ui/widgets/reads.js +122 -0
  211. package/dist/tui/ui/widgets/reads.js.map +1 -0
  212. package/dist/tui/ui/widgets/runs.d.ts +51 -0
  213. package/dist/tui/ui/widgets/runs.d.ts.map +1 -0
  214. package/dist/tui/ui/widgets/runs.js +86 -0
  215. package/dist/tui/ui/widgets/runs.js.map +1 -0
  216. package/dist/tui/ui/widgets/tree.d.ts +149 -0
  217. package/dist/tui/ui/widgets/tree.d.ts.map +1 -0
  218. package/dist/tui/ui/widgets/tree.js +584 -0
  219. package/dist/tui/ui/widgets/tree.js.map +1 -0
  220. package/package.json +22 -12
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+ import { describeError } from './api.js';
6
+ import { cancelRun, startRun } from './data/dataflow.js';
7
+ import { viewWorkspace } from './data/feeds.js';
8
+ import { complete } from './input/completion.js';
9
+ import { describe, parseCommand } from './input/commands.js';
10
+ import { resolve as resolveKey } from './input/keymap.js';
11
+ import { isMouseInput, parseSgr } from './input/mouse.js';
12
+ import { hitAt, lastFrame, paneTopFromRow } from './ui/frame.js';
13
+ import { buildCatalogue } from './model/catalogue.js';
14
+ import { HELP_TABS, helpTabFor } from './model/help.js';
15
+ import { listModel } from './model/index.js';
16
+ import { inputView, isLogTab, taskTabsOf, taskView } from './state/actions.js';
17
+ import { dirtyCount } from './state/reducer.js';
18
+ import { repoEntry } from './state/persist.js';
19
+ import { restoreTree } from './ui/widgets/tree.js';
20
+ const hooks = new Map();
21
+ /**
22
+ * Registers a view's hooks (called by each view module on load).
23
+ *
24
+ * @param kind - The view kind
25
+ * @param viewHooks - The hooks
26
+ */
27
+ export function registerViewHooks(kind, viewHooks) {
28
+ hooks.set(kind, { ...hooks.get(kind), ...viewHooks });
29
+ }
30
+ let toastSeq = 0;
31
+ /** Rows one wheel report scrolls (design §13). */
32
+ const WHEEL_ROWS = 3;
33
+ /**
34
+ * Creates the controller.
35
+ *
36
+ * @param deps - The dependencies
37
+ * @returns The controller
38
+ */
39
+ export function createController(deps) {
40
+ const { store } = deps;
41
+ let stdout = null;
42
+ const state = () => store.getState();
43
+ const dispatch = (action) => store.dispatch(action);
44
+ const workspace = () => {
45
+ const v = state().view;
46
+ if (v.kind === 'dashboard' || v.kind === 'task' || v.kind === 'input')
47
+ return v.ws;
48
+ const persisted = deps.persist?.state;
49
+ const session = state().session;
50
+ if (persisted !== undefined && session !== null)
51
+ return persisted.repos[session.stateKey]?.workspace ?? null;
52
+ return null;
53
+ };
54
+ const remember = (view) => {
55
+ const session = state().session;
56
+ if (deps.persist === null || session === null)
57
+ return;
58
+ deps.persist.update(s => {
59
+ const entry = repoEntry(s, session.stateKey);
60
+ if (view.kind === 'dashboard' || view.kind === 'task' || view.kind === 'input')
61
+ entry.workspace = view.ws;
62
+ entry.view = view.kind === 'task' ? `task:${view.task}` : view.kind === 'input' ? `input:${view.name}` : view.kind;
63
+ });
64
+ };
65
+ const controller = {
66
+ deps,
67
+ dispatch,
68
+ state,
69
+ workspace,
70
+ onKey(input, key) {
71
+ if (isMouseInput(input)) {
72
+ // Mouse reports never reach the keymap (they would read as a paste).
73
+ if (state().mouse)
74
+ for (const event of parseSgr(input))
75
+ onMouse(event);
76
+ return;
77
+ }
78
+ const s = state();
79
+ const v = s.view;
80
+ const viewHooks = hooks.get(v.kind);
81
+ const ctx = {
82
+ commandMode: s.command.mode,
83
+ editingLeaf: v.kind === 'input' && v.editing !== null,
84
+ scope: v.kind === 'repos' || v.kind === 'workspaces' || v.kind === 'dashboard' ? 'list'
85
+ : v.kind === 'task' ? (v.tab === 'output' ? 'tree' : isLogTab(v.tab) ? 'logs' : 'list')
86
+ : v.kind === 'input' ? 'tree'
87
+ : 'none',
88
+ editable: v.kind === 'input',
89
+ tabs: v.kind === 'task' ? taskTabsOf(s, v.ws, v.task).length : v.kind === 'help' ? HELP_TABS.length : 0,
90
+ pendingKey: s.pendingKey,
91
+ };
92
+ const action = resolveKey(input, key, ctx);
93
+ if (s.pendingKey !== null)
94
+ dispatch({ type: 'pendingKey', key: null });
95
+ if (action === null)
96
+ return;
97
+ deps.log(`key ${JSON.stringify(input)} → ${action.kind}`);
98
+ if (viewHooks?.key?.(action, s, controller) === true)
99
+ return;
100
+ handleAction(action, s, viewHooks);
101
+ },
102
+ async execute(text) {
103
+ const trimmed = text.trim();
104
+ if (trimmed === '') {
105
+ dispatch({ type: 'command/clear' });
106
+ return;
107
+ }
108
+ if (!trimmed.startsWith('/')) {
109
+ const candidates = complete(trimmed, buildCatalogue(state(), deps.glyphs));
110
+ const first = candidates[0];
111
+ if (first === undefined) {
112
+ controller.toast(`no match for ${trimmed}`, 'warn');
113
+ return;
114
+ }
115
+ await controller.execute(first.insert);
116
+ return;
117
+ }
118
+ const parsed = parseCommand(trimmed);
119
+ if (!parsed.ok) {
120
+ controller.toast(parsed.error, 'neg');
121
+ return;
122
+ }
123
+ dispatch({ type: 'command/clear' });
124
+ try {
125
+ await runCommand(parsed.command, trimmed);
126
+ }
127
+ catch (err) {
128
+ deps.log(`command ${trimmed} failed: ${describeError(err)}`);
129
+ controller.toast(describeError(err), 'neg');
130
+ }
131
+ },
132
+ toast(text, tone = 'pos', glyph) {
133
+ const id = ++toastSeq;
134
+ dispatch({ type: 'toast', toast: { id, text, tone, ...(glyph !== undefined ? { glyph } : {}), until: deps.now() + 3_000 } });
135
+ const timer = setTimeout(() => dispatch({ type: 'toast/clear', id }), 3_000);
136
+ timer.unref?.();
137
+ },
138
+ navigate(view, push = false) {
139
+ dispatch(push ? { type: 'view/push', view } : { type: 'view/set', view });
140
+ remember(view);
141
+ },
142
+ openTask(ws, task, tab = 'output') {
143
+ const s = state();
144
+ const v = s.view;
145
+ if (v.kind === 'task' && v.ws === ws && v.task === task) {
146
+ dispatch({ type: 'task/tab', tab });
147
+ return;
148
+ }
149
+ controller.navigate(taskView(ws, task, tab), v.kind === 'dashboard' || v.kind === 'task' || v.kind === 'input');
150
+ restoreTree(controller, ws, `.tasks.${task}.output`);
151
+ },
152
+ openInput(ws, name) {
153
+ const v = state().view;
154
+ controller.navigate(inputView(ws, name), v.kind === 'dashboard' || v.kind === 'task' || v.kind === 'input');
155
+ restoreTree(controller, ws, `.inputs.${name}`);
156
+ },
157
+ openWorkspace(ws) {
158
+ dispatch({ type: 'view/root', view: { kind: 'dashboard', ws, list: { sel: 0, top: 0 } } });
159
+ remember({ kind: 'dashboard', ws, list: { sel: 0, top: 0 } });
160
+ },
161
+ back() {
162
+ const s = state();
163
+ if (s.command.mode !== 'idle') {
164
+ dispatch({ type: 'command/clear' });
165
+ return;
166
+ }
167
+ if (s.history.length > 0) {
168
+ dispatch({ type: 'view/pop' });
169
+ remember(state().view);
170
+ return;
171
+ }
172
+ const v = s.view;
173
+ if (v.kind === 'task' || v.kind === 'input')
174
+ controller.openWorkspace(v.ws);
175
+ },
176
+ quit(force) {
177
+ const dirty = dirtyCount(state());
178
+ if (dirty > 0 && !force) {
179
+ dispatch({ type: 'command/confirm', confirm: { question: `quit with ${dirty} unsaved edit${dirty === 1 ? '' : 's'}?`, command: '/quit --force' } });
180
+ return;
181
+ }
182
+ deps.exit(0);
183
+ },
184
+ setSize(size) {
185
+ const current = state().size;
186
+ if (current.columns !== size.columns || current.rows !== size.rows)
187
+ dispatch({ type: 'size', size });
188
+ },
189
+ attachStdout(stream) {
190
+ stdout = stream;
191
+ void stdout;
192
+ },
193
+ updateCompletion() {
194
+ const s = state();
195
+ if (s.command.mode !== 'edit')
196
+ return;
197
+ const tags = hooks.get(s.view.kind)?.tags?.(s);
198
+ dispatch({ type: 'command/completion', items: complete(s.command.text, buildCatalogue(s, deps.glyphs, tags)) });
199
+ },
200
+ };
201
+ const startTyping = (text) => {
202
+ dispatch({ type: 'command/edit', text });
203
+ controller.updateCompletion();
204
+ };
205
+ const handleAction = (action, s, viewHooks) => {
206
+ switch (action.kind) {
207
+ case 'quit':
208
+ controller.quit(false);
209
+ return;
210
+ case 'help':
211
+ controller.navigate({ kind: 'help', tab: helpTabFor(s.view.kind) }, true);
212
+ return;
213
+ case 'back':
214
+ controller.back();
215
+ return;
216
+ case 'refresh':
217
+ deps.feeds.refresh();
218
+ return;
219
+ case 'type':
220
+ startTyping(action.text);
221
+ return;
222
+ case 'prefill':
223
+ startTyping(action.text);
224
+ return;
225
+ case 'pending':
226
+ dispatch({ type: 'pendingKey', key: action.key });
227
+ return;
228
+ case 'paste': {
229
+ if (s.command.mode === 'edit')
230
+ dispatch({ type: 'command/insert', text: action.text });
231
+ else
232
+ dispatch({ type: 'command/edit', text: action.text });
233
+ controller.updateCompletion();
234
+ if (action.submit)
235
+ void controller.execute(state().command.text);
236
+ return;
237
+ }
238
+ case 'tab': {
239
+ if (s.view.kind === 'help') {
240
+ dispatch({ type: 'help/tab', tab: HELP_TABS[action.index]?.tab ?? 'everywhere' });
241
+ }
242
+ else if (s.view.kind === 'task') {
243
+ dispatch({ type: 'task/tab', tab: taskTabsOf(s, s.view.ws, s.view.task)[action.index] ?? 'output' });
244
+ }
245
+ return;
246
+ }
247
+ case 'tab.cycle': {
248
+ const wrap = (i, n) => ((i + action.delta) % n + n) % n;
249
+ if (s.view.kind === 'help') {
250
+ const tab = s.view.tab;
251
+ const i = Math.max(0, HELP_TABS.findIndex(x => x.tab === tab));
252
+ dispatch({ type: 'help/tab', tab: HELP_TABS[wrap(i, HELP_TABS.length)].tab });
253
+ }
254
+ else if (s.view.kind === 'task') {
255
+ const tabs = taskTabsOf(s, s.view.ws, s.view.task);
256
+ dispatch({ type: 'task/tab', tab: tabs[wrap(Math.max(0, tabs.indexOf(s.view.tab)), tabs.length)] });
257
+ }
258
+ return;
259
+ }
260
+ case 'move': {
261
+ if (s.view.kind === 'help') {
262
+ const tabs = ['everywhere', 'repos', 'workspaces', 'dashboard', 'task', 'input'];
263
+ const i = tabs.indexOf(s.view.tab);
264
+ if (action.op === 'up' || action.op === 'down')
265
+ return;
266
+ return void i;
267
+ }
268
+ if (viewHooks?.tree?.(action, s, controller) === true)
269
+ return;
270
+ const model = listModel(s);
271
+ dispatch({ type: 'list/move', op: action.op, count: model.count, visible: model.visible });
272
+ return;
273
+ }
274
+ case 'open':
275
+ viewHooks?.open?.(s, controller);
276
+ return;
277
+ case 'expand':
278
+ case 'collapse':
279
+ case 'toggle':
280
+ case 'collapseDeep':
281
+ case 'next':
282
+ case 'prev':
283
+ case 'save':
284
+ viewHooks?.tree?.(action, s, controller);
285
+ return;
286
+ case 'retry': {
287
+ if (s.view.kind === 'refusal')
288
+ void deps.openTarget(s.session?.target ?? refusalTarget(s));
289
+ return;
290
+ }
291
+ // -- the command box ------------------------------------------
292
+ case 'cmd.char':
293
+ dispatch({ type: 'command/insert', text: action.text });
294
+ controller.updateCompletion();
295
+ return;
296
+ case 'cmd.backspace': {
297
+ if (s.command.text.length <= 1) {
298
+ dispatch({ type: 'command/clear' });
299
+ return;
300
+ }
301
+ dispatch({ type: 'command/backspace' });
302
+ controller.updateCompletion();
303
+ return;
304
+ }
305
+ case 'cmd.delete':
306
+ dispatch({ type: 'command/delete' });
307
+ controller.updateCompletion();
308
+ return;
309
+ case 'cmd.left':
310
+ dispatch({ type: 'command/cursor', to: 'left' });
311
+ return;
312
+ case 'cmd.right':
313
+ dispatch({ type: 'command/cursor', to: 'right' });
314
+ return;
315
+ case 'cmd.home':
316
+ dispatch({ type: 'command/cursor', to: 'home' });
317
+ return;
318
+ case 'cmd.end':
319
+ dispatch({ type: 'command/cursor', to: 'end' });
320
+ return;
321
+ case 'cmd.up':
322
+ dispatch({ type: 'command/completionMove', delta: -1 });
323
+ return;
324
+ case 'cmd.down':
325
+ dispatch({ type: 'command/completionMove', delta: 1 });
326
+ return;
327
+ case 'cmd.cancel':
328
+ dispatch({ type: 'command/clear' });
329
+ return;
330
+ case 'cmd.complete': {
331
+ const completion = s.command.completion;
332
+ const item = completion?.items[completion.index];
333
+ if (item === undefined)
334
+ return;
335
+ dispatch({ type: 'command/edit', text: item.insert });
336
+ controller.updateCompletion();
337
+ return;
338
+ }
339
+ case 'cmd.submit': {
340
+ if (s.command.mode === 'confirm' && s.command.confirm !== null) {
341
+ const command = s.command.confirm.command;
342
+ dispatch({ type: 'command/clear' });
343
+ void controller.execute(command);
344
+ return;
345
+ }
346
+ const completion = s.command.completion;
347
+ const item = completion?.items[completion.index];
348
+ // A picked completion row runs that item; a fully typed command runs as typed.
349
+ if (item !== undefined && (!s.command.text.startsWith('/') || item.kind !== 'command' && item.kind !== 'flag' && !parseCommand(s.command.text).ok)) {
350
+ void controller.execute(item.insert);
351
+ return;
352
+ }
353
+ if (item !== undefined && item.kind !== 'command' && item.kind !== 'flag' && parseCommand(s.command.text).ok) {
354
+ const parsed = parseCommand(s.command.text);
355
+ // `/task fore` with a highlighted row opens the row, not the literal.
356
+ if (parsed.ok && 'target' in parsed.command && item.insert !== s.command.text) {
357
+ void controller.execute(item.insert);
358
+ return;
359
+ }
360
+ }
361
+ void controller.execute(s.command.text);
362
+ return;
363
+ }
364
+ default:
365
+ return;
366
+ }
367
+ };
368
+ // -- the mouse ------------------------------------------------------
369
+ let dragging = false;
370
+ /** The primary list's first visible row (for absolute scrolls). */
371
+ const listTop = (s) => {
372
+ const v = s.view;
373
+ if (v.kind === 'repos' || v.kind === 'workspaces' || v.kind === 'dashboard')
374
+ return v.list.top;
375
+ if (v.kind === 'task')
376
+ return v.tab === 'runs' ? v.runs.top : v.tab === 'reads' ? v.reads.top : v.tree.top;
377
+ if (v.kind === 'input')
378
+ return v.tree.top;
379
+ return 0;
380
+ };
381
+ const scrollBy = (s, to) => {
382
+ if (hooks.get(s.view.kind)?.scroll?.(to, s, controller) === true)
383
+ return;
384
+ const model = listModel(s);
385
+ if (model.count === 0)
386
+ return;
387
+ const delta = 'delta' in to ? to.delta : to.top - listTop(s);
388
+ dispatch({ type: 'list/scroll', delta, count: model.count, visible: model.visible });
389
+ };
390
+ // Terminals send several wheel reports per notch. The rows they add up
391
+ // to are applied once per tick — one scroll, one dispatch — instead of
392
+ // a scroll per report.
393
+ let wheelRows = 0;
394
+ let wheelFlush = null;
395
+ const flushWheel = () => {
396
+ if (wheelFlush !== null) {
397
+ clearImmediate(wheelFlush);
398
+ wheelFlush = null;
399
+ }
400
+ const delta = wheelRows;
401
+ wheelRows = 0;
402
+ if (delta !== 0)
403
+ scrollBy(state(), { delta });
404
+ };
405
+ const wheel = (rows) => {
406
+ wheelRows += rows;
407
+ if (wheelFlush === null)
408
+ wheelFlush = setImmediate(flushWheel);
409
+ };
410
+ const onMouse = (event) => {
411
+ const frame = lastFrame();
412
+ if (frame === null)
413
+ return;
414
+ if (event.kind === 'wheelUp') {
415
+ wheel(-WHEEL_ROWS);
416
+ return;
417
+ }
418
+ if (event.kind === 'wheelDown') {
419
+ wheel(WHEEL_ROWS);
420
+ return;
421
+ }
422
+ // A click or a drag lands after the wheel rows before it.
423
+ flushWheel();
424
+ const s = state();
425
+ if (event.kind === 'release') {
426
+ dragging = false;
427
+ return;
428
+ }
429
+ if (event.kind === 'drag') {
430
+ if (dragging && frame.pane !== null)
431
+ scrollBy(s, { top: paneTopFromRow(frame.pane, event.y) });
432
+ return;
433
+ }
434
+ if (event.kind !== 'press' || event.button !== 'left')
435
+ return;
436
+ const pane = frame.pane;
437
+ if (pane !== null && event.x === frame.layout.columns - 1 && event.y >= pane.top && event.y < pane.top + pane.rows && pane.total > pane.visible) {
438
+ dragging = true;
439
+ scrollBy(s, { top: paneTopFromRow(pane, event.y) });
440
+ return;
441
+ }
442
+ const target = hitAt(frame, event.x, event.y);
443
+ if (target === null)
444
+ return;
445
+ deps.log(`click ${event.x},${event.y} → ${target.kind}`);
446
+ const viewHooks = hooks.get(s.view.kind);
447
+ if (viewHooks?.click?.(target, event, s, controller) === true)
448
+ return;
449
+ switch (target.kind) {
450
+ case 'list': {
451
+ const model = listModel(s);
452
+ dispatch({ type: 'list/select', index: target.index, count: model.count, visible: model.visible });
453
+ return;
454
+ }
455
+ case 'tab':
456
+ handleAction({ kind: 'tab', index: target.index }, s, viewHooks);
457
+ return;
458
+ case 'crumb': {
459
+ if (target.index === 0) {
460
+ if (s.session?.repo === null)
461
+ controller.navigate({ kind: 'repos', list: { sel: 0, top: 0 } }, s.view.kind !== 'repos');
462
+ else
463
+ controller.navigate({ kind: 'workspaces', list: { sel: 0, top: 0 } }, s.view.kind !== 'workspaces');
464
+ }
465
+ else if (target.index === 1) {
466
+ const ws = workspace();
467
+ if (ws !== null && s.view.kind !== 'dashboard')
468
+ controller.openWorkspace(ws);
469
+ }
470
+ return;
471
+ }
472
+ case 'pill': {
473
+ if (target.pill === 'running')
474
+ startTyping('/stop');
475
+ else if (target.pill === 'dirty')
476
+ startTyping('/apply');
477
+ else
478
+ void controller.execute('/refresh');
479
+ return;
480
+ }
481
+ case 'completion': {
482
+ const item = s.command.completion?.items[target.index];
483
+ if (item !== undefined)
484
+ void controller.execute(item.insert);
485
+ return;
486
+ }
487
+ default:
488
+ return;
489
+ }
490
+ };
491
+ const refusalTarget = (s) => {
492
+ if (s.view.kind !== 'refusal')
493
+ return '.';
494
+ const r = s.view.refusal;
495
+ if (r.kind === 'not-repo')
496
+ return r.target;
497
+ if (r.kind === 'not-logged-in')
498
+ return r.repo !== null ? `${r.origin}/repos/${r.repo}` : r.origin;
499
+ if (r.kind === 'unreachable')
500
+ return r.url.replace(/\/api\/repos(\/[^/]+)?.*$/, (_m, repo) => (repo !== undefined ? `/repos${repo}` : ''));
501
+ return '.';
502
+ };
503
+ /** Commands that leave the current view (they ask first while edits are pending). */
504
+ const LEAVES = new Set(['task', 'input', 'dataset', 'workspace', 'workspaces', 'repos', 'repo', 'logs', 'runs']);
505
+ const runCommand = async (command, text) => {
506
+ const s = state();
507
+ const ws = workspace();
508
+ const viewHooks = hooks.get(s.view.kind);
509
+ const dirty = dirtyCount(s);
510
+ if (dirty > 0 && LEAVES.has(command.name) && command.name !== 'repo') {
511
+ dispatch({ type: 'command/confirm', confirm: { question: `discard ${dirty} unsaved edit${dirty === 1 ? '' : 's'} and ${describe(command, { workspace: ws, taskCount: 0, running: false, concurrency: 4, dirty }).text}?`, command: `/discard --then "${text.replace(/"/g, '')}"` } });
512
+ return;
513
+ }
514
+ if (viewHooks?.command !== undefined && await viewHooks.command(command, s, controller))
515
+ return;
516
+ switch (command.name) {
517
+ case 'task': {
518
+ if (ws === null) {
519
+ controller.toast('open a workspace first', 'warn');
520
+ return;
521
+ }
522
+ controller.openTask(ws, command.target);
523
+ return;
524
+ }
525
+ case 'input': {
526
+ if (ws === null) {
527
+ controller.toast('open a workspace first', 'warn');
528
+ return;
529
+ }
530
+ controller.openInput(ws, command.target);
531
+ return;
532
+ }
533
+ case 'dataset': {
534
+ if (ws === null) {
535
+ controller.toast('open a workspace first', 'warn');
536
+ return;
537
+ }
538
+ const path = `.${command.target.replace(/^\./, '')}`;
539
+ const input = /^\.inputs\.([^.]+)$/.exec(path);
540
+ const task = /^\.tasks\.([^.]+)\.output$/.exec(path);
541
+ if (input !== null)
542
+ controller.openInput(ws, input[1]);
543
+ else if (task !== null)
544
+ controller.openTask(ws, task[1]);
545
+ else
546
+ controller.toast(`${path} is not an input or a task output`, 'warn');
547
+ return;
548
+ }
549
+ case 'workspace': {
550
+ const known = s.data.workspaces;
551
+ if (known !== null && !known.some(w => w.name === command.target)) {
552
+ controller.toast(`no workspace ${command.target}`, 'warn');
553
+ return;
554
+ }
555
+ controller.openWorkspace(command.target);
556
+ return;
557
+ }
558
+ case 'workspaces':
559
+ controller.navigate({ kind: 'workspaces', list: { sel: 0, top: 0 } }, s.view.kind !== 'workspaces');
560
+ return;
561
+ case 'repos': {
562
+ if (s.session?.origin === null) {
563
+ controller.toast('a local repository has no repository list', 'warn');
564
+ return;
565
+ }
566
+ controller.navigate({ kind: 'repos', list: { sel: 0, top: 0 } }, s.view.kind !== 'repos');
567
+ return;
568
+ }
569
+ case 'repo': {
570
+ const dirty = dirtyCount(s);
571
+ if (dirty > 0) {
572
+ dispatch({ type: 'command/confirm', confirm: { question: `discard ${dirty} unsaved edit${dirty === 1 ? '' : 's'} and open ${command.target}?`, command: `/discard --then "/repo ${command.target}"` } });
573
+ return;
574
+ }
575
+ await deps.openTarget(command.target);
576
+ return;
577
+ }
578
+ case 'login':
579
+ await deps.login(command.url);
580
+ return;
581
+ case 'run': {
582
+ const here = viewWorkspace(s);
583
+ if (here === null) {
584
+ controller.toast('open a workspace first', 'warn');
585
+ return;
586
+ }
587
+ await startRun(controller, here, { force: command.force, filter: command.filter, concurrency: command.concurrency });
588
+ return;
589
+ }
590
+ case 'stop': {
591
+ const here = viewWorkspace(s);
592
+ if (here === null) {
593
+ controller.toast('open a workspace first', 'warn');
594
+ return;
595
+ }
596
+ await cancelRun(controller, here);
597
+ return;
598
+ }
599
+ case 'logs': {
600
+ if (ws === null) {
601
+ controller.toast('open a workspace first', 'warn');
602
+ return;
603
+ }
604
+ controller.openTask(ws, command.task, command.stream ?? 'stdout');
605
+ return;
606
+ }
607
+ case 'runs': {
608
+ if (ws === null) {
609
+ controller.toast('open a workspace first', 'warn');
610
+ return;
611
+ }
612
+ controller.openTask(ws, command.task, 'runs');
613
+ return;
614
+ }
615
+ case 'refresh':
616
+ deps.feeds.refresh();
617
+ controller.toast('polling every feed now', 'info');
618
+ return;
619
+ case 'help':
620
+ controller.navigate({ kind: 'help', tab: helpTabFor(s.view.kind) }, true);
621
+ return;
622
+ case 'about':
623
+ controller.navigate({ kind: 'about' }, true);
624
+ return;
625
+ case 'quit':
626
+ controller.quit(command.force);
627
+ return;
628
+ case 'discard': {
629
+ if (state().edit !== null) {
630
+ dispatch({ type: 'edit/set', edit: null });
631
+ dispatch({ type: 'input/editing', editing: null });
632
+ }
633
+ if (command.then !== undefined)
634
+ await controller.execute(command.then);
635
+ return;
636
+ }
637
+ default:
638
+ controller.toast(`/${command.name} does nothing here`, 'warn');
639
+ return;
640
+ }
641
+ };
642
+ return controller;
643
+ }
644
+ //# sourceMappingURL=controller.js.map