quoxcli 0.0.0-stage → 0.2.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 (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,270 @@
1
+ /**
2
+ * DataTable — Paginated, selectable data table for list views
3
+ *
4
+ * Designed for the left panel of dual-panel layouts.
5
+ * Supports vim-style navigation, pagination, and keyboard selection.
6
+ *
7
+ * Props:
8
+ * columns - array of { key, label, width? } — a column with no
9
+ * `width` (or explicit `flex: true`) absorbs a share
10
+ * of the remaining terminal width alongside other
11
+ * flex columns. Columns with a numeric `width` are
12
+ * fixed and unaffected by flex distribution.
13
+ * rows - array of data objects
14
+ * selected - controlled selected row index
15
+ * onSelect - callback on Enter (receives row object)
16
+ * onSelectionChange- callback on cursor move (receives index)
17
+ * pageSize - rows per page (default: derived from terminal rows)
18
+ * loading - show spinner overlay
19
+ * emptyMessage - text when rows is empty
20
+ * active - accept keyboard input (default: true)
21
+ */
22
+
23
+ import React, { useState, useMemo, useEffect } from 'react';
24
+ import { Box, Text, useInput } from 'ink';
25
+ import { Spinner } from '@inkjs/ui';
26
+ import { matchKey } from '../keys.js';
27
+ import { colors } from '../theme.js';
28
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
29
+
30
+ // Minimum rows per page when deriving from a short terminal.
31
+ const MIN_PAGE_SIZE = 5;
32
+ // Rows reserved for header/footer/borders/etc. when deriving page size.
33
+ const CHROME_ROWS = 8;
34
+ // Width reserved for the selection marker ("▶ " / " ") before columns start.
35
+ const MARKER_WIDTH = 2;
36
+ // Floor width for a flex column so it never collapses to unreadable size.
37
+ const MIN_FLEX_WIDTH = 6;
38
+
39
+ export function DataTable({
40
+ columns,
41
+ rows = [],
42
+ selected: controlledSelected,
43
+ onSelect,
44
+ onSelectionChange,
45
+ pageSize: pageSizeProp,
46
+ loading = false,
47
+ emptyMessage = 'No items',
48
+ active = true,
49
+ }) {
50
+ // ── Responsive page size (only when caller doesn't pass one) ────────
51
+ const { rows: termRows, columns: termColumns } = useTerminalSize();
52
+ const pageSize = pageSizeProp != null
53
+ ? pageSizeProp
54
+ : Math.max(MIN_PAGE_SIZE, termRows - CHROME_ROWS);
55
+
56
+ // ── Internal cursor state ───────────────────────────────────────────
57
+ const [internalSelected, setInternalSelected] = useState(0);
58
+
59
+ // Use controlled selection if provided, otherwise internal
60
+ const isControlled = controlledSelected !== undefined;
61
+ const selected = isControlled ? controlledSelected : internalSelected;
62
+
63
+ const setSelected = (valOrFn) => {
64
+ const nextVal = typeof valOrFn === 'function' ? valOrFn(selected) : valOrFn;
65
+ if (!isControlled) {
66
+ setInternalSelected(nextVal);
67
+ }
68
+ if (onSelectionChange) {
69
+ onSelectionChange(nextVal);
70
+ }
71
+ };
72
+
73
+ // ── Pagination ──────────────────────────────────────────────────────
74
+ const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
75
+
76
+ const currentPage = useMemo(() => {
77
+ return Math.floor(selected / pageSize);
78
+ }, [selected, pageSize]);
79
+
80
+ const pageRows = useMemo(() => {
81
+ const start = currentPage * pageSize;
82
+ return rows.slice(start, start + pageSize);
83
+ }, [rows, currentPage, pageSize]);
84
+
85
+ const pageOffset = currentPage * pageSize;
86
+
87
+ // ── Reset selection when rows change ────────────────────────────────
88
+ useEffect(() => {
89
+ if (rows.length > 0 && selected >= rows.length) {
90
+ setSelected(Math.max(0, rows.length - 1));
91
+ }
92
+ }, [rows.length]);
93
+
94
+ // ── Keyboard handling ───────────────────────────────────────────────
95
+ useInput((input, key) => {
96
+ if (!active || rows.length === 0) return;
97
+
98
+ // Cursor movement: j/down, k/up
99
+ if (matchKey('down', input, key)) {
100
+ setSelected((s) => (s < rows.length - 1 ? s + 1 : 0));
101
+ return;
102
+ }
103
+ if (matchKey('up', input, key)) {
104
+ setSelected((s) => (s > 0 ? s - 1 : rows.length - 1));
105
+ return;
106
+ }
107
+
108
+ // Page navigation: n = next page, p = prev page
109
+ if (matchKey('nextPage', input, key)) {
110
+ const nextPageStart = Math.min((currentPage + 1) * pageSize, rows.length - 1);
111
+ if (currentPage < totalPages - 1) {
112
+ setSelected(nextPageStart);
113
+ }
114
+ return;
115
+ }
116
+ if (matchKey('prevPage', input, key)) {
117
+ const prevPageStart = Math.max((currentPage - 1) * pageSize, 0);
118
+ if (currentPage > 0) {
119
+ setSelected(prevPageStart);
120
+ }
121
+ return;
122
+ }
123
+
124
+ // Select with Enter
125
+ if (matchKey('select', input, key)) {
126
+ if (onSelect && rows[selected]) {
127
+ onSelect(rows[selected]);
128
+ }
129
+ return;
130
+ }
131
+ });
132
+
133
+ // ── Column widths ───────────────────────────────────────────────────
134
+ // A column with an explicit numeric `width` is fixed (unchanged from
135
+ // legacy behavior). A column with `flex: true` or no `width` at all is a
136
+ // flex column: it absorbs an equal share of whatever terminal width is
137
+ // left over after fixed columns and the selection marker are accounted
138
+ // for, so long values (titles, URLs) expand on wide terminals instead of
139
+ // always being clipped at the old hard-coded 40-char cap.
140
+ const colWidths = useMemo(() => {
141
+ if (columns.length === 0) return [];
142
+
143
+ const isFlex = (col) => col.flex === true || col.width == null;
144
+
145
+ // Content-based width (legacy auto-calc): used as the fallback when no
146
+ // usable terminal width is available.
147
+ const contentWidth = (col) => {
148
+ const headerLen = col.label.length;
149
+ const maxDataLen = rows.reduce((max, row) => {
150
+ const val = String(row[col.key] ?? '');
151
+ return Math.max(max, val.length);
152
+ }, 0);
153
+ return Math.min(Math.max(headerLen, maxDataLen) + 2, 40);
154
+ };
155
+
156
+ const flexIndices = columns
157
+ .map((col, i) => (isFlex(col) ? i : -1))
158
+ .filter((i) => i !== -1);
159
+
160
+ // No flex columns — every width is explicit, behavior is unchanged.
161
+ if (flexIndices.length === 0) {
162
+ return columns.map((col) => col.width);
163
+ }
164
+
165
+ const fixedTotal = columns.reduce(
166
+ (sum, col) => sum + (isFlex(col) ? 0 : col.width),
167
+ 0
168
+ );
169
+
170
+ // No terminal width info at all — fall back to legacy content-based sizing.
171
+ if (!termColumns) {
172
+ return columns.map((col) => (isFlex(col) ? contentWidth(col) : col.width));
173
+ }
174
+
175
+ const available = termColumns - MARKER_WIDTH - fixedTotal;
176
+ const perFlex = Math.floor(available / flexIndices.length);
177
+ const flexWidth = Math.max(MIN_FLEX_WIDTH, perFlex);
178
+
179
+ return columns.map((col) => (isFlex(col) ? flexWidth : col.width));
180
+ }, [columns, rows, termColumns]);
181
+
182
+ // ── Render: Loading state ───────────────────────────────────────────
183
+ if (loading) {
184
+ return (
185
+ <Box flexDirection="column" paddingY={1}>
186
+ <Spinner label="Loading..." />
187
+ </Box>
188
+ );
189
+ }
190
+
191
+ // ── Render: Empty state ─────────────────────────────────────────────
192
+ if (rows.length === 0) {
193
+ return (
194
+ <Box flexDirection="column" paddingY={1}>
195
+ <Text color={colors.dim}>{emptyMessage}</Text>
196
+ </Box>
197
+ );
198
+ }
199
+
200
+ // ── Render: Table ───────────────────────────────────────────────────
201
+ return (
202
+ <Box flexDirection="column">
203
+ {/* Column headers */}
204
+ <Box>
205
+ <Text color={colors.dim}> </Text>
206
+ {columns.map((col, ci) => (
207
+ <Box key={col.key} width={colWidths[ci]}>
208
+ <Text bold dimColor>
209
+ {col.label}
210
+ </Text>
211
+ </Box>
212
+ ))}
213
+ </Box>
214
+
215
+ {/* Header separator */}
216
+ <Box>
217
+ <Text color={colors.subtle}>
218
+ {' '}{columns.map((_, ci) => '\u2500'.repeat(Math.max(1, colWidths[ci] - 1))).join(' ')}
219
+ </Text>
220
+ </Box>
221
+
222
+ {/* Data rows */}
223
+ {pageRows.map((row, ri) => {
224
+ const absoluteIndex = pageOffset + ri;
225
+ const isSelected = absoluteIndex === selected;
226
+ return (
227
+ <Box key={absoluteIndex}>
228
+ {/* Selection marker */}
229
+ <Text color={isSelected ? colors.cyberBlue : colors.subtle}>
230
+ {isSelected ? '\u25B6 ' : ' '}
231
+ </Text>
232
+
233
+ {/* Cell values */}
234
+ {columns.map((col, ci) => {
235
+ const rawVal = row[col.key];
236
+ const val = rawVal === null || rawVal === undefined ? '' : String(rawVal);
237
+ return (
238
+ <Box key={col.key} width={colWidths[ci]}>
239
+ <Text
240
+ color={isSelected ? colors.text : colors.textMuted}
241
+ bold={isSelected}
242
+ >
243
+ {val.length > colWidths[ci] - 1
244
+ ? val.slice(0, colWidths[ci] - 2) + '\u2026'
245
+ : val}
246
+ </Text>
247
+ </Box>
248
+ );
249
+ })}
250
+ </Box>
251
+ );
252
+ })}
253
+
254
+ {/* Page indicator */}
255
+ {totalPages > 1 && (
256
+ <Box marginTop={1}>
257
+ <Text color={colors.dim}>
258
+ Page {currentPage + 1} of {totalPages}
259
+ {' '}({rows.length} items)
260
+ {' '}
261
+ </Text>
262
+ <Text color={colors.cyberBlue}>n</Text>
263
+ <Text color={colors.dim}>/</Text>
264
+ <Text color={colors.cyberBlue}>p</Text>
265
+ <Text color={colors.dim}> next/prev</Text>
266
+ </Box>
267
+ )}
268
+ </Box>
269
+ );
270
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * DetailPanel — Scrollable key-value display for the right panel
3
+ *
4
+ * Shows properties of a selected item as labeled key-value pairs.
5
+ * Handles nested objects (JSON.stringify) and arrays (join with ', ').
6
+ *
7
+ * When content exceeds the available vertical space (derived from
8
+ * useTerminalSize().rows, or the `maxHeight` prop), the panel renders a
9
+ * windowed slice of its content lines instead of silently overflowing, and
10
+ * shows a scroll indicator (`▲ N more` / `▼ N more`) for whatever is
11
+ * hidden off-screen.
12
+ *
13
+ * Props:
14
+ * data - object to display (keys become labels, values become text)
15
+ * title - optional heading string
16
+ * loading - show spinner when true
17
+ * focused - when true, the panel handles Up/Down/k/j/PgUp/PgDn itself
18
+ * to move the scroll offset (default: false — content is
19
+ * top-anchored, clamped, with an overflow hint if needed)
20
+ * maxHeight - override the auto-derived (terminal-size-based) viewport
21
+ * height used for windowing
22
+ */
23
+
24
+ import React, { useState, useEffect, useMemo } from 'react';
25
+ import { Box, Text, useInput } from 'ink';
26
+ import { Spinner } from '@inkjs/ui';
27
+ import { matchKey } from '../keys.js';
28
+ import { colors } from '../theme.js';
29
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
30
+
31
+ // Rows reserved for title/borders/scroll-indicator when deriving viewport height.
32
+ const CHROME_ROWS = 6;
33
+ // Minimum viewport height when deriving from a short terminal.
34
+ const MIN_HEIGHT = 5;
35
+
36
+ /**
37
+ * Format a key string into a readable label.
38
+ * Converts camelCase and snake_case to Title Case.
39
+ */
40
+ function formatLabel(key) {
41
+ return key
42
+ .replace(/_/g, ' ')
43
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
44
+ .replace(/\b\w/g, (c) => c.toUpperCase());
45
+ }
46
+
47
+ /**
48
+ * Format a value for display.
49
+ * - null/undefined => dim dash
50
+ * - arrays => comma-separated
51
+ * - objects => JSON string
52
+ * - booleans => Yes/No with color
53
+ * - everything else => string
54
+ */
55
+ function formatValue(value) {
56
+ if (value === null || value === undefined) {
57
+ return { text: '—', color: colors.dim };
58
+ }
59
+
60
+ if (typeof value === 'boolean') {
61
+ return {
62
+ text: value ? 'Yes' : 'No',
63
+ color: value ? colors.success : colors.dim,
64
+ };
65
+ }
66
+
67
+ if (Array.isArray(value)) {
68
+ if (value.length === 0) return { text: '(empty)', color: colors.dim };
69
+ return { text: value.join(', '), color: colors.text };
70
+ }
71
+
72
+ if (typeof value === 'object') {
73
+ try {
74
+ return { text: JSON.stringify(value, null, 2), color: colors.textMuted };
75
+ } catch {
76
+ return { text: String(value), color: colors.textMuted };
77
+ }
78
+ }
79
+
80
+ return { text: String(value), color: colors.text };
81
+ }
82
+
83
+ /**
84
+ * Flatten data entries into a linear list of renderable "lines" so the
85
+ * panel can window/scroll through them regardless of whether an entry is a
86
+ * single inline row or a multi-line JSON block.
87
+ */
88
+ function buildLines(entries) {
89
+ const lines = [];
90
+
91
+ for (const [key, value] of entries) {
92
+ const { text, color } = formatValue(value);
93
+ const label = formatLabel(key);
94
+ const isBlock = typeof value === 'object' && value !== null && !Array.isArray(value);
95
+
96
+ if (isBlock) {
97
+ lines.push({ id: `${key}:label`, kind: 'label', label: `${label}:` });
98
+ text.split('\n').forEach((jsonLine, i) => {
99
+ lines.push({ id: `${key}:${i}`, kind: 'block', text: jsonLine, color });
100
+ });
101
+ } else {
102
+ lines.push({ id: key, kind: 'inline', label: `${label}: `, text, color });
103
+ }
104
+ }
105
+
106
+ return lines;
107
+ }
108
+
109
+ export function DetailPanel({ data, title, loading = false, focused = false, maxHeight }) {
110
+ const { rows: termRows } = useTerminalSize();
111
+ const viewportHeight = maxHeight != null
112
+ ? maxHeight
113
+ : Math.max(MIN_HEIGHT, termRows - CHROME_ROWS);
114
+
115
+ const entries = useMemo(() => (data ? Object.entries(data) : []), [data]);
116
+ const lines = useMemo(() => buildLines(entries), [entries]);
117
+
118
+ const total = lines.length;
119
+ const needsScroll = total > viewportHeight;
120
+ const maxOffset = needsScroll ? total - viewportHeight : 0;
121
+
122
+ const [offset, setOffset] = useState(0);
123
+
124
+ // Reset scroll position whenever the underlying data changes.
125
+ useEffect(() => {
126
+ setOffset(0);
127
+ }, [data]);
128
+
129
+ // Clamp if the viewport grew (e.g. terminal resize) past the current offset.
130
+ useEffect(() => {
131
+ setOffset((o) => Math.min(o, maxOffset));
132
+ }, [maxOffset]);
133
+
134
+ useInput((input, key) => {
135
+ if (!focused || !needsScroll) return;
136
+
137
+ if (matchKey('down', input, key)) {
138
+ setOffset((o) => Math.min(maxOffset, o + 1));
139
+ return;
140
+ }
141
+ if (matchKey('up', input, key)) {
142
+ setOffset((o) => Math.max(0, o - 1));
143
+ return;
144
+ }
145
+ if (key.pageDown) {
146
+ setOffset((o) => Math.min(maxOffset, o + viewportHeight));
147
+ return;
148
+ }
149
+ if (key.pageUp) {
150
+ setOffset((o) => Math.max(0, o - viewportHeight));
151
+ return;
152
+ }
153
+ });
154
+
155
+ // ── Loading state ───────────────────────────────────────────────────
156
+ if (loading) {
157
+ return (
158
+ <Box flexDirection="column" paddingY={1}>
159
+ <Spinner label="Loading details..." />
160
+ </Box>
161
+ );
162
+ }
163
+
164
+ // ── Empty state ─────────────────────────────────────────────────────
165
+ if (!data) {
166
+ return (
167
+ <Box flexDirection="column" paddingY={1}>
168
+ <Text color={colors.dim}>No item selected</Text>
169
+ </Box>
170
+ );
171
+ }
172
+
173
+ const visibleLines = needsScroll ? lines.slice(offset, offset + viewportHeight) : lines;
174
+ // Gate on needsScroll, not just offset>0: `offset` state can be briefly
175
+ // stale for one render after `data` changes (before the reset effect
176
+ // runs), and without this guard that stale offset would flash a false
177
+ // "more above" hint even though the new content fits without scrolling.
178
+ const hiddenAbove = needsScroll ? offset : 0;
179
+ const hiddenBelow = needsScroll ? total - (offset + visibleLines.length) : 0;
180
+
181
+ // ── Render ──────────────────────────────────────────────────────────
182
+ return (
183
+ <Box flexDirection="column">
184
+ {/* Title */}
185
+ {title && (
186
+ <Box marginBottom={1}>
187
+ <Text color={colors.accent} bold>
188
+ {title}
189
+ </Text>
190
+ </Box>
191
+ )}
192
+
193
+ {/* Scroll-up indicator */}
194
+ {hiddenAbove > 0 && (
195
+ <Box>
196
+ <Text color={colors.dim}>{'▲'} {hiddenAbove} more</Text>
197
+ </Box>
198
+ )}
199
+
200
+ {/* Windowed key-value lines */}
201
+ {visibleLines.map((line, _i) => {
202
+ if (line.kind === 'label') {
203
+ return (
204
+ <Box key={`${line.id}-${_i}`}>
205
+ <Text color={colors.textMuted} bold>
206
+ {line.label}
207
+ </Text>
208
+ </Box>
209
+ );
210
+ }
211
+ if (line.kind === 'block') {
212
+ return (
213
+ <Box key={`${line.id}-${_i}`} marginLeft={2}>
214
+ <Text color={line.color}>{line.text}</Text>
215
+ </Box>
216
+ );
217
+ }
218
+ return (
219
+ <Box key={`${line.id}-${_i}`}>
220
+ <Text color={colors.textMuted} bold>
221
+ {line.label}
222
+ </Text>
223
+ <Text color={line.color}>{line.text}</Text>
224
+ </Box>
225
+ );
226
+ })}
227
+
228
+ {/* Scroll-down indicator */}
229
+ {hiddenBelow > 0 && (
230
+ <Box>
231
+ <Text color={colors.dim}>{'▼'} {hiddenBelow} more</Text>
232
+ </Box>
233
+ )}
234
+
235
+ {/* Empty data object */}
236
+ {entries.length === 0 && (
237
+ <Text color={colors.dim}>No properties to display</Text>
238
+ )}
239
+ </Box>
240
+ );
241
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * HelpOverlay — Keybinding reference overlay
3
+ *
4
+ * Renders on top of the current view when `?` is pressed.
5
+ * Shows all keybindings organized in a clean layout.
6
+ * Press `?` or `Esc` to dismiss.
7
+ *
8
+ * Props:
9
+ * visible - whether the overlay is shown
10
+ * onClose - called when user dismisses (? or Esc)
11
+ */
12
+
13
+ import React from 'react';
14
+ import { Box, Text, useInput } from 'ink';
15
+ import { KEY_HELP } from '../keys.js';
16
+ import { colors } from '../theme.js';
17
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
18
+
19
+ // Preferred overlay width; clamped down on narrow terminals.
20
+ const PREFERRED_WIDTH = 44;
21
+ const MIN_WIDTH = 20;
22
+
23
+ export function HelpOverlay({ visible = false, onClose }) {
24
+ const { columns } = useTerminalSize();
25
+
26
+ useInput((input, key) => {
27
+ if (!visible) return;
28
+ if (input === '?' || key.escape) {
29
+ onClose?.();
30
+ }
31
+ }, { isActive: visible });
32
+
33
+ if (!visible) return null;
34
+
35
+ const width = Math.max(MIN_WIDTH, Math.min(PREFERRED_WIDTH, columns - 4));
36
+
37
+ return (
38
+ <Box
39
+ flexDirection="column"
40
+ borderStyle="round"
41
+ borderColor={colors.primary}
42
+ paddingX={2}
43
+ paddingY={1}
44
+ width={width}
45
+ >
46
+ {/* Header */}
47
+ <Box justifyContent="center" marginBottom={1}>
48
+ <Text color={colors.primary} bold>Keyboard Shortcuts</Text>
49
+ </Box>
50
+
51
+ {/* Key entries */}
52
+ {KEY_HELP.map((entry, i) => (
53
+ <Box key={i} marginBottom={0}>
54
+ <Box width={14}>
55
+ <Text color={colors.cyberBlue} bold>{entry.keys}</Text>
56
+ </Box>
57
+ <Text color={colors.textMuted}>{entry.desc}</Text>
58
+ </Box>
59
+ ))}
60
+
61
+ {/* Dismiss hint */}
62
+ <Box marginTop={1} justifyContent="center">
63
+ <Text color={colors.dim}>Press ? or Esc to close</Text>
64
+ </Box>
65
+ </Box>
66
+ );
67
+ }