@build-astron-co/nimbus 0.2.0 → 0.4.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.
- package/bin/nimbus +26 -10
- package/bin/nimbus.cmd +41 -0
- package/bin/nimbus.mjs +70 -0
- package/completions/nimbus.bash +38 -0
- package/completions/nimbus.fish +48 -0
- package/completions/nimbus.zsh +81 -0
- package/dist/src/agent/compaction-agent.js +215 -0
- package/dist/src/agent/context-manager.js +385 -0
- package/dist/src/agent/context.js +322 -0
- package/dist/src/agent/deploy-preview.js +395 -0
- package/dist/src/agent/expand-files.js +95 -0
- package/dist/src/agent/index.js +18 -0
- package/dist/src/agent/loop.js +1535 -0
- package/dist/src/agent/modes.js +347 -0
- package/dist/src/agent/permissions.js +396 -0
- package/dist/src/agent/subagents/base.js +67 -0
- package/dist/src/agent/subagents/cost.js +45 -0
- package/dist/src/agent/subagents/explore.js +36 -0
- package/dist/src/agent/subagents/general.js +41 -0
- package/dist/src/agent/subagents/index.js +88 -0
- package/dist/src/agent/subagents/infra.js +52 -0
- package/dist/src/agent/subagents/security.js +60 -0
- package/dist/src/agent/system-prompt.js +860 -0
- package/dist/src/app.js +152 -0
- package/dist/src/audit/activity-log.js +209 -0
- package/dist/src/audit/compliance-checker.js +419 -0
- package/dist/src/audit/cost-tracker.js +231 -0
- package/dist/src/audit/index.js +10 -0
- package/dist/src/audit/security-scanner.js +490 -0
- package/dist/src/auth/guard.js +64 -0
- package/dist/src/auth/index.js +19 -0
- package/dist/src/auth/keychain.js +79 -0
- package/dist/src/auth/oauth.js +389 -0
- package/dist/src/auth/providers.js +415 -0
- package/dist/src/auth/sso.js +87 -0
- package/dist/src/auth/store.js +424 -0
- package/dist/src/auth/types.js +5 -0
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/init.js +1048 -0
- package/dist/src/cli/openapi-spec.js +346 -0
- package/dist/src/cli/run.js +505 -0
- package/dist/src/cli/serve-auth.js +56 -0
- package/dist/src/cli/serve.js +432 -0
- package/dist/src/cli/web.js +50 -0
- package/dist/src/cli.js +1574 -0
- package/dist/src/clients/core-engine-client.js +156 -0
- package/dist/src/clients/enterprise-client.js +246 -0
- package/dist/src/clients/generator-client.js +219 -0
- package/dist/src/clients/git-client.js +367 -0
- package/dist/src/clients/github-client.js +229 -0
- package/dist/src/clients/helm-client.js +299 -0
- package/dist/src/clients/index.js +18 -0
- package/dist/src/clients/k8s-client.js +270 -0
- package/dist/src/clients/llm-client.js +119 -0
- package/dist/src/clients/rest-client.js +104 -0
- package/dist/src/clients/service-discovery.js +35 -0
- package/dist/src/clients/terraform-client.js +302 -0
- package/dist/src/clients/tools-client.js +1227 -0
- package/dist/src/clients/ws-client.js +93 -0
- package/dist/src/commands/alias.js +91 -0
- package/dist/src/commands/analyze/index.js +313 -0
- package/dist/src/commands/apply/helm.js +375 -0
- package/dist/src/commands/apply/index.js +176 -0
- package/dist/src/commands/apply/k8s.js +350 -0
- package/dist/src/commands/apply/terraform.js +465 -0
- package/dist/src/commands/ask.js +137 -0
- package/dist/src/commands/audit/index.js +322 -0
- package/dist/src/commands/auth-cloud.js +345 -0
- package/dist/src/commands/auth-list.js +112 -0
- package/dist/src/commands/auth-profile.js +104 -0
- package/dist/src/commands/auth-refresh.js +161 -0
- package/dist/src/commands/auth-status.js +122 -0
- package/dist/src/commands/aws/ec2.js +402 -0
- package/dist/src/commands/aws/iam.js +304 -0
- package/dist/src/commands/aws/index.js +108 -0
- package/dist/src/commands/aws/lambda.js +317 -0
- package/dist/src/commands/aws/rds.js +345 -0
- package/dist/src/commands/aws/s3.js +346 -0
- package/dist/src/commands/aws/vpc.js +302 -0
- package/dist/src/commands/aws-discover.js +413 -0
- package/dist/src/commands/aws-terraform.js +618 -0
- package/dist/src/commands/azure/aks.js +305 -0
- package/dist/src/commands/azure/functions.js +200 -0
- package/dist/src/commands/azure/index.js +93 -0
- package/dist/src/commands/azure/storage.js +378 -0
- package/dist/src/commands/azure/vm.js +291 -0
- package/dist/src/commands/billing/index.js +224 -0
- package/dist/src/commands/chat.js +259 -0
- package/dist/src/commands/completions.js +255 -0
- package/dist/src/commands/config.js +291 -0
- package/dist/src/commands/cost/cloud-cost-estimator.js +211 -0
- package/dist/src/commands/cost/estimator.js +73 -0
- package/dist/src/commands/cost/index.js +625 -0
- package/dist/src/commands/cost/parsers/terraform.js +234 -0
- package/dist/src/commands/cost/parsers/types.js +4 -0
- package/dist/src/commands/cost/pricing/aws.js +501 -0
- package/dist/src/commands/cost/pricing/azure.js +462 -0
- package/dist/src/commands/cost/pricing/gcp.js +359 -0
- package/dist/src/commands/cost/pricing/index.js +24 -0
- package/dist/src/commands/demo.js +196 -0
- package/dist/src/commands/deploy.js +215 -0
- package/dist/src/commands/doctor.js +1291 -0
- package/dist/src/commands/drift/index.js +674 -0
- package/dist/src/commands/explain.js +235 -0
- package/dist/src/commands/export.js +120 -0
- package/dist/src/commands/feedback.js +319 -0
- package/dist/src/commands/fix.js +263 -0
- package/dist/src/commands/fs/index.js +338 -0
- package/dist/src/commands/gcp/compute.js +266 -0
- package/dist/src/commands/gcp/functions.js +221 -0
- package/dist/src/commands/gcp/gke.js +357 -0
- package/dist/src/commands/gcp/iam.js +295 -0
- package/dist/src/commands/gcp/index.js +105 -0
- package/dist/src/commands/gcp/storage.js +232 -0
- package/dist/src/commands/generate-helm.js +1026 -0
- package/dist/src/commands/generate-k8s.js +1263 -0
- package/dist/src/commands/generate-terraform.js +1058 -0
- package/dist/src/commands/gh/index.js +663 -0
- package/dist/src/commands/git/index.js +1208 -0
- package/dist/src/commands/helm/index.js +985 -0
- package/dist/src/commands/help.js +639 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/src/commands/import.js +782 -0
- package/dist/src/commands/incident.js +144 -0
- package/dist/src/commands/index.js +109 -0
- package/dist/src/commands/init.js +955 -0
- package/dist/src/commands/k8s/index.js +979 -0
- package/dist/src/commands/login.js +588 -0
- package/dist/src/commands/logout.js +61 -0
- package/dist/src/commands/logs.js +160 -0
- package/dist/src/commands/onboarding.js +382 -0
- package/dist/src/commands/pipeline.js +153 -0
- package/dist/src/commands/plan/display.js +216 -0
- package/dist/src/commands/plan/index.js +525 -0
- package/dist/src/commands/plugin.js +325 -0
- package/dist/src/commands/preview.js +356 -0
- package/dist/src/commands/profile.js +297 -0
- package/dist/src/commands/questionnaire.js +1021 -0
- package/dist/src/commands/resume.js +35 -0
- package/dist/src/commands/rollback.js +259 -0
- package/dist/src/commands/rollout.js +74 -0
- package/dist/src/commands/runbook.js +307 -0
- package/dist/src/commands/schedule.js +202 -0
- package/dist/src/commands/status.js +213 -0
- package/dist/src/commands/team/index.js +309 -0
- package/dist/src/commands/team-context.js +200 -0
- package/dist/src/commands/template.js +204 -0
- package/dist/src/commands/tf/index.js +989 -0
- package/dist/src/commands/upgrade.js +515 -0
- package/dist/src/commands/usage/index.js +118 -0
- package/dist/src/commands/version.js +145 -0
- package/dist/src/commands/watch.js +127 -0
- package/dist/src/compat/index.js +2 -0
- package/dist/src/compat/runtime.js +10 -0
- package/dist/src/compat/sqlite.js +144 -0
- package/dist/src/config/index.js +6 -0
- package/dist/src/config/manager.js +469 -0
- package/dist/src/config/mode-store.js +57 -0
- package/dist/src/config/profiles.js +66 -0
- package/dist/src/config/safety-policy.js +251 -0
- package/dist/src/config/schema.js +107 -0
- package/dist/src/config/types.js +311 -0
- package/dist/src/config/workspace-state.js +38 -0
- package/dist/src/context/context-db.js +138 -0
- package/dist/src/demo/index.js +295 -0
- package/dist/src/demo/scenarios/full-journey.js +226 -0
- package/dist/src/demo/scenarios/getting-started.js +124 -0
- package/dist/src/demo/scenarios/helm-release.js +334 -0
- package/dist/src/demo/scenarios/k8s-deployment.js +190 -0
- package/dist/src/demo/scenarios/terraform-vpc.js +167 -0
- package/dist/src/demo/types.js +6 -0
- package/dist/src/engine/cost-estimator.js +334 -0
- package/dist/src/engine/diagram-generator.js +192 -0
- package/dist/src/engine/drift-detector.js +688 -0
- package/dist/src/engine/executor.js +832 -0
- package/dist/src/engine/index.js +39 -0
- package/dist/src/engine/orchestrator.js +436 -0
- package/dist/src/engine/planner.js +616 -0
- package/dist/src/engine/safety.js +609 -0
- package/dist/src/engine/verifier.js +664 -0
- package/dist/src/enterprise/audit.js +241 -0
- package/dist/src/enterprise/auth.js +189 -0
- package/dist/src/enterprise/billing.js +512 -0
- package/dist/src/enterprise/index.js +16 -0
- package/dist/src/enterprise/teams.js +315 -0
- package/dist/src/generator/best-practices.js +1375 -0
- package/dist/src/generator/helm.js +495 -0
- package/dist/src/generator/index.js +11 -0
- package/dist/src/generator/intent-parser.js +420 -0
- package/dist/src/generator/kubernetes.js +773 -0
- package/dist/src/generator/terraform.js +1472 -0
- package/dist/src/history/index.js +6 -0
- package/dist/src/history/manager.js +199 -0
- package/dist/src/history/types.js +6 -0
- package/dist/src/hooks/config.js +318 -0
- package/dist/src/hooks/engine.js +317 -0
- package/dist/src/hooks/index.js +2 -0
- package/dist/src/llm/auth-bridge.js +157 -0
- package/dist/src/llm/circuit-breaker.js +116 -0
- package/dist/src/llm/config-loader.js +172 -0
- package/dist/src/llm/cost-calculator.js +137 -0
- package/dist/src/llm/index.js +7 -0
- package/dist/src/llm/model-aliases.js +99 -0
- package/dist/src/llm/provider-registry.js +57 -0
- package/dist/src/llm/providers/anthropic.js +430 -0
- package/dist/src/llm/providers/bedrock.js +409 -0
- package/dist/src/llm/providers/google.js +344 -0
- package/dist/src/llm/providers/ollama.js +661 -0
- package/dist/src/llm/providers/openai-compatible.js +289 -0
- package/dist/src/llm/providers/openai.js +284 -0
- package/dist/src/llm/providers/openrouter.js +293 -0
- package/dist/src/llm/router.js +844 -0
- package/dist/src/llm/types.js +69 -0
- package/dist/src/lsp/client.js +239 -0
- package/dist/src/lsp/languages.js +95 -0
- package/dist/src/lsp/manager.js +243 -0
- package/dist/src/mcp/client.js +289 -0
- package/dist/src/mcp/index.js +5 -0
- package/dist/src/mcp/manager.js +113 -0
- package/dist/src/nimbus.js +212 -0
- package/dist/src/plugins/index.js +13 -0
- package/dist/src/plugins/loader.js +280 -0
- package/dist/src/plugins/manager.js +282 -0
- package/dist/src/plugins/types.js +23 -0
- package/dist/src/scanners/cicd-scanner.js +230 -0
- package/dist/src/scanners/cloud-scanner.js +415 -0
- package/dist/src/scanners/framework-scanner.js +430 -0
- package/dist/src/scanners/iac-scanner.js +350 -0
- package/dist/src/scanners/index.js +454 -0
- package/dist/src/scanners/language-scanner.js +258 -0
- package/dist/src/scanners/package-manager-scanner.js +252 -0
- package/dist/src/scanners/types.js +6 -0
- package/dist/src/sessions/manager.js +395 -0
- package/dist/src/sessions/types.js +4 -0
- package/dist/src/sharing/sync.js +238 -0
- package/dist/src/sharing/viewer.js +131 -0
- package/dist/src/snapshots/index.js +1 -0
- package/dist/src/snapshots/manager.js +432 -0
- package/dist/src/state/artifacts.js +94 -0
- package/dist/src/state/audit.js +73 -0
- package/dist/src/state/billing.js +126 -0
- package/dist/src/state/checkpoints.js +81 -0
- package/dist/src/state/config.js +58 -0
- package/dist/src/state/conversations.js +7 -0
- package/dist/src/state/credentials.js +96 -0
- package/dist/src/state/db.js +53 -0
- package/dist/src/state/index.js +23 -0
- package/dist/src/state/messages.js +76 -0
- package/dist/src/state/projects.js +92 -0
- package/dist/src/state/schema.js +233 -0
- package/dist/src/state/sessions.js +79 -0
- package/dist/src/state/teams.js +131 -0
- package/dist/src/telemetry.js +91 -0
- package/dist/src/tools/aws-ops.js +747 -0
- package/dist/src/tools/azure-ops.js +491 -0
- package/dist/src/tools/file-ops.js +451 -0
- package/dist/src/tools/gcp-ops.js +559 -0
- package/dist/src/tools/git-ops.js +557 -0
- package/dist/src/tools/github-ops.js +460 -0
- package/dist/src/tools/helm-ops.js +634 -0
- package/dist/src/tools/index.js +16 -0
- package/dist/src/tools/k8s-ops.js +579 -0
- package/dist/src/tools/schemas/converter.js +129 -0
- package/dist/src/tools/schemas/devops.js +3319 -0
- package/dist/src/tools/schemas/index.js +19 -0
- package/dist/src/tools/schemas/standard.js +966 -0
- package/dist/src/tools/schemas/types.js +409 -0
- package/dist/src/tools/spawn-exec.js +109 -0
- package/dist/src/tools/terraform-ops.js +627 -0
- package/dist/src/types/config.js +1 -0
- package/dist/src/types/drift.js +4 -0
- package/dist/src/types/enterprise.js +5 -0
- package/dist/src/types/index.js +14 -0
- package/dist/src/types/plan.js +1 -0
- package/dist/src/types/request.js +1 -0
- package/dist/src/types/response.js +1 -0
- package/dist/src/types/service.js +1 -0
- package/dist/src/ui/App.js +1672 -0
- package/dist/src/ui/DeployPreview.js +60 -0
- package/dist/src/ui/FileDiffModal.js +108 -0
- package/dist/src/ui/Header.js +46 -0
- package/dist/src/ui/HelpModal.js +9 -0
- package/dist/src/ui/InputBox.js +408 -0
- package/dist/src/ui/MessageList.js +795 -0
- package/dist/src/ui/PermissionPrompt.js +72 -0
- package/dist/src/ui/StatusBar.js +109 -0
- package/dist/src/ui/TerminalPane.js +31 -0
- package/dist/src/ui/ToolCallDisplay.js +303 -0
- package/dist/src/ui/TreePane.js +83 -0
- package/dist/src/ui/chat-ui.js +721 -0
- package/dist/src/ui/index.js +11 -0
- package/dist/src/ui/ink/index.js +1325 -0
- package/dist/src/ui/streaming.js +137 -0
- package/dist/src/ui/theme.js +78 -0
- package/dist/src/ui/types.js +7 -0
- package/dist/src/utils/analytics.js +61 -0
- package/dist/src/utils/cost-warning.js +25 -0
- package/dist/src/utils/env.js +42 -0
- package/dist/src/utils/errors.js +54 -0
- package/dist/src/utils/event-bus.js +22 -0
- package/dist/src/utils/index.js +16 -0
- package/dist/src/utils/logger.js +150 -0
- package/dist/src/utils/rate-limiter.js +90 -0
- package/dist/src/utils/service-auth.js +36 -0
- package/dist/src/utils/validation.js +39 -0
- package/dist/src/version.js +3 -0
- package/dist/src/watcher/index.js +192 -0
- package/dist/src/wizard/approval.js +275 -0
- package/dist/src/wizard/index.js +13 -0
- package/dist/src/wizard/prompts.js +273 -0
- package/dist/src/wizard/types.js +4 -0
- package/dist/src/wizard/ui.js +453 -0
- package/dist/src/wizard/wizard.js +227 -0
- package/package.json +31 -23
- package/src/__tests__/alias.test.ts +133 -0
- package/src/__tests__/app.test.ts +1 -1
- package/src/__tests__/audit.test.ts +1 -1
- package/src/__tests__/circuit-breaker.test.ts +1 -1
- package/src/__tests__/cli-run.test.ts +237 -1
- package/src/__tests__/compat-sqlite.test.ts +68 -0
- package/src/__tests__/context-manager.test.ts +131 -1
- package/src/__tests__/context.test.ts +1 -1
- package/src/__tests__/devops-terminal-gaps.test.ts +718 -0
- package/src/__tests__/doctor.test.ts +48 -0
- package/src/__tests__/enterprise.test.ts +1 -1
- package/src/__tests__/export.test.ts +236 -0
- package/src/__tests__/gap-11-18-20.test.ts +958 -0
- package/src/__tests__/generator.test.ts +1 -1
- package/src/__tests__/helm-streaming.test.ts +127 -0
- package/src/__tests__/hooks.test.ts +1 -1
- package/src/__tests__/incident.test.ts +179 -0
- package/src/__tests__/init.test.ts +55 -4
- package/src/__tests__/intent-parser.test.ts +1 -1
- package/src/__tests__/llm-router.test.ts +1 -1
- package/src/__tests__/logs.test.ts +107 -0
- package/src/__tests__/loop-errors.test.ts +244 -0
- package/src/__tests__/lsp.test.ts +1 -1
- package/src/__tests__/modes.test.ts +1 -1
- package/src/__tests__/perf-optimizations.test.ts +847 -0
- package/src/__tests__/permissions.test.ts +1 -1
- package/src/__tests__/pipeline.test.ts +50 -0
- package/src/__tests__/polish-phase3.test.ts +340 -0
- package/src/__tests__/profile.test.ts +237 -0
- package/src/__tests__/rollback.test.ts +83 -0
- package/src/__tests__/runbook.test.ts +219 -0
- package/src/__tests__/schedule.test.ts +206 -0
- package/src/__tests__/serve.test.ts +1 -1
- package/src/__tests__/sessions.test.ts +96 -1
- package/src/__tests__/sharing.test.ts +53 -1
- package/src/__tests__/snapshots.test.ts +1 -1
- package/src/__tests__/standalone-migration.test.ts +199 -0
- package/src/__tests__/state-db.test.ts +1 -1
- package/src/__tests__/status.test.ts +158 -0
- package/src/__tests__/stream-with-tools.test.ts +71 -25
- package/src/__tests__/subagents.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +82 -3
- package/src/__tests__/terminal-gap-v2.test.ts +395 -0
- package/src/__tests__/terminal-parity.test.ts +393 -0
- package/src/__tests__/tf-apply.test.ts +187 -0
- package/src/__tests__/tool-converter.test.ts +1 -1
- package/src/__tests__/tool-schemas.test.ts +209 -4
- package/src/__tests__/tools.test.ts +4 -3
- package/src/__tests__/version-json.test.ts +184 -0
- package/src/__tests__/version.test.ts +1 -1
- package/src/__tests__/watch.test.ts +129 -0
- package/src/agent/compaction-agent.ts +40 -1
- package/src/agent/context-manager.ts +67 -3
- package/src/agent/deploy-preview.ts +62 -1
- package/src/agent/expand-files.ts +108 -0
- package/src/agent/loop.ts +1312 -31
- package/src/agent/permissions.ts +51 -4
- package/src/agent/system-prompt.ts +573 -19
- package/src/app.ts +58 -0
- package/src/audit/security-scanner.ts +45 -0
- package/src/auth/keychain.ts +82 -0
- package/src/auth/oauth.ts +15 -5
- package/src/cli/init.ts +378 -5
- package/src/cli/run.ts +407 -16
- package/src/cli/serve.ts +78 -1
- package/src/cli/web.ts +10 -6
- package/src/cli.ts +312 -1
- package/src/clients/service-discovery.ts +30 -25
- package/src/commands/alias.ts +100 -0
- package/src/commands/audit/index.ts +121 -2
- package/src/commands/auth-cloud.ts +113 -0
- package/src/commands/auth-refresh.ts +187 -0
- package/src/commands/aws-discover.ts +144 -251
- package/src/commands/aws-terraform.ts +68 -118
- package/src/commands/chat.ts +9 -3
- package/src/commands/completions.ts +268 -0
- package/src/commands/config.ts +26 -0
- package/src/commands/cost/index.ts +218 -2
- package/src/commands/deploy.ts +260 -0
- package/src/commands/doctor.ts +744 -152
- package/src/commands/drift/index.ts +371 -23
- package/src/commands/export.ts +146 -0
- package/src/commands/generate-k8s.ts +9 -61
- package/src/commands/generate-terraform.ts +191 -449
- package/src/commands/help.ts +212 -36
- package/src/commands/history.ts +8 -1
- package/src/commands/incident.ts +166 -0
- package/src/commands/init.ts +5 -0
- package/src/commands/login.ts +86 -1
- package/src/commands/logs.ts +167 -0
- package/src/commands/onboarding.ts +211 -34
- package/src/commands/pipeline.ts +186 -0
- package/src/commands/plugin.ts +398 -0
- package/src/commands/profile.ts +342 -0
- package/src/commands/questionnaire.ts +0 -98
- package/src/commands/resume.ts +26 -34
- package/src/commands/rollback.ts +315 -0
- package/src/commands/rollout.ts +88 -0
- package/src/commands/runbook.ts +346 -0
- package/src/commands/schedule.ts +236 -0
- package/src/commands/status.ts +252 -0
- package/src/commands/team-context.ts +220 -0
- package/src/commands/template.ts +58 -57
- package/src/commands/tf/index.ts +70 -11
- package/src/commands/upgrade.ts +57 -0
- package/src/commands/version.ts +54 -50
- package/src/commands/watch.ts +153 -0
- package/src/compat/runtime.ts +1 -1
- package/src/compat/sqlite.ts +75 -5
- package/src/config/mode-store.ts +62 -0
- package/src/config/profiles.ts +84 -0
- package/src/config/types.ts +83 -1
- package/src/config/workspace-state.ts +53 -0
- package/src/engine/cost-estimator.ts +52 -10
- package/src/engine/executor.ts +33 -2
- package/src/engine/planner.ts +68 -1
- package/src/generator/terraform.ts +8 -0
- package/src/history/manager.ts +2 -74
- package/src/hooks/engine.ts +5 -4
- package/src/llm/cost-calculator.ts +2 -2
- package/src/llm/providers/anthropic.ts +50 -21
- package/src/llm/router.ts +76 -7
- package/src/lsp/languages.ts +3 -0
- package/src/lsp/manager.ts +21 -5
- package/src/nimbus.ts +37 -18
- package/src/sessions/manager.ts +108 -1
- package/src/sharing/sync.ts +4 -0
- package/src/sharing/viewer.ts +66 -0
- package/src/tools/file-ops.ts +22 -0
- package/src/tools/schemas/devops.ts +3007 -117
- package/src/tools/schemas/standard.ts +5 -1
- package/src/tools/schemas/types.ts +31 -1
- package/src/tools/spawn-exec.ts +148 -0
- package/src/ui/App.tsx +1183 -66
- package/src/ui/DeployPreview.tsx +62 -57
- package/src/ui/FileDiffModal.tsx +162 -0
- package/src/ui/Header.tsx +87 -24
- package/src/ui/HelpModal.tsx +57 -0
- package/src/ui/InputBox.tsx +163 -10
- package/src/ui/MessageList.tsx +487 -40
- package/src/ui/PermissionPrompt.tsx +17 -5
- package/src/ui/StatusBar.tsx +122 -3
- package/src/ui/TerminalPane.tsx +84 -0
- package/src/ui/ToolCallDisplay.tsx +252 -18
- package/src/ui/TreePane.tsx +132 -0
- package/src/ui/chat-ui.ts +41 -44
- package/src/ui/ink/index.ts +771 -38
- package/src/ui/streaming.ts +1 -1
- package/src/ui/theme.ts +104 -0
- package/src/ui/types.ts +18 -0
- package/src/version.ts +1 -1
- package/src/watcher/index.ts +66 -15
- package/src/wizard/types.ts +1 -0
- package/src/wizard/ui.ts +1 -1
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* PermissionPrompt Component
|
|
4
|
+
*
|
|
5
|
+
* Modal-style overlay that asks the user to approve or reject a tool
|
|
6
|
+
* invocation before it executes. Displays the tool name, its input
|
|
7
|
+
* parameters, and a risk-level indicator.
|
|
8
|
+
*
|
|
9
|
+
* Keyboard shortcuts:
|
|
10
|
+
* a - Approve this invocation
|
|
11
|
+
* r - Reject this invocation
|
|
12
|
+
* A - Approve all invocations for this tool
|
|
13
|
+
* s - Approve for the remainder of this session
|
|
14
|
+
*/
|
|
15
|
+
import { useState } from 'react';
|
|
16
|
+
import { Box, Text, useInput } from 'ink';
|
|
17
|
+
/** Map risk levels to display colours. */
|
|
18
|
+
const RISK_COLORS = {
|
|
19
|
+
low: 'green',
|
|
20
|
+
medium: 'yellow',
|
|
21
|
+
high: 'red',
|
|
22
|
+
critical: 'magenta',
|
|
23
|
+
};
|
|
24
|
+
/** H4: Human-readable descriptions of each risk tier. */
|
|
25
|
+
const RISK_DESCRIPTIONS = {
|
|
26
|
+
low: 'Read-only — safe to auto-approve',
|
|
27
|
+
medium: 'Modifies local files — review before approving',
|
|
28
|
+
high: 'System or cloud operation — approve with caution',
|
|
29
|
+
critical: 'Destructive or irreversible — explicit confirmation required',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Format tool input into a list of truncated key=value lines.
|
|
33
|
+
*/
|
|
34
|
+
function formatInput(input, maxEntries = 6) {
|
|
35
|
+
return Object.entries(input)
|
|
36
|
+
.slice(0, maxEntries)
|
|
37
|
+
.map(([key, value]) => {
|
|
38
|
+
const str = typeof value === 'string' ? value : JSON.stringify(value);
|
|
39
|
+
const truncated = str.length > 80 ? `${str.slice(0, 77)}...` : str;
|
|
40
|
+
return `${key}: ${truncated}`;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* PermissionPrompt renders a bordered box with tool information and waits for
|
|
45
|
+
* a single keypress to determine the user's decision.
|
|
46
|
+
*/
|
|
47
|
+
export function PermissionPrompt({ toolName, toolInput, riskLevel, onDecide, }) {
|
|
48
|
+
const [pressed, setPressed] = useState(null);
|
|
49
|
+
useInput(input => {
|
|
50
|
+
switch (input) {
|
|
51
|
+
case 'a':
|
|
52
|
+
setPressed('a');
|
|
53
|
+
onDecide('approve');
|
|
54
|
+
break;
|
|
55
|
+
case 'r':
|
|
56
|
+
setPressed('r');
|
|
57
|
+
onDecide('reject');
|
|
58
|
+
break;
|
|
59
|
+
case 'A':
|
|
60
|
+
setPressed('A');
|
|
61
|
+
onDecide('approve_all');
|
|
62
|
+
break;
|
|
63
|
+
case 's':
|
|
64
|
+
setPressed('s');
|
|
65
|
+
onDecide('session');
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const inputLines = formatInput(toolInput);
|
|
70
|
+
const riskColor = RISK_COLORS[riskLevel];
|
|
71
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: riskColor, paddingX: 1, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "yellow", children: "Permission Required" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Tool: " }), _jsx(Text, { bold: true, children: toolName })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Risk: " }), _jsx(Text, { bold: true, color: riskColor, children: riskLevel.toUpperCase() })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: RISK_DESCRIPTIONS[riskLevel] }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Parameters:" }), inputLines.map((line, idx) => (_jsxs(Text, { children: [" ", line] }, idx)))] }), _jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, inverse: pressed === 'a', children: "[a]" }), _jsx(Text, { children: "=approve " }), _jsx(Text, { color: "red", bold: true, inverse: pressed === 'r', children: "[r]" }), _jsx(Text, { children: "=reject " }), _jsx(Text, { color: "cyan", bold: true, inverse: pressed === 'A', children: "[A]" }), _jsx(Text, { children: "=approve-all-session " }), _jsx(Text, { color: "blue", bold: true, inverse: pressed === 's', children: "[s]" }), _jsx(Text, { children: "=approve-this-tool-session" })] })] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* StatusBar Component
|
|
4
|
+
*
|
|
5
|
+
* A single-line footer that shows the active agent mode, token usage with a
|
|
6
|
+
* percentage bar, estimated cost, snapshot count, and elapsed processing time.
|
|
7
|
+
*
|
|
8
|
+
* Layout:
|
|
9
|
+
* [Plan] [Build] [Deploy] | Tokens: 45.2k/200k (22%) | Cost: $0.03 | Snapshots: 3 (undo) | tf:default | k8s:prod-cluster | delta:+$1.20 | 12s
|
|
10
|
+
*
|
|
11
|
+
* Token percentage colour:
|
|
12
|
+
* green < 50%
|
|
13
|
+
* yellow 50-80%
|
|
14
|
+
* red > 80%
|
|
15
|
+
*/
|
|
16
|
+
import { useState, useEffect, useRef } from 'react';
|
|
17
|
+
import { Box, Text } from 'ink';
|
|
18
|
+
/** All modes in display order. */
|
|
19
|
+
const MODES = ['plan', 'build', 'deploy'];
|
|
20
|
+
/**
|
|
21
|
+
* Format a token count into a human-readable string (e.g. 45200 -> "45.2k").
|
|
22
|
+
*/
|
|
23
|
+
function formatTokens(count) {
|
|
24
|
+
if (count >= 1_000_000) {
|
|
25
|
+
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
26
|
+
}
|
|
27
|
+
if (count >= 1_000) {
|
|
28
|
+
return `${(count / 1_000).toFixed(1)}k`;
|
|
29
|
+
}
|
|
30
|
+
return String(count);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Pick the colour for the token percentage indicator.
|
|
34
|
+
*/
|
|
35
|
+
function tokenColor(pct) {
|
|
36
|
+
if (pct > 80) {
|
|
37
|
+
return 'red';
|
|
38
|
+
}
|
|
39
|
+
if (pct >= 50) {
|
|
40
|
+
return 'yellow';
|
|
41
|
+
}
|
|
42
|
+
return 'green';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Determine if an environment name is production-like (G1).
|
|
46
|
+
*/
|
|
47
|
+
function isProdEnvironment(name) {
|
|
48
|
+
return /prod|production|live/i.test(name);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A single mode badge. The active mode is rendered with inverse styling.
|
|
52
|
+
*/
|
|
53
|
+
function ModeBadge({ mode, active }) {
|
|
54
|
+
const label = ` ${mode.charAt(0).toUpperCase() + mode.slice(1)} `;
|
|
55
|
+
if (active) {
|
|
56
|
+
return (_jsx(Text, { bold: true, inverse: true, children: label }));
|
|
57
|
+
}
|
|
58
|
+
return _jsx(Text, { dimColor: true, children: label });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* StatusBar renders the bottom status line of the TUI.
|
|
62
|
+
*/
|
|
63
|
+
export function StatusBar({ session, isProcessing = false, processingStartTime = null, inputLineCount = 1, showScrollHint = false, copyToast = '', showStreamingHint = false, modeToast, searchQuery, searchResultCount, }) {
|
|
64
|
+
const pct = session.maxTokens > 0 ? Math.round((session.tokenCount / session.maxTokens) * 100) : 0;
|
|
65
|
+
const pctColor = tokenColor(pct);
|
|
66
|
+
const costStr = session.costUSD < 0.01 && session.costUSD > 0
|
|
67
|
+
? `$${session.costUSD.toFixed(4)}`
|
|
68
|
+
: `$${session.costUSD.toFixed(2)}`;
|
|
69
|
+
// Elapsed time counter
|
|
70
|
+
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
71
|
+
const intervalRef = useRef(null);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (isProcessing && processingStartTime) {
|
|
74
|
+
// Immediately calculate current elapsed
|
|
75
|
+
setElapsedSeconds(Math.floor((Date.now() - processingStartTime) / 1000));
|
|
76
|
+
intervalRef.current = setInterval(() => {
|
|
77
|
+
setElapsedSeconds(Math.floor((Date.now() - processingStartTime) / 1000));
|
|
78
|
+
}, 1000);
|
|
79
|
+
return () => {
|
|
80
|
+
if (intervalRef.current) {
|
|
81
|
+
clearInterval(intervalRef.current);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
setElapsedSeconds(0);
|
|
87
|
+
if (intervalRef.current) {
|
|
88
|
+
clearInterval(intervalRef.current);
|
|
89
|
+
intervalRef.current = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}, [isProcessing, processingStartTime]);
|
|
93
|
+
// Snapshot display: show undo hint when snapshots exist (G19)
|
|
94
|
+
const snapshotDisplay = session.snapshotCount > 0
|
|
95
|
+
? `${session.snapshotCount} (↶ undo)`
|
|
96
|
+
: String(session.snapshotCount);
|
|
97
|
+
// Infra context colors (G1)
|
|
98
|
+
const tfColor = session.terraformWorkspace && isProdEnvironment(session.terraformWorkspace)
|
|
99
|
+
? 'yellow'
|
|
100
|
+
: 'green';
|
|
101
|
+
const k8sColor = session.kubectlContext && isProdEnvironment(session.kubectlContext)
|
|
102
|
+
? 'yellow'
|
|
103
|
+
: 'green';
|
|
104
|
+
// L5: Build a compact visual progress bar for context budget
|
|
105
|
+
const BAR_WIDTH = 8;
|
|
106
|
+
const filledBars = Math.round((pct / 100) * BAR_WIDTH);
|
|
107
|
+
const progressBar = '█'.repeat(filledBars) + '░'.repeat(BAR_WIDTH - filledBars);
|
|
108
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", width: "100%", children: [_jsx(Box, { children: MODES.map(m => (_jsx(ModeBadge, { mode: m, active: m === session.mode }, m))) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Ctx: " }), _jsx(Text, { color: pctColor, children: progressBar }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: pctColor, children: [formatTokens(session.tokenCount), "/", formatTokens(session.maxTokens), " (", pct, "%)"] }), _jsx(Text, { dimColor: true, children: " | Cost: " }), _jsx(Text, { children: costStr }), _jsx(Text, { dimColor: true, children: " | Snapshots: " }), _jsx(Text, { children: snapshotDisplay }), session.terraformWorkspace && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsxs(Text, { color: tfColor, children: ["tf:", session.terraformWorkspace] })] })), session.kubectlContext && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsxs(Text, { color: k8sColor, children: ["k8s:", session.kubectlContext] })] })), session.infraCostDelta && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsxs(Text, { color: "green", children: ["delta:", session.infraCostDelta] })] })), isProcessing && elapsedSeconds > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsxs(Text, { color: "cyan", children: [elapsedSeconds, "s"] })] })), inputLineCount > 1 && !isProcessing && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsxs(Text, { color: "cyan", children: [inputLineCount, " lines"] })] })), _jsx(Text, { dimColor: true, children: " | Tab" }), _jsx(Text, { dimColor: true, children: ":mode " }), _jsx(Text, { dimColor: true, children: "Esc" }), _jsx(Text, { dimColor: true, children: ":cancel " }), _jsx(Text, { dimColor: true, children: "^C" }), _jsx(Text, { dimColor: true, children: ":exit" }), !isProcessing && inputLineCount <= 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { dimColor: true, children: "F1:help /tree /terminal" }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: "? help" })] })), showScrollHint && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: "cyan", children: "\u2191\u2193 scroll | G bottom" })] })), showStreamingHint && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: "cyan", children: "Esc:stop stream" })] })), copyToast && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: "green", children: copyToast })] })), modeToast && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { color: "cyan", italic: true, children: modeToast })] })), searchQuery && searchResultCount !== undefined && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " | " }), _jsxs(Text, { color: "yellow", children: ["Search: \"", searchQuery, "\" \u2014 ", searchResultCount, " results"] })] }))] })] }));
|
|
109
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* A scrollable pane showing the last tool outputs as a rolling buffer.
|
|
5
|
+
*/
|
|
6
|
+
export function TerminalPane({ toolCalls, maxLines = 20 }) {
|
|
7
|
+
// Collect lines from all tool calls (completed + running with streaming output)
|
|
8
|
+
const outputLines = [];
|
|
9
|
+
for (const tc of toolCalls) {
|
|
10
|
+
if (tc.status === 'running') {
|
|
11
|
+
// Show live streaming output for in-progress tool calls (Gap 1)
|
|
12
|
+
const liveOutput = tc.streamingOutput ?? '(waiting for output...)';
|
|
13
|
+
const lines = liveOutput.split('\n').filter(l => l.length > 0);
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
outputLines.push({ text: line, isError: false, toolName: tc.name, live: true });
|
|
16
|
+
}
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (tc.status !== 'completed' && tc.status !== 'failed')
|
|
20
|
+
continue;
|
|
21
|
+
const output = tc.result?.output ?? '';
|
|
22
|
+
const isError = tc.result?.isError ?? false;
|
|
23
|
+
const lines = output.split('\n').filter(l => l.length > 0);
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
outputLines.push({ text: line, isError, toolName: tc.name });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Show only the last maxLines lines
|
|
29
|
+
const visible = outputLines.slice(-maxLines);
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Terminal Output" }), _jsx(Text, { dimColor: true, children: " (read-only \u00B7 /terminal to toggle)" })] }), visible.length === 0 ? (_jsx(Text, { dimColor: true, italic: true, children: "No tool output yet." })) : (visible.map((line, i) => (_jsxs(Text, { color: line.isError ? 'red' : line.live ? 'cyan' : undefined, dimColor: !line.isError && !line.live, wrap: "truncate", children: [line.live ? '[>] ' : '', line.text] }, i))))] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ToolCallDisplay Component
|
|
4
|
+
*
|
|
5
|
+
* Renders one or more tool invocations inline with the conversation. Each tool
|
|
6
|
+
* call is shown in a bordered box with a header containing the tool name and
|
|
7
|
+
* status indicator. While a tool is running the box shows a Spinner; on
|
|
8
|
+
* completion or failure it shows a condensed result summary.
|
|
9
|
+
*
|
|
10
|
+
* Specialised renderers exist for common tools:
|
|
11
|
+
* - read_file: filename + optional line range
|
|
12
|
+
* - edit_file: unified diff with context lines, red/green colouring
|
|
13
|
+
* - bash: command + expandable/collapsible output
|
|
14
|
+
* - terraform: resource table
|
|
15
|
+
*
|
|
16
|
+
* All other tools fall through to a generic key/value display.
|
|
17
|
+
*/
|
|
18
|
+
import { useState, useEffect } from 'react';
|
|
19
|
+
import { Box, Text, useInput } from 'ink';
|
|
20
|
+
import Spinner from 'ink-spinner';
|
|
21
|
+
import { parseTerraformPlanOutput } from '../agent/deploy-preview';
|
|
22
|
+
/** Maximum number of output lines shown for bash tool results. */
|
|
23
|
+
const MAX_BASH_OUTPUT_LINES = 50;
|
|
24
|
+
/** Lines shown in collapsed view. */
|
|
25
|
+
const COLLAPSED_LINES = 20;
|
|
26
|
+
/* ---------------------------------------------------------------------------
|
|
27
|
+
* Status badge
|
|
28
|
+
* -------------------------------------------------------------------------*/
|
|
29
|
+
/** Long-running tools that get a context hint about expected duration. */
|
|
30
|
+
const LONG_RUNNING_TOOLS = new Set([
|
|
31
|
+
'terraform', 'terraform_plan_analyze', 'deploy_preview', 'drift_detect',
|
|
32
|
+
'helm', 'k8s_rbac', 'cfn', 'gitops',
|
|
33
|
+
]);
|
|
34
|
+
function StatusBadge({ status, startTime }) {
|
|
35
|
+
const [elapsed, setElapsed] = useState(0);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (status !== 'running' || !startTime)
|
|
38
|
+
return;
|
|
39
|
+
const initial = Math.floor((Date.now() - startTime) / 1000);
|
|
40
|
+
setElapsed(initial);
|
|
41
|
+
const id = setInterval(() => {
|
|
42
|
+
setElapsed(Math.floor((Date.now() - startTime) / 1000));
|
|
43
|
+
}, 1000);
|
|
44
|
+
return () => clearInterval(id);
|
|
45
|
+
}, [status, startTime]);
|
|
46
|
+
switch (status) {
|
|
47
|
+
case 'pending':
|
|
48
|
+
return _jsx(Text, { dimColor: true, children: "[pending]" });
|
|
49
|
+
case 'running':
|
|
50
|
+
return (_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), startTime && elapsed > 0 ? ` ${elapsed}s` : ''] }));
|
|
51
|
+
case 'completed':
|
|
52
|
+
return _jsx(Text, { color: "green", children: "[done]" });
|
|
53
|
+
case 'failed':
|
|
54
|
+
return _jsx(Text, { color: "red", children: "[failed]" });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/* ---------------------------------------------------------------------------
|
|
58
|
+
* Per-tool body renderers
|
|
59
|
+
* -------------------------------------------------------------------------*/
|
|
60
|
+
function ReadFileBody({ input, result, }) {
|
|
61
|
+
const filePath = String(input.file_path ?? input.path ?? '');
|
|
62
|
+
const startLine = input.start_line;
|
|
63
|
+
const endLine = input.end_line;
|
|
64
|
+
const rangeLabel = startLine != null
|
|
65
|
+
? endLine != null
|
|
66
|
+
? ` (lines ${startLine}-${endLine})`
|
|
67
|
+
: ` (from line ${startLine})`
|
|
68
|
+
: '';
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "file: " }), _jsx(Text, { color: "cyan", children: filePath }), _jsx(Text, { dimColor: true, children: rangeLabel })] }), result && !result.isError && (_jsxs(Text, { dimColor: true, children: [result.output.split('\n').length, " lines read"] })), result && result.isError && _jsx(Text, { color: "red", children: result.output })] }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Compute a minimal unified diff between old and new text with context lines.
|
|
73
|
+
*/
|
|
74
|
+
function computeDiff(oldStr, newStr) {
|
|
75
|
+
const oldLines = oldStr.split('\n');
|
|
76
|
+
const newLines = newStr.split('\n');
|
|
77
|
+
const elements = [];
|
|
78
|
+
// Find common prefix
|
|
79
|
+
let prefixLen = 0;
|
|
80
|
+
while (prefixLen < oldLines.length &&
|
|
81
|
+
prefixLen < newLines.length &&
|
|
82
|
+
oldLines[prefixLen] === newLines[prefixLen]) {
|
|
83
|
+
prefixLen++;
|
|
84
|
+
}
|
|
85
|
+
// Find common suffix (from the end, but not overlapping with prefix)
|
|
86
|
+
let suffixLen = 0;
|
|
87
|
+
while (suffixLen < oldLines.length - prefixLen &&
|
|
88
|
+
suffixLen < newLines.length - prefixLen &&
|
|
89
|
+
oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]) {
|
|
90
|
+
suffixLen++;
|
|
91
|
+
}
|
|
92
|
+
// Context: show up to 2 lines before the diff
|
|
93
|
+
const contextStart = Math.max(0, prefixLen - 2);
|
|
94
|
+
for (let i = contextStart; i < prefixLen; i++) {
|
|
95
|
+
elements.push(_jsxs(Text, { dimColor: true, children: [' ', oldLines[i]] }, `ctx-pre-${i}`));
|
|
96
|
+
}
|
|
97
|
+
// Removed lines (from old)
|
|
98
|
+
const oldDiffEnd = oldLines.length - suffixLen;
|
|
99
|
+
for (let i = prefixLen; i < oldDiffEnd; i++) {
|
|
100
|
+
elements.push(_jsxs(Text, { color: "red", children: ["- ", oldLines[i]] }, `rm-${i}`));
|
|
101
|
+
}
|
|
102
|
+
// Added lines (from new)
|
|
103
|
+
const newDiffEnd = newLines.length - suffixLen;
|
|
104
|
+
for (let i = prefixLen; i < newDiffEnd; i++) {
|
|
105
|
+
elements.push(_jsxs(Text, { color: "green", children: ["+ ", newLines[i]] }, `add-${i}`));
|
|
106
|
+
}
|
|
107
|
+
// Context: show up to 2 lines after the diff
|
|
108
|
+
const contextEnd = Math.min(oldLines.length, oldDiffEnd + 2);
|
|
109
|
+
for (let i = oldDiffEnd; i < contextEnd; i++) {
|
|
110
|
+
elements.push(_jsxs(Text, { dimColor: true, children: [' ', oldLines[i]] }, `ctx-post-${i}`));
|
|
111
|
+
}
|
|
112
|
+
return elements;
|
|
113
|
+
}
|
|
114
|
+
function EditFileBody({ input, result, }) {
|
|
115
|
+
const filePath = String(input.file_path ?? input.path ?? '');
|
|
116
|
+
const oldStr = String(input.old_string ?? '');
|
|
117
|
+
const newStr = String(input.new_string ?? '');
|
|
118
|
+
const replaceAll = input.replace_all === true;
|
|
119
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "file: " }), _jsx(Text, { color: "cyan", children: filePath }), replaceAll && _jsx(Text, { dimColor: true, children: " (replace all)" })] }), oldStr && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: computeDiff(oldStr, newStr) })), result && result.isError && _jsx(Text, { color: "red", children: result.output })] }));
|
|
120
|
+
}
|
|
121
|
+
function BashBody({ input, result, }) {
|
|
122
|
+
const command = String(input.command ?? '');
|
|
123
|
+
const [expanded, setExpanded] = useState(false);
|
|
124
|
+
useInput((_input, key) => {
|
|
125
|
+
if (_input === 'e' && !key.ctrl && !key.meta) {
|
|
126
|
+
setExpanded(prev => !prev);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "$ " }), _jsx(Text, { bold: true, children: command })] }), result && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: (() => {
|
|
130
|
+
const lines = result.output.split('\n');
|
|
131
|
+
const showLimit = expanded ? MAX_BASH_OUTPUT_LINES : COLLAPSED_LINES;
|
|
132
|
+
const truncated = lines.length > showLimit;
|
|
133
|
+
const visible = truncated ? lines.slice(0, showLimit) : lines;
|
|
134
|
+
return (_jsxs(_Fragment, { children: [visible.map((line, i) => (_jsx(Text, { color: result.isError ? 'red' : undefined, dimColor: !result.isError, children: line }, i))), truncated && (_jsxs(Text, { dimColor: true, italic: true, children: ["... ", lines.length - showLimit, " more lines", ' ', expanded ? "(press 'e' to collapse)" : "(press 'e' to expand)"] })), !truncated && lines.length > COLLAPSED_LINES && expanded && (_jsx(Text, { dimColor: true, italic: true, children: "(press 'e' to collapse)" }))] }));
|
|
135
|
+
})() }))] }));
|
|
136
|
+
}
|
|
137
|
+
function TerraformBody({ input, result, }) {
|
|
138
|
+
const action = String(input.action ?? input.command ?? input.subcommand ?? 'plan');
|
|
139
|
+
// Gap 8: Show structured plan summary when output contains plan data
|
|
140
|
+
const isPlan = action === 'plan' || (result?.output ?? '').includes('Plan:');
|
|
141
|
+
const changes = isPlan && result && !result.isError
|
|
142
|
+
? parseTerraformPlanOutput(result.output)
|
|
143
|
+
: [];
|
|
144
|
+
const creates = changes.filter(c => c.action === 'create').length;
|
|
145
|
+
const updates = changes.filter(c => c.action === 'update').length;
|
|
146
|
+
const destroys = changes.filter(c => c.action === 'destroy').length;
|
|
147
|
+
const replaces = changes.filter(c => c.action === 'replace').length;
|
|
148
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "terraform " }), _jsx(Text, { bold: true, children: action })] }), isPlan && changes.length > 0 && result && !result.isError && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Plan Summary" }), _jsxs(Box, { children: [creates > 0 && _jsxs(Text, { color: "green", children: ["+", creates, " create "] }), updates > 0 && _jsxs(Text, { color: "yellow", children: ["~", updates, " change "] }), destroys > 0 && _jsxs(Text, { color: "red", children: ["-", destroys, " destroy "] }), replaces > 0 && _jsxs(Text, { color: "magenta", children: ["\u00B1", replaces, " replace "] }), creates === 0 && updates === 0 && destroys === 0 && replaces === 0 && (_jsx(Text, { dimColor: true, children: "No changes" }))] }), changes.slice(0, 10).map((c, i) => {
|
|
149
|
+
const icon = c.action === 'create' ? '+' : c.action === 'destroy' ? '-' : c.action === 'replace' ? '±' : '~';
|
|
150
|
+
const color = c.action === 'create' ? 'green' : c.action === 'destroy' ? 'red' : c.action === 'replace' ? 'magenta' : 'yellow';
|
|
151
|
+
return (_jsxs(Text, { color: color, children: [icon, " ", c.resource] }, i));
|
|
152
|
+
}), changes.length > 10 && _jsxs(Text, { dimColor: true, children: ["... and ", changes.length - 10, " more"] })] })), !(isPlan && changes.length > 0) && result && !result.isError && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: (() => {
|
|
153
|
+
const lines = result.output.split('\n');
|
|
154
|
+
const MAX_LINES = 200;
|
|
155
|
+
const displayLines = lines.slice(0, MAX_LINES);
|
|
156
|
+
return (_jsxs(_Fragment, { children: [displayLines.map((line, i) => {
|
|
157
|
+
let color;
|
|
158
|
+
if (line.startsWith('+') || line.includes('will be created')) {
|
|
159
|
+
color = 'green';
|
|
160
|
+
}
|
|
161
|
+
else if (line.startsWith('-') || line.includes('will be destroyed')) {
|
|
162
|
+
color = 'red';
|
|
163
|
+
}
|
|
164
|
+
else if (line.startsWith('~') || line.includes('will be updated')) {
|
|
165
|
+
color = 'yellow';
|
|
166
|
+
}
|
|
167
|
+
return (_jsx(Text, { color: color, dimColor: !color, children: line }, i));
|
|
168
|
+
}), lines.length > MAX_LINES && (_jsxs(Text, { dimColor: true, children: ["... ", lines.length - MAX_LINES, " more lines (full output saved to tool history)"] }))] }));
|
|
169
|
+
})() })), result && result.isError && _jsx(Text, { color: "red", children: result.output })] }));
|
|
170
|
+
}
|
|
171
|
+
/** G12: Kubectl output renderer with status colorization */
|
|
172
|
+
function KubectlBody({ input, result, }) {
|
|
173
|
+
const action = String(input.action ?? input.command ?? 'get');
|
|
174
|
+
const colorizeKubectlLine = (line, i) => {
|
|
175
|
+
// Status coloring for kubectl get output
|
|
176
|
+
if (/\bRunning\b/.test(line)) {
|
|
177
|
+
return _jsx(Text, { color: "green", children: line }, i);
|
|
178
|
+
}
|
|
179
|
+
if (/\b(Pending|ContainerCreating|Init:|PodInitializing)\b/.test(line)) {
|
|
180
|
+
return _jsx(Text, { color: "yellow", children: line }, i);
|
|
181
|
+
}
|
|
182
|
+
if (/\b(CrashLoopBackOff|Error|Failed|OOMKilled|ImagePullBackOff|ErrImagePull)\b/.test(line)) {
|
|
183
|
+
return _jsx(Text, { color: "red", children: line }, i);
|
|
184
|
+
}
|
|
185
|
+
if (/\bCompleted\b/.test(line)) {
|
|
186
|
+
return _jsx(Text, { color: "green", dimColor: true, children: line }, i);
|
|
187
|
+
}
|
|
188
|
+
if (/\bTerminating\b/.test(line)) {
|
|
189
|
+
return _jsx(Text, { color: "yellow", dimColor: true, children: line }, i);
|
|
190
|
+
}
|
|
191
|
+
return _jsx(Text, { dimColor: true, children: line }, i);
|
|
192
|
+
};
|
|
193
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "kubectl " }), _jsx(Text, { bold: true, children: action })] }), result && !result.isError && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: result.output.split('\n').slice(0, 80).map((line, i) => colorizeKubectlLine(line, i)) })), result && result.isError && _jsx(Text, { color: "red", children: result.output })] }));
|
|
194
|
+
}
|
|
195
|
+
/** M2: Docker build progress renderer */
|
|
196
|
+
function DockerBuildBody({ input, result, streamingOutput, }) {
|
|
197
|
+
const action = String(input.action ?? '');
|
|
198
|
+
if (action !== 'build') {
|
|
199
|
+
// Non-build actions: show raw output
|
|
200
|
+
if (result && !result.isError) {
|
|
201
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: result.output.split('\n').slice(0, 20).map((line, i) => (_jsx(Text, { dimColor: true, children: line }, i))) }));
|
|
202
|
+
}
|
|
203
|
+
if (result?.isError)
|
|
204
|
+
return _jsx(Text, { color: "red", children: result.output });
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
// Parse step progress from streaming output or result
|
|
208
|
+
const outputText = streamingOutput || result?.output || '';
|
|
209
|
+
const stepMatch = outputText.match(/Step\s+(\d+)\/(\d+)/gi);
|
|
210
|
+
const lastStep = stepMatch ? stepMatch[stepMatch.length - 1] : null;
|
|
211
|
+
const totalSteps = lastStep ? parseInt(lastStep.match(/\/(\d+)/)[1]) : 0;
|
|
212
|
+
const currentStep = lastStep ? parseInt(lastStep.match(/Step\s+(\d+)/i)[1]) : 0;
|
|
213
|
+
const succeeded = outputText.includes('Successfully built') || outputText.includes('Successfully tagged');
|
|
214
|
+
const failed = result?.isError || false;
|
|
215
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [totalSteps > 0 && (_jsx(Box, { children: _jsxs(Text, { color: succeeded ? 'green' : failed ? 'red' : 'cyan', children: [succeeded ? '[ok] ' : failed ? '[xx] ' : '[..] ', "[", currentStep, "/", totalSteps, " steps]"] }) })), outputText.split('\n').filter(l => l.trim()).slice(-5).map((line, i) => {
|
|
216
|
+
const isStep = /^Step\s+\d+\/\d+/i.test(line.trim());
|
|
217
|
+
const isSuccess = /Successfully/i.test(line);
|
|
218
|
+
const isError = /error/i.test(line);
|
|
219
|
+
return (_jsx(Text, { color: isSuccess ? 'green' : isError ? 'red' : isStep ? 'cyan' : undefined, dimColor: !isStep && !isSuccess && !isError, children: line }, i));
|
|
220
|
+
}), result?.isError && _jsx(Text, { color: "red", children: result.output })] }));
|
|
221
|
+
}
|
|
222
|
+
function GenericBody({ input, result, }) {
|
|
223
|
+
const entries = Object.entries(input).slice(0, 6);
|
|
224
|
+
const omitted = Object.keys(input).length - entries.length;
|
|
225
|
+
return (_jsxs(Box, { flexDirection: "column", children: [entries.map(([key, value]) => {
|
|
226
|
+
const str = String(value);
|
|
227
|
+
const truncated = str.length > 120;
|
|
228
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [key, ": "] }), _jsx(Text, { children: truncated ? `${str.slice(0, 120)}...` : str })] }, key));
|
|
229
|
+
}), omitted > 0 && (_jsxs(Text, { dimColor: true, italic: true, children: ["... ", omitted, " more fields"] })), result && result.isError && _jsx(Text, { color: "red", children: result.output }), result && !result.isError && (_jsx(Text, { dimColor: true, children: result.output.length > 120 ? `${result.output.slice(0, 120)}...` : result.output }))] }));
|
|
230
|
+
}
|
|
231
|
+
/* ---------------------------------------------------------------------------
|
|
232
|
+
* Single tool call box
|
|
233
|
+
* -------------------------------------------------------------------------*/
|
|
234
|
+
function ToolCallBox({ toolCall, expanded }) {
|
|
235
|
+
const durationLabel = toolCall.duration != null ? ` (${toolCall.duration}ms)` : '';
|
|
236
|
+
const isLongRunning = LONG_RUNNING_TOOLS.has(toolCall.name.toLowerCase());
|
|
237
|
+
// Choose specialised body renderer based on tool name
|
|
238
|
+
const renderBody = () => {
|
|
239
|
+
if (!expanded && toolCall.status === 'completed') {
|
|
240
|
+
return (_jsx(Text, { dimColor: true, children: toolCall.result
|
|
241
|
+
? toolCall.result.isError
|
|
242
|
+
? toolCall.result.output.slice(0, 80)
|
|
243
|
+
: 'completed'
|
|
244
|
+
: 'completed' }));
|
|
245
|
+
}
|
|
246
|
+
const name = toolCall.name.toLowerCase();
|
|
247
|
+
const props = { input: toolCall.input, result: toolCall.result };
|
|
248
|
+
if (name === 'read_file' || name === 'read') {
|
|
249
|
+
return _jsx(ReadFileBody, { ...props });
|
|
250
|
+
}
|
|
251
|
+
if (name === 'edit_file' || name === 'edit') {
|
|
252
|
+
return _jsx(EditFileBody, { ...props });
|
|
253
|
+
}
|
|
254
|
+
if (name === 'bash' || name === 'execute' || name === 'run_command') {
|
|
255
|
+
return _jsx(BashBody, { ...props });
|
|
256
|
+
}
|
|
257
|
+
if (name.startsWith('terraform') || name === 'tf_plan' || name === 'tf_apply') {
|
|
258
|
+
return _jsx(TerraformBody, { ...props });
|
|
259
|
+
}
|
|
260
|
+
if (name === 'kubectl' || name === 'k8s') {
|
|
261
|
+
return _jsx(KubectlBody, { ...props });
|
|
262
|
+
}
|
|
263
|
+
if (name === 'docker') {
|
|
264
|
+
return _jsx(DockerBuildBody, { input: toolCall.input, result: toolCall.result, streamingOutput: toolCall.streamingOutput });
|
|
265
|
+
}
|
|
266
|
+
return _jsx(GenericBody, { ...props });
|
|
267
|
+
};
|
|
268
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: toolCall.status === 'failed' ? 'red' : 'gray', paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusBadge, { status: toolCall.status, startTime: toolCall.startTime }), _jsxs(Text, { bold: true, children: [" ", toolCall.name] }), _jsx(Text, { dimColor: true, children: durationLabel }), toolCall.status === 'completed' && toolCall.duration != null && toolCall.duration > 5000 && (_jsxs(Text, { dimColor: true, children: [" [", (toolCall.duration / 1000).toFixed(1), "s]"] })), toolCall.status === 'running' && toolCall.name === 'logs' && (_jsx(Text, { color: "cyan", children: " \u25CF LIVE" }))] }), toolCall.status === 'running' && isLongRunning && (_jsx(Text, { dimColor: true, italic: true, children: "This may take several minutes for large infrastructure changes." })), toolCall.status === 'running' && toolCall.streamingOutput && toolCall.streamingOutput.trim() && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: "[LIVE] " }), _jsx(Text, { children: '─'.repeat(34) })] }), (() => {
|
|
269
|
+
const allLines = toolCall.streamingOutput.split('\n');
|
|
270
|
+
const isTerraformOrKubectl = toolCall.name === 'terraform' || toolCall.name === 'kubectl' || toolCall.name === 'logs';
|
|
271
|
+
// M1: Increased streaming window — 60 lines for terraform/kubectl/logs, 40 for others
|
|
272
|
+
const windowSize = isTerraformOrKubectl ? 60 : 40;
|
|
273
|
+
const visibleLines = allLines.slice(-windowSize);
|
|
274
|
+
// Pad to minimum 4 lines so the live area is always visible
|
|
275
|
+
while (visibleLines.length < 4)
|
|
276
|
+
visibleLines.push('');
|
|
277
|
+
const hiddenCount = Math.max(0, allLines.length - windowSize);
|
|
278
|
+
return (_jsxs(_Fragment, { children: [hiddenCount > 0 && (_jsxs(Text, { dimColor: true, children: ["... ", hiddenCount, " earlier lines"] })), visibleLines.map((line, i) => {
|
|
279
|
+
// M2: Color terraform/kubectl streaming output lines
|
|
280
|
+
let lineColor;
|
|
281
|
+
if (line.match(/^\s*\+/) || line.includes('will be created') || line.includes(' created'))
|
|
282
|
+
lineColor = 'green';
|
|
283
|
+
else if (line.match(/^\s*-/) || line.includes('will be destroyed') || line.includes(' destroyed'))
|
|
284
|
+
lineColor = 'red';
|
|
285
|
+
else if (line.match(/^\s*~/) || line.includes('will be updated') || line.includes(' modified'))
|
|
286
|
+
lineColor = 'yellow';
|
|
287
|
+
return _jsx(Text, { color: lineColor ?? 'gray', dimColor: !lineColor, children: line }, i);
|
|
288
|
+
})] }));
|
|
289
|
+
})()] })), _jsx(Box, { marginTop: 1, children: renderBody() })] }));
|
|
290
|
+
}
|
|
291
|
+
/* ---------------------------------------------------------------------------
|
|
292
|
+
* Public component
|
|
293
|
+
* -------------------------------------------------------------------------*/
|
|
294
|
+
/**
|
|
295
|
+
* ToolCallDisplay renders a list of tool invocations. When `expanded` is
|
|
296
|
+
* false, completed calls are collapsed to a single summary line.
|
|
297
|
+
*/
|
|
298
|
+
export function ToolCallDisplay({ toolCalls, expanded = true }) {
|
|
299
|
+
if (toolCalls.length === 0) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: toolCalls.map(tc => (_jsx(ToolCallBox, { toolCall: tc, expanded: expanded }, tc.id))) }));
|
|
303
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* TreePane Component (L1)
|
|
4
|
+
*
|
|
5
|
+
* Collapsible file tree sidebar using box-drawing characters.
|
|
6
|
+
* Shows 2 levels deep by default; toggle via /tree slash command.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useEffect } from 'react';
|
|
9
|
+
import { Box, Text, useInput } from 'ink';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
function buildTree(dir, depth, maxDepth) {
|
|
13
|
+
if (depth > maxDepth)
|
|
14
|
+
return [];
|
|
15
|
+
try {
|
|
16
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
17
|
+
return entries
|
|
18
|
+
.filter(e => !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist')
|
|
19
|
+
.slice(0, 30) // limit entries per level
|
|
20
|
+
.map(e => {
|
|
21
|
+
const fullPath = path.join(dir, e.name);
|
|
22
|
+
const node = { name: e.name, fullPath, isDir: e.isDirectory() };
|
|
23
|
+
if (e.isDirectory() && depth < maxDepth) {
|
|
24
|
+
node.children = buildTree(fullPath, depth + 1, maxDepth);
|
|
25
|
+
}
|
|
26
|
+
return node;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function renderTree(nodes, prefix, selectedIdx, flatList, recentPaths) {
|
|
34
|
+
const elements = [];
|
|
35
|
+
nodes.forEach((node, i) => {
|
|
36
|
+
const isLast = i === nodes.length - 1;
|
|
37
|
+
const connector = isLast ? '└─' : '├─';
|
|
38
|
+
const childPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
39
|
+
const globalIdx = flatList.indexOf(node);
|
|
40
|
+
const isSelected = globalIdx === selectedIdx;
|
|
41
|
+
const isRecent = recentPaths.has(node.fullPath);
|
|
42
|
+
elements.push(_jsxs(Text, { color: isSelected ? 'cyan' : isRecent ? 'yellow' : undefined, bold: isSelected, children: [prefix, connector, " ", node.isDir ? '[/] ' : '', node.name] }, node.fullPath));
|
|
43
|
+
if (node.children && node.children.length > 0) {
|
|
44
|
+
elements.push(...renderTree(node.children, childPrefix, selectedIdx, flatList, recentPaths));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return elements;
|
|
48
|
+
}
|
|
49
|
+
function flattenTree(nodes) {
|
|
50
|
+
const result = [];
|
|
51
|
+
for (const node of nodes) {
|
|
52
|
+
result.push(node);
|
|
53
|
+
if (node.children) {
|
|
54
|
+
result.push(...flattenTree(node.children));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
export function TreePane({ cwd = process.cwd(), recentFiles = [], onSelectFile }) {
|
|
60
|
+
const [tree, setTree] = useState([]);
|
|
61
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
62
|
+
const recentPaths = new Set(recentFiles);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const nodes = buildTree(cwd, 0, 2);
|
|
65
|
+
setTree(nodes);
|
|
66
|
+
}, [cwd]);
|
|
67
|
+
const flat = flattenTree(tree);
|
|
68
|
+
useInput((input, key) => {
|
|
69
|
+
if (key.upArrow) {
|
|
70
|
+
setSelectedIdx(prev => Math.max(0, prev - 1));
|
|
71
|
+
}
|
|
72
|
+
else if (key.downArrow) {
|
|
73
|
+
setSelectedIdx(prev => Math.min(flat.length - 1, prev + 1));
|
|
74
|
+
}
|
|
75
|
+
else if (key.return && flat[selectedIdx]) {
|
|
76
|
+
const node = flat[selectedIdx];
|
|
77
|
+
if (!node.isDir && onSelectFile) {
|
|
78
|
+
onSelectFile(node.fullPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, overflow: "hidden", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Files" }), _jsx(Text, { dimColor: true, children: " (\u2191\u2193 navigate \u00B7 Enter to @ref \u00B7 /tree to toggle)" })] }), _jsxs(Text, { bold: true, dimColor: true, children: [path.basename(cwd), "/"] }), renderTree(tree, '', selectedIdx, flat, recentPaths)] }));
|
|
83
|
+
}
|