arc402-cli 0.7.5 → 0.9.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 (309) hide show
  1. package/INK6-UX-SPEC.md +446 -0
  2. package/MIGRATION-SPEC.md +108 -0
  3. package/dist/abis.js +14 -17
  4. package/dist/abis.js.map +1 -1
  5. package/dist/bundler.d.ts +1 -1
  6. package/dist/bundler.d.ts.map +1 -1
  7. package/dist/bundler.js +27 -61
  8. package/dist/bundler.js.map +1 -1
  9. package/dist/client.d.ts +1 -1
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +5 -9
  12. package/dist/client.js.map +1 -1
  13. package/dist/coinbase-smart-wallet.js +1 -4
  14. package/dist/coinbase-smart-wallet.js.map +1 -1
  15. package/dist/commands/accept.js +25 -28
  16. package/dist/commands/accept.js.map +1 -1
  17. package/dist/commands/agent-handshake.js +15 -18
  18. package/dist/commands/agent-handshake.js.map +1 -1
  19. package/dist/commands/agent.js +98 -104
  20. package/dist/commands/agent.js.map +1 -1
  21. package/dist/commands/agreements.js +62 -98
  22. package/dist/commands/agreements.js.map +1 -1
  23. package/dist/commands/arbitrator.js +45 -81
  24. package/dist/commands/arbitrator.js.map +1 -1
  25. package/dist/commands/arena-handshake.js +27 -30
  26. package/dist/commands/arena-handshake.js.map +1 -1
  27. package/dist/commands/arena.js +12 -18
  28. package/dist/commands/arena.js.map +1 -1
  29. package/dist/commands/backup.js +30 -36
  30. package/dist/commands/backup.js.map +1 -1
  31. package/dist/commands/cancel.js +15 -18
  32. package/dist/commands/cancel.js.map +1 -1
  33. package/dist/commands/channel.js +45 -81
  34. package/dist/commands/channel.js.map +1 -1
  35. package/dist/commands/coldstart.js +31 -34
  36. package/dist/commands/coldstart.js.map +1 -1
  37. package/dist/commands/config.js +23 -29
  38. package/dist/commands/config.js.map +1 -1
  39. package/dist/commands/contract-interaction.js +12 -15
  40. package/dist/commands/contract-interaction.js.map +1 -1
  41. package/dist/commands/daemon.d.ts.map +1 -1
  42. package/dist/commands/daemon.js +98 -135
  43. package/dist/commands/daemon.js.map +1 -1
  44. package/dist/commands/deliver.js +37 -76
  45. package/dist/commands/deliver.js.map +1 -1
  46. package/dist/commands/discover.js +24 -27
  47. package/dist/commands/discover.js.map +1 -1
  48. package/dist/commands/dispute.js +104 -110
  49. package/dist/commands/dispute.js.map +1 -1
  50. package/dist/commands/doctor.js +16 -55
  51. package/dist/commands/doctor.js.map +1 -1
  52. package/dist/commands/endpoint.js +56 -95
  53. package/dist/commands/endpoint.js.map +1 -1
  54. package/dist/commands/feed.js +11 -18
  55. package/dist/commands/feed.js.map +1 -1
  56. package/dist/commands/hire.js +37 -40
  57. package/dist/commands/hire.js.map +1 -1
  58. package/dist/commands/migrate.js +30 -33
  59. package/dist/commands/migrate.js.map +1 -1
  60. package/dist/commands/negotiate.d.ts.map +1 -1
  61. package/dist/commands/negotiate.js +34 -36
  62. package/dist/commands/negotiate.js.map +1 -1
  63. package/dist/commands/openshell.js +68 -104
  64. package/dist/commands/openshell.js.map +1 -1
  65. package/dist/commands/owner.js +17 -20
  66. package/dist/commands/owner.js.map +1 -1
  67. package/dist/commands/policy.js +41 -43
  68. package/dist/commands/policy.js.map +1 -1
  69. package/dist/commands/relay.d.ts.map +1 -1
  70. package/dist/commands/relay.js +18 -51
  71. package/dist/commands/relay.js.map +1 -1
  72. package/dist/commands/remediate.js +20 -23
  73. package/dist/commands/remediate.js.map +1 -1
  74. package/dist/commands/reputation.js +25 -27
  75. package/dist/commands/reputation.js.map +1 -1
  76. package/dist/commands/setup.js +65 -104
  77. package/dist/commands/setup.js.map +1 -1
  78. package/dist/commands/trust.js +17 -20
  79. package/dist/commands/trust.js.map +1 -1
  80. package/dist/commands/verify.js +18 -21
  81. package/dist/commands/verify.js.map +1 -1
  82. package/dist/commands/wallet.js +619 -625
  83. package/dist/commands/wallet.js.map +1 -1
  84. package/dist/commands/watch.js +33 -36
  85. package/dist/commands/watch.js.map +1 -1
  86. package/dist/commands/watchtower.js +37 -73
  87. package/dist/commands/watchtower.js.map +1 -1
  88. package/dist/commands/workroom.d.ts.map +1 -1
  89. package/dist/commands/workroom.js +138 -171
  90. package/dist/commands/workroom.js.map +1 -1
  91. package/dist/config.js +21 -65
  92. package/dist/config.js.map +1 -1
  93. package/dist/daemon/config.d.ts.map +1 -1
  94. package/dist/daemon/config.js +16 -53
  95. package/dist/daemon/config.js.map +1 -1
  96. package/dist/daemon/hire-listener.d.ts +3 -3
  97. package/dist/daemon/hire-listener.d.ts.map +1 -1
  98. package/dist/daemon/hire-listener.js +13 -47
  99. package/dist/daemon/hire-listener.js.map +1 -1
  100. package/dist/daemon/index.d.ts +1 -1
  101. package/dist/daemon/index.d.ts.map +1 -1
  102. package/dist/daemon/index.js +50 -88
  103. package/dist/daemon/index.js.map +1 -1
  104. package/dist/daemon/job-lifecycle.d.ts +1 -1
  105. package/dist/daemon/job-lifecycle.d.ts.map +1 -1
  106. package/dist/daemon/job-lifecycle.js +11 -51
  107. package/dist/daemon/job-lifecycle.js.map +1 -1
  108. package/dist/daemon/notify.d.ts +1 -1
  109. package/dist/daemon/notify.d.ts.map +1 -1
  110. package/dist/daemon/notify.js +19 -53
  111. package/dist/daemon/notify.js.map +1 -1
  112. package/dist/daemon/token-metering.js +8 -47
  113. package/dist/daemon/token-metering.js.map +1 -1
  114. package/dist/daemon/userops.d.ts +2 -2
  115. package/dist/daemon/userops.d.ts.map +1 -1
  116. package/dist/daemon/userops.js +23 -27
  117. package/dist/daemon/userops.js.map +1 -1
  118. package/dist/daemon/wallet-monitor.d.ts +1 -1
  119. package/dist/daemon/wallet-monitor.d.ts.map +1 -1
  120. package/dist/daemon/wallet-monitor.js +8 -12
  121. package/dist/daemon/wallet-monitor.js.map +1 -1
  122. package/dist/drain-v4.js +26 -64
  123. package/dist/drain-v4.js.map +1 -1
  124. package/dist/endpoint-config.js +20 -63
  125. package/dist/endpoint-config.js.map +1 -1
  126. package/dist/endpoint-notify.js +9 -48
  127. package/dist/endpoint-notify.js.map +1 -1
  128. package/dist/index.js +16 -50
  129. package/dist/index.js.map +1 -1
  130. package/dist/openshell-runtime.d.ts.map +1 -1
  131. package/dist/openshell-runtime.js +38 -82
  132. package/dist/openshell-runtime.js.map +1 -1
  133. package/dist/program.d.ts.map +1 -1
  134. package/dist/program.js +77 -83
  135. package/dist/program.js.map +1 -1
  136. package/dist/repl.js +25 -31
  137. package/dist/repl.js.map +1 -1
  138. package/dist/signing.js +3 -6
  139. package/dist/signing.js.map +1 -1
  140. package/dist/telegram-notify.js +3 -40
  141. package/dist/telegram-notify.js.map +1 -1
  142. package/dist/tui/App.d.ts +1 -9
  143. package/dist/tui/App.d.ts.map +1 -1
  144. package/dist/tui/App.js +87 -65
  145. package/dist/tui/App.js.map +1 -1
  146. package/dist/tui/Footer.js +4 -7
  147. package/dist/tui/Footer.js.map +1 -1
  148. package/dist/tui/Header.d.ts +1 -2
  149. package/dist/tui/Header.d.ts.map +1 -1
  150. package/dist/tui/Header.js +9 -14
  151. package/dist/tui/Header.js.map +1 -1
  152. package/dist/tui/InputLine.d.ts +1 -2
  153. package/dist/tui/InputLine.d.ts.map +1 -1
  154. package/dist/tui/InputLine.js +92 -46
  155. package/dist/tui/InputLine.js.map +1 -1
  156. package/dist/tui/Viewport.d.ts +5 -4
  157. package/dist/tui/Viewport.d.ts.map +1 -1
  158. package/dist/tui/Viewport.js +20 -13
  159. package/dist/tui/Viewport.js.map +1 -1
  160. package/dist/tui/WalletConnectPairing.d.ts +23 -0
  161. package/dist/tui/WalletConnectPairing.d.ts.map +1 -0
  162. package/dist/tui/WalletConnectPairing.js +75 -0
  163. package/dist/tui/WalletConnectPairing.js.map +1 -0
  164. package/dist/tui/components/Button.d.ts +7 -0
  165. package/dist/tui/components/Button.d.ts.map +1 -0
  166. package/dist/tui/components/Button.js +18 -0
  167. package/dist/tui/components/Button.js.map +1 -0
  168. package/dist/tui/components/CeremonyView.d.ts +13 -0
  169. package/dist/tui/components/CeremonyView.d.ts.map +1 -0
  170. package/dist/tui/components/CeremonyView.js +7 -0
  171. package/dist/tui/components/CeremonyView.js.map +1 -0
  172. package/dist/tui/components/CompletionDropdown.d.ts +7 -0
  173. package/dist/tui/components/CompletionDropdown.d.ts.map +1 -0
  174. package/dist/tui/components/CompletionDropdown.js +20 -0
  175. package/dist/tui/components/CompletionDropdown.js.map +1 -0
  176. package/dist/tui/components/ConfirmPrompt.d.ts +9 -0
  177. package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
  178. package/dist/tui/components/ConfirmPrompt.js +7 -0
  179. package/dist/tui/components/ConfirmPrompt.js.map +1 -0
  180. package/dist/tui/components/InteractiveTable.d.ts +14 -0
  181. package/dist/tui/components/InteractiveTable.d.ts.map +1 -0
  182. package/dist/tui/components/InteractiveTable.js +58 -0
  183. package/dist/tui/components/InteractiveTable.js.map +1 -0
  184. package/dist/tui/components/StepSpinner.d.ts +11 -0
  185. package/dist/tui/components/StepSpinner.d.ts.map +1 -0
  186. package/dist/tui/components/StepSpinner.js +29 -0
  187. package/dist/tui/components/StepSpinner.js.map +1 -0
  188. package/dist/tui/components/Toast.d.ts +18 -0
  189. package/dist/tui/components/Toast.d.ts.map +1 -0
  190. package/dist/tui/components/Toast.js +25 -0
  191. package/dist/tui/components/Toast.js.map +1 -0
  192. package/dist/tui/index.d.ts.map +1 -1
  193. package/dist/tui/index.js +28 -21
  194. package/dist/tui/index.js.map +1 -1
  195. package/dist/tui/useChat.js +13 -19
  196. package/dist/tui/useChat.js.map +1 -1
  197. package/dist/tui/useCommand.d.ts +2 -7
  198. package/dist/tui/useCommand.d.ts.map +1 -1
  199. package/dist/tui/useCommand.js +77 -165
  200. package/dist/tui/useCommand.js.map +1 -1
  201. package/dist/tui/useNotifications.d.ts +9 -0
  202. package/dist/tui/useNotifications.d.ts.map +1 -0
  203. package/dist/tui/useNotifications.js +14 -0
  204. package/dist/tui/useNotifications.js.map +1 -0
  205. package/dist/tui/useScroll.js +9 -12
  206. package/dist/tui/useScroll.js.map +1 -1
  207. package/dist/ui/banner.d.ts +12 -0
  208. package/dist/ui/banner.d.ts.map +1 -1
  209. package/dist/ui/banner.js +35 -19
  210. package/dist/ui/banner.js.map +1 -1
  211. package/dist/ui/colors.js +13 -19
  212. package/dist/ui/colors.js.map +1 -1
  213. package/dist/ui/format.js +6 -14
  214. package/dist/ui/format.js.map +1 -1
  215. package/dist/ui/spinner.js +6 -12
  216. package/dist/ui/spinner.js.map +1 -1
  217. package/dist/ui/tree.js +3 -6
  218. package/dist/ui/tree.js.map +1 -1
  219. package/dist/utils/format.js +27 -41
  220. package/dist/utils/format.js.map +1 -1
  221. package/dist/utils/hash.js +4 -42
  222. package/dist/utils/hash.js.map +1 -1
  223. package/dist/utils/time.js +2 -6
  224. package/dist/utils/time.js.map +1 -1
  225. package/dist/wallet-router.d.ts +1 -1
  226. package/dist/wallet-router.d.ts.map +1 -1
  227. package/dist/wallet-router.js +12 -19
  228. package/dist/wallet-router.js.map +1 -1
  229. package/dist/walletconnect-session.d.ts +1 -1
  230. package/dist/walletconnect-session.d.ts.map +1 -1
  231. package/dist/walletconnect-session.js +6 -11
  232. package/dist/walletconnect-session.js.map +1 -1
  233. package/dist/walletconnect.d.ts +6 -1
  234. package/dist/walletconnect.d.ts.map +1 -1
  235. package/dist/walletconnect.js +32 -35
  236. package/dist/walletconnect.js.map +1 -1
  237. package/package.json +7 -6
  238. package/src/bundler.ts +1 -1
  239. package/src/client.ts +1 -1
  240. package/src/commands/accept.ts +7 -7
  241. package/src/commands/agent-handshake.ts +4 -4
  242. package/src/commands/agent.ts +9 -9
  243. package/src/commands/agreements.ts +8 -8
  244. package/src/commands/arbitrator.ts +5 -5
  245. package/src/commands/arena-handshake.ts +6 -6
  246. package/src/commands/arena.ts +2 -2
  247. package/src/commands/backup.ts +1 -1
  248. package/src/commands/cancel.ts +6 -6
  249. package/src/commands/channel.ts +6 -6
  250. package/src/commands/coldstart.ts +5 -5
  251. package/src/commands/config.ts +2 -2
  252. package/src/commands/contract-interaction.ts +2 -2
  253. package/src/commands/daemon.ts +14 -11
  254. package/src/commands/deliver.ts +9 -9
  255. package/src/commands/discover.ts +5 -5
  256. package/src/commands/dispute.ts +7 -7
  257. package/src/commands/doctor.ts +2 -2
  258. package/src/commands/endpoint.ts +6 -6
  259. package/src/commands/feed.ts +1 -1
  260. package/src/commands/hire.ts +10 -10
  261. package/src/commands/migrate.ts +7 -7
  262. package/src/commands/negotiate.ts +6 -5
  263. package/src/commands/openshell.ts +4 -4
  264. package/src/commands/owner.ts +5 -5
  265. package/src/commands/policy.ts +5 -5
  266. package/src/commands/relay.ts +5 -1
  267. package/src/commands/remediate.ts +5 -5
  268. package/src/commands/reputation.ts +6 -6
  269. package/src/commands/setup.ts +1 -1
  270. package/src/commands/trust.ts +6 -6
  271. package/src/commands/verify.ts +6 -6
  272. package/src/commands/wallet.ts +15 -15
  273. package/src/commands/watch.ts +3 -3
  274. package/src/commands/watchtower.ts +6 -6
  275. package/src/commands/workroom.ts +14 -10
  276. package/src/daemon/config.ts +2 -1
  277. package/src/daemon/hire-listener.ts +3 -3
  278. package/src/daemon/index.ts +10 -9
  279. package/src/daemon/job-lifecycle.ts +1 -1
  280. package/src/daemon/notify.ts +4 -4
  281. package/src/daemon/userops.ts +4 -4
  282. package/src/daemon/wallet-monitor.ts +2 -2
  283. package/src/endpoint-notify.ts +1 -1
  284. package/src/index.ts +8 -7
  285. package/src/openshell-runtime.ts +5 -1
  286. package/src/program.ts +36 -36
  287. package/src/repl.ts +3 -3
  288. package/src/tui/App.tsx +75 -52
  289. package/src/tui/Header.tsx +26 -12
  290. package/src/tui/InputLine.tsx +108 -33
  291. package/src/tui/Viewport.tsx +22 -18
  292. package/src/tui/WalletConnectPairing.tsx +131 -0
  293. package/src/tui/components/Button.tsx +38 -0
  294. package/src/tui/components/CeremonyView.tsx +39 -0
  295. package/src/tui/components/CompletionDropdown.tsx +59 -0
  296. package/src/tui/components/ConfirmPrompt.tsx +36 -0
  297. package/src/tui/components/InteractiveTable.tsx +112 -0
  298. package/src/tui/components/StepSpinner.tsx +84 -0
  299. package/src/tui/components/Toast.tsx +59 -0
  300. package/src/tui/index.tsx +27 -9
  301. package/src/tui/useChat.ts +1 -1
  302. package/src/tui/useCommand.ts +86 -183
  303. package/src/tui/useNotifications.ts +28 -0
  304. package/src/ui/banner.ts +29 -2
  305. package/src/ui/tree.ts +1 -1
  306. package/src/wallet-router.ts +2 -2
  307. package/src/walletconnect-session.ts +1 -1
  308. package/src/walletconnect.ts +20 -5
  309. package/tsconfig.json +16 -7
@@ -1,7 +1,8 @@
1
- import React, { useState, useCallback } from "react";
1
+ import React, { useState, useCallback, useMemo } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import TextInput from "ink-text-input";
4
- import { createProgram } from "../program";
4
+ import { createProgram } from "../program.js";
5
+ import { CompletionDropdown } from "./components/CompletionDropdown.js";
5
6
 
6
7
  const BUILTIN_CMDS = ["help", "exit", "quit", "clear", "status"];
7
8
 
@@ -11,8 +12,7 @@ interface InputLineProps {
11
12
  }
12
13
 
13
14
  /**
14
- * Input line with command history navigation and tab completion.
15
- * Uses ink-text-input for text input with cursor.
15
+ * Input line with command history navigation, tab completion, and dropdown.
16
16
  */
17
17
  export function InputLine({ onSubmit, isDisabled = false }: InputLineProps) {
18
18
  const [value, setValue] = useState("");
@@ -20,6 +20,11 @@ export function InputLine({ onSubmit, isDisabled = false }: InputLineProps) {
20
20
  const [historyIdx, setHistoryIdx] = useState(-1);
21
21
  const [historyTemp, setHistoryTemp] = useState("");
22
22
 
23
+ // Dropdown state
24
+ const [showDropdown, setShowDropdown] = useState(false);
25
+ const [dropdownIdx, setDropdownIdx] = useState(0);
26
+ const [dropdownCandidates, setDropdownCandidates] = useState<string[]>([]);
27
+
23
28
  // Lazily build command list for tab completion
24
29
  const [topCmds] = useState<string[]>(() => {
25
30
  try {
@@ -44,12 +49,30 @@ export function InputLine({ onSubmit, isDisabled = false }: InputLineProps) {
44
49
  }
45
50
  });
46
51
 
52
+ const getCompletions = useCallback(
53
+ (input: string): string[] => {
54
+ const allTop = [...BUILTIN_CMDS, ...topCmds];
55
+ const trimmed = input.trimStart();
56
+ const spaceIdx = trimmed.indexOf(" ");
57
+
58
+ if (spaceIdx === -1) {
59
+ return allTop.filter((cmd) => cmd.startsWith(trimmed));
60
+ }
61
+ const parent = trimmed.slice(0, spaceIdx);
62
+ const rest = trimmed.slice(spaceIdx + 1);
63
+ const subs = subCmds.get(parent) ?? [];
64
+ return subs
65
+ .filter((s) => s.startsWith(rest))
66
+ .map((s) => `${parent} ${s}`);
67
+ },
68
+ [topCmds, subCmds]
69
+ );
70
+
47
71
  const handleSubmit = useCallback(
48
72
  (val: string) => {
49
73
  const trimmed = val.trim();
50
74
  if (!trimmed) return;
51
75
 
52
- // Add to history (avoid duplicate of last entry)
53
76
  setHistory((prev) => {
54
77
  if (prev[prev.length - 1] === trimmed) return prev;
55
78
  return [...prev, trimmed];
@@ -57,6 +80,7 @@ export function InputLine({ onSubmit, isDisabled = false }: InputLineProps) {
57
80
  setHistoryIdx(-1);
58
81
  setHistoryTemp("");
59
82
  setValue("");
83
+ setShowDropdown(false);
60
84
 
61
85
  onSubmit(trimmed);
62
86
  },
@@ -67,7 +91,33 @@ export function InputLine({ onSubmit, isDisabled = false }: InputLineProps) {
67
91
  (_input, key) => {
68
92
  if (isDisabled) return;
69
93
 
70
- // Up arrow history prev
94
+ // ── Dropdown navigation ───────────────────────────────────────────
95
+ if (showDropdown) {
96
+ if (key.upArrow) {
97
+ setDropdownIdx((i) => Math.max(0, i - 1));
98
+ return;
99
+ }
100
+ if (key.downArrow) {
101
+ setDropdownIdx((i) =>
102
+ Math.min(dropdownCandidates.length - 1, i + 1)
103
+ );
104
+ return;
105
+ }
106
+ if (key.return) {
107
+ // Select the highlighted candidate
108
+ if (dropdownCandidates[dropdownIdx]) {
109
+ setValue(dropdownCandidates[dropdownIdx] + " ");
110
+ }
111
+ setShowDropdown(false);
112
+ return;
113
+ }
114
+ if (key.escape) {
115
+ setShowDropdown(false);
116
+ return;
117
+ }
118
+ }
119
+
120
+ // ── History navigation ────────────────────────────────────────────
71
121
  if (key.upArrow) {
72
122
  setHistory((hist) => {
73
123
  setHistoryIdx((idx) => {
@@ -88,7 +138,6 @@ export function InputLine({ onSubmit, isDisabled = false }: InputLineProps) {
88
138
  return;
89
139
  }
90
140
 
91
- // Down arrow — history next
92
141
  if (key.downArrow) {
93
142
  setHistory((hist) => {
94
143
  setHistoryIdx((idx) => {
@@ -109,32 +158,27 @@ export function InputLine({ onSubmit, isDisabled = false }: InputLineProps) {
109
158
  return;
110
159
  }
111
160
 
112
- // Tab — completion
161
+ // ── Tab — completion / dropdown ───────────────────────────────────
113
162
  if (_input === "\t") {
114
- const allTop = [...BUILTIN_CMDS, ...topCmds];
115
- const trimmed = value.trimStart();
116
- const spaceIdx = trimmed.indexOf(" ");
163
+ const completions = getCompletions(value);
117
164
 
118
- let completions: string[];
119
- if (spaceIdx === -1) {
120
- completions = allTop.filter((cmd) => cmd.startsWith(trimmed));
121
- } else {
122
- const parent = trimmed.slice(0, spaceIdx);
123
- const rest = trimmed.slice(spaceIdx + 1);
124
- const subs = subCmds.get(parent) ?? [];
125
- completions = subs
126
- .filter((s) => s.startsWith(rest))
127
- .map((s) => `${parent} ${s}`);
165
+ if (completions.length === 0) {
166
+ setShowDropdown(false);
167
+ return;
128
168
  }
129
169
 
130
- if (completions.length === 0) return;
131
-
132
170
  if (completions.length === 1) {
133
171
  setValue(completions[0] + " ");
172
+ setShowDropdown(false);
134
173
  return;
135
174
  }
136
175
 
137
- // Find common prefix
176
+ // Show dropdown with multiple candidates
177
+ setDropdownCandidates(completions);
178
+ setDropdownIdx(0);
179
+ setShowDropdown(true);
180
+
181
+ // Also advance to common prefix
138
182
  const common = completions.reduce((a, b) => {
139
183
  let i = 0;
140
184
  while (i < a.length && i < b.length && a[i] === b[i]) i++;
@@ -144,21 +188,52 @@ export function InputLine({ onSubmit, isDisabled = false }: InputLineProps) {
144
188
  setValue(common);
145
189
  }
146
190
  }
191
+
192
+ // ── Escape — dismiss dropdown ─────────────────────────────────────
193
+ if (key.escape) {
194
+ setShowDropdown(false);
195
+ }
147
196
  },
148
197
  { isActive: !isDisabled }
149
198
  );
150
199
 
200
+ // Dismiss dropdown when value changes (user types more)
201
+ const handleChange = useCallback(
202
+ (newVal: string) => {
203
+ setValue(newVal);
204
+ // If dropdown is open, update candidates live
205
+ if (showDropdown) {
206
+ const completions = getCompletions(newVal);
207
+ if (completions.length <= 1) {
208
+ setShowDropdown(false);
209
+ } else {
210
+ setDropdownCandidates(completions);
211
+ setDropdownIdx(0);
212
+ }
213
+ }
214
+ },
215
+ [showDropdown, getCompletions]
216
+ );
217
+
151
218
  return (
152
- <Box>
153
- <Text color="cyan">◈</Text>
154
- <Text dimColor> arc402 </Text>
155
- <Text color="white">{">"} </Text>
156
- <TextInput
157
- value={value}
158
- onChange={setValue}
159
- onSubmit={handleSubmit}
160
- focus={!isDisabled}
219
+ <Box flexDirection="column">
220
+ {/* Completion dropdown renders above the input */}
221
+ <CompletionDropdown
222
+ candidates={dropdownCandidates}
223
+ selectedIndex={dropdownIdx}
224
+ visible={showDropdown}
161
225
  />
226
+ <Box>
227
+ <Text color="cyan">◈</Text>
228
+ <Text dimColor> arc402 </Text>
229
+ <Text color="white">{">"} </Text>
230
+ <TextInput
231
+ value={value}
232
+ onChange={handleChange}
233
+ onSubmit={handleSubmit}
234
+ focus={!isDisabled}
235
+ />
236
+ </Box>
162
237
  </Box>
163
238
  );
164
239
  }
@@ -5,30 +5,39 @@ interface ViewportProps {
5
5
  lines: string[];
6
6
  scrollOffset: number;
7
7
  isAutoScroll: boolean;
8
- innerWidth?: number;
9
8
  }
10
9
 
11
10
  /**
12
- * Scrollable output area framed with │ box-drawing borders.
13
- * Fills remaining terminal space between header and footer separators.
11
+ * Scrollable output area that fills remaining terminal space.
12
+ * Renders a window slice of the buffer, not terminal scroll.
13
+ * scrollOffset=0 means pinned to bottom (auto-scroll).
14
+ * Positive scrollOffset means scrolled up by that many lines.
14
15
  */
15
- export function Viewport({ lines, scrollOffset, isAutoScroll, innerWidth = 58 }: ViewportProps) {
16
+ export function Viewport({ lines, scrollOffset, isAutoScroll }: ViewportProps) {
16
17
  const { stdout } = useStdout();
17
18
  const termRows = stdout?.rows ?? 24;
18
19
 
19
- // Header (~14 rows) + top/mid/mid/bot borders (4) + footer input (1) = ~19 fixed rows
20
- const FIXED_ROWS = 19;
21
- const viewportHeight = Math.max(1, termRows - FIXED_ROWS);
20
+ // We'll compute the viewport height: total rows minus fixed areas
21
+ // Header is approximately bannerLines + separator (~14-16 rows)
22
+ // Footer is 1 row
23
+ // We'll use a reasonable estimate here; the parent App can pass exact height
24
+ const HEADER_ROWS = 15; // approximate
25
+ const FOOTER_ROWS = 1;
26
+ const viewportHeight = Math.max(1, termRows - HEADER_ROWS - FOOTER_ROWS);
22
27
 
23
28
  // Compute the window slice
29
+ // scrollOffset=0 → show last viewportHeight lines
30
+ // scrollOffset=N → show lines ending viewportHeight+N from end
24
31
  const totalLines = lines.length;
25
32
  let endIdx: number;
26
33
  let startIdx: number;
27
34
 
28
35
  if (scrollOffset === 0) {
36
+ // Auto-scroll: pinned to bottom
29
37
  endIdx = totalLines;
30
38
  startIdx = Math.max(0, endIdx - viewportHeight);
31
39
  } else {
40
+ // Scrolled up: scrollOffset lines from bottom
32
41
  endIdx = Math.max(0, totalLines - scrollOffset);
33
42
  startIdx = Math.max(0, endIdx - viewportHeight);
34
43
  }
@@ -46,19 +55,14 @@ export function Viewport({ lines, scrollOffset, isAutoScroll, innerWidth = 58 }:
46
55
 
47
56
  return (
48
57
  <Box flexDirection="column" flexGrow={1}>
49
- {paddedLines.map((line, i) => (
50
- <Box key={i}>
51
- <Text dimColor>│</Text>
52
- <Text>{" " + line}</Text>
53
- <Box flexGrow={1} />
54
- <Text dimColor>│</Text>
55
- </Box>
56
- ))}
58
+ <Box flexDirection="column" flexGrow={1}>
59
+ {paddedLines.map((line, i) => (
60
+ <Text key={i}>{line}</Text>
61
+ ))}
62
+ </Box>
57
63
  {canScrollDown && !isAutoScroll && (
58
64
  <Box justifyContent="flex-end">
59
- <Text dimColor>│ more</Text>
60
- <Box flexGrow={1} />
61
- <Text dimColor>│</Text>
65
+ <Text dimColor>↓ more</Text>
62
66
  </Box>
63
67
  )}
64
68
  </Box>
@@ -0,0 +1,131 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { WCCallbacks } from "../walletconnect.js";
4
+
5
+ export type WCStage = "connecting" | "connected" | "chain-switching" | "ready" | "error";
6
+
7
+ interface WalletConnectPairingProps {
8
+ projectId: string;
9
+ chainId: number;
10
+ onComplete: (result: { account: string }) => void;
11
+ onError: (err: string) => void;
12
+ /** Called once the component mounts — parent passes the connect function */
13
+ connect: (callbacks: WCCallbacks) => Promise<{ account: string }>;
14
+ }
15
+
16
+ /**
17
+ * Renders WalletConnect pairing inside the Ink TUI viewport:
18
+ * - ASCII QR code
19
+ * - Deep links for MetaMask, Rainbow, Trust, etc.
20
+ * - Status transitions: connecting → connected → chain-switching → ready
21
+ */
22
+ export function WalletConnectPairing({
23
+ onComplete,
24
+ onError,
25
+ connect,
26
+ }: WalletConnectPairingProps) {
27
+ const [stage, setStage] = useState<WCStage>("connecting");
28
+ const [uri, setUri] = useState<string | null>(null);
29
+ const [links, setLinks] = useState<Record<string, string>>({});
30
+ const [account, setAccount] = useState<string | null>(null);
31
+ const [qrLines, setQrLines] = useState<string[]>([]);
32
+ const [detail, setDetail] = useState<string>("");
33
+
34
+ const handleUri = useCallback((wcUri: string, wcLinks: Record<string, string>) => {
35
+ setUri(wcUri);
36
+ setLinks(wcLinks);
37
+ // Generate ASCII QR
38
+ try {
39
+ // qrcode-terminal writes to stdout — capture it
40
+ const origWrite = process.stdout.write.bind(process.stdout);
41
+ const captured: string[] = [];
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ (process.stdout as any).write = (chunk: string | Uint8Array) => {
44
+ const str = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
45
+ captured.push(str);
46
+ return true;
47
+ };
48
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
49
+ import("qrcode-terminal").then((qr) => {
50
+ qr.default.generate(wcUri, { small: true }, (code: string) => {
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ (process.stdout as any).write = origWrite;
53
+ setQrLines(code.split("\n"));
54
+ });
55
+ }).catch(() => {
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ (process.stdout as any).write = origWrite;
58
+ });
59
+ } catch {
60
+ // QR rendering is best-effort
61
+ }
62
+ }, []);
63
+
64
+ const handleStatus = useCallback((status: WCStage, statusDetail?: string) => {
65
+ setStage(status);
66
+ if (statusDetail) setDetail(statusDetail);
67
+ if (status === "connected" && statusDetail) {
68
+ setAccount(statusDetail);
69
+ }
70
+ }, []);
71
+
72
+ useEffect(() => {
73
+ const callbacks: WCCallbacks = {
74
+ onUri: handleUri,
75
+ onStatus: handleStatus,
76
+ };
77
+ connect(callbacks)
78
+ .then((result) => onComplete(result))
79
+ .catch((err: unknown) => onError(err instanceof Error ? err.message : String(err)));
80
+ }, [connect, handleUri, handleStatus, onComplete, onError]);
81
+
82
+ const statusIcon = stage === "error" ? "✗" : stage === "ready" ? "✓" : "◈";
83
+ const statusColor = stage === "error" ? "red" : stage === "ready" ? "green" : "cyan";
84
+
85
+ const statusMessages: Record<WCStage, string> = {
86
+ connecting: "Waiting for wallet approval...",
87
+ connected: `Connected: ${account ?? ""}`,
88
+ "chain-switching": `Switching chain${detail ? `: ${detail}` : ""}...`,
89
+ ready: `Ready — ${account ?? ""}`,
90
+ error: detail || "Connection failed",
91
+ };
92
+
93
+ return (
94
+ <Box flexDirection="column" paddingLeft={1}>
95
+ <Text color="cyan" bold>WalletConnect Pairing</Text>
96
+ <Text> </Text>
97
+
98
+ {/* Status */}
99
+ <Box>
100
+ <Text color={statusColor}>{statusIcon} </Text>
101
+ <Text>{statusMessages[stage]}</Text>
102
+ </Box>
103
+ <Text> </Text>
104
+
105
+ {/* Deep links */}
106
+ {uri && stage === "connecting" && (
107
+ <>
108
+ <Text dimColor>Tap a link for your wallet app:</Text>
109
+ <Text> </Text>
110
+ {Object.entries(links).map(([name, link]) => (
111
+ <Box key={name} flexDirection="column">
112
+ <Text color="white">{name}:</Text>
113
+ <Text dimColor>{link}</Text>
114
+ <Text> </Text>
115
+ </Box>
116
+ ))}
117
+
118
+ {/* QR code */}
119
+ {qrLines.length > 0 && (
120
+ <>
121
+ <Text dimColor>Or scan QR:</Text>
122
+ {qrLines.map((line, i) => (
123
+ <Text key={i}>{line}</Text>
124
+ ))}
125
+ </>
126
+ )}
127
+ </>
128
+ )}
129
+ </Box>
130
+ );
131
+ }
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import { Box, Text, useFocus, useInput } from "ink";
3
+
4
+ export interface ButtonProps {
5
+ label: string;
6
+ onPress: () => void;
7
+ variant?: "primary" | "danger" | "dim";
8
+ }
9
+
10
+ const VARIANT_COLORS: Record<string, string> = {
11
+ primary: "cyan",
12
+ danger: "red",
13
+ dim: "gray",
14
+ };
15
+
16
+ export function Button({ label, onPress, variant = "primary" }: ButtonProps) {
17
+ const { isFocused } = useFocus();
18
+
19
+ useInput(
20
+ (_input, key) => {
21
+ if (key.return) {
22
+ onPress();
23
+ }
24
+ },
25
+ { isActive: isFocused }
26
+ );
27
+
28
+ const color = isFocused ? VARIANT_COLORS[variant] ?? "cyan" : "white";
29
+
30
+ return (
31
+ <Box>
32
+ <Text color={color} bold={isFocused}>
33
+ {isFocused ? "▸ " : " "}
34
+ {label}
35
+ </Text>
36
+ </Box>
37
+ );
38
+ }
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { StepSpinner } from "./StepSpinner.js";
4
+ import type { StepStatus } from "./StepSpinner.js";
5
+
6
+ export interface CeremonyStep {
7
+ label: string;
8
+ status: StepStatus;
9
+ detail?: string;
10
+ error?: string;
11
+ }
12
+
13
+ export interface CeremonyViewProps {
14
+ title: string;
15
+ steps: CeremonyStep[];
16
+ }
17
+
18
+ export function CeremonyView({ title, steps }: CeremonyViewProps) {
19
+ return (
20
+ <Box flexDirection="column">
21
+ <Box marginBottom={1}>
22
+ <Text bold color="cyan">
23
+ ◈ {title}
24
+ </Text>
25
+ </Box>
26
+ {steps.map((step, i) => (
27
+ <StepSpinner
28
+ key={i}
29
+ step={i + 1}
30
+ total={steps.length}
31
+ label={step.label}
32
+ status={step.status}
33
+ detail={step.detail}
34
+ error={step.error}
35
+ />
36
+ ))}
37
+ </Box>
38
+ );
39
+ }
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ export interface CompletionDropdownProps {
5
+ candidates: string[];
6
+ selectedIndex: number;
7
+ visible: boolean;
8
+ }
9
+
10
+ const MAX_VISIBLE = 8;
11
+
12
+ export function CompletionDropdown({
13
+ candidates,
14
+ selectedIndex,
15
+ visible,
16
+ }: CompletionDropdownProps) {
17
+ if (!visible || candidates.length === 0) return null;
18
+
19
+ // Window the list if there are too many candidates
20
+ let startIdx = 0;
21
+ if (candidates.length > MAX_VISIBLE) {
22
+ startIdx = Math.max(0, selectedIndex - Math.floor(MAX_VISIBLE / 2));
23
+ startIdx = Math.min(startIdx, candidates.length - MAX_VISIBLE);
24
+ }
25
+ const visibleCandidates = candidates.slice(
26
+ startIdx,
27
+ startIdx + MAX_VISIBLE
28
+ );
29
+
30
+ return (
31
+ <Box flexDirection="column" marginLeft={4}>
32
+ <Box>
33
+ <Text dimColor>{"┌─ completions ─"}</Text>
34
+ </Box>
35
+ {visibleCandidates.map((candidate, i) => {
36
+ const actualIdx = startIdx + i;
37
+ const isSelected = actualIdx === selectedIndex;
38
+ return (
39
+ <Box key={candidate}>
40
+ <Text dimColor>{"│"}</Text>
41
+ <Text color={isSelected ? "cyan" : "white"} bold={isSelected}>
42
+ {isSelected ? " ▸ " : " "}
43
+ {candidate}
44
+ </Text>
45
+ </Box>
46
+ );
47
+ })}
48
+ <Box>
49
+ <Text dimColor>{"└─"}</Text>
50
+ {candidates.length > MAX_VISIBLE && (
51
+ <Text dimColor>
52
+ {" "}
53
+ ({candidates.length} total)
54
+ </Text>
55
+ )}
56
+ </Box>
57
+ </Box>
58
+ );
59
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { Button } from "./Button.js";
4
+
5
+ export interface ConfirmPromptProps {
6
+ message: string;
7
+ onConfirm: () => void;
8
+ onCancel: () => void;
9
+ confirmLabel?: string;
10
+ cancelLabel?: string;
11
+ }
12
+
13
+ export function ConfirmPrompt({
14
+ message,
15
+ onConfirm,
16
+ onCancel,
17
+ confirmLabel = "Confirm",
18
+ cancelLabel = "Cancel",
19
+ }: ConfirmPromptProps) {
20
+ return (
21
+ <Box flexDirection="column">
22
+ <Box marginBottom={1}>
23
+ <Text bold color="cyan">
24
+ ◈ {message}
25
+ </Text>
26
+ </Box>
27
+ <Box gap={2}>
28
+ <Button label={confirmLabel} onPress={onConfirm} variant="primary" />
29
+ <Button label={cancelLabel} onPress={onCancel} variant="dim" />
30
+ </Box>
31
+ <Box marginTop={1}>
32
+ <Text dimColor>Tab to switch · Enter to select</Text>
33
+ </Box>
34
+ </Box>
35
+ );
36
+ }