@linxiraos/pi-tui 1.2.1 → 1.3.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 (274) hide show
  1. package/CHANGELOG.md +188 -125
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/app-keybindings.d.ts +18 -8
  5. package/dist/types/apps/git/diff-pane.d.ts +5 -0
  6. package/dist/types/apps/git/git-tui.d.ts +16 -2
  7. package/dist/types/apps/git/state.d.ts +11 -0
  8. package/dist/types/autocomplete.d.ts +2 -0
  9. package/dist/types/charts/chart-svg.d.ts +7 -13
  10. package/dist/types/chat/advisor-message.d.ts +4 -1
  11. package/dist/types/chat/assistant-message.d.ts +9 -4
  12. package/dist/types/chat/figure-markdown.d.ts +7 -2
  13. package/dist/types/chat/messages.d.ts +3 -1
  14. package/dist/types/chat/read-tool-group.d.ts +2 -2
  15. package/dist/types/chat/table-chart.d.ts +11 -15
  16. package/dist/types/chat/tool-execution.d.ts +32 -20
  17. package/dist/types/chat/user-message.d.ts +3 -2
  18. package/dist/types/chrome/countdown-timer.d.ts +3 -2
  19. package/dist/types/chrome/diff.d.ts +2 -3
  20. package/dist/types/chrome/transcript-container.d.ts +2 -0
  21. package/dist/types/components/image.d.ts +2 -0
  22. package/dist/types/components/markdown.d.ts +49 -3
  23. package/dist/types/components/text.d.ts +2 -0
  24. package/dist/types/debug-server.d.ts +2 -0
  25. package/dist/types/keybinding-matchers.d.ts +6 -0
  26. package/dist/types/multiplexers/tmux.d.ts +1 -0
  27. package/dist/types/multiplexers/types.d.ts +6 -0
  28. package/dist/types/native/backend.d.ts +5 -4
  29. package/dist/types/native/blobs.d.ts +24 -12
  30. package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
  31. package/dist/types/overlays/annotation-overlay.d.ts +19 -0
  32. package/dist/types/overlays/approval-dialog.d.ts +79 -0
  33. package/dist/types/overlays/ask-dialog.d.ts +11 -6
  34. package/dist/types/overlays/btw-history-panel.d.ts +2 -6
  35. package/dist/types/overlays/hook-editor.d.ts +7 -2
  36. package/dist/types/overlays/hook-input.d.ts +12 -4
  37. package/dist/types/overlays/job-tree.d.ts +50 -0
  38. package/dist/types/overlays/jobs-panel.d.ts +25 -11
  39. package/dist/types/overlays/login-dialog.d.ts +8 -3
  40. package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
  41. package/dist/types/overlays/model-browser.d.ts +6 -1
  42. package/dist/types/overlays/model-picker.d.ts +2 -0
  43. package/dist/types/overlays/pause-screen.d.ts +5 -0
  44. package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
  45. package/dist/types/overlays/report-panel.d.ts +5 -1
  46. package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
  47. package/dist/types/overlays/session-account-selector.d.ts +4 -0
  48. package/dist/types/overlays/session-info-overlay.d.ts +7 -3
  49. package/dist/types/overlays/session-selector.d.ts +3 -1
  50. package/dist/types/overlays/usage-dashboard.d.ts +6 -6
  51. package/dist/types/prompt/composer.d.ts +21 -20
  52. package/dist/types/prompt/custom-editor.d.ts +20 -10
  53. package/dist/types/prompt/image-references.d.ts +22 -0
  54. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  55. package/dist/types/prompt/macos-spelling.d.ts +2 -1
  56. package/dist/types/prompt/prose-gate.d.ts +3 -0
  57. package/dist/types/prompt/queued-messages.d.ts +1 -1
  58. package/dist/types/prompt/typing-word.d.ts +25 -0
  59. package/dist/types/prompt/welcome.d.ts +26 -6
  60. package/dist/types/render/hanging-wrap.d.ts +7 -0
  61. package/dist/types/render/hyperlink.d.ts +2 -0
  62. package/dist/types/render/render-utils.d.ts +9 -1
  63. package/dist/types/render/utils.d.ts +1 -1
  64. package/dist/types/setup/scenes/splash.d.ts +6 -5
  65. package/dist/types/status-line/context-usage.d.ts +4 -1
  66. package/dist/types/status-line/host.d.ts +6 -4
  67. package/dist/types/status-line/startup.d.ts +6 -6
  68. package/dist/types/status-line/types.d.ts +3 -0
  69. package/dist/types/stdin-buffer.d.ts +7 -0
  70. package/dist/types/terminal-capabilities.d.ts +16 -1
  71. package/dist/types/terminal-multiplexer.d.ts +7 -0
  72. package/dist/types/terminal.d.ts +8 -4
  73. package/dist/types/theme/symbols.d.ts +1 -1
  74. package/dist/types/theme/theme.d.ts +11 -0
  75. package/dist/types/thinking.d.ts +5 -0
  76. package/dist/types/tmux.d.ts +39 -4
  77. package/dist/types/tools/ast-edit.d.ts +2 -2
  78. package/dist/types/tools/ast-grep.d.ts +2 -2
  79. package/dist/types/tools/bash.d.ts +6 -1
  80. package/dist/types/tools/edit.d.ts +2 -0
  81. package/dist/types/tools/eval.d.ts +53 -4
  82. package/dist/types/tools/find.d.ts +14 -4
  83. package/dist/types/tools/glob.d.ts +3 -3
  84. package/dist/types/tools/grep.d.ts +2 -2
  85. package/dist/types/tools/grouped-file-output.d.ts +9 -4
  86. package/dist/types/tools/native-view.d.ts +1 -1
  87. package/dist/types/tools/read.d.ts +32 -10
  88. package/dist/types/tools/renderer.d.ts +2 -0
  89. package/dist/types/tools/task.d.ts +31 -0
  90. package/dist/types/tools/write.d.ts +2 -0
  91. package/dist/types/tui.d.ts +20 -3
  92. package/package.json +10 -10
  93. package/src/app-keybindings.d.ts +18 -8
  94. package/src/app-keybindings.ts +20 -8
  95. package/src/apps/autoresearch-dashboard.ts +444 -27
  96. package/src/apps/debug/raw-sse.ts +2 -2
  97. package/src/apps/git/diff-pane.ts +39 -41
  98. package/src/apps/git/git-tui.ts +80 -14
  99. package/src/apps/git/sidebar.ts +10 -1
  100. package/src/apps/git/state.ts +12 -0
  101. package/src/autocomplete.d.ts +2 -0
  102. package/src/autocomplete.ts +21 -19
  103. package/src/bracketed-paste.ts +9 -2
  104. package/src/charts/chart-svg.d.ts +7 -13
  105. package/src/charts/chart-svg.ts +19 -22
  106. package/src/chat/advisor-message.d.ts +4 -1
  107. package/src/chat/advisor-message.ts +77 -24
  108. package/src/chat/assistant-message.d.ts +9 -4
  109. package/src/chat/assistant-message.ts +127 -31
  110. package/src/chat/bash-execution.ts +7 -2
  111. package/src/chat/figure-markdown.d.ts +7 -2
  112. package/src/chat/figure-markdown.ts +10 -3
  113. package/src/chat/image-loading.ts +9 -6
  114. package/src/chat/messages.d.ts +3 -1
  115. package/src/chat/messages.ts +4 -1
  116. package/src/chat/read-tool-group.d.ts +2 -2
  117. package/src/chat/read-tool-group.ts +97 -42
  118. package/src/chat/svg-figure.ts +3 -1
  119. package/src/chat/svg-source.ts +6 -3
  120. package/src/chat/table-chart.d.ts +11 -15
  121. package/src/chat/table-chart.ts +24 -57
  122. package/src/chat/tool-execution.d.ts +32 -20
  123. package/src/chat/tool-execution.ts +103 -36
  124. package/src/chat/user-message.d.ts +3 -2
  125. package/src/chat/user-message.ts +54 -34
  126. package/src/chrome/countdown-timer.ts +3 -3
  127. package/src/chrome/diff.d.ts +2 -3
  128. package/src/chrome/diff.ts +4 -9
  129. package/src/chrome/transcript-container.d.ts +2 -0
  130. package/src/chrome/transcript-container.ts +212 -28
  131. package/src/components/editor.ts +12 -12
  132. package/src/components/image.d.ts +2 -0
  133. package/src/components/image.ts +54 -20
  134. package/src/components/markdown.d.ts +49 -3
  135. package/src/components/markdown.ts +461 -11
  136. package/src/components/settings-list.ts +2 -4
  137. package/src/components/text.d.ts +2 -0
  138. package/src/components/text.ts +11 -1
  139. package/src/debug-server.d.ts +2 -0
  140. package/src/debug-server.ts +51 -69
  141. package/src/hotkeys-markdown.ts +2 -0
  142. package/src/keybinding-matchers.d.ts +6 -0
  143. package/src/keybinding-matchers.ts +19 -0
  144. package/src/multiplexers/tmux.d.ts +1 -0
  145. package/src/multiplexers/tmux.ts +3 -0
  146. package/src/multiplexers/types.d.ts +6 -0
  147. package/src/multiplexers/types.ts +6 -0
  148. package/src/native/backend.d.ts +5 -4
  149. package/src/native/backend.ts +13 -5
  150. package/src/native/blobs.d.ts +24 -12
  151. package/src/native/blobs.ts +56 -7
  152. package/src/native/reconcile.ts +43 -21
  153. package/src/overlays/advisor-config.ts +259 -8
  154. package/src/overlays/agent-hub-projection.d.ts +10 -5
  155. package/src/overlays/agent-hub-projection.ts +63 -12
  156. package/src/overlays/annotation-overlay.ts +422 -168
  157. package/src/overlays/approval-dialog.ts +389 -0
  158. package/src/overlays/ask-dialog.ts +244 -105
  159. package/src/overlays/btw-history-panel.ts +208 -147
  160. package/src/overlays/extensions/extension-list.ts +4 -6
  161. package/src/overlays/hook-editor.ts +90 -32
  162. package/src/overlays/hook-input.ts +105 -19
  163. package/src/overlays/job-tree.ts +177 -0
  164. package/src/overlays/jobs-panel.ts +222 -142
  165. package/src/overlays/login-dialog.ts +201 -91
  166. package/src/overlays/mcp-add-wizard.ts +562 -279
  167. package/src/overlays/model-browser.ts +32 -7
  168. package/src/overlays/model-hub.ts +4 -10
  169. package/src/overlays/model-picker.ts +58 -21
  170. package/src/overlays/pause-screen.ts +81 -23
  171. package/src/overlays/plan-review-overlay.ts +16 -0
  172. package/src/overlays/plugin-selector.ts +17 -1
  173. package/src/overlays/queue-mode-selector.ts +10 -0
  174. package/src/overlays/report-panel.ts +6 -2
  175. package/src/overlays/reset-usage-selector.ts +130 -21
  176. package/src/overlays/session-account-selector.ts +36 -0
  177. package/src/overlays/session-info-overlay.ts +199 -76
  178. package/src/overlays/session-selector.ts +15 -10
  179. package/src/overlays/settings-selector.ts +3 -0
  180. package/src/overlays/show-images-selector.ts +1 -0
  181. package/src/overlays/snapcompact-shape-preview.ts +13 -6
  182. package/src/overlays/theme-selector.ts +6 -1
  183. package/src/overlays/thinking-selector.ts +8 -4
  184. package/src/overlays/tree-selector.ts +117 -2
  185. package/src/overlays/usage-dashboard.ts +498 -144
  186. package/src/prompt/composer-cache.ts +1 -1
  187. package/src/prompt/composer.ts +194 -74
  188. package/src/prompt/custom-editor.ts +168 -65
  189. package/src/prompt/image-references.d.ts +22 -0
  190. package/src/prompt/image-references.ts +79 -2
  191. package/src/prompt/internal-url-autocomplete.ts +1 -0
  192. package/src/prompt/macos-spelling.ts +28 -5
  193. package/src/prompt/prose-gate.ts +3 -0
  194. package/src/prompt/queued-messages.ts +9 -2
  195. package/src/prompt/tips.txt +1 -2
  196. package/src/prompt/typing-word.ts +59 -0
  197. package/src/prompt/welcome.ts +97 -45
  198. package/src/render/code-cell.ts +18 -5
  199. package/src/render/hanging-wrap.d.ts +7 -0
  200. package/src/render/hanging-wrap.ts +58 -0
  201. package/src/render/hyperlink.d.ts +2 -0
  202. package/src/render/hyperlink.ts +5 -0
  203. package/src/render/render-utils.d.ts +9 -1
  204. package/src/render/render-utils.ts +16 -2
  205. package/src/render/utils.d.ts +1 -1
  206. package/src/render/utils.ts +1 -1
  207. package/src/render/width-aware-text.ts +6 -5
  208. package/src/setup/scenes/splash.ts +22 -8
  209. package/src/status-line/component.ts +23 -10
  210. package/src/status-line/context-usage.ts +176 -73
  211. package/src/status-line/footer.ts +18 -35
  212. package/src/status-line/host.ts +6 -1
  213. package/src/status-line/segments.ts +16 -5
  214. package/src/status-line/startup.ts +6 -6
  215. package/src/status-line/types.ts +1 -0
  216. package/src/stdin-buffer.d.ts +7 -0
  217. package/src/stdin-buffer.ts +11 -1
  218. package/src/terminal-capabilities.d.ts +16 -1
  219. package/src/terminal-capabilities.ts +66 -28
  220. package/src/terminal-multiplexer.d.ts +7 -0
  221. package/src/terminal-multiplexer.ts +14 -0
  222. package/src/terminal.d.ts +8 -4
  223. package/src/terminal.ts +160 -46
  224. package/src/theme/symbols.d.ts +1 -1
  225. package/src/theme/symbols.ts +1 -0
  226. package/src/theme/theme-class.ts +1 -0
  227. package/src/theme/theme.d.ts +11 -0
  228. package/src/theme/theme.ts +46 -14
  229. package/src/thinking.d.ts +5 -0
  230. package/src/thinking.ts +9 -1
  231. package/src/tmux.d.ts +39 -4
  232. package/src/tmux.ts +110 -16
  233. package/src/tools/ask.ts +20 -9
  234. package/src/tools/ast-edit.d.ts +2 -2
  235. package/src/tools/ast-edit.ts +30 -7
  236. package/src/tools/ast-grep.d.ts +2 -2
  237. package/src/tools/ast-grep.ts +39 -35
  238. package/src/tools/bash.d.ts +6 -1
  239. package/src/tools/bash.ts +15 -9
  240. package/src/tools/debug.ts +2 -1
  241. package/src/tools/default-renderer.ts +3 -2
  242. package/src/tools/edit.d.ts +2 -0
  243. package/src/tools/edit.ts +100 -21
  244. package/src/tools/eval.d.ts +53 -4
  245. package/src/tools/eval.ts +173 -137
  246. package/src/tools/find.d.ts +14 -4
  247. package/src/tools/find.ts +71 -18
  248. package/src/tools/glob.d.ts +3 -3
  249. package/src/tools/glob.ts +51 -17
  250. package/src/tools/grep.d.ts +2 -2
  251. package/src/tools/grep.ts +99 -22
  252. package/src/tools/grouped-file-output.d.ts +9 -4
  253. package/src/tools/grouped-file-output.ts +21 -6
  254. package/src/tools/lsp.ts +30 -18
  255. package/src/tools/mcp.ts +2 -2
  256. package/src/tools/native-view.d.ts +1 -1
  257. package/src/tools/native-view.ts +2 -2
  258. package/src/tools/read.d.ts +32 -10
  259. package/src/tools/read.ts +89 -43
  260. package/src/tools/renderer.d.ts +2 -0
  261. package/src/tools/renderer.ts +2 -0
  262. package/src/tools/task.d.ts +31 -0
  263. package/src/tools/task.ts +40 -7
  264. package/src/tools/todo.ts +3 -2
  265. package/src/tools/wait.ts +16 -30
  266. package/src/tools/web-search.ts +2 -1
  267. package/src/tools/write.d.ts +2 -0
  268. package/src/tools/write.ts +36 -12
  269. package/src/tools/yield.ts +2 -2
  270. package/src/tui.d.ts +20 -3
  271. package/src/tui.ts +140 -36
  272. package/src/utils.ts +5 -3
  273. package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
  274. package/src/tools/eval-format/code-mode-declarations.ts +0 -61
@@ -16,11 +16,48 @@ import { plainText } from "../native/spans";
16
16
  /** `Enter code: ABCD-1234` style device-flow instructions: the lead-in and the code. */
17
17
  const DEVICE_CODE_INSTRUCTIONS = /^(.*\bcode:?\s+)([A-Za-z0-9][A-Za-z0-9-]{3,})\s*$/s;
18
18
 
19
+ /** A prompt's trailing `, then press Enter:` — the sheet's Continue button says it. */
20
+ const PRESS_KEY_SUFFIX = /,?\s*then press\s+\S+\s*:?\s*$/i;
21
+
22
+ /** Progress that reports a problem rather than a finished stage. */
23
+ const PROGRESS_PROBLEM = /\b(?:fail(?:ed|ure)?|error|unavailable|in use|could ?n[o']t|unable)\b/i;
24
+
25
+ /** Shown on the field when the flow asks for the manual code again: the last paste had no usable code. */
26
+ const RETRY_MESSAGE = "Couldn't read a code from that. Paste the authorization code or the full redirect URL.";
27
+
28
+ /** Shown while the browser half of the flow runs and the provider reports nothing. */
29
+ const BROWSER_WAIT = "Waiting for you to finish in the browser…";
30
+
31
+ /** The sign-in link from `showAuth`: the provider's instructions, or the device code split out of them. */
32
+ interface LoginAuth {
33
+ url: string;
34
+ launchUrl?: string;
35
+ instructions?: string;
36
+ device?: { code: string };
37
+ }
38
+
19
39
  /** One native step after the sign-in link: a text answer (live or given) or a progress message. */
20
40
  type LoginStep =
21
- | { kind: "input"; message: string; placeholder?: string; paste: boolean; field: TextFormField; answer?: string }
41
+ | { kind: "input"; message: string; field: TextFormField; answer?: string }
22
42
  | { kind: "progress"; message: string };
23
43
 
44
+ /** Sheet copy: plain text, a trailing ASCII ellipsis as `…`. */
45
+ function sheetText(message: string): string {
46
+ return plainText(message)
47
+ .trim()
48
+ .replace(/\.\.\.$/, "…");
49
+ }
50
+
51
+ /** `github.com/login/device` for a tooltip; the input when it isn't a URL. */
52
+ function urlPlace(url: string): string {
53
+ try {
54
+ const parsed = new URL(url);
55
+ return `${parsed.host}${parsed.pathname === "/" ? "" : parsed.pathname}`;
56
+ } catch {
57
+ return url;
58
+ }
59
+ }
60
+
24
61
  /**
25
62
  * Login dialog component - replaces editor during OAuth login flow
26
63
  */
@@ -35,8 +72,7 @@ export class LoginDialogComponent extends OverlayPanel {
35
72
  #inputRejecter?: (error: Error) => void;
36
73
  #inputAbortCleanup?: () => void;
37
74
  readonly #providerName: string;
38
- /** The sign-in link from `showAuth` (native step 1 and the device code). */
39
- #nativeAuth: { url: string; launchUrl?: string; instructions?: string } | undefined;
75
+ #nativeAuth: LoginAuth | undefined;
40
76
  /** Native steps after the link, in arrival order; the live input field is embedded by identity. */
41
77
  #nativeSteps: LoginStep[] = [];
42
78
  #nativeRoot: NativeNode | undefined;
@@ -134,10 +170,13 @@ export class LoginDialogComponent extends OverlayPanel {
134
170
  this.#contentContainer.addChild(new Text(theme.fg("warning", instructions), 0, 0));
135
171
  }
136
172
 
173
+ const plain = instructions ? plainText(instructions) : undefined;
174
+ const device = plain ? DEVICE_CODE_INSTRUCTIONS.exec(plain) : null;
137
175
  this.#nativeAuth = {
138
176
  url,
139
177
  launchUrl: launchUrl && launchUrl !== url ? launchUrl : undefined,
140
- instructions: instructions ? plainText(instructions) : undefined,
178
+ instructions: plain,
179
+ device: device ? { code: device[2]! } : undefined,
141
180
  };
142
181
  this.#nativeSteps = [];
143
182
  this.#nativeRoot = undefined;
@@ -158,10 +197,12 @@ export class LoginDialogComponent extends OverlayPanel {
158
197
  this.#input = this.#createInput();
159
198
  const nativeMounted = this.#nativeSteps.find(step => step.kind === "input" && step.field === previousInput);
160
199
  if (mounted !== -1) {
200
+ // The flow asks again only when the last paste carried no usable code.
201
+ this.#input.setError(RETRY_MESSAGE);
161
202
  this.#contentContainer.children.splice(mounted, 1, this.#input);
162
203
  if (nativeMounted?.kind === "input") nativeMounted.field = this.#input;
163
204
  } else {
164
- this.#nativeSteps.push({ kind: "input", message: plainText(prompt), paste: true, field: this.#input });
205
+ this.#nativeSteps.push({ kind: "input", message: plainText(prompt), field: this.#input });
165
206
  this.#contentContainer.addChild(new Spacer(1));
166
207
  this.#contentContainer.addChild(new Text(theme.fg("dim", prompt), 0, 0));
167
208
  this.#contentContainer.addChild(this.#input);
@@ -203,10 +244,11 @@ export class LoginDialogComponent extends OverlayPanel {
203
244
  this.#contentContainer.removeChild(this.#input);
204
245
  this.#contentContainer.children.splice(mounted, 0, answer);
205
246
  const answered = this.#nativeSteps.find(step => step.kind === "input" && step.field === this.#input);
206
- if (answered?.kind === "input") answered.answer = `${plainText(this.#input.input.prompt)}${value}`;
247
+ if (answered?.kind === "input") answered.answer = value;
207
248
  }
208
249
  // A new prompt must not recover a previous secret through undo or yank.
209
250
  this.#input = this.#createInput(prompt.secret === true);
251
+ if (prompt.placeholder) this.#input.input.placeholder = plainText(prompt.placeholder);
210
252
  this.#contentContainer.addChild(new Spacer(1));
211
253
  this.#contentContainer.addChild(new Text(theme.fg("text", prompt.message), 0, 0));
212
254
  if (prompt.placeholder) {
@@ -223,13 +265,7 @@ export class LoginDialogComponent extends OverlayPanel {
223
265
  0,
224
266
  ),
225
267
  );
226
- this.#nativeSteps.push({
227
- kind: "input",
228
- message: plainText(prompt.message),
229
- placeholder: prompt.placeholder ? plainText(prompt.placeholder) : undefined,
230
- paste: false,
231
- field: this.#input,
232
- });
268
+ this.#nativeSteps.push({ kind: "input", message: plainText(prompt.message), field: this.#input });
233
269
  this.#nativeRoot = undefined;
234
270
 
235
271
  this.#tui.requestRender();
@@ -273,56 +309,91 @@ export class LoginDialogComponent extends OverlayPanel {
273
309
 
274
310
  /**
275
311
  * A glass sheet over the composer (the dialog replaces the editor in the
276
- * dock; the `overlay` hoists into the terminal's layer): numbered steps —
277
- * open or copy the sign-in link, the device code, the live wait, the paste
278
- * field — and Cancel.
312
+ * dock; the `overlay` hoists into the terminal's layer). Top to bottom: one
313
+ * sentence saying what to do, the device code as a large click-to-copy
314
+ * well, the sign-in link as a read-only field with Copy, the questions
315
+ * (answered ones as label/value lines, the live one as a field), and the
316
+ * flow's status — finished stages checked, the current one spinning. The
317
+ * footer holds Cancel and one primary action: Continue while a field waits
318
+ * for an answer (Open sign-in page then leads the footer as a plain
319
+ * button), else Open sign-in page.
279
320
  */
280
321
  override describe(): NativeNode {
281
322
  if (this.#nativeRoot) return this.#nativeRoot;
282
- const steps: NativeChild[] = [];
283
323
  const auth = this.#nativeAuth;
324
+ const field = this.#activeField();
325
+ const body: NativeChild[] = [];
284
326
  if (auth) {
285
- steps.push(this.#describeLinkStep(auth.url, auth.launchUrl));
286
- if (auth.instructions) steps.push(this.#describeInstructionsStep(auth.instructions));
327
+ body.push(keyed(text(this.#lead(auth), { role: "zeta.login.lead", wrap: "word" }), "lead"));
328
+ if (auth.device) body.push(this.#describeCode(auth.device.code));
329
+ body.push(this.#describeLink(auth.url));
330
+ if (auth.launchUrl) {
331
+ body.push(
332
+ keyed(
333
+ text([span("On this machine ", "dim"), span(auth.launchUrl, "mono", { href: auth.launchUrl })], {
334
+ role: "zeta.login.shortcut",
335
+ truncate: "middle",
336
+ lines: 1,
337
+ title: auth.launchUrl,
338
+ }),
339
+ "shortcut",
340
+ ),
341
+ );
342
+ }
287
343
  }
344
+ const status: NativeNode[] = [];
288
345
  const live = this.#nativeSteps.findLastIndex(step => step.kind === "progress");
289
- // Progress messages share one step: settled ones dim, the latest one the live spinner.
290
- let progress: NativeChild[] | undefined;
291
346
  for (const [index, step] of this.#nativeSteps.entries()) {
292
347
  if (step.kind === "progress") {
293
- const line =
348
+ status.push(
294
349
  index === live
295
- ? node("row", { role: "zeta.login.waiting", gap: "sm", align: "center" }, [
296
- node("spinner", { label: step.message, tone: "muted" }),
297
- ])
298
- : text([span(step.message, "dim")], { wrap: "word" });
299
- if (progress) {
300
- progress.push(line);
301
- } else {
302
- progress = [line];
303
- steps.push(node("col", { role: "zeta.login.step", gap: "xs" }, progress, `p${index}`));
304
- }
350
+ ? this.#describeLive(step.message, `p${index}`)
351
+ : this.#describeDone(step.message, `p${index}`),
352
+ );
305
353
  continue;
306
354
  }
355
+ // A field label: the footer's Continue says the prompt's `then press Enter`.
356
+ const label = step.message.trim().replace(PRESS_KEY_SUFFIX, "").replace(/:$/, "");
307
357
  if (step.answer !== undefined) {
308
- steps.push(keyed(text([span(step.answer, "dim")]), `a${index}`));
309
- continue;
358
+ body.push(
359
+ node(
360
+ "row",
361
+ { role: "zeta.login.answer", gap: "sm", align: "baseline" },
362
+ [
363
+ keyed(text(label, { truncate: "end", lines: 1 }), "label"),
364
+ keyed(text(step.answer || "—", { truncate: "end", lines: 1 }), "value"),
365
+ ],
366
+ `a${index}`,
367
+ ),
368
+ );
369
+ } else {
370
+ body.push(
371
+ node(
372
+ "col",
373
+ { role: "zeta.login.prompt", gap: "sm" },
374
+ [keyed(text(label, { role: "zeta.login.label", wrap: "word" }), "label"), step.field],
375
+ `i${index}`,
376
+ ),
377
+ );
310
378
  }
311
- const children: NativeChild[] = [text(step.message, { wrap: "word" })];
312
- if (step.placeholder) children.push(text([span(`e.g., ${step.placeholder}`, "dim")]));
313
- children.push(node("col", { role: "zeta.login.paste" }, [step.field], "field"));
314
- steps.push(node("col", { role: "zeta.login.step", gap: "xs" }, children, `i${index}`));
315
379
  }
380
+ // The provider reports nothing while the browser half runs (GitHub's device poll): say so.
381
+ if (auth && live === -1 && !field) status.push(this.#describeLive(BROWSER_WAIT, "browser"));
382
+ if (status.length > 0) body.push(node("col", { role: "zeta.login.status", gap: "xs" }, status, "status"));
383
+
316
384
  const cancelKey = getKeybindings().getKeys("tui.select.cancel")[0];
317
- const buttons: (NativeNode | null)[] = [
318
- null,
319
- actionButton("Cancel", "cancel", cancelKey ? { keys: cancelKey } : {}),
320
- ];
321
- if (this.#activeField()) {
385
+ const cancel = actionButton("Cancel", "cancel", cancelKey ? { keys: cancelKey } : {});
386
+ const open = auth ? this.#describeOpen(auth.url, !field) : null;
387
+ let buttons: (NativeNode | null)[];
388
+ if (field) {
322
389
  const submitKey = getKeybindings().getKeys("tui.input.submit")[0];
323
- buttons.push(
324
- actionButton("Continue", "submit", { tone: "accent", ...(submitKey ? { keys: submitKey } : {}) }),
325
- );
390
+ const submit = actionButton("Continue", "submit", {
391
+ tone: "accent",
392
+ ...(submitKey ? { keys: submitKey } : {}),
393
+ });
394
+ buttons = open ? [open, null, cancel, submit] : [null, cancel, submit];
395
+ } else {
396
+ buttons = open ? [null, cancel, open] : [null, cancel];
326
397
  }
327
398
  const sheet = node(
328
399
  "overlay",
@@ -333,62 +404,101 @@ export class LoginDialogComponent extends OverlayPanel {
333
404
  size: "md",
334
405
  modal: true,
335
406
  },
336
- [
337
- col([node("col", { role: "zeta.login.steps", gap: "md" }, steps, "steps"), actionBar(buttons)], {
338
- gap: "lg",
339
- }),
340
- ],
407
+ [col([node("col", { role: "zeta.login.body", gap: "md" }, body, "body"), actionBar(buttons)], { gap: "lg" })],
341
408
  "sheet",
342
409
  );
343
410
  this.#nativeRoot = col([sheet]);
344
411
  return this.#nativeRoot;
345
412
  }
346
413
 
347
- /** Step 1: open the sign-in page (Tern opens the link) or copy it; the full URL beneath on one mono line. */
348
- #describeLinkStep(url: string, launchUrl: string | undefined): NativeNode {
349
- const children: NativeChild[] = [
350
- actionBar(
351
- [
352
- actionButton("Open sign-in page ↗", "open", { href: url, key: "openLink", title: url }),
353
- actionButton("Copy link", "copy", { href: url, key: "copyLink", title: "Copy the sign-in link" }),
354
- ],
355
- "links",
356
- ),
357
- text([span(url, "dim mono", { href: url })], {
358
- role: "zeta.login.url",
359
- truncate: "middle",
360
- lines: 1,
361
- title: url,
362
- }),
363
- ];
364
- if (launchUrl) {
365
- children.push(
366
- text(
367
- [span("Local shortcut (this machine only): ", "dim"), span(launchUrl, "dim link", { href: launchUrl })],
414
+ /** What to do, in one sentence: enter the code, follow the provider's instructions, or finish in the browser. */
415
+ #lead(auth: LoginAuth): string {
416
+ if (auth.device) return "Enter this code on the sign-in page to connect your account.";
417
+ if (auth.instructions) return sheetText(auth.instructions);
418
+ return "Continue in your browser. If it didn't open, open or copy the link below.";
419
+ }
420
+
421
+ /** The device code, large in a well; a click copies it. */
422
+ #describeCode(code: string): NativeNode {
423
+ return node(
424
+ "row",
425
+ { role: "zeta.login.code", align: "center", actions: { click: "copy" }, href: code, title: "Copy code" },
426
+ [keyed(text([span(code, "mono")]), "value"), node("icon", { name: "copy" }, undefined, "icon")],
427
+ "code",
428
+ );
429
+ }
430
+
431
+ /** The sign-in link as a read-only field: the full URL (middle-truncated; a click opens it) and Copy. */
432
+ #describeLink(url: string): NativeNode {
433
+ const shown = text([span(url, "mono", { href: url })], {
434
+ role: "zeta.login.url",
435
+ truncate: "middle",
436
+ lines: 1,
437
+ title: url,
438
+ });
439
+ return node(
440
+ "row",
441
+ { role: "zeta.login.link", gap: "sm", align: "center" },
442
+ [
443
+ node("icon", { name: "link" }, undefined, "icon"),
444
+ keyed(shown, "url"),
445
+ node(
446
+ "row",
368
447
  {
369
- truncate: "middle",
370
- lines: 1,
448
+ role: "zeta.btn",
449
+ align: "center",
450
+ actions: { click: "copy" },
451
+ href: url,
452
+ title: "Copy the sign-in link",
371
453
  },
454
+ [text("Copy")],
455
+ "copyLink",
372
456
  ),
373
- );
374
- }
375
- return node("col", { role: "zeta.login.step", gap: "sm" }, children, "link");
457
+ ],
458
+ "link",
459
+ );
376
460
  }
377
461
 
378
- /** Step 2: a device code large and click-to-copy, or the provider's instructions as they came. */
379
- #describeInstructionsStep(instructions: string): NativeNode {
380
- const device = DEVICE_CODE_INSTRUCTIONS.exec(instructions);
381
- const children: NativeChild[] = device
382
- ? [
383
- text(device[1]!.trimEnd()),
384
- text([span(device[2]!, "mono")], {
385
- role: "zeta.login.code",
386
- actions: { click: "copy" },
387
- title: "Copy code",
388
- }),
389
- ]
390
- : [text([span(instructions, "warning")], { wrap: "word" })];
391
- return node("col", { role: "zeta.login.step", gap: "sm" }, children, "code");
462
+ /** Open sign-in page (Tern opens the link itself): the primary action while nothing waits for an answer. */
463
+ #describeOpen(url: string, primary: boolean): NativeNode {
464
+ return node(
465
+ "row",
466
+ {
467
+ role: "zeta.btn",
468
+ gap: "xs",
469
+ align: "center",
470
+ actions: { click: "open" },
471
+ href: url,
472
+ title: `Open ${urlPlace(url)} in your browser`,
473
+ ...(primary ? { tone: "accent" as const } : {}),
474
+ },
475
+ [text("Open sign-in page"), node("icon", { name: "open" }, undefined, "icon")],
476
+ "openLink",
477
+ );
478
+ }
479
+
480
+ /** The stage running now: a spinner and what it waits for. */
481
+ #describeLive(message: string, key: string): NativeNode {
482
+ return node(
483
+ "row",
484
+ { role: "zeta.login.waiting", gap: "sm", align: "center" },
485
+ [node("spinner", { style: "orbit", label: sheetText(message), tone: "muted" })],
486
+ key,
487
+ );
488
+ }
489
+
490
+ /** A finished stage, checked; one that reported a problem keeps a warning mark. */
491
+ #describeDone(message: string, key: string): NativeNode {
492
+ const problem = PROGRESS_PROBLEM.test(message);
493
+ return node(
494
+ "row",
495
+ { role: "zeta.login.done", gap: "sm", align: "baseline", ...(problem ? { tone: "warning" as const } : {}) },
496
+ [
497
+ node("icon", { name: problem ? "warn" : "check" }, undefined, "icon"),
498
+ keyed(text(sheetText(message).replace(/…$/, ""), { wrap: "word" }), "text"),
499
+ ],
500
+ key,
501
+ );
392
502
  }
393
503
 
394
504
  /** The input field still waiting for an answer, if any. */