codeply-cli 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +63 -0
- package/bin/codeply.js +562 -259
- package/lib/agent.mjs +1721 -116
- package/lib/ai.js +1107 -184
- package/lib/apply-limit.js +33 -41
- package/lib/apply-patch.js +217 -0
- package/lib/arity-table.json +138 -0
- package/lib/arity.js +70 -0
- package/lib/auth.js +118 -112
- package/lib/commands.js +103 -0
- package/lib/config.js +392 -98
- package/lib/design-library/artifact-template.html +500 -0
- package/lib/design-library/assemble-artifact.mjs +21 -0
- package/lib/design-library/build.mjs +226 -0
- package/lib/design-library/cleanup.mjs +45 -0
- package/lib/design-library/index.json +22380 -0
- package/lib/design-library/make-table-data.mjs +39 -0
- package/lib/design-library/query.mjs +104 -0
- package/lib/diagnostics.js +192 -0
- package/lib/edit-engine.js +171 -14
- package/lib/github-agent.mjs +290 -0
- package/lib/history.js +75 -0
- package/lib/mcp.js +258 -0
- package/lib/native-tools.mjs +189 -0
- package/lib/oauth-connectors.js +478 -0
- package/lib/permissions.js +109 -0
- package/lib/plugins.js +353 -0
- package/lib/render-md.mjs +617 -617
- package/lib/server.mjs +457 -0
- package/lib/session-db.js +220 -0
- package/lib/share.js +168 -0
- package/lib/skills.js +416 -0
- package/lib/snapshot.js +146 -0
- package/lib/subagents.js +164 -0
- package/lib/terminal-hints.js +154 -0
- package/lib/textarea.mjs +258 -258
- package/lib/tools.mjs +1709 -73
- package/lib/tui.js +7 -7
- package/lib/tui.mjs +1086 -899
- package/lib/typecheck-worker.js +147 -0
- package/lib/web-tools.js +158 -0
- package/package.json +5 -3
- package/skills/SOURCE.md +58 -0
- package/skills/accessibility/SKILL.md +147 -0
- package/skills/agent-architecture-audit/SKILL.md +257 -0
- package/skills/agent-eval/SKILL.md +147 -0
- package/skills/agent-harness-construction/SKILL.md +74 -0
- package/skills/agent-introspection-debugging/SKILL.md +154 -0
- package/skills/agent-payment-x402/SKILL.md +225 -0
- package/skills/agent-self-evaluation/SKILL.md +182 -0
- package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -0
- package/skills/agent-self-evaluation/references/hook-integration.md +64 -0
- package/skills/agent-sort/SKILL.md +216 -0
- package/skills/agentic-engineering/SKILL.md +64 -0
- package/skills/agentic-os/SKILL.md +388 -0
- package/skills/ai-first-engineering/SKILL.md +52 -0
- package/skills/ai-regression-testing/SKILL.md +386 -0
- package/skills/android-clean-architecture/SKILL.md +340 -0
- package/skills/angular-developer/SKILL.md +155 -0
- package/skills/angular-developer/references/angular-animations.md +160 -0
- package/skills/angular-developer/references/angular-aria.md +410 -0
- package/skills/angular-developer/references/cli.md +86 -0
- package/skills/angular-developer/references/component-harnesses.md +59 -0
- package/skills/angular-developer/references/component-styling.md +91 -0
- package/skills/angular-developer/references/components.md +117 -0
- package/skills/angular-developer/references/creating-services.md +97 -0
- package/skills/angular-developer/references/data-resolvers.md +69 -0
- package/skills/angular-developer/references/define-routes.md +67 -0
- package/skills/angular-developer/references/defining-providers.md +72 -0
- package/skills/angular-developer/references/di-fundamentals.md +120 -0
- package/skills/angular-developer/references/e2e-testing.md +56 -0
- package/skills/angular-developer/references/effects.md +83 -0
- package/skills/angular-developer/references/hierarchical-injectors.md +43 -0
- package/skills/angular-developer/references/host-elements.md +80 -0
- package/skills/angular-developer/references/injection-context.md +63 -0
- package/skills/angular-developer/references/inputs.md +101 -0
- package/skills/angular-developer/references/linked-signal.md +59 -0
- package/skills/angular-developer/references/loading-strategies.md +61 -0
- package/skills/angular-developer/references/mcp.md +108 -0
- package/skills/angular-developer/references/navigate-to-routes.md +69 -0
- package/skills/angular-developer/references/outputs.md +86 -0
- package/skills/angular-developer/references/reactive-forms.md +122 -0
- package/skills/angular-developer/references/rendering-strategies.md +44 -0
- package/skills/angular-developer/references/resource.md +77 -0
- package/skills/angular-developer/references/route-animations.md +56 -0
- package/skills/angular-developer/references/route-guards.md +52 -0
- package/skills/angular-developer/references/router-lifecycle.md +45 -0
- package/skills/angular-developer/references/router-testing.md +87 -0
- package/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
- package/skills/angular-developer/references/signal-forms.md +795 -0
- package/skills/angular-developer/references/signals-overview.md +94 -0
- package/skills/angular-developer/references/tailwind-css.md +69 -0
- package/skills/angular-developer/references/template-driven-forms.md +114 -0
- package/skills/angular-developer/references/testing-fundamentals.md +65 -0
- package/skills/api-connector-builder/SKILL.md +121 -0
- package/skills/api-design/SKILL.md +524 -0
- package/skills/architecture-decision-records/SKILL.md +180 -0
- package/skills/article-writing/SKILL.md +80 -0
- package/skills/automation-audit-ops/SKILL.md +143 -0
- package/skills/autonomous-agent-harness/SKILL.md +274 -0
- package/skills/autonomous-loops/SKILL.md +611 -0
- package/skills/backend-patterns/SKILL.md +562 -0
- package/skills/benchmark/SKILL.md +95 -0
- package/skills/benchmark-methodology/SKILL.md +191 -0
- package/skills/benchmark-optimization-loop/SKILL.md +71 -0
- package/skills/blender-motion-state-inspection/SKILL.md +165 -0
- package/skills/blueprint/SKILL.md +106 -0
- package/skills/brand-discovery/SKILL.md +145 -0
- package/skills/brand-discovery/references/10_purpose-why.md +40 -0
- package/skills/brand-discovery/references/20_positioning.md +44 -0
- package/skills/brand-discovery/references/30_audience-niche.md +52 -0
- package/skills/brand-discovery/references/40_personality-archetype.md +57 -0
- package/skills/brand-discovery/references/50_voice-tone.md +59 -0
- package/skills/brand-discovery/references/60_narrative-story.md +50 -0
- package/skills/brand-discovery/references/70_founder-tension.md +49 -0
- package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -0
- package/skills/brand-voice/SKILL.md +98 -0
- package/skills/brand-voice/references/voice-profile-schema.md +55 -0
- package/skills/browser-qa/SKILL.md +105 -0
- package/skills/bun-runtime/SKILL.md +85 -0
- package/skills/canary-watch/SKILL.md +108 -0
- package/skills/carrier-relationship-management/SKILL.md +212 -0
- package/skills/cisco-ios-patterns/SKILL.md +164 -0
- package/skills/ck/SKILL.md +148 -0
- package/skills/claude-devfleet/SKILL.md +112 -0
- package/skills/click-path-audit/SKILL.md +245 -0
- package/skills/clickhouse-io/SKILL.md +445 -0
- package/skills/code-tour/SKILL.md +254 -0
- package/skills/codebase-onboarding/SKILL.md +234 -0
- package/skills/codehealth-mcp/SKILL.md +167 -0
- package/skills/coding-standards/SKILL.md +551 -0
- package/skills/competitive-platform-analysis/SKILL.md +214 -0
- package/skills/competitive-report-structure/SKILL.md +162 -0
- package/skills/compose-multiplatform-patterns/SKILL.md +300 -0
- package/skills/config-gc/SKILL.md +120 -0
- package/skills/configure-ecc/SKILL.md +385 -0
- package/skills/connections-optimizer/SKILL.md +190 -0
- package/skills/content-engine/SKILL.md +132 -0
- package/skills/content-hash-cache-pattern/SKILL.md +162 -0
- package/skills/context-budget/SKILL.md +136 -0
- package/skills/continuous-agent-loop/SKILL.md +46 -0
- package/skills/continuous-learning/SKILL.md +132 -0
- package/skills/continuous-learning-v2/SKILL.md +361 -0
- package/skills/contract-first/SKILL.md +287 -0
- package/skills/cost-aware-llm-pipeline/SKILL.md +184 -0
- package/skills/cost-tracking/SKILL.md +97 -0
- package/skills/council/SKILL.md +204 -0
- package/skills/cpp-coding-standards/SKILL.md +724 -0
- package/skills/cpp-testing/SKILL.md +325 -0
- package/skills/crosspost/SKILL.md +112 -0
- package/skills/csharp-testing/SKILL.md +322 -0
- package/skills/customer-billing-ops/SKILL.md +141 -0
- package/skills/customs-trade-compliance/SKILL.md +263 -0
- package/skills/dart-flutter-patterns/SKILL.md +564 -0
- package/skills/dashboard-builder/SKILL.md +109 -0
- package/skills/data-scraper-agent/SKILL.md +765 -0
- package/skills/data-throughput-accelerator/SKILL.md +74 -0
- package/skills/database-migrations/SKILL.md +430 -0
- package/skills/deep-research/SKILL.md +160 -0
- package/skills/defi-amm-security/SKILL.md +167 -0
- package/skills/delivery-gate/SKILL.md +126 -0
- package/skills/deployment-patterns/SKILL.md +428 -0
- package/skills/design-system/SKILL.md +83 -0
- package/skills/django-celery/SKILL.md +458 -0
- package/skills/django-patterns/SKILL.md +735 -0
- package/skills/django-security/SKILL.md +644 -0
- package/skills/django-tdd/SKILL.md +730 -0
- package/skills/django-verification/SKILL.md +470 -0
- package/skills/dmux-workflows/SKILL.md +192 -0
- package/skills/docker-patterns/SKILL.md +365 -0
- package/skills/documentation-lookup/SKILL.md +91 -0
- package/skills/dotnet-patterns/SKILL.md +322 -0
- package/skills/dynamic-workflow-mode/SKILL.md +124 -0
- package/skills/e2e-testing/SKILL.md +327 -0
- package/skills/ecc-guide/SKILL.md +190 -0
- package/skills/ecc-recipes/SKILL.md +149 -0
- package/skills/ecc-tools-cost-audit/SKILL.md +161 -0
- package/skills/email-ops/SKILL.md +122 -0
- package/skills/energy-procurement/SKILL.md +228 -0
- package/skills/enterprise-agent-ops/SKILL.md +51 -0
- package/skills/error-handling/SKILL.md +377 -0
- package/skills/eval-harness/SKILL.md +271 -0
- package/skills/evm-token-decimals/SKILL.md +131 -0
- package/skills/exa-search/SKILL.md +108 -0
- package/skills/fal-ai-media/SKILL.md +289 -0
- package/skills/fastapi-patterns/SKILL.md +514 -0
- package/skills/finance-billing-ops/SKILL.md +128 -0
- package/skills/flox-environments/SKILL.md +497 -0
- package/skills/flutter-dart-code-review/SKILL.md +436 -0
- package/skills/foundation-models-on-device/SKILL.md +243 -0
- package/skills/frontend-a11y/SKILL.md +446 -0
- package/skills/frontend-design-direction/SKILL.md +93 -0
- package/skills/frontend-patterns/SKILL.md +657 -0
- package/skills/frontend-slides/SKILL.md +185 -0
- package/skills/fsharp-testing/SKILL.md +281 -0
- package/skills/gan-style-harness/SKILL.md +279 -0
- package/skills/gateguard/SKILL.md +133 -0
- package/skills/generating-python-installer/SKILL.md +820 -0
- package/skills/git-workflow/SKILL.md +716 -0
- package/skills/github-ops/SKILL.md +145 -0
- package/skills/golang-patterns/SKILL.md +676 -0
- package/skills/golang-testing/SKILL.md +721 -0
- package/skills/google-workspace-ops/SKILL.md +96 -0
- package/skills/growth-log/SKILL.md +128 -0
- package/skills/healthcare-cdss-patterns/SKILL.md +246 -0
- package/skills/healthcare-emr-patterns/SKILL.md +160 -0
- package/skills/healthcare-eval-harness/SKILL.md +208 -0
- package/skills/healthcare-phi-compliance/SKILL.md +146 -0
- package/skills/hermes-imports/SKILL.md +89 -0
- package/skills/hexagonal-architecture/SKILL.md +277 -0
- package/skills/hipaa-compliance/SKILL.md +79 -0
- package/skills/homelab-network-readiness/SKILL.md +170 -0
- package/skills/homelab-network-setup/SKILL.md +130 -0
- package/skills/homelab-pihole-dns/SKILL.md +275 -0
- package/skills/homelab-vlan-segmentation/SKILL.md +312 -0
- package/skills/homelab-wireguard-vpn/SKILL.md +306 -0
- package/skills/hookify-rules/SKILL.md +128 -0
- package/skills/inherit-legacy-style/SKILL.md +157 -0
- package/skills/intent-driven-development/SKILL.md +360 -0
- package/skills/inventory-demand-planning/SKILL.md +247 -0
- package/skills/investor-materials/SKILL.md +97 -0
- package/skills/investor-outreach/SKILL.md +92 -0
- package/skills/ios-icon-gen/SKILL.md +158 -0
- package/skills/iterative-retrieval/SKILL.md +212 -0
- package/skills/ito-basket-compare/SKILL.md +64 -0
- package/skills/ito-compute/SKILL.md +129 -0
- package/skills/ito-data-atlas-agent/SKILL.md +64 -0
- package/skills/ito-market-intelligence/SKILL.md +61 -0
- package/skills/ito-trade-planner/SKILL.md +68 -0
- package/skills/java-coding-standards/SKILL.md +384 -0
- package/skills/jira-integration/SKILL.md +303 -0
- package/skills/jpa-patterns/SKILL.md +152 -0
- package/skills/knowledge-ops/SKILL.md +155 -0
- package/skills/kotlin-coroutines-flows/SKILL.md +285 -0
- package/skills/kotlin-exposed-patterns/SKILL.md +720 -0
- package/skills/kotlin-ktor-patterns/SKILL.md +690 -0
- package/skills/kotlin-patterns/SKILL.md +712 -0
- package/skills/kotlin-testing/SKILL.md +825 -0
- package/skills/kubernetes-patterns/SKILL.md +756 -0
- package/skills/laravel-patterns/SKILL.md +416 -0
- package/skills/laravel-plugin-discovery/SKILL.md +230 -0
- package/skills/laravel-security/SKILL.md +948 -0
- package/skills/laravel-tdd/SKILL.md +675 -0
- package/skills/laravel-verification/SKILL.md +180 -0
- package/skills/latency-critical-systems/SKILL.md +75 -0
- package/skills/lead-intelligence/SKILL.md +322 -0
- package/skills/liquid-glass-design/SKILL.md +279 -0
- package/skills/llm-trading-agent-security/SKILL.md +147 -0
- package/skills/logistics-exception-management/SKILL.md +222 -0
- package/skills/loop-design-check/SKILL.md +143 -0
- package/skills/mailtrap-email-integration/SKILL.md +77 -0
- package/skills/make-interfaces-feel-better/SKILL.md +152 -0
- package/skills/manim-video/SKILL.md +90 -0
- package/skills/market-research/SKILL.md +76 -0
- package/skills/marketing-campaign/SKILL.md +114 -0
- package/skills/mcp-server-patterns/SKILL.md +70 -0
- package/skills/messages-ops/SKILL.md +105 -0
- package/skills/ml-adoption-playbook/SKILL.md +57 -0
- package/skills/mle-workflow/SKILL.md +348 -0
- package/skills/motion-advanced/SKILL.md +596 -0
- package/skills/motion-foundations/SKILL.md +299 -0
- package/skills/motion-patterns/SKILL.md +434 -0
- package/skills/motion-ui/SKILL.md +576 -0
- package/skills/mysql-patterns/SKILL.md +413 -0
- package/skills/nanoclaw-repl/SKILL.md +34 -0
- package/skills/nestjs-patterns/SKILL.md +231 -0
- package/skills/netmiko-ssh-automation/SKILL.md +174 -0
- package/skills/network-bgp-diagnostics/SKILL.md +168 -0
- package/skills/network-config-validation/SKILL.md +211 -0
- package/skills/network-interface-health/SKILL.md +153 -0
- package/skills/nextjs-turbopack/SKILL.md +58 -0
- package/skills/nodejs-keccak256/SKILL.md +103 -0
- package/skills/nutrient-document-processing/SKILL.md +168 -0
- package/skills/nuxt4-patterns/SKILL.md +101 -0
- package/skills/openclaw-persona-forge/SKILL.md +289 -0
- package/skills/openclaw-persona-forge/references/avatar-style.md +124 -0
- package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -0
- package/skills/openclaw-persona-forge/references/error-handling.md +53 -0
- package/skills/openclaw-persona-forge/references/identity-tension.md +48 -0
- package/skills/openclaw-persona-forge/references/naming-system.md +39 -0
- package/skills/openclaw-persona-forge/references/output-template.md +166 -0
- package/skills/opensource-pipeline/SKILL.md +256 -0
- package/skills/orch-add-feature/SKILL.md +45 -0
- package/skills/orch-build-mvp/SKILL.md +49 -0
- package/skills/orch-change-feature/SKILL.md +43 -0
- package/skills/orch-fix-defect/SKILL.md +43 -0
- package/skills/orch-pipeline/SKILL.md +121 -0
- package/skills/orch-refine-code/SKILL.md +44 -0
- package/skills/parallel-execution-optimizer/SKILL.md +74 -0
- package/skills/perl-patterns/SKILL.md +505 -0
- package/skills/perl-security/SKILL.md +504 -0
- package/skills/perl-testing/SKILL.md +476 -0
- package/skills/plan-canvas/SKILL.md +153 -0
- package/skills/plan-orchestrate/SKILL.md +263 -0
- package/skills/plankton-code-quality/SKILL.md +237 -0
- package/skills/postgres-patterns/SKILL.md +148 -0
- package/skills/prediction-market-oracle-research/SKILL.md +64 -0
- package/skills/prediction-market-risk-review/SKILL.md +61 -0
- package/skills/premium-web-design/SKILL.md +154 -0
- package/skills/prisma-patterns/SKILL.md +401 -0
- package/skills/product-capability/SKILL.md +142 -0
- package/skills/product-lens/SKILL.md +93 -0
- package/skills/production-audit/SKILL.md +207 -0
- package/skills/production-scheduling/SKILL.md +238 -0
- package/skills/project-flow-ops/SKILL.md +112 -0
- package/skills/prompt-optimizer/SKILL.md +398 -0
- package/skills/python-patterns/SKILL.md +751 -0
- package/skills/python-testing/SKILL.md +817 -0
- package/skills/pytorch-patterns/SKILL.md +397 -0
- package/skills/quality-nonconformance/SKILL.md +260 -0
- package/skills/quarkus-patterns/SKILL.md +723 -0
- package/skills/quarkus-security/SKILL.md +468 -0
- package/skills/quarkus-tdd/SKILL.md +812 -0
- package/skills/quarkus-verification/SKILL.md +480 -0
- package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -0
- package/skills/react-native-patterns/SKILL.md +326 -0
- package/skills/react-patterns/SKILL.md +342 -0
- package/skills/react-performance/SKILL.md +575 -0
- package/skills/react-testing/SKILL.md +424 -0
- package/skills/recsys-pipeline-architect/SKILL.md +115 -0
- package/skills/recursive-decision-ledger/SKILL.md +81 -0
- package/skills/redis-patterns/SKILL.md +404 -0
- package/skills/regex-vs-llm-structured-text/SKILL.md +221 -0
- package/skills/remotion-video-creation/SKILL.md +43 -0
- package/skills/repo-scan/SKILL.md +79 -0
- package/skills/research-ops/SKILL.md +113 -0
- package/skills/returns-reverse-logistics/SKILL.md +240 -0
- package/skills/rules-distill/SKILL.md +265 -0
- package/skills/rust-patterns/SKILL.md +500 -0
- package/skills/rust-testing/SKILL.md +501 -0
- package/skills/safety-guard/SKILL.md +76 -0
- package/skills/santa-method/SKILL.md +307 -0
- package/skills/scientific-db-pubmed-database/SKILL.md +176 -0
- package/skills/scientific-db-uspto-database/SKILL.md +178 -0
- package/skills/scientific-pkg-gget/SKILL.md +167 -0
- package/skills/scientific-thinking-literature-review/SKILL.md +193 -0
- package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -0
- package/skills/search-first/SKILL.md +183 -0
- package/skills/security-bounty-hunter/SKILL.md +100 -0
- package/skills/security-review/SKILL.md +504 -0
- package/skills/security-scan/SKILL.md +166 -0
- package/skills/seo/SKILL.md +155 -0
- package/skills/skill-comply/SKILL.md +59 -0
- package/skills/skill-scout/SKILL.md +141 -0
- package/skills/skill-stocktake/SKILL.md +195 -0
- package/skills/social-graph-ranker/SKILL.md +155 -0
- package/skills/social-publisher/SKILL.md +130 -0
- package/skills/springboot-patterns/SKILL.md +315 -0
- package/skills/springboot-security/SKILL.md +273 -0
- package/skills/springboot-tdd/SKILL.md +159 -0
- package/skills/springboot-verification/SKILL.md +232 -0
- package/skills/strategic-compact/SKILL.md +142 -0
- package/skills/swift-actor-persistence/SKILL.md +144 -0
- package/skills/swift-concurrency-6-2/SKILL.md +216 -0
- package/skills/swift-protocol-di-testing/SKILL.md +191 -0
- package/skills/swiftui-patterns/SKILL.md +259 -0
- package/skills/taste/SKILL.md +264 -0
- package/skills/taste/references/genre-taxonomy.md +87 -0
- package/skills/tdd-workflow/SKILL.md +583 -0
- package/skills/team-agent-orchestration/SKILL.md +111 -0
- package/skills/team-builder/SKILL.md +169 -0
- package/skills/terminal-ops/SKILL.md +110 -0
- package/skills/tinystruct-patterns/SKILL.md +279 -0
- package/skills/tinystruct-patterns/references/architecture.md +90 -0
- package/skills/tinystruct-patterns/references/data-handling.md +60 -0
- package/skills/tinystruct-patterns/references/database.md +99 -0
- package/skills/tinystruct-patterns/references/routing.md +64 -0
- package/skills/tinystruct-patterns/references/system-usage.md +97 -0
- package/skills/tinystruct-patterns/references/testing.md +72 -0
- package/skills/token-budget-advisor/SKILL.md +134 -0
- package/skills/ui-demo/SKILL.md +466 -0
- package/skills/ui-to-vue/SKILL.md +135 -0
- package/skills/uncloud/SKILL.md +344 -0
- package/skills/unified-memory/SKILL.md +170 -0
- package/skills/unified-notifications-ops/SKILL.md +188 -0
- package/skills/verification-loop/SKILL.md +129 -0
- package/skills/video-editing/SKILL.md +311 -0
- package/skills/videodb/SKILL.md +375 -0
- package/skills/visa-doc-translate/SKILL.md +117 -0
- package/skills/vite-patterns/SKILL.md +450 -0
- package/skills/vue-patterns/SKILL.md +471 -0
- package/skills/windows-desktop-e2e/SKILL.md +888 -0
- package/skills/workspace-surface-audit/SKILL.md +126 -0
- package/skills/x-api/SKILL.md +235 -0
package/lib/render-md.mjs
CHANGED
|
@@ -1,617 +1,617 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codeply TUI
|
|
3
|
-
* Parses markdown into Ink-compatible React elements with syntax coloring.
|
|
4
|
-
*/
|
|
5
|
-
import React from 'react';
|
|
6
|
-
import { Box, Text } from 'ink';
|
|
7
|
-
|
|
8
|
-
const G = {
|
|
9
|
-
blue: '#4285F4',
|
|
10
|
-
red: '#EA4335',
|
|
11
|
-
yellow: '#FBBC05',
|
|
12
|
-
green: '#34A853',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// Minimal syntax highlighter for code blocks
|
|
16
|
-
function highlightCode(code, lang) {
|
|
17
|
-
const lines = code.split('\n');
|
|
18
|
-
return lines.map((line) => {
|
|
19
|
-
let colored = line;
|
|
20
|
-
|
|
21
|
-
// Comments
|
|
22
|
-
if (/^\s*\/\//.test(line) || /^\s*#/.test(line) || /^\s*\/\*/.test(line) || /^\s*\*/.test(line) || /^\s*<!--/.test(line)) {
|
|
23
|
-
return [{ text: line, color: '#5f6368' }];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Strings (basic)
|
|
27
|
-
const parts = [];
|
|
28
|
-
let remaining = line;
|
|
29
|
-
// Match strings, keywords, tags, numbers, properties
|
|
30
|
-
const regex = /("[^"]*"|'[^']*'|`[^`]*`)|(\b(?:function|const|let|var|if|else|return|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|from|default|async|await|try|catch|throw|typeof|instanceof|in|of|void|delete|null|undefined|true|false)\b)|(<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s[^>]*)?>?)|(\b\d+\.?\d*\b)|(#[0-9a-fA-F]{3,8})|(\b(?:style|color|display|margin|padding|border|background|width|height|font|position|top|left|right|bottom|text|align|overflow|flex|grid|gap|opacity|transform|transition|animation|content|justify|items|center|none|block|inline|relative|absolute|fixed|solid|dashed|hidden|auto|inherit|initial|unset)\b)/g;
|
|
31
|
-
|
|
32
|
-
let lastIdx = 0;
|
|
33
|
-
let match;
|
|
34
|
-
const result = [];
|
|
35
|
-
|
|
36
|
-
while ((match = regex.exec(remaining)) !== null) {
|
|
37
|
-
// Text before match
|
|
38
|
-
if (match.index > lastIdx) {
|
|
39
|
-
result.push({ text: remaining.slice(lastIdx, match.index), color: '#e8eaed' });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (match[1]) {
|
|
43
|
-
// String
|
|
44
|
-
result.push({ text: match[0], color: '#34A853' });
|
|
45
|
-
} else if (match[2]) {
|
|
46
|
-
// Keyword
|
|
47
|
-
result.push({ text: match[0], color: '#4285F4' });
|
|
48
|
-
} else if (match[3]) {
|
|
49
|
-
// HTML tag
|
|
50
|
-
result.push({ text: match[0], color: '#EA4335' });
|
|
51
|
-
} else if (match[4]) {
|
|
52
|
-
// Number
|
|
53
|
-
result.push({ text: match[0], color: '#FBBC05' });
|
|
54
|
-
} else if (match[5]) {
|
|
55
|
-
// Hex color
|
|
56
|
-
result.push({ text: match[0], color: '#FBBC05' });
|
|
57
|
-
} else if (match[6]) {
|
|
58
|
-
// CSS property
|
|
59
|
-
result.push({ text: match[0], color: '#e8eaed' });
|
|
60
|
-
} else {
|
|
61
|
-
result.push({ text: match[0], color: '#e8eaed' });
|
|
62
|
-
}
|
|
63
|
-
lastIdx = match.index + match[0].length;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (lastIdx < remaining.length) {
|
|
67
|
-
result.push({ text: remaining.slice(lastIdx), color: '#e8eaed' });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return result.length ? result : [{ text: line, color: '#e8eaed' }];
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Convert hex color to ANSI escape sequence
|
|
75
|
-
function hexToAnsi(hex) {
|
|
76
|
-
if (!hex) return '';
|
|
77
|
-
const h = hex.replace('#', '');
|
|
78
|
-
const r = parseInt(h.substring(0, 2), 16);
|
|
79
|
-
const g = parseInt(h.substring(2, 4), 16);
|
|
80
|
-
const b = parseInt(h.substring(4, 6), 16);
|
|
81
|
-
return `\x1b[38;2;${r};${g};${b}m`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Background counterpart of hexToAnsi. */
|
|
85
|
-
function hexToAnsiBg(hex) {
|
|
86
|
-
if (!hex) return '';
|
|
87
|
-
const h = hex.replace('#', '');
|
|
88
|
-
const r = parseInt(h.substring(0, 2), 16);
|
|
89
|
-
const g = parseInt(h.substring(2, 4), 16);
|
|
90
|
-
const b = parseInt(h.substring(4, 6), 16);
|
|
91
|
-
return `\x1b[48;2;${r};${g};${b}m`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const ANSI_RESET = '\x1b[0m';
|
|
95
|
-
const ANSI_BOLD = '\x1b[1m';
|
|
96
|
-
const ANSI_DIM = '\x1b[2m';
|
|
97
|
-
const ANSI_ITALIC = '\x1b[3m';
|
|
98
|
-
const ANSI_BG_CODE = '\x1b[48;2;45;46;48m'; // #2d2e30
|
|
99
|
-
|
|
100
|
-
// Surface colours for filled blocks. Ink 5's <Box> has no backgroundColor, so
|
|
101
|
-
// every filled row is built by hand: set the bg, emit the content, pad out to
|
|
102
|
-
// the panel width so the fill reaches the right edge, then reset. Padding has
|
|
103
|
-
// to be measured on the *visible* text, which is why widths are tracked
|
|
104
|
-
// explicitly rather than using String#padEnd on an ANSI-laden string.
|
|
105
|
-
const SURFACE = {
|
|
106
|
-
codeHeader: '#20242b',
|
|
107
|
-
codeBody: '#15181c',
|
|
108
|
-
gutter: '#1b1f24',
|
|
109
|
-
plan: '#161b22',
|
|
110
|
-
planHeader: '#1d2530',
|
|
111
|
-
diffAdd: '#0e2417',
|
|
112
|
-
diffDel: '#2a1418',
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
116
|
-
const visibleLength = (s) => s.replace(ANSI_PATTERN, '').length;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Render coloured segments clipped to `width`, returning the ANSI string and
|
|
120
|
-
* how many visible columns it used. Clipping happens per segment so a cut never
|
|
121
|
-
* lands inside an escape sequence.
|
|
122
|
-
*/
|
|
123
|
-
function clipSegments(segments, width) {
|
|
124
|
-
let out = '';
|
|
125
|
-
let used = 0;
|
|
126
|
-
for (const seg of segments) {
|
|
127
|
-
if (used >= width) break;
|
|
128
|
-
const room = width - used;
|
|
129
|
-
const text = seg.text.length > room ? seg.text.slice(0, room) : seg.text;
|
|
130
|
-
if (!text) continue;
|
|
131
|
-
out += (seg.color ? hexToAnsi(seg.color) : '') + text;
|
|
132
|
-
used += text.length;
|
|
133
|
-
}
|
|
134
|
-
return { text: out, used };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** One full-width row of a filled panel. */
|
|
138
|
-
function fillRow(content, usedWidth, width, bg) {
|
|
139
|
-
const pad = Math.max(0, width - usedWidth);
|
|
140
|
-
return hexToAnsiBg(bg) + content + ' '.repeat(pad) + ANSI_RESET;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function InlineText({ segments }) {
|
|
144
|
-
// Build a single string with ANSI escape codes for styling.
|
|
145
|
-
// This avoids creating multiple <Text> elements which Ink wraps independently.
|
|
146
|
-
let flat = '';
|
|
147
|
-
for (const seg of segments) {
|
|
148
|
-
let prefix = '';
|
|
149
|
-
let suffix = '';
|
|
150
|
-
if (seg.bold) prefix += ANSI_BOLD;
|
|
151
|
-
if (seg.italic) prefix += ANSI_ITALIC;
|
|
152
|
-
if (seg.code) {
|
|
153
|
-
prefix += hexToAnsi(G.yellow) + ANSI_BG_CODE;
|
|
154
|
-
suffix = ANSI_RESET;
|
|
155
|
-
flat += prefix + ` ${seg.text} ` + suffix;
|
|
156
|
-
} else if (seg.color) {
|
|
157
|
-
prefix += hexToAnsi(seg.color);
|
|
158
|
-
suffix = ANSI_RESET;
|
|
159
|
-
flat += prefix + seg.text + suffix;
|
|
160
|
-
} else {
|
|
161
|
-
flat += seg.text;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return React.createElement(Text, { wrap: 'wrap' }, flat);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function parseInline(text) {
|
|
168
|
-
const segments = [];
|
|
169
|
-
let remaining = text;
|
|
170
|
-
|
|
171
|
-
while (remaining.length > 0) {
|
|
172
|
-
// Bold: **text** or __text__
|
|
173
|
-
let match = remaining.match(/^(.*?)\*\*(.+?)\*\*/s);
|
|
174
|
-
if (!match) match = remaining.match(/^(.*?)__(.+?)__/s);
|
|
175
|
-
if (match && match[1].length < remaining.length) {
|
|
176
|
-
if (match[1]) segments.push({ text: match[1] });
|
|
177
|
-
segments.push({ text: match[2], bold: true });
|
|
178
|
-
remaining = remaining.slice(match[0].length);
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Italic: *text* or _text_
|
|
183
|
-
match = remaining.match(/^(.*?)\*(.+?)\*/s);
|
|
184
|
-
if (!match) match = remaining.match(/^(.*?)_(.+?)_/s);
|
|
185
|
-
if (match && match[1].length < remaining.length) {
|
|
186
|
-
if (match[1]) segments.push({ text: match[1] });
|
|
187
|
-
segments.push({ text: match[2], italic: true });
|
|
188
|
-
remaining = remaining.slice(match[0].length);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Inline code: `text`
|
|
193
|
-
match = remaining.match(/^(.*?)`(.+?)`/s);
|
|
194
|
-
if (match && match[1].length < remaining.length) {
|
|
195
|
-
if (match[1]) segments.push({ text: match[1] });
|
|
196
|
-
segments.push({ text: match[2], code: true });
|
|
197
|
-
remaining = remaining.slice(match[0].length);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// No more formatting
|
|
202
|
-
segments.push({ text: remaining });
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return segments;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const MAX_VISIBLE_LINES = 30;
|
|
210
|
-
const DEFAULT_PANEL_WIDTH = 76;
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* A fenced block with no language that is plainly prose, not code.
|
|
214
|
-
*
|
|
215
|
-
* Models routinely wrap their closing summary in a bare ``` fence. Rendering
|
|
216
|
-
* that as a code panel
|
|
217
|
-
* misleading, so it gets rendered as markdown instead. Kept deliberately
|
|
218
|
-
* conservative: any whiff of real code and it stays a code block.
|
|
219
|
-
*/
|
|
220
|
-
function looksLikeProse(lang, code) {
|
|
221
|
-
if (lang) return false;
|
|
222
|
-
const lines = code.split('\n').filter((l) => l.trim());
|
|
223
|
-
if (lines.length === 0) return false;
|
|
224
|
-
|
|
225
|
-
const codeish = lines.filter((l) =>
|
|
226
|
-
/[;{}]\s*$/.test(l) ||
|
|
227
|
-
/^\s*(?:const|let|var|function|import|export|class|def|return|if|for|while|#include)\b/.test(l) ||
|
|
228
|
-
/^\s*<\/?[a-zA-Z]/.test(l) ||
|
|
229
|
-
/^\s*[\w-]+\s*:\s*[^\s].*;/.test(l)
|
|
230
|
-
).length;
|
|
231
|
-
if (codeish > 0) return false;
|
|
232
|
-
|
|
233
|
-
const proseish = lines.filter((l) =>
|
|
234
|
-
/\*\*[^*]+\*\*/.test(l) || // bold
|
|
235
|
-
/^\s*#{1,6}\s+\S/.test(l) || // heading
|
|
236
|
-
/^\s*[-*]\s+\S/.test(l) || // bullet
|
|
237
|
-
/^\s*\d+\.\s+\S/.test(l) // numbered
|
|
238
|
-
).length;
|
|
239
|
-
|
|
240
|
-
return proseish / lines.length >= 0.4;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function CodeBlock({ lang, code, truncated, width }) {
|
|
244
|
-
const allLines = code.split('\n');
|
|
245
|
-
const totalLines = allLines.length;
|
|
246
|
-
const shouldTruncate = truncated !== false && totalLines > MAX_VISIBLE_LINES;
|
|
247
|
-
const visibleLines = shouldTruncate ? allLines.slice(0, MAX_VISIBLE_LINES) : allLines;
|
|
248
|
-
const highlighted = highlightCode(visibleLines.join('\n'), lang);
|
|
249
|
-
const langLabel = lang || 'code';
|
|
250
|
-
|
|
251
|
-
const panelWidth = Math.max(24, Math.min(width || DEFAULT_PANEL_WIDTH, 120));
|
|
252
|
-
const gutterWidth = String(visibleLines.length).length + 1;
|
|
253
|
-
const bodyWidth = panelWidth - gutterWidth - 2;
|
|
254
|
-
|
|
255
|
-
const countLabel = shouldTruncate
|
|
256
|
-
? `${totalLines} lines · first ${MAX_VISIBLE_LINES}`
|
|
257
|
-
: `${totalLines} line${totalLines === 1 ? '' : 's'}`;
|
|
258
|
-
|
|
259
|
-
// Header: language on the left, line count pushed to the right edge.
|
|
260
|
-
const headLeft = ' ' + langLabel;
|
|
261
|
-
const headGap = Math.max(1, panelWidth - headLeft.length - countLabel.length - 1);
|
|
262
|
-
const header = fillRow(
|
|
263
|
-
hexToAnsi(G.blue) + ANSI_BOLD + headLeft + ANSI_RESET + hexToAnsiBg(SURFACE.codeHeader) +
|
|
264
|
-
' '.repeat(headGap) + hexToAnsi('#6b7280') + countLabel + ' ',
|
|
265
|
-
headLeft.length + headGap + countLabel.length + 1,
|
|
266
|
-
panelWidth,
|
|
267
|
-
SURFACE.codeHeader
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const rows = highlighted.map((segments, i) => {
|
|
271
|
-
const raw = visibleLines[i] ?? '';
|
|
272
|
-
// Diff hunks get a tinted row so +/- reads at a glance.
|
|
273
|
-
const isAdd = /^\+(?!\+)/.test(raw);
|
|
274
|
-
const isDel = /^-(?!-)/.test(raw);
|
|
275
|
-
const bg = isAdd ? SURFACE.diffAdd : isDel ? SURFACE.diffDel : SURFACE.codeBody;
|
|
276
|
-
|
|
277
|
-
const num = String(i + 1).padStart(gutterWidth - 1);
|
|
278
|
-
const gutter = hexToAnsiBg(SURFACE.gutter) + hexToAnsi('#4b5563') + ' ' + num + ANSI_RESET;
|
|
279
|
-
const { text, used } = clipSegments(segments, bodyWidth);
|
|
280
|
-
const body = hexToAnsiBg(bg) + ' ' + text;
|
|
281
|
-
|
|
282
|
-
return React.createElement(
|
|
283
|
-
Text,
|
|
284
|
-
{ key: i },
|
|
285
|
-
gutter + fillRow(body, used + 1, panelWidth - gutterWidth, bg)
|
|
286
|
-
);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
return React.createElement(
|
|
290
|
-
Box,
|
|
291
|
-
{ flexDirection: 'column', marginBottom: 1 },
|
|
292
|
-
React.createElement(Text, null, header),
|
|
293
|
-
// One <Text> per line, pre-flattened to ANSI. Ink allocates a Yoga layout
|
|
294
|
-
// node per element, so per-token <Text> nodes make long blocks crawl.
|
|
295
|
-
...rows,
|
|
296
|
-
shouldTruncate && React.createElement(
|
|
297
|
-
Text,
|
|
298
|
-
null,
|
|
299
|
-
fillRow(
|
|
300
|
-
hexToAnsiBg(SURFACE.codeHeader) + hexToAnsi('#6b7280') +
|
|
301
|
-
` ⋯ ${totalLines - MAX_VISIBLE_LINES} more lines`,
|
|
302
|
-
` ⋯ ${totalLines - MAX_VISIBLE_LINES} more lines`.length,
|
|
303
|
-
panelWidth,
|
|
304
|
-
SURFACE.codeHeader
|
|
305
|
-
)
|
|
306
|
-
)
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const ANSI_BOLD_OFF = '\x1b[22m';
|
|
311
|
-
const ANSI_ITALIC_OFF = '\x1b[23m';
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Style one segment without ever emitting a full reset.
|
|
315
|
-
* ANSI_RESET would clear the panel's background mid-row and punch a hole in the
|
|
316
|
-
* fill, so attributes are turned off individually instead.
|
|
317
|
-
*/
|
|
318
|
-
function styleSegment(seg, defaultColor) {
|
|
319
|
-
let out = '';
|
|
320
|
-
if (seg.bold) out += ANSI_BOLD;
|
|
321
|
-
if (seg.italic) out += ANSI_ITALIC;
|
|
322
|
-
out += hexToAnsi(seg.color || defaultColor);
|
|
323
|
-
out += seg.text;
|
|
324
|
-
if (seg.italic) out += ANSI_ITALIC_OFF;
|
|
325
|
-
if (seg.bold) out += ANSI_BOLD_OFF;
|
|
326
|
-
return out;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Word-wrap styled segments to `width`, returning rows of segments.
|
|
331
|
-
* Wrapping happens on words rather than characters, and a word longer than the
|
|
332
|
-
* whole width is hard-split so it can never overflow the panel.
|
|
333
|
-
*/
|
|
334
|
-
function wrapStyledSegments(segments, width, nextWidth = width) {
|
|
335
|
-
const rows = [];
|
|
336
|
-
let row = [];
|
|
337
|
-
let used = 0;
|
|
338
|
-
// Continuation rows carry a hanging indent, so they have less room than the
|
|
339
|
-
// first. Wrapping every row at the first row's width overflows the panel by
|
|
340
|
-
// exactly the indent.
|
|
341
|
-
let limit = width;
|
|
342
|
-
const flush = () => { rows.push(row); row = []; used = 0; limit = nextWidth; };
|
|
343
|
-
|
|
344
|
-
for (const seg of segments) {
|
|
345
|
-
for (const word of seg.text.split(/(\s+)/)) {
|
|
346
|
-
if (!word) continue;
|
|
347
|
-
const isSpace = /^\s+$/.test(word);
|
|
348
|
-
if (isSpace && used === 0) continue; // no leading space on a wrapped row
|
|
349
|
-
if (used + word.length <= limit) {
|
|
350
|
-
row.push({ ...seg, text: word });
|
|
351
|
-
used += word.length;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
if (isSpace) { flush(); continue; } // break at the space itself
|
|
355
|
-
if (used > 0) flush();
|
|
356
|
-
let rest = word;
|
|
357
|
-
while (rest.length > limit) { // pathologically long token
|
|
358
|
-
row.push({ ...seg, text: rest.slice(0, limit) });
|
|
359
|
-
used = limit;
|
|
360
|
-
flush();
|
|
361
|
-
rest = rest.slice(limit);
|
|
362
|
-
}
|
|
363
|
-
if (rest) { row.push({ ...seg, text: rest }); used += rest.length; }
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (row.length || rows.length === 0) flush();
|
|
367
|
-
return rows;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Turn one raw markdown line into an indent + styled segments. */
|
|
371
|
-
function formatPanelLine(raw) {
|
|
372
|
-
const indentMatch = raw.match(/^(\s*)/);
|
|
373
|
-
const indent = Math.min(indentMatch ? indentMatch[1].length : 0, 12);
|
|
374
|
-
const line = raw.slice(indent);
|
|
375
|
-
|
|
376
|
-
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
377
|
-
if (heading) {
|
|
378
|
-
const level = heading[1].length;
|
|
379
|
-
const color = level <= 2 ? G.blue : level === 3 ? G.green : G.yellow;
|
|
380
|
-
return {
|
|
381
|
-
indent,
|
|
382
|
-
hanging: 0,
|
|
383
|
-
segments: parseInline(heading[2]).map((s) => ({ ...s, bold: true, color })),
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
388
|
-
return { indent, hanging: 0, rule: true, segments: [] };
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const bullet = line.match(/^([-*])\s+(.*)$/);
|
|
392
|
-
if (bullet) {
|
|
393
|
-
return {
|
|
394
|
-
indent,
|
|
395
|
-
hanging: 2,
|
|
396
|
-
segments: [{ text: '• ', color: G.blue }, ...parseInline(bullet[2])],
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const numbered = line.match(/^(\d+)\.\s+(.*)$/);
|
|
401
|
-
if (numbered) {
|
|
402
|
-
const marker = `${numbered[1]}. `;
|
|
403
|
-
return {
|
|
404
|
-
indent,
|
|
405
|
-
hanging: marker.length,
|
|
406
|
-
segments: [{ text: marker, color: G.blue }, ...parseInline(numbered[2])],
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return { indent, hanging: 0, segments: parseInline(line) };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Filled panel for a plan / reasoning block
|
|
415
|
-
* so the transcript reads as one system, but without the line-number gutter
|
|
416
|
-
* since prose is not addressed by line.
|
|
417
|
-
*
|
|
418
|
-
* Content is wrapped, never clipped: a plan is meant to be read, so losing the
|
|
419
|
-
* end of every long sentence defeats the point.
|
|
420
|
-
*/
|
|
421
|
-
function Panel({ title, lines, width, accent = G.yellow, bg = SURFACE.plan, headerBg = SURFACE.planHeader }) {
|
|
422
|
-
const panelWidth = Math.max(24, Math.min(width || DEFAULT_PANEL_WIDTH, 120));
|
|
423
|
-
const textColor = '#d5d9df';
|
|
424
|
-
|
|
425
|
-
const head = ' ' + title;
|
|
426
|
-
const header = fillRow(
|
|
427
|
-
hexToAnsi(accent) + ANSI_BOLD + head + ANSI_BOLD_OFF,
|
|
428
|
-
head.length,
|
|
429
|
-
panelWidth,
|
|
430
|
-
headerBg
|
|
431
|
-
);
|
|
432
|
-
|
|
433
|
-
const rendered = [];
|
|
434
|
-
for (const raw of lines) {
|
|
435
|
-
const { indent, hanging, segments, rule } = formatPanelLine(raw);
|
|
436
|
-
|
|
437
|
-
if (rule) {
|
|
438
|
-
const dash = '─'.repeat(Math.max(4, panelWidth - 4));
|
|
439
|
-
rendered.push(hexToAnsiBg(bg) + ' ' + hexToAnsi('#3a4048') + dash);
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
if (segments.length === 0) {
|
|
443
|
-
rendered.push(hexToAnsiBg(bg) + ' ');
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const avail = Math.max(8, panelWidth - 2 - indent);
|
|
448
|
-
const rows = wrapStyledSegments(segments, avail, Math.max(4, avail - hanging));
|
|
449
|
-
rows.forEach((row, i) => {
|
|
450
|
-
// Continuation rows line up under the text, not under the bullet.
|
|
451
|
-
const pad = ' '.repeat(indent + (i > 0 ? hanging : 0));
|
|
452
|
-
const body = row.map((s) => styleSegment(s, textColor)).join('');
|
|
453
|
-
rendered.push(hexToAnsiBg(bg) + ' ' + pad + body);
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return React.createElement(
|
|
458
|
-
Box,
|
|
459
|
-
{ flexDirection: 'column', marginBottom: 1 },
|
|
460
|
-
React.createElement(Text, { key: 'h' }, header),
|
|
461
|
-
...rendered.map((content, i) => {
|
|
462
|
-
const used = visibleLength(content);
|
|
463
|
-
return React.createElement(
|
|
464
|
-
Text,
|
|
465
|
-
{ key: i },
|
|
466
|
-
hexToAnsiBg(bg) + content + ' '.repeat(Math.max(0, panelWidth - used)) + ANSI_RESET
|
|
467
|
-
);
|
|
468
|
-
})
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function Heading({ level, children }) {
|
|
473
|
-
const color = level === 1 ? G.blue : level === 2 ? G.green : G.yellow;
|
|
474
|
-
return React.createElement(
|
|
475
|
-
Box,
|
|
476
|
-
{ marginBottom: 1, marginTop: level <= 2 ? 1 : 0 },
|
|
477
|
-
React.createElement(Text, { color, bold: level <= 2, wrap: 'wrap' }, children)
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function ListItem({ children, ordered, index }) {
|
|
482
|
-
const bullet = ordered ? `${index}. ` : ' • ';
|
|
483
|
-
const bulletColored = hexToAnsi(G.blue) + bullet + ANSI_RESET;
|
|
484
|
-
const segments = parseInline(children);
|
|
485
|
-
let content = '';
|
|
486
|
-
for (const seg of segments) {
|
|
487
|
-
let prefix = '';
|
|
488
|
-
if (seg.bold) prefix += ANSI_BOLD;
|
|
489
|
-
if (seg.italic) prefix += ANSI_ITALIC;
|
|
490
|
-
if (seg.color) prefix += hexToAnsi(seg.color);
|
|
491
|
-
content += prefix + seg.text + (prefix ? ANSI_RESET : '');
|
|
492
|
-
}
|
|
493
|
-
return React.createElement(Text, { wrap: 'wrap' }, ' ' + bulletColored + content);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function HorizontalRule({ width }) {
|
|
497
|
-
return React.createElement(
|
|
498
|
-
Box,
|
|
499
|
-
{ marginY: 1 },
|
|
500
|
-
React.createElement(Text, { color: '#3c4043' }, '─'.repeat(Math.max(8, Math.min(width || 50, 72))))
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function Blockquote({ children }) {
|
|
505
|
-
return React.createElement(
|
|
506
|
-
Box,
|
|
507
|
-
{
|
|
508
|
-
borderStyle: 'bold',
|
|
509
|
-
borderTop: false,
|
|
510
|
-
borderRight: false,
|
|
511
|
-
borderBottom: false,
|
|
512
|
-
borderLeftColor: G.blue,
|
|
513
|
-
paddingLeft: 1,
|
|
514
|
-
marginBottom: 1,
|
|
515
|
-
},
|
|
516
|
-
React.createElement(Text, { color: '#9aa0a6', italic: true, wrap: 'wrap' }, children)
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function renderMarkdown(md, width) {
|
|
521
|
-
const elements = [];
|
|
522
|
-
const lines = md.split('\n');
|
|
523
|
-
let i = 0;
|
|
524
|
-
|
|
525
|
-
while (i < lines.length) {
|
|
526
|
-
const line = lines[i];
|
|
527
|
-
|
|
528
|
-
// Code block: ```lang ... ```
|
|
529
|
-
if (line.trim().startsWith('```')) {
|
|
530
|
-
const lang = line.trim().slice(3).trim();
|
|
531
|
-
const codeLines = [];
|
|
532
|
-
i++;
|
|
533
|
-
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
534
|
-
codeLines.push(lines[i]);
|
|
535
|
-
i++;
|
|
536
|
-
}
|
|
537
|
-
i++; // skip closing ```
|
|
538
|
-
const body = codeLines.join('\n');
|
|
539
|
-
if (looksLikeProse(lang, body)) {
|
|
540
|
-
// A bare fence around a written summary
|
|
541
|
-
for (const el of renderMarkdown(body, width)) {
|
|
542
|
-
elements.push(React.cloneElement(el, { key: `fenced-${elements.length}-${el.key}` }));
|
|
543
|
-
}
|
|
544
|
-
} else {
|
|
545
|
-
elements.push(React.createElement(CodeBlock, {
|
|
546
|
-
key: `code-${elements.length}`, lang, code: body, width,
|
|
547
|
-
}));
|
|
548
|
-
}
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Heading: # ## ###
|
|
553
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
554
|
-
if (headingMatch) {
|
|
555
|
-
elements.push(React.createElement(Heading, { key: `h-${elements.length}`, level: headingMatch[1].length }, headingMatch[2]));
|
|
556
|
-
i++;
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Horizontal rule: --- or *** or ___
|
|
561
|
-
if (/^[-*_]{3,}\s*$/.test(line.trim())) {
|
|
562
|
-
elements.push(React.createElement(HorizontalRule, { key: `hr-${elements.length}`, width }));
|
|
563
|
-
i++;
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Blockquote: > text
|
|
568
|
-
if (line.trim().startsWith('>')) {
|
|
569
|
-
elements.push(React.createElement(Blockquote, { key: `bq-${elements.length}`, children: line.trim().slice(1).trim() }));
|
|
570
|
-
i++;
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Unordered list: - or * text
|
|
575
|
-
const listMatch = line.match(/^(\s*)([-*])\s+(.+)/);
|
|
576
|
-
if (listMatch) {
|
|
577
|
-
elements.push(React.createElement(ListItem, { key: `li-${elements.length}`, children: listMatch[3] }));
|
|
578
|
-
i++;
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Ordered list: 1. text
|
|
583
|
-
const olMatch = line.match(/^(\s*)(\d+)\.\s+(.+)/);
|
|
584
|
-
if (olMatch) {
|
|
585
|
-
elements.push(React.createElement(ListItem, { key: `oli-${elements.length}`, children: olMatch[3], ordered: true, index: parseInt(olMatch[2]) }));
|
|
586
|
-
i++;
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Empty line
|
|
591
|
-
if (line.trim() === '') {
|
|
592
|
-
i++;
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Paragraph: collect consecutive non-empty lines
|
|
597
|
-
const paraLines = [];
|
|
598
|
-
while (i < lines.length && lines[i].trim() !== '' && !lines[i].trim().startsWith('```') && !lines[i].trim().startsWith('#') && !lines[i].trim().startsWith('>') && !/^[-*]\s/.test(lines[i].trim()) && !/^\d+\.\s/.test(lines[i].trim()) && !/^[-*_]{3,}\s*$/.test(lines[i].trim())) {
|
|
599
|
-
paraLines.push(lines[i]);
|
|
600
|
-
i++;
|
|
601
|
-
}
|
|
602
|
-
if (paraLines.length) {
|
|
603
|
-
elements.push(
|
|
604
|
-
React.createElement(InlineText, { key: `p-${elements.length}`, segments: parseInline(paraLines.join(' ')) })
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return elements;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
export {
|
|
613
|
-
renderMarkdown, CodeBlock, Panel, parseInline, looksLikeProse,
|
|
614
|
-
hexToAnsi, hexToAnsiBg, fillRow, clipSegments, visibleLength, SURFACE,
|
|
615
|
-
ANSI_RESET, ANSI_BOLD, ANSI_DIM, ANSI_ITALIC,
|
|
616
|
-
};
|
|
617
|
-
export default renderMarkdown;
|
|
1
|
+
/**
|
|
2
|
+
* Codeply TUI - Markdown renderer for AI responses.
|
|
3
|
+
* Parses markdown into Ink-compatible React elements with syntax coloring.
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { Box, Text } from 'ink';
|
|
7
|
+
|
|
8
|
+
const G = {
|
|
9
|
+
blue: '#4285F4',
|
|
10
|
+
red: '#EA4335',
|
|
11
|
+
yellow: '#FBBC05',
|
|
12
|
+
green: '#34A853',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Minimal syntax highlighter for code blocks
|
|
16
|
+
function highlightCode(code, lang) {
|
|
17
|
+
const lines = code.split('\n');
|
|
18
|
+
return lines.map((line) => {
|
|
19
|
+
let colored = line;
|
|
20
|
+
|
|
21
|
+
// Comments
|
|
22
|
+
if (/^\s*\/\//.test(line) || /^\s*#/.test(line) || /^\s*\/\*/.test(line) || /^\s*\*/.test(line) || /^\s*<!--/.test(line)) {
|
|
23
|
+
return [{ text: line, color: '#5f6368' }];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Strings (basic)
|
|
27
|
+
const parts = [];
|
|
28
|
+
let remaining = line;
|
|
29
|
+
// Match strings, keywords, tags, numbers, properties
|
|
30
|
+
const regex = /("[^"]*"|'[^']*'|`[^`]*`)|(\b(?:function|const|let|var|if|else|return|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|from|default|async|await|try|catch|throw|typeof|instanceof|in|of|void|delete|null|undefined|true|false)\b)|(<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s[^>]*)?>?)|(\b\d+\.?\d*\b)|(#[0-9a-fA-F]{3,8})|(\b(?:style|color|display|margin|padding|border|background|width|height|font|position|top|left|right|bottom|text|align|overflow|flex|grid|gap|opacity|transform|transition|animation|content|justify|items|center|none|block|inline|relative|absolute|fixed|solid|dashed|hidden|auto|inherit|initial|unset)\b)/g;
|
|
31
|
+
|
|
32
|
+
let lastIdx = 0;
|
|
33
|
+
let match;
|
|
34
|
+
const result = [];
|
|
35
|
+
|
|
36
|
+
while ((match = regex.exec(remaining)) !== null) {
|
|
37
|
+
// Text before match
|
|
38
|
+
if (match.index > lastIdx) {
|
|
39
|
+
result.push({ text: remaining.slice(lastIdx, match.index), color: '#e8eaed' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (match[1]) {
|
|
43
|
+
// String
|
|
44
|
+
result.push({ text: match[0], color: '#34A853' });
|
|
45
|
+
} else if (match[2]) {
|
|
46
|
+
// Keyword
|
|
47
|
+
result.push({ text: match[0], color: '#4285F4' });
|
|
48
|
+
} else if (match[3]) {
|
|
49
|
+
// HTML tag
|
|
50
|
+
result.push({ text: match[0], color: '#EA4335' });
|
|
51
|
+
} else if (match[4]) {
|
|
52
|
+
// Number
|
|
53
|
+
result.push({ text: match[0], color: '#FBBC05' });
|
|
54
|
+
} else if (match[5]) {
|
|
55
|
+
// Hex color
|
|
56
|
+
result.push({ text: match[0], color: '#FBBC05' });
|
|
57
|
+
} else if (match[6]) {
|
|
58
|
+
// CSS property
|
|
59
|
+
result.push({ text: match[0], color: '#e8eaed' });
|
|
60
|
+
} else {
|
|
61
|
+
result.push({ text: match[0], color: '#e8eaed' });
|
|
62
|
+
}
|
|
63
|
+
lastIdx = match.index + match[0].length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (lastIdx < remaining.length) {
|
|
67
|
+
result.push({ text: remaining.slice(lastIdx), color: '#e8eaed' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result.length ? result : [{ text: line, color: '#e8eaed' }];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Convert hex color to ANSI escape sequence
|
|
75
|
+
function hexToAnsi(hex) {
|
|
76
|
+
if (!hex) return '';
|
|
77
|
+
const h = hex.replace('#', '');
|
|
78
|
+
const r = parseInt(h.substring(0, 2), 16);
|
|
79
|
+
const g = parseInt(h.substring(2, 4), 16);
|
|
80
|
+
const b = parseInt(h.substring(4, 6), 16);
|
|
81
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Background counterpart of hexToAnsi. */
|
|
85
|
+
function hexToAnsiBg(hex) {
|
|
86
|
+
if (!hex) return '';
|
|
87
|
+
const h = hex.replace('#', '');
|
|
88
|
+
const r = parseInt(h.substring(0, 2), 16);
|
|
89
|
+
const g = parseInt(h.substring(2, 4), 16);
|
|
90
|
+
const b = parseInt(h.substring(4, 6), 16);
|
|
91
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ANSI_RESET = '\x1b[0m';
|
|
95
|
+
const ANSI_BOLD = '\x1b[1m';
|
|
96
|
+
const ANSI_DIM = '\x1b[2m';
|
|
97
|
+
const ANSI_ITALIC = '\x1b[3m';
|
|
98
|
+
const ANSI_BG_CODE = '\x1b[48;2;45;46;48m'; // #2d2e30
|
|
99
|
+
|
|
100
|
+
// Surface colours for filled blocks. Ink 5's <Box> has no backgroundColor, so
|
|
101
|
+
// every filled row is built by hand: set the bg, emit the content, pad out to
|
|
102
|
+
// the panel width so the fill reaches the right edge, then reset. Padding has
|
|
103
|
+
// to be measured on the *visible* text, which is why widths are tracked
|
|
104
|
+
// explicitly rather than using String#padEnd on an ANSI-laden string.
|
|
105
|
+
const SURFACE = {
|
|
106
|
+
codeHeader: '#20242b',
|
|
107
|
+
codeBody: '#15181c',
|
|
108
|
+
gutter: '#1b1f24',
|
|
109
|
+
plan: '#161b22',
|
|
110
|
+
planHeader: '#1d2530',
|
|
111
|
+
diffAdd: '#0e2417',
|
|
112
|
+
diffDel: '#2a1418',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
116
|
+
const visibleLength = (s) => s.replace(ANSI_PATTERN, '').length;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Render coloured segments clipped to `width`, returning the ANSI string and
|
|
120
|
+
* how many visible columns it used. Clipping happens per segment so a cut never
|
|
121
|
+
* lands inside an escape sequence.
|
|
122
|
+
*/
|
|
123
|
+
function clipSegments(segments, width) {
|
|
124
|
+
let out = '';
|
|
125
|
+
let used = 0;
|
|
126
|
+
for (const seg of segments) {
|
|
127
|
+
if (used >= width) break;
|
|
128
|
+
const room = width - used;
|
|
129
|
+
const text = seg.text.length > room ? seg.text.slice(0, room) : seg.text;
|
|
130
|
+
if (!text) continue;
|
|
131
|
+
out += (seg.color ? hexToAnsi(seg.color) : '') + text;
|
|
132
|
+
used += text.length;
|
|
133
|
+
}
|
|
134
|
+
return { text: out, used };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One full-width row of a filled panel. */
|
|
138
|
+
function fillRow(content, usedWidth, width, bg) {
|
|
139
|
+
const pad = Math.max(0, width - usedWidth);
|
|
140
|
+
return hexToAnsiBg(bg) + content + ' '.repeat(pad) + ANSI_RESET;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function InlineText({ segments }) {
|
|
144
|
+
// Build a single string with ANSI escape codes for styling.
|
|
145
|
+
// This avoids creating multiple <Text> elements which Ink wraps independently.
|
|
146
|
+
let flat = '';
|
|
147
|
+
for (const seg of segments) {
|
|
148
|
+
let prefix = '';
|
|
149
|
+
let suffix = '';
|
|
150
|
+
if (seg.bold) prefix += ANSI_BOLD;
|
|
151
|
+
if (seg.italic) prefix += ANSI_ITALIC;
|
|
152
|
+
if (seg.code) {
|
|
153
|
+
prefix += hexToAnsi(G.yellow) + ANSI_BG_CODE;
|
|
154
|
+
suffix = ANSI_RESET;
|
|
155
|
+
flat += prefix + ` ${seg.text} ` + suffix;
|
|
156
|
+
} else if (seg.color) {
|
|
157
|
+
prefix += hexToAnsi(seg.color);
|
|
158
|
+
suffix = ANSI_RESET;
|
|
159
|
+
flat += prefix + seg.text + suffix;
|
|
160
|
+
} else {
|
|
161
|
+
flat += seg.text;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return React.createElement(Text, { wrap: 'wrap' }, flat);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseInline(text) {
|
|
168
|
+
const segments = [];
|
|
169
|
+
let remaining = text;
|
|
170
|
+
|
|
171
|
+
while (remaining.length > 0) {
|
|
172
|
+
// Bold: **text** or __text__
|
|
173
|
+
let match = remaining.match(/^(.*?)\*\*(.+?)\*\*/s);
|
|
174
|
+
if (!match) match = remaining.match(/^(.*?)__(.+?)__/s);
|
|
175
|
+
if (match && match[1].length < remaining.length) {
|
|
176
|
+
if (match[1]) segments.push({ text: match[1] });
|
|
177
|
+
segments.push({ text: match[2], bold: true });
|
|
178
|
+
remaining = remaining.slice(match[0].length);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Italic: *text* or _text_
|
|
183
|
+
match = remaining.match(/^(.*?)\*(.+?)\*/s);
|
|
184
|
+
if (!match) match = remaining.match(/^(.*?)_(.+?)_/s);
|
|
185
|
+
if (match && match[1].length < remaining.length) {
|
|
186
|
+
if (match[1]) segments.push({ text: match[1] });
|
|
187
|
+
segments.push({ text: match[2], italic: true });
|
|
188
|
+
remaining = remaining.slice(match[0].length);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Inline code: `text`
|
|
193
|
+
match = remaining.match(/^(.*?)`(.+?)`/s);
|
|
194
|
+
if (match && match[1].length < remaining.length) {
|
|
195
|
+
if (match[1]) segments.push({ text: match[1] });
|
|
196
|
+
segments.push({ text: match[2], code: true });
|
|
197
|
+
remaining = remaining.slice(match[0].length);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// No more formatting
|
|
202
|
+
segments.push({ text: remaining });
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return segments;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const MAX_VISIBLE_LINES = 30;
|
|
210
|
+
const DEFAULT_PANEL_WIDTH = 76;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A fenced block with no language that is plainly prose, not code.
|
|
214
|
+
*
|
|
215
|
+
* Models routinely wrap their closing summary in a bare ``` fence. Rendering
|
|
216
|
+
* that as a code panel - line numbers, syntax colours, "15 lines" - is actively
|
|
217
|
+
* misleading, so it gets rendered as markdown instead. Kept deliberately
|
|
218
|
+
* conservative: any whiff of real code and it stays a code block.
|
|
219
|
+
*/
|
|
220
|
+
function looksLikeProse(lang, code) {
|
|
221
|
+
if (lang) return false;
|
|
222
|
+
const lines = code.split('\n').filter((l) => l.trim());
|
|
223
|
+
if (lines.length === 0) return false;
|
|
224
|
+
|
|
225
|
+
const codeish = lines.filter((l) =>
|
|
226
|
+
/[;{}]\s*$/.test(l) ||
|
|
227
|
+
/^\s*(?:const|let|var|function|import|export|class|def|return|if|for|while|#include)\b/.test(l) ||
|
|
228
|
+
/^\s*<\/?[a-zA-Z]/.test(l) ||
|
|
229
|
+
/^\s*[\w-]+\s*:\s*[^\s].*;/.test(l)
|
|
230
|
+
).length;
|
|
231
|
+
if (codeish > 0) return false;
|
|
232
|
+
|
|
233
|
+
const proseish = lines.filter((l) =>
|
|
234
|
+
/\*\*[^*]+\*\*/.test(l) || // bold
|
|
235
|
+
/^\s*#{1,6}\s+\S/.test(l) || // heading
|
|
236
|
+
/^\s*[-*]\s+\S/.test(l) || // bullet
|
|
237
|
+
/^\s*\d+\.\s+\S/.test(l) // numbered
|
|
238
|
+
).length;
|
|
239
|
+
|
|
240
|
+
return proseish / lines.length >= 0.4;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function CodeBlock({ lang, code, truncated, width }) {
|
|
244
|
+
const allLines = code.split('\n');
|
|
245
|
+
const totalLines = allLines.length;
|
|
246
|
+
const shouldTruncate = truncated !== false && totalLines > MAX_VISIBLE_LINES;
|
|
247
|
+
const visibleLines = shouldTruncate ? allLines.slice(0, MAX_VISIBLE_LINES) : allLines;
|
|
248
|
+
const highlighted = highlightCode(visibleLines.join('\n'), lang);
|
|
249
|
+
const langLabel = lang || 'code';
|
|
250
|
+
|
|
251
|
+
const panelWidth = Math.max(24, Math.min(width || DEFAULT_PANEL_WIDTH, 120));
|
|
252
|
+
const gutterWidth = String(visibleLines.length).length + 1;
|
|
253
|
+
const bodyWidth = panelWidth - gutterWidth - 2;
|
|
254
|
+
|
|
255
|
+
const countLabel = shouldTruncate
|
|
256
|
+
? `${totalLines} lines · first ${MAX_VISIBLE_LINES}`
|
|
257
|
+
: `${totalLines} line${totalLines === 1 ? '' : 's'}`;
|
|
258
|
+
|
|
259
|
+
// Header: language on the left, line count pushed to the right edge.
|
|
260
|
+
const headLeft = ' ' + langLabel;
|
|
261
|
+
const headGap = Math.max(1, panelWidth - headLeft.length - countLabel.length - 1);
|
|
262
|
+
const header = fillRow(
|
|
263
|
+
hexToAnsi(G.blue) + ANSI_BOLD + headLeft + ANSI_RESET + hexToAnsiBg(SURFACE.codeHeader) +
|
|
264
|
+
' '.repeat(headGap) + hexToAnsi('#6b7280') + countLabel + ' ',
|
|
265
|
+
headLeft.length + headGap + countLabel.length + 1,
|
|
266
|
+
panelWidth,
|
|
267
|
+
SURFACE.codeHeader
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const rows = highlighted.map((segments, i) => {
|
|
271
|
+
const raw = visibleLines[i] ?? '';
|
|
272
|
+
// Diff hunks get a tinted row so +/- reads at a glance.
|
|
273
|
+
const isAdd = /^\+(?!\+)/.test(raw);
|
|
274
|
+
const isDel = /^-(?!-)/.test(raw);
|
|
275
|
+
const bg = isAdd ? SURFACE.diffAdd : isDel ? SURFACE.diffDel : SURFACE.codeBody;
|
|
276
|
+
|
|
277
|
+
const num = String(i + 1).padStart(gutterWidth - 1);
|
|
278
|
+
const gutter = hexToAnsiBg(SURFACE.gutter) + hexToAnsi('#4b5563') + ' ' + num + ANSI_RESET;
|
|
279
|
+
const { text, used } = clipSegments(segments, bodyWidth);
|
|
280
|
+
const body = hexToAnsiBg(bg) + ' ' + text;
|
|
281
|
+
|
|
282
|
+
return React.createElement(
|
|
283
|
+
Text,
|
|
284
|
+
{ key: i },
|
|
285
|
+
gutter + fillRow(body, used + 1, panelWidth - gutterWidth, bg)
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return React.createElement(
|
|
290
|
+
Box,
|
|
291
|
+
{ flexDirection: 'column', marginBottom: 1 },
|
|
292
|
+
React.createElement(Text, null, header),
|
|
293
|
+
// One <Text> per line, pre-flattened to ANSI. Ink allocates a Yoga layout
|
|
294
|
+
// node per element, so per-token <Text> nodes make long blocks crawl.
|
|
295
|
+
...rows,
|
|
296
|
+
shouldTruncate && React.createElement(
|
|
297
|
+
Text,
|
|
298
|
+
null,
|
|
299
|
+
fillRow(
|
|
300
|
+
hexToAnsiBg(SURFACE.codeHeader) + hexToAnsi('#6b7280') +
|
|
301
|
+
` ⋯ ${totalLines - MAX_VISIBLE_LINES} more lines`,
|
|
302
|
+
` ⋯ ${totalLines - MAX_VISIBLE_LINES} more lines`.length,
|
|
303
|
+
panelWidth,
|
|
304
|
+
SURFACE.codeHeader
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const ANSI_BOLD_OFF = '\x1b[22m';
|
|
311
|
+
const ANSI_ITALIC_OFF = '\x1b[23m';
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Style one segment without ever emitting a full reset.
|
|
315
|
+
* ANSI_RESET would clear the panel's background mid-row and punch a hole in the
|
|
316
|
+
* fill, so attributes are turned off individually instead.
|
|
317
|
+
*/
|
|
318
|
+
function styleSegment(seg, defaultColor) {
|
|
319
|
+
let out = '';
|
|
320
|
+
if (seg.bold) out += ANSI_BOLD;
|
|
321
|
+
if (seg.italic) out += ANSI_ITALIC;
|
|
322
|
+
out += hexToAnsi(seg.color || defaultColor);
|
|
323
|
+
out += seg.text;
|
|
324
|
+
if (seg.italic) out += ANSI_ITALIC_OFF;
|
|
325
|
+
if (seg.bold) out += ANSI_BOLD_OFF;
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Word-wrap styled segments to `width`, returning rows of segments.
|
|
331
|
+
* Wrapping happens on words rather than characters, and a word longer than the
|
|
332
|
+
* whole width is hard-split so it can never overflow the panel.
|
|
333
|
+
*/
|
|
334
|
+
function wrapStyledSegments(segments, width, nextWidth = width) {
|
|
335
|
+
const rows = [];
|
|
336
|
+
let row = [];
|
|
337
|
+
let used = 0;
|
|
338
|
+
// Continuation rows carry a hanging indent, so they have less room than the
|
|
339
|
+
// first. Wrapping every row at the first row's width overflows the panel by
|
|
340
|
+
// exactly the indent.
|
|
341
|
+
let limit = width;
|
|
342
|
+
const flush = () => { rows.push(row); row = []; used = 0; limit = nextWidth; };
|
|
343
|
+
|
|
344
|
+
for (const seg of segments) {
|
|
345
|
+
for (const word of seg.text.split(/(\s+)/)) {
|
|
346
|
+
if (!word) continue;
|
|
347
|
+
const isSpace = /^\s+$/.test(word);
|
|
348
|
+
if (isSpace && used === 0) continue; // no leading space on a wrapped row
|
|
349
|
+
if (used + word.length <= limit) {
|
|
350
|
+
row.push({ ...seg, text: word });
|
|
351
|
+
used += word.length;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (isSpace) { flush(); continue; } // break at the space itself
|
|
355
|
+
if (used > 0) flush();
|
|
356
|
+
let rest = word;
|
|
357
|
+
while (rest.length > limit) { // pathologically long token
|
|
358
|
+
row.push({ ...seg, text: rest.slice(0, limit) });
|
|
359
|
+
used = limit;
|
|
360
|
+
flush();
|
|
361
|
+
rest = rest.slice(limit);
|
|
362
|
+
}
|
|
363
|
+
if (rest) { row.push({ ...seg, text: rest }); used += rest.length; }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (row.length || rows.length === 0) flush();
|
|
367
|
+
return rows;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Turn one raw markdown line into an indent + styled segments. */
|
|
371
|
+
function formatPanelLine(raw) {
|
|
372
|
+
const indentMatch = raw.match(/^(\s*)/);
|
|
373
|
+
const indent = Math.min(indentMatch ? indentMatch[1].length : 0, 12);
|
|
374
|
+
const line = raw.slice(indent);
|
|
375
|
+
|
|
376
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
377
|
+
if (heading) {
|
|
378
|
+
const level = heading[1].length;
|
|
379
|
+
const color = level <= 2 ? G.blue : level === 3 ? G.green : G.yellow;
|
|
380
|
+
return {
|
|
381
|
+
indent,
|
|
382
|
+
hanging: 0,
|
|
383
|
+
segments: parseInline(heading[2]).map((s) => ({ ...s, bold: true, color })),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
388
|
+
return { indent, hanging: 0, rule: true, segments: [] };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const bullet = line.match(/^([-*])\s+(.*)$/);
|
|
392
|
+
if (bullet) {
|
|
393
|
+
return {
|
|
394
|
+
indent,
|
|
395
|
+
hanging: 2,
|
|
396
|
+
segments: [{ text: '• ', color: G.blue }, ...parseInline(bullet[2])],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const numbered = line.match(/^(\d+)\.\s+(.*)$/);
|
|
401
|
+
if (numbered) {
|
|
402
|
+
const marker = `${numbered[1]}. `;
|
|
403
|
+
return {
|
|
404
|
+
indent,
|
|
405
|
+
hanging: marker.length,
|
|
406
|
+
segments: [{ text: marker, color: G.blue }, ...parseInline(numbered[2])],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return { indent, hanging: 0, segments: parseInline(line) };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Filled panel for a plan / reasoning block - same visual family as CodeBlock
|
|
415
|
+
* so the transcript reads as one system, but without the line-number gutter
|
|
416
|
+
* since prose is not addressed by line.
|
|
417
|
+
*
|
|
418
|
+
* Content is wrapped, never clipped: a plan is meant to be read, so losing the
|
|
419
|
+
* end of every long sentence defeats the point.
|
|
420
|
+
*/
|
|
421
|
+
function Panel({ title, lines, width, accent = G.yellow, bg = SURFACE.plan, headerBg = SURFACE.planHeader }) {
|
|
422
|
+
const panelWidth = Math.max(24, Math.min(width || DEFAULT_PANEL_WIDTH, 120));
|
|
423
|
+
const textColor = '#d5d9df';
|
|
424
|
+
|
|
425
|
+
const head = ' ' + title;
|
|
426
|
+
const header = fillRow(
|
|
427
|
+
hexToAnsi(accent) + ANSI_BOLD + head + ANSI_BOLD_OFF,
|
|
428
|
+
head.length,
|
|
429
|
+
panelWidth,
|
|
430
|
+
headerBg
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const rendered = [];
|
|
434
|
+
for (const raw of lines) {
|
|
435
|
+
const { indent, hanging, segments, rule } = formatPanelLine(raw);
|
|
436
|
+
|
|
437
|
+
if (rule) {
|
|
438
|
+
const dash = '─'.repeat(Math.max(4, panelWidth - 4));
|
|
439
|
+
rendered.push(hexToAnsiBg(bg) + ' ' + hexToAnsi('#3a4048') + dash);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (segments.length === 0) {
|
|
443
|
+
rendered.push(hexToAnsiBg(bg) + ' ');
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const avail = Math.max(8, panelWidth - 2 - indent);
|
|
448
|
+
const rows = wrapStyledSegments(segments, avail, Math.max(4, avail - hanging));
|
|
449
|
+
rows.forEach((row, i) => {
|
|
450
|
+
// Continuation rows line up under the text, not under the bullet.
|
|
451
|
+
const pad = ' '.repeat(indent + (i > 0 ? hanging : 0));
|
|
452
|
+
const body = row.map((s) => styleSegment(s, textColor)).join('');
|
|
453
|
+
rendered.push(hexToAnsiBg(bg) + ' ' + pad + body);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return React.createElement(
|
|
458
|
+
Box,
|
|
459
|
+
{ flexDirection: 'column', marginBottom: 1 },
|
|
460
|
+
React.createElement(Text, { key: 'h' }, header),
|
|
461
|
+
...rendered.map((content, i) => {
|
|
462
|
+
const used = visibleLength(content);
|
|
463
|
+
return React.createElement(
|
|
464
|
+
Text,
|
|
465
|
+
{ key: i },
|
|
466
|
+
hexToAnsiBg(bg) + content + ' '.repeat(Math.max(0, panelWidth - used)) + ANSI_RESET
|
|
467
|
+
);
|
|
468
|
+
})
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function Heading({ level, children }) {
|
|
473
|
+
const color = level === 1 ? G.blue : level === 2 ? G.green : G.yellow;
|
|
474
|
+
return React.createElement(
|
|
475
|
+
Box,
|
|
476
|
+
{ marginBottom: 1, marginTop: level <= 2 ? 1 : 0 },
|
|
477
|
+
React.createElement(Text, { color, bold: level <= 2, wrap: 'wrap' }, children)
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function ListItem({ children, ordered, index }) {
|
|
482
|
+
const bullet = ordered ? `${index}. ` : ' • ';
|
|
483
|
+
const bulletColored = hexToAnsi(G.blue) + bullet + ANSI_RESET;
|
|
484
|
+
const segments = parseInline(children);
|
|
485
|
+
let content = '';
|
|
486
|
+
for (const seg of segments) {
|
|
487
|
+
let prefix = '';
|
|
488
|
+
if (seg.bold) prefix += ANSI_BOLD;
|
|
489
|
+
if (seg.italic) prefix += ANSI_ITALIC;
|
|
490
|
+
if (seg.color) prefix += hexToAnsi(seg.color);
|
|
491
|
+
content += prefix + seg.text + (prefix ? ANSI_RESET : '');
|
|
492
|
+
}
|
|
493
|
+
return React.createElement(Text, { wrap: 'wrap' }, ' ' + bulletColored + content);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function HorizontalRule({ width }) {
|
|
497
|
+
return React.createElement(
|
|
498
|
+
Box,
|
|
499
|
+
{ marginY: 1 },
|
|
500
|
+
React.createElement(Text, { color: '#3c4043' }, '─'.repeat(Math.max(8, Math.min(width || 50, 72))))
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function Blockquote({ children }) {
|
|
505
|
+
return React.createElement(
|
|
506
|
+
Box,
|
|
507
|
+
{
|
|
508
|
+
borderStyle: 'bold',
|
|
509
|
+
borderTop: false,
|
|
510
|
+
borderRight: false,
|
|
511
|
+
borderBottom: false,
|
|
512
|
+
borderLeftColor: G.blue,
|
|
513
|
+
paddingLeft: 1,
|
|
514
|
+
marginBottom: 1,
|
|
515
|
+
},
|
|
516
|
+
React.createElement(Text, { color: '#9aa0a6', italic: true, wrap: 'wrap' }, children)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function renderMarkdown(md, width) {
|
|
521
|
+
const elements = [];
|
|
522
|
+
const lines = md.split('\n');
|
|
523
|
+
let i = 0;
|
|
524
|
+
|
|
525
|
+
while (i < lines.length) {
|
|
526
|
+
const line = lines[i];
|
|
527
|
+
|
|
528
|
+
// Code block: ```lang ... ```
|
|
529
|
+
if (line.trim().startsWith('```')) {
|
|
530
|
+
const lang = line.trim().slice(3).trim();
|
|
531
|
+
const codeLines = [];
|
|
532
|
+
i++;
|
|
533
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
534
|
+
codeLines.push(lines[i]);
|
|
535
|
+
i++;
|
|
536
|
+
}
|
|
537
|
+
i++; // skip closing ```
|
|
538
|
+
const body = codeLines.join('\n');
|
|
539
|
+
if (looksLikeProse(lang, body)) {
|
|
540
|
+
// A bare fence around a written summary - render it as what it is.
|
|
541
|
+
for (const el of renderMarkdown(body, width)) {
|
|
542
|
+
elements.push(React.cloneElement(el, { key: `fenced-${elements.length}-${el.key}` }));
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
elements.push(React.createElement(CodeBlock, {
|
|
546
|
+
key: `code-${elements.length}`, lang, code: body, width,
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Heading: # ## ###
|
|
553
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
554
|
+
if (headingMatch) {
|
|
555
|
+
elements.push(React.createElement(Heading, { key: `h-${elements.length}`, level: headingMatch[1].length }, headingMatch[2]));
|
|
556
|
+
i++;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Horizontal rule: --- or *** or ___
|
|
561
|
+
if (/^[-*_]{3,}\s*$/.test(line.trim())) {
|
|
562
|
+
elements.push(React.createElement(HorizontalRule, { key: `hr-${elements.length}`, width }));
|
|
563
|
+
i++;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Blockquote: > text
|
|
568
|
+
if (line.trim().startsWith('>')) {
|
|
569
|
+
elements.push(React.createElement(Blockquote, { key: `bq-${elements.length}`, children: line.trim().slice(1).trim() }));
|
|
570
|
+
i++;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Unordered list: - or * text
|
|
575
|
+
const listMatch = line.match(/^(\s*)([-*])\s+(.+)/);
|
|
576
|
+
if (listMatch) {
|
|
577
|
+
elements.push(React.createElement(ListItem, { key: `li-${elements.length}`, children: listMatch[3] }));
|
|
578
|
+
i++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Ordered list: 1. text
|
|
583
|
+
const olMatch = line.match(/^(\s*)(\d+)\.\s+(.+)/);
|
|
584
|
+
if (olMatch) {
|
|
585
|
+
elements.push(React.createElement(ListItem, { key: `oli-${elements.length}`, children: olMatch[3], ordered: true, index: parseInt(olMatch[2]) }));
|
|
586
|
+
i++;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Empty line
|
|
591
|
+
if (line.trim() === '') {
|
|
592
|
+
i++;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Paragraph: collect consecutive non-empty lines
|
|
597
|
+
const paraLines = [];
|
|
598
|
+
while (i < lines.length && lines[i].trim() !== '' && !lines[i].trim().startsWith('```') && !lines[i].trim().startsWith('#') && !lines[i].trim().startsWith('>') && !/^[-*]\s/.test(lines[i].trim()) && !/^\d+\.\s/.test(lines[i].trim()) && !/^[-*_]{3,}\s*$/.test(lines[i].trim())) {
|
|
599
|
+
paraLines.push(lines[i]);
|
|
600
|
+
i++;
|
|
601
|
+
}
|
|
602
|
+
if (paraLines.length) {
|
|
603
|
+
elements.push(
|
|
604
|
+
React.createElement(InlineText, { key: `p-${elements.length}`, segments: parseInline(paraLines.join(' ')) })
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return elements;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export {
|
|
613
|
+
renderMarkdown, CodeBlock, Panel, parseInline, looksLikeProse,
|
|
614
|
+
hexToAnsi, hexToAnsiBg, fillRow, clipSegments, visibleLength, SURFACE,
|
|
615
|
+
ANSI_RESET, ANSI_BOLD, ANSI_DIM, ANSI_ITALIC,
|
|
616
|
+
};
|
|
617
|
+
export default renderMarkdown;
|