@build-astron-co/nimbus 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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * DeployPreview Component
3
+ *
4
+ * Renders a resource-change table before a deploy action is applied. Each
5
+ * change is prefixed with a symbol indicating the action:
6
+ *
7
+ * + create (green)
8
+ * ~ modify (yellow)
9
+ * - destroy (red)
10
+ * -/+ replace (magenta)
11
+ *
12
+ * Below the table the component shows optional cost impact, blast radius, and
13
+ * affected services. Keyboard shortcuts let the user approve, reject, or
14
+ * request the full plan output.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { Box, Text, useInput } from 'ink';
19
+ import type { DeployPreviewData, DeployChange } from './types';
20
+
21
+ /** Possible decisions from the deploy preview prompt. */
22
+ export type DeployDecision = 'approve' | 'reject' | 'show_plan';
23
+
24
+ /** Props accepted by the DeployPreview component. */
25
+ export interface DeployPreviewProps {
26
+ preview: DeployPreviewData;
27
+ onDecide: (decision: DeployDecision) => void;
28
+ }
29
+
30
+ /** Map change action to a prefix character and colour. */
31
+ const ACTION_DISPLAY: Record<DeployChange['action'], { prefix: string; color: string }> = {
32
+ create: { prefix: '+', color: 'green' },
33
+ modify: { prefix: '~', color: 'yellow' },
34
+ destroy: { prefix: '-', color: 'red' },
35
+ replace: { prefix: '-/+', color: 'magenta' },
36
+ };
37
+
38
+ /**
39
+ * Compute summary counts for the banner line.
40
+ */
41
+ function summaryCounts(changes: DeployChange[]): { add: number; change: number; destroy: number } {
42
+ let add = 0;
43
+ let change = 0;
44
+ let destroy = 0;
45
+ for (const c of changes) {
46
+ switch (c.action) {
47
+ case 'create':
48
+ add++;
49
+ break;
50
+ case 'modify':
51
+ case 'replace':
52
+ change++;
53
+ break;
54
+ case 'destroy':
55
+ destroy++;
56
+ break;
57
+ }
58
+ }
59
+ return { add, change, destroy };
60
+ }
61
+
62
+ /**
63
+ * A single row in the change table.
64
+ */
65
+ function ChangeRow({ change }: { change: DeployChange }) {
66
+ const display = ACTION_DISPLAY[change.action];
67
+ return (
68
+ <Box>
69
+ <Text color={display.color} bold>
70
+ {display.prefix.padEnd(4)}
71
+ </Text>
72
+ <Text>{change.resourceType}</Text>
73
+ <Text dimColor>.</Text>
74
+ <Text bold>{change.resourceName}</Text>
75
+ {change.details && <Text dimColor> ({change.details})</Text>}
76
+ </Box>
77
+ );
78
+ }
79
+
80
+ /**
81
+ * DeployPreview renders the full preview modal with a change table and
82
+ * action key legend.
83
+ */
84
+ export function DeployPreview({ preview, onDecide }: DeployPreviewProps) {
85
+ useInput(input => {
86
+ switch (input) {
87
+ case 'a':
88
+ onDecide('approve');
89
+ break;
90
+ case 'r':
91
+ onDecide('reject');
92
+ break;
93
+ case 'p':
94
+ onDecide('show_plan');
95
+ break;
96
+ }
97
+ });
98
+
99
+ const counts = summaryCounts(preview.changes);
100
+
101
+ return (
102
+ <Box flexDirection="column" borderStyle="double" borderColor="yellow" paddingX={1} paddingY={1}>
103
+ {/* Title */}
104
+ <Box marginBottom={1}>
105
+ <Text bold color="yellow">
106
+ Deploy Preview
107
+ </Text>
108
+ <Text dimColor> ({preview.tool})</Text>
109
+ </Box>
110
+
111
+ {/* Summary counts */}
112
+ <Box marginBottom={1}>
113
+ <Text color="green">+{counts.add} to add</Text>
114
+ <Text> </Text>
115
+ <Text color="yellow">~{counts.change} to change</Text>
116
+ <Text> </Text>
117
+ <Text color="red">-{counts.destroy} to destroy</Text>
118
+ </Box>
119
+
120
+ {/* Change table */}
121
+ <Box flexDirection="column" marginBottom={1}>
122
+ {preview.changes.map((change, idx) => (
123
+ <ChangeRow key={idx} change={change} />
124
+ ))}
125
+ {preview.changes.length === 0 && <Text dimColor>No resource changes detected.</Text>}
126
+ </Box>
127
+
128
+ {/* Cost impact */}
129
+ {preview.costImpact && (
130
+ <Box>
131
+ <Text dimColor>Cost impact: </Text>
132
+ <Text>{preview.costImpact}</Text>
133
+ </Box>
134
+ )}
135
+
136
+ {/* Blast radius */}
137
+ {preview.blastRadius && (
138
+ <Box>
139
+ <Text dimColor>Blast radius: </Text>
140
+ <Text color="yellow">{preview.blastRadius}</Text>
141
+ </Box>
142
+ )}
143
+
144
+ {/* Affected services */}
145
+ {preview.affectedServices && preview.affectedServices.length > 0 && (
146
+ <Box>
147
+ <Text dimColor>Affected services: </Text>
148
+ <Text>{preview.affectedServices.join(', ')}</Text>
149
+ </Box>
150
+ )}
151
+
152
+ {/* Action keys */}
153
+ <Box marginTop={1}>
154
+ <Text color="green" bold>
155
+ [a]
156
+ </Text>
157
+ <Text> Approve </Text>
158
+ <Text color="red" bold>
159
+ [r]
160
+ </Text>
161
+ <Text> Reject </Text>
162
+ <Text color="cyan" bold>
163
+ [p]
164
+ </Text>
165
+ <Text> Show full plan</Text>
166
+ </Box>
167
+ </Box>
168
+ );
169
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Header Component
3
+ *
4
+ * Displays the Nimbus banner at the top of the TUI: version string, active
5
+ * model, session ID, and a color-coded mode indicator (plan / build / deploy).
6
+ */
7
+
8
+ import React from 'react';
9
+ import { Box, Text } from 'ink';
10
+ import type { SessionInfo, AgentMode } from './types';
11
+ import { VERSION } from '../version';
12
+
13
+ /** Props accepted by the Header component. */
14
+ export interface HeaderProps {
15
+ session: SessionInfo;
16
+ }
17
+
18
+ /** Map each mode to its display colour. */
19
+ const MODE_COLORS: Record<AgentMode, string> = {
20
+ plan: 'blue',
21
+ build: 'yellow',
22
+ deploy: 'red',
23
+ };
24
+
25
+ /**
26
+ * Truncate a session ID to a short prefix for display purposes.
27
+ */
28
+ function shortId(id: string): string {
29
+ return id.length > 8 ? id.slice(0, 8) : id;
30
+ }
31
+
32
+ /**
33
+ * Header renders a single-line banner containing the CLI version, the model
34
+ * name, the abbreviated session ID, and a colour-coded mode badge.
35
+ */
36
+ export function Header({ session }: HeaderProps) {
37
+ const modeColor = MODE_COLORS[session.mode];
38
+
39
+ return (
40
+ <Box
41
+ borderStyle="round"
42
+ borderColor="cyan"
43
+ paddingX={1}
44
+ justifyContent="space-between"
45
+ width="100%"
46
+ >
47
+ {/* Left: branding + version */}
48
+ <Box>
49
+ <Text bold color="cyan">
50
+ nimbus
51
+ </Text>
52
+ <Text dimColor> v{VERSION}</Text>
53
+ <Text dimColor> {' \u2014 '}</Text>
54
+ <Text>{session.model}</Text>
55
+ <Text dimColor> {' \u2014 '}</Text>
56
+ <Text dimColor>session: {shortId(session.id)}</Text>
57
+ </Box>
58
+
59
+ {/* Right: mode badge */}
60
+ <Box>
61
+ <Text color={modeColor} bold inverse>
62
+ {' '}
63
+ {session.mode.toUpperCase()}{' '}
64
+ </Text>
65
+ </Box>
66
+ </Box>
67
+ );
68
+ }
@@ -0,0 +1,350 @@
1
+ /**
2
+ * InputBox Component
3
+ *
4
+ * A text input area with a "> " prompt character. Uses ink-text-input for
5
+ * editing and submits on Enter. The parent component receives the submitted
6
+ * text via the `onSubmit` callback.
7
+ *
8
+ * Features:
9
+ * - Input history (Up/Down arrows)
10
+ * - Multi-line paste detection with line count indicator
11
+ * - Slash command autocomplete (Tab to cycle)
12
+ * - @file mention with Tab completion (type @ then Tab to cycle files)
13
+ * - Reverse search (Ctrl+R) through history
14
+ */
15
+
16
+ import React, { useState, useCallback, useRef } from 'react';
17
+ import { Box, Text, useInput } from 'ink';
18
+ import TextInput from 'ink-text-input';
19
+
20
+ /** Maximum number of history entries to keep. */
21
+ const MAX_HISTORY = 100;
22
+
23
+ /** All recognized slash commands for autocomplete. */
24
+ const SLASH_COMMANDS = [
25
+ '/clear',
26
+ '/compact',
27
+ '/context',
28
+ '/help',
29
+ '/model',
30
+ '/models',
31
+ '/mode',
32
+ '/new',
33
+ '/redo',
34
+ '/sessions',
35
+ '/switch',
36
+ '/undo',
37
+ ];
38
+
39
+ /** Props accepted by the InputBox component. */
40
+ export interface InputBoxProps {
41
+ /** Called when the user presses Enter with non-empty input. */
42
+ onSubmit: (text: string) => void;
43
+ /** Called when the user presses Escape to abort the current operation. */
44
+ onAbort?: () => void;
45
+ /** Placeholder text shown when the input is empty. */
46
+ placeholder?: string;
47
+ /** Whether the input is disabled (e.g. while the agent is processing). */
48
+ disabled?: boolean;
49
+ }
50
+
51
+ /**
52
+ * InputBox renders a ">" prompt followed by an editable text field.
53
+ * Pressing Enter submits the value and clears the field. Pressing Escape
54
+ * fires the optional onAbort callback. Up/Down arrows navigate history.
55
+ * Tab autocompletes slash commands. Ctrl+R opens reverse search.
56
+ */
57
+ export function InputBox({ onSubmit, onAbort, placeholder, disabled = false }: InputBoxProps) {
58
+ const [value, setValue] = useState('') as [string, React.Dispatch<React.SetStateAction<string>>];
59
+
60
+ // History: most recent entry is at the end
61
+ const history = useRef<string[]>([]);
62
+ // -1 means "not browsing history" (showing current draft)
63
+ const historyIndex = useRef(-1);
64
+ // Stores the in-progress text before the user started browsing history
65
+ const draft = useRef('');
66
+
67
+ // Slash command autocomplete state
68
+ const [slashHint, setSlashHint] = useState('');
69
+ const suggestionIndex = useRef(0);
70
+ const lastSuggestions = useRef<string[]>([]);
71
+
72
+ // @file completion state
73
+ const [fileSuggestions, setFileSuggestions] = useState<string[]>([]);
74
+ const [fileHint, setFileHint] = useState('');
75
+ const fileSuggestionIndex = useRef(0);
76
+
77
+ // Ctrl+R search mode
78
+ const [searchMode, setSearchMode] = useState(false);
79
+ const [searchQuery, setSearchQuery] = useState('');
80
+ const [searchResults, setSearchResults] = useState<string[]>([]);
81
+
82
+ const handleSubmit = useCallback(
83
+ (submitted: string) => {
84
+ const trimmed = submitted.trim();
85
+ if (trimmed.length === 0) {
86
+ return;
87
+ }
88
+ onSubmit(trimmed);
89
+
90
+ // Add to history (avoid consecutive duplicates)
91
+ const h = history.current;
92
+ if (h.length === 0 || h[h.length - 1] !== trimmed) {
93
+ h.push(trimmed);
94
+ if (h.length > MAX_HISTORY) {
95
+ h.shift();
96
+ }
97
+ }
98
+
99
+ // Reset history navigation
100
+ historyIndex.current = -1;
101
+ draft.current = '';
102
+ setValue('');
103
+ setSlashHint('');
104
+ },
105
+ [onSubmit]
106
+ );
107
+
108
+ // Handle Escape, Up/Down arrows, Tab autocomplete, Ctrl+R
109
+ useInput(
110
+ (input, key) => {
111
+ // --- Ctrl+R: toggle search mode ---
112
+ if (input === 'r' && key.ctrl) {
113
+ if (!searchMode) {
114
+ setSearchMode(true);
115
+ setSearchQuery('');
116
+ setSearchResults([]);
117
+ } else {
118
+ setSearchMode(false);
119
+ }
120
+ return;
121
+ }
122
+
123
+ // --- Search mode key handling ---
124
+ if (searchMode) {
125
+ if (key.escape) {
126
+ setSearchMode(false);
127
+ return;
128
+ }
129
+ if (key.return) {
130
+ // Select top result
131
+ if (searchResults.length > 0) {
132
+ setValue(searchResults[0]);
133
+ }
134
+ setSearchMode(false);
135
+ return;
136
+ }
137
+ // Let the search TextInput handle other keys
138
+ return;
139
+ }
140
+
141
+ if (key.escape && onAbort) {
142
+ onAbort();
143
+ return;
144
+ }
145
+
146
+ // --- Tab: autocomplete ---
147
+ if (key.tab) {
148
+ // @file completion
149
+ const atMatch = value.match(/@(\S*)$/);
150
+ if (atMatch && fileSuggestions.length > 0) {
151
+ const idx = fileSuggestionIndex.current % fileSuggestions.length;
152
+ const replacement = `${value.slice(0, value.length - atMatch[0].length)}@${fileSuggestions[idx]}`;
153
+ setValue(replacement);
154
+ fileSuggestionIndex.current = idx + 1;
155
+ setFileHint(`[${fileSuggestions.length} files, Tab to cycle]`);
156
+ return;
157
+ }
158
+
159
+ // Slash command completion
160
+ if (value.startsWith('/')) {
161
+ const prefix = value.toLowerCase();
162
+ const matches = SLASH_COMMANDS.filter(cmd => cmd.startsWith(prefix));
163
+ if (matches.length === 0) {
164
+ setSlashHint('');
165
+ return;
166
+ }
167
+ if (matches.length === 1) {
168
+ setValue(`${matches[0]} `);
169
+ setSlashHint('');
170
+ lastSuggestions.current = [];
171
+ return;
172
+ }
173
+ // Multiple matches: cycle through them
174
+ if (
175
+ lastSuggestions.current.length === matches.length &&
176
+ lastSuggestions.current.every((s, i) => s === matches[i])
177
+ ) {
178
+ suggestionIndex.current = (suggestionIndex.current + 1) % matches.length;
179
+ } else {
180
+ lastSuggestions.current = matches;
181
+ suggestionIndex.current = 0;
182
+ }
183
+ setValue(matches[suggestionIndex.current]);
184
+ setSlashHint(`[${matches.length} matches, Tab to cycle]`);
185
+ }
186
+ return;
187
+ }
188
+
189
+ const h = history.current;
190
+ if (h.length === 0) {
191
+ return;
192
+ }
193
+
194
+ if (key.upArrow) {
195
+ if (historyIndex.current === -1) {
196
+ // Starting to browse: save current draft
197
+ draft.current = value;
198
+ historyIndex.current = h.length - 1;
199
+ } else if (historyIndex.current > 0) {
200
+ historyIndex.current--;
201
+ }
202
+ setValue(h[historyIndex.current]);
203
+ return;
204
+ }
205
+
206
+ if (key.downArrow) {
207
+ if (historyIndex.current === -1) {
208
+ return;
209
+ } // not browsing
210
+
211
+ if (historyIndex.current < h.length - 1) {
212
+ historyIndex.current++;
213
+ setValue(h[historyIndex.current]);
214
+ } else {
215
+ // Past the end of history: restore draft
216
+ historyIndex.current = -1;
217
+ setValue(draft.current);
218
+ }
219
+ return;
220
+ }
221
+ },
222
+ { isActive: !disabled }
223
+ );
224
+
225
+ // Count lines for multi-line paste indicator
226
+ const lineCount = value.split('\n').length;
227
+ const isMultiLine = lineCount > 1;
228
+
229
+ if (disabled) {
230
+ return (
231
+ <Box paddingX={1}>
232
+ <Text dimColor>{'> '}</Text>
233
+ <Text dimColor italic>
234
+ {placeholder ?? 'waiting...'}
235
+ </Text>
236
+ </Box>
237
+ );
238
+ }
239
+
240
+ // --- Search mode UI ---
241
+ if (searchMode) {
242
+ return (
243
+ <Box flexDirection="column" paddingX={1}>
244
+ <Box>
245
+ <Text color="yellow">{'(reverse-search): '}</Text>
246
+ <TextInput
247
+ value={searchQuery}
248
+ onChange={q => {
249
+ setSearchQuery(q);
250
+ if (q.length > 0) {
251
+ const results = history.current
252
+ .filter(entry => entry.toLowerCase().includes(q.toLowerCase()))
253
+ .reverse()
254
+ .slice(0, 10);
255
+ setSearchResults(results);
256
+ } else {
257
+ setSearchResults([]);
258
+ }
259
+ }}
260
+ onSubmit={() => {
261
+ if (searchResults.length > 0) {
262
+ setValue(searchResults[0]);
263
+ }
264
+ setSearchMode(false);
265
+ }}
266
+ placeholder="type to search history..."
267
+ />
268
+ </Box>
269
+ {searchResults.length > 0 && (
270
+ <Box flexDirection="column" marginLeft={2}>
271
+ {searchResults.slice(0, 5).map((result, i) => (
272
+ <Text key={i} dimColor={i > 0}>
273
+ {i === 0 ? '> ' : ' '}
274
+ {result.length > 80 ? `${result.slice(0, 77)}...` : result}
275
+ </Text>
276
+ ))}
277
+ {searchResults.length > 5 && (
278
+ <Text dimColor italic>
279
+ {' '}
280
+ ... {searchResults.length - 5} more
281
+ </Text>
282
+ )}
283
+ </Box>
284
+ )}
285
+ </Box>
286
+ );
287
+ }
288
+
289
+ // --- Normal input UI ---
290
+ return (
291
+ <Box paddingX={1}>
292
+ <Text bold color="green">
293
+ {'> '}
294
+ </Text>
295
+ <TextInput
296
+ value={value}
297
+ onChange={v => {
298
+ setValue(v);
299
+ // If user types while browsing history, exit history mode
300
+ if (historyIndex.current !== -1) {
301
+ historyIndex.current = -1;
302
+ }
303
+ // Reset slash autocomplete on any change
304
+ if (!v.startsWith('/')) {
305
+ setSlashHint('');
306
+ lastSuggestions.current = [];
307
+ }
308
+ // @file mention detection
309
+ const atMatch = v.match(/@(\S*)$/);
310
+ if (atMatch) {
311
+ const partial = atMatch[1];
312
+ try {
313
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
314
+ const fs = require('node:fs');
315
+ const cwd = process.cwd();
316
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
317
+ const matches = (entries as Array<{ name: string; isDirectory(): boolean }>)
318
+ .filter(
319
+ e =>
320
+ !e.name.startsWith('.') && e.name.toLowerCase().includes(partial.toLowerCase())
321
+ )
322
+ .map(e => (e.isDirectory() ? `${e.name}/` : e.name))
323
+ .slice(0, 10);
324
+ setFileSuggestions(matches);
325
+ fileSuggestionIndex.current = 0;
326
+ if (matches.length > 0) {
327
+ setFileHint(`[${matches.length} files, Tab to complete]`);
328
+ } else {
329
+ setFileHint('');
330
+ }
331
+ } catch {
332
+ setFileSuggestions([]);
333
+ setFileHint('');
334
+ }
335
+ } else {
336
+ if (fileSuggestions.length > 0) {
337
+ setFileSuggestions([]);
338
+ setFileHint('');
339
+ }
340
+ }
341
+ }}
342
+ onSubmit={handleSubmit}
343
+ placeholder={placeholder ?? 'Type a message... (paste multi-line supported)'}
344
+ />
345
+ {isMultiLine && <Text color="cyan">{` [${lineCount} lines]`}</Text>}
346
+ {slashHint && <Text dimColor>{` ${slashHint}`}</Text>}
347
+ {fileHint && <Text dimColor>{` ${fileHint}`}</Text>}
348
+ </Box>
349
+ );
350
+ }