Skip to content

feat: add openapi support#11

Merged
rsbh merged 27 commits intomainfrom
feat_add_openapi_support
Feb 18, 2026
Merged

feat: add openapi support#11
rsbh merged 27 commits intomainfrom
feat_add_openapi_support

Conversation

@rsbh
Copy link
Copy Markdown
Member

@rsbh rsbh commented Feb 18, 2026

Summary

Adds full OpenAPI/Swagger API documentation support to Chronicle.

  • OpenAPI parser with automatic Swagger 2.0 → OpenAPI 3.0 conversion and $ref resolution
  • API routes & pages at /apis/[spec]/[operationId] with method-colored sidebar navigation
  • Endpoint page with request/response schema tables, code snippets, and interactive "Try it out" panel
  • UI components built on Apsara (Tabs, Accordion, Badge, CodeMirror editor, key-value header editor)
  • Global search integration — API endpoints indexed in Cmd+K search with HTTP method badges and URL deduplication
  • Config-driven — API specs declared in chronicle.yaml, supports multiple specs

Test plan

  • Run pnpm build — no type errors
  • Start dev server from examples/basic, verify /apis/petstore/addPet renders
  • Press Cmd+K, search for an operation name (e.g. "addPet") — result appears with method badge
  • Click result — navigates to correct endpoint page
  • Test "Try it out" panel sends request and displays response
  • Verify sidebar shows API sections with method icons

🤖 Generated with Claude Code

rsbh and others added 21 commits February 16, 2026 11:20
Add ApiConfig, OpenAPI normalized types, and pass api config through loadConfig.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse OpenAPI 2.0 and 3.0 specs, normalize to V3 Document
- Use openapi-types package types directly, no custom normalized types
- Resolve $ref references recursively with cycle detection
- flattenSchema utility for table rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- api-routes.ts: route building, operation lookup, sidebar tree
- apis/[[...slug]]/page.tsx: landing + endpoint pages
- apis/[[...slug]]/layout.tsx: API layout with sidebar
- PageTreeItem: add optional icon field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Map icon strings to Heroicons (code-bracket, rectangle-stack)
- Use NextLink for client-side nav instead of full page reloads

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Separate browser-safe schema utils from fs-dependent openapi parser.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- method-badge: colored HTTP method badges
- params-table: field/type/required/description table with nested indentation
- endpoint-page: two-column layout with method badge, path, params, responses
- index.ts: re-exports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Sidebar items show colored method badges (GET/POST/PUT/DELETE/PATCH)
- Use NextLink for client-side navigation
- API page uses EndpointPage component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ara Flex and Button

- Add FieldSection and FieldRow components with Apsara Flex/Button
- Two-column endpoint page layout with Fields/JSON toggle for body
- Add generateExampleJson to schema utils
- Delete ParamsTable (replaced by inline field display)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use Apsara Tabs for Fields/JSON toggle in body and response sections
- Use Apsara CodeBlock for syntax-highlighted JSON
- Use Apsara Accordion for expandable nested fields
- Use Apsara Badge for method badges (GET/POST/PUT/DELETE/PATCH)
- Add micro size badges in sidebar
- Replace hardcoded color fallbacks with Apsara semantic tokens
- Fix URL overflow with text-overflow ellipsis
- Add response JSON examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add right-column code snippets panel to endpoint page with cURL, Python,
Go, and TypeScript generators using Apsara CodeBlock language select.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ResponsePanel component showing response examples with dropdown
to switch between status codes. Remove tag label, align copy buttons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support inline editing for all parameter types with two-way data binding.
FieldRow handles string, number, boolean, enum, object and array fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Inline editable fields sync with code snippets panel. API requests
proxied through /api/apis-proxy to avoid CORS. Two-way body sync
between form fields and JSON editor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ThemeLayoutProps now accepts classNames for layout, body, sidebar,
and content. API docs layout uses this for 100vh sticky sidebar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Custom headers via dynamic key-value pairs with add/remove.
FieldSection supports children slot. Single scroll for both panels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Index all OpenAPI operations in Cmd+K search. Results show colored
method badges (GET/POST/PUT/DELETE/PATCH) and deduplicate by URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 18, 2026

Warning

Rate limit exceeded

@rsbh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds OpenAPI support and an interactive API reference: loads/normalizes specs (v2→v3), builds routes/indexes, integrates API operations into search and sidebar, provides an Endpoint UI with Try‑It using a proxy, code-snippet generators, editors, and related UI components and styles.

Changes

Cohort / File(s) Summary
Examples
examples/basic/chronicle.yaml, examples/basic/petstore.json
Added example chronicle config with two APIs and a Swagger v2 Petstore spec.
Config & Types
packages/chronicle/src/types/config.ts, packages/chronicle/src/types/content.ts, packages/chronicle/src/types/theme.ts, packages/chronicle/src/lib/config.ts
Added ApiConfig types, exposed api in runtime config, added icon to PageTreeItem and classNames to ThemeLayoutProps (public shape changes).
OpenAPI loader & schema
packages/chronicle/src/lib/openapi.ts, packages/chronicle/src/lib/schema.ts
New OpenAPI loader with v2→v3 conversion, $ref resolution/dereference, ApiSpec shape, schema flattening and example generation.
Routing & page tree
packages/chronicle/src/lib/api-routes.ts, packages/chronicle/src/app/apis/[[...slug]]/layout.tsx, .../layout.module.css
Spec slugging, route generation, find-by-slug, page tree builder; ApiLayout loads specs and supplies sidebar tree with new layout CSS.
Pages & indexing
packages/chronicle/src/app/apis/[[...slug]]/page.tsx, packages/chronicle/src/app/api/search/route.ts
API reference page with static params and per-operation rendering; search route extended to index API operations.
Proxy API
packages/chronicle/src/app/api/apis-proxy/route.ts
New POST proxy endpoint that resolves spec, composes target URL, forwards request (method/headers/body), and returns normalized responses or 502 on network errors.
UI — Endpoint & Fields
packages/chronicle/src/components/api/endpoint-page.tsx, .../endpoint-page.module.css, packages/chronicle/src/components/api/field-section.tsx, .../field-section.module.css, packages/chronicle/src/components/api/field-row.tsx, .../field-row.module.css
Endpoint explorer UI with nested parameter editors, JSON/body editor, Try‑It flow wired to proxy, response panel, and complex state/validation.
UI — Helpers & Editors
packages/chronicle/src/components/api/key-value-editor.tsx, .../key-value-editor.module.css, packages/chronicle/src/components/api/json-editor.tsx, .../json-editor.module.css, packages/chronicle/src/components/api/code-snippets.tsx, .../code-snippets.module.css
Key/value header editor, CodeMirror-based JSON editor, and multi-language code snippet component.
UI — Small utilities & panels
packages/chronicle/src/components/api/response-panel.tsx, .../response-panel.module.css, packages/chronicle/src/components/api/method-badge.tsx, .../method-badge.module.css, packages/chronicle/src/components/api/index.ts
Response examples panel, HTTP method badge, and central re-exports for API components.
Theme/Layout
packages/chronicle/src/themes/default/Layout.tsx, packages/chronicle/src/app/layout.tsx
Layout now accepts optional classNames and renders icons (iconMap with MethodBadge); added normalize.css import.
Snippets & generators
packages/chronicle/src/lib/snippet-generators.ts
Code snippet generators for curl, Python, Go, and TypeScript.
Search UI
packages/chronicle/src/components/ui/search.tsx
Deduplicate search results by URL; detect API results and render MethodBadge; strip method prefixes from displayed content.
Deps & typings
packages/chronicle/package.json, packages/chronicle/next-env.d.ts
Added CodeMirror, openapi-types, slugify, class-variance-authority and updated Next route typing import path.
API index & routes helpers
packages/chronicle/src/lib/api-routes.ts
Builds API routes, find-by-slug, and page tree grouping operations by tag/spec for sidebar and static params.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant EndpointPage as Endpoint Page
    participant ApisProxy as /api/apis-proxy
    participant ExternalAPI as External API

    User->>EndpointPage: Populate params/headers/body and click "Send"
    EndpointPage->>EndpointPage: Build payload (specName, method, path, headers, body)
    EndpointPage->>ApisProxy: POST payload
    ApisProxy->>ApisProxy: Resolve spec -> compute server URL + substitute path params
    ApisProxy->>ExternalAPI: Forward request (method, headers, body)
    ExternalAPI-->>ApisProxy: Return response (status, headers, body)
    ApisProxy->>ApisProxy: Normalize/parse body (JSON or text)
    ApisProxy-->>EndpointPage: Return { status, statusText, body }
    EndpointPage->>User: Render response panel
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add openapi support' clearly and concisely summarizes the main change: adding OpenAPI/Swagger support to Chronicle. It is specific, directly related to the changeset, and follows conventional commit conventions.
Description check ✅ Passed The description is well-structured and directly related to the changeset. It explains the key features added (OpenAPI parser, API routes, endpoint pages, UI components, search integration, config-driven specs) and includes a comprehensive test plan, demonstrating clear intent and scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat_add_openapi_support

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (8)
packages/chronicle/src/components/ui/search.tsx (1)

163-173: Consider adding HEAD and OPTIONS to API_METHODS.

OpenAPI specs commonly include HEAD and OPTIONS methods. If any API endpoints use these methods, they won't be recognized, causing the badge not to render and the method prefix to remain in the displayed text.

Additionally, stripMethod returns an empty string when content is just the method (e.g., "GET"), which could result in blank text in the search results.

♻️ Suggested improvement
-const API_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH"]);
+const API_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/ui/search.tsx` around lines 163 - 173, Add
missing HTTP verbs HEAD and OPTIONS to the API_METHODS set and update
stripMethod to avoid returning a blank string when content is only the method;
specifically, in API_METHODS include "HEAD" and "OPTIONS", and in stripMethod
(which currently uses content.split and content.slice) detect if the sliced
remainder is empty or just whitespace and in that case return the original first
token (or the original content) instead of an empty string so search results
don't render blank; leave extractMethod behavior as-is.
packages/chronicle/src/components/api/field-row.module.css (1)

73-94: Stylelint false positive: :global() is valid CSS Modules syntax.

The :global() pseudo-class is used correctly here to escape CSS Modules' local scoping and target third-party accordion component classes. The Stylelint errors are false positives.

Consider adding a Stylelint configuration to recognize :global():

{
  "rules": {
    "selector-pseudo-class-no-unknown": [true, {
      "ignorePseudoClasses": ["global"]
    }]
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/api/field-row.module.css` around lines 73 -
94, Stylelint is flagging the CSS Modules :global() pseudo-class used in
selectors like :global([class*="accordion-header"]) and
:global([class*="accordion-content-inner"]); update the project's Stylelint
configuration to treat "global" as a known pseudo-class by adding the
selector-pseudo-class-no-unknown rule with ignorePseudoClasses including
"global" (i.e., modify your Stylelint config used by the repo to include
"selector-pseudo-class-no-unknown": [true, { "ignorePseudoClasses": ["global"]
}]). Ensure the updated config is in the same config file the linter uses so the
:global(...) selectors in field-row.module.css are no longer reported as
unknown.
examples/basic/petstore.json (1)

1-1: Consider removing HTTP scheme from example spec.

This is the standard Petstore demo spec, which is appropriate for examples. However, static analysis flagged that it allows both https and http schemes. For better security posture even in examples, consider keeping only HTTPS:

-"schemes":["https","http"]
+"schemes":["https"]

The other security warnings (implicit OAuth flow, missing global security) are inherent to this demo spec and acceptable for example purposes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/basic/petstore.json` at line 1, The example OpenAPI JSON includes
both "https" and "http" in the "schemes" array which enables insecure HTTP;
update the "schemes" array (the JSON key "schemes") to remove the "http" entry
so it contains only "https" (i.e., change ["https","http"] to ["https"]) and run
any example validations to ensure no other code depends on plain-http in this
spec.
packages/chronicle/src/lib/openapi.ts (1)

125-133: Consider preserving array/enum metadata for v2 params.
For non‑body parameters, only type and format are mapped, so array parameters lose items and enums/defaults disappear. This will render as unknown[] and can mislead users in the request editor.

♻️ Suggested enrichment
-  const v3Params: OpenAPIV3.ParameterObject[] = params
-    .filter((p) => p.in !== 'body')
-    .map((p) => ({
-      name: p.name,
-      in: p.in as 'path' | 'query' | 'header' | 'cookie',
-      required: p.required ?? false,
-      description: p.description,
-      schema: { type: (p as JsonObject).type as string, format: (p as JsonObject).format as string | undefined } as OpenAPIV3.SchemaObject,
-    }))
+  const v3Params: OpenAPIV3.ParameterObject[] = params
+    .filter((p) => p.in !== 'body')
+    .map((p) => {
+      const pJson = p as JsonObject
+      return {
+        name: p.name,
+        in: p.in as 'path' | 'query' | 'header' | 'cookie',
+        required: p.required ?? false,
+        description: p.description,
+        schema: {
+          type: pJson.type as string,
+          format: pJson.format as string | undefined,
+          items: pJson.items as OpenAPIV3.SchemaObject | undefined,
+          enum: pJson.enum as unknown[] | undefined,
+          default: pJson.default,
+        } as OpenAPIV3.SchemaObject,
+      }
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/lib/openapi.ts` around lines 125 - 133, The v3Params
mapping currently only copies type and format, which loses array/items, enum,
and default metadata; update the params -> v3Params conversion (the mapping that
builds OpenAPIV3.ParameterObject) to build the schema by copying relevant fields
from the original param (handle when (p as JsonObject).type === 'array' to copy
items, and always copy enum and default if present), ensuring the resulting
schema is a proper OpenAPIV3.SchemaObject for the ParameterObject; modify the
mapping in the v3Params creation to merge type/format with items/enum/default
from the source param instead of only setting type/format.
packages/chronicle/src/components/api/index.ts (1)

8-8: Consider also exporting the KeyValueEntry type.

The KeyValueEntry interface is exported from key-value-editor.tsx but not re-exported here. Consumers of KeyValueEditor will need to import this type separately to properly type their state.

💡 Proposed fix
-export { KeyValueEditor } from './key-value-editor'
+export { KeyValueEditor, type KeyValueEntry } from './key-value-editor'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/api/index.ts` at line 8, The index
re-export currently exposes KeyValueEditor but omits the KeyValueEntry type used
by consumers; update the export list in this module to re-export the
KeyValueEntry type (alongside KeyValueEditor) from './key-value-editor' so
callers can import both KeyValueEditor and the KeyValueEntry interface from this
entry point.
packages/chronicle/src/components/api/endpoint-page.tsx (1)

111-112: Catch block swallows the original error; consider logging for debugging.

The catch block at line 111 loses the original error information. While the user sees a generic message, developers would benefit from having the error logged for debugging purposes.

💡 Proposed improvement
-    } catch {
+    } catch (err) {
+      console.error('API request failed:', err)
       setResponseBody({ status: 0, statusText: 'Error', body: 'Failed to send request' })
     } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/api/endpoint-page.tsx` around lines 111 -
112, The catch block that currently calls setResponseBody({ status: 0,
statusText: 'Error', body: 'Failed to send request' }) swallows the original
error; update that catch to capture the error (e.g., catch (err)) and log it
(e.g., console.error or the app logger) before calling setResponseBody so
developers can see the original exception when debugging; locate the try/catch
around the request/send logic where setResponseBody is used to identify the
exact spot to change.
packages/chronicle/src/components/api/key-value-editor.tsx (2)

67-69: Icon button lacks accessible label for screen readers.

The trash button uses only an icon without an accessible label. Screen reader users won't understand the button's purpose.

♿ Proposed fix for accessibility
-          <IconButton size="small" variant="ghost" onClick={() => removeEntry(i)}>
+          <IconButton size="small" variant="ghost" onClick={() => removeEntry(i)} aria-label="Remove header">
             <TrashIcon />
           </IconButton>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/api/key-value-editor.tsx` around lines 67 -
69, The IconButton rendering the TrashIcon in key-value-editor.tsx lacks an
accessible label, so add a clear accessible name (e.g., aria-label="Remove
entry" or aria-label={`Remove entry ${i + 1}`} or aria-labelledby) to the
IconButton that calls removeEntry(i) so screen readers can announce its purpose;
ensure the label is descriptive and unique enough for list context and use the
existing IconButton/TrashIcon elements (no visual text required).

49-50: Using array index as React key can cause issues if entries are reordered.

While this simple editor likely won't have reordering, using index as key (key={i}) can cause unexpected behavior with animations or component state if entries are added/removed from the middle. Consider using a stable identifier.

💡 Proposed improvement using a unique ID

Update the interface:

 export interface KeyValueEntry {
+  id: string
   key: string
   value: string
 }

Then use entry.id as the key and generate IDs when adding entries:

   const addEntry = () => {
-    onChange([...entries, { key: '', value: '' }])
+    onChange([...entries, { id: crypto.randomUUID(), key: '', value: '' }])
   }
-        <Flex key={i} align="center" gap="small" className={styles.row}>
+        <Flex key={entry.id} align="center" gap="small" className={styles.row}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/api/key-value-editor.tsx` around lines 49 -
50, The list is using the array index as a React key in the entries.map
(key={i}) which can break reconciliation when items are inserted/removed; update
the entry shape to include a stable id (e.g., add an id: string field on the
entry interface/type used by KeyValueEditor), change the mapping to use entry.id
as the key, and ensure any code paths that create new entries (the add-entry
handler or reducer used by the component) generate and assign a unique id (UUID,
nanoid, or a monotonic counter/timestamp) so keys remain stable across reorders
and edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/src/app/api/apis-proxy/route.ts`:
- Around line 29-56: The proxy currently always returns HTTP 200 because it uses
NextResponse.json(...) without setting the HTTP status; update both the success
and error returns to set the actual HTTP status via NextResponse.json(..., {
status: <code> }) so clients see the upstream response code: for the normal
response use response.status (and keep response.statusText/body as before), and
in the catch block set the status to 502 when returning the error payload.
Ensure you modify the returns that call NextResponse.json (the success return
after fetching and the catch return) to pass the appropriate { status: ... }
option.

In `@packages/chronicle/src/app/api/search/route.ts`:
- Around line 31-38: operationToIndex is building URLs using raw op.operationId
which can contain characters like '/', spaces or '#' that will break routing;
create a single helper (e.g., slugifyOrEncodeOperationId) and use it to encode
the operationId before constructing url/id in operationToIndex and reuse the
exact same helper in buildApiRoutes, findApiOperation, and buildApiPageTree (and
the helper imported into api-routes.ts) so generated routes and lookup logic
share the same encoded mapping; ensure the helper deterministically
replaces/encodes problematic characters and is applied wherever op.operationId
is used as a URL segment.

In `@packages/chronicle/src/components/api/endpoint-page.tsx`:
- Around line 89-91: The code currently only treats POST and PUT as methods that
send a body and set the Content-Type header; update every conditional that
checks (method === 'POST' || method === 'PUT') so it also accepts 'PATCH' (e.g.,
the block that sets reqHeaders['Content-Type'] using body?.contentType and any
logic that attaches bodyJsonStr/payload to the request, plus the
snippet/header-generation code that only includes Content-Type for POST/PUT).
Locate the checks using the method variable, reqHeaders['Content-Type'],
bodyJsonStr/body payload assignment, and the snippet/header rendering logic and
add 'PATCH' to those conditionals so PATCH requests include the request body and
Content-Type header.
- Around line 172-185: The Body rendering condition and any related snippet-body
checks currently only treat POST and PUT as methods that show the body; update
the conditional that uses the variable method (e.g., the JSX around FieldSection
and any similar checks used for snippet generation) to also include 'PATCH'
(e.g., method === 'POST' || method === 'PUT' -> include method === 'PATCH'),
ensuring handlers like handleBodyJsonChange, handleBodyValuesChange, and props
passed to FieldSection (jsonExample, editableJson, values) continue to be used
unchanged so PATCH requests render and handle body input the same as POST/PUT.

In `@packages/chronicle/src/components/api/field-row.tsx`:
- Around line 72-118: Array items without children are being coerced to strings
(InputField/string) which loses original types for numbers/booleans and enum
values; update the array-item rendering in FieldRow so primitive items use the
same type-aware editor as single-value fields (reuse EditableInput instead of
InputField) and when handling enums map the selected string back to the original
typed value (use field.values or the enum definition to convert to
number/boolean as needed) before calling onChange; ensure the onChange handlers
in the array branch (the add-item PlusIcon handler and the per-item onChange for
primitive items) create/propagate values with correct types and apply the same
fix to the other array rendering block referenced (lines ~166-179) so enums and
numeric/boolean array elements are preserved.

In `@packages/chronicle/src/components/api/field-section.tsx`:
- Around line 63-65: The Fields tab currently renders only fieldsContent inside
Tabs.Content (value="fields") so when jsonExample or alwaysShow triggers the
tabbed layout the children prop is omitted; update the tabbed path to include
children alongside fieldsContent (e.g., render fieldsContent followed by
children inside Tabs.Content for value="fields") so custom components like
KeyValueEditor appear in the tabbed Fields view—refer to Tabs.Content
(value="fields"), fieldsContent, children, and the jsonExample/alwaysShow
conditional that determines the tabbed layout.

In `@packages/chronicle/src/components/api/json-editor.tsx`:
- Around line 17-49: The JsonEditor effect currently only runs on isDark so
updates to value, readOnly, and onChange are ignored; update the useEffect in
JsonEditor to depend on isDark, value, readOnly, and onChange and then inside
the effect: if viewRef.current exists, sync the buffer by dispatching a change
to replace the whole document with the new value (use viewRef.current.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: value } })) and update
readOnly by reinitializing extensions or recreating the view when
readOnly/onChange/isDark change; otherwise create the EditorState/EditorView as
before and assign viewRef.current; ensure the effect cleans up by destroying the
previous view before creating a new one to avoid leaks (references: JsonEditor,
containerRef, viewRef, EditorState, EditorView, useEffect).

In `@packages/chronicle/src/components/api/response-panel.module.css`:
- Around line 5-6: Stylelint flags the :global pseudo-class in the CSS rule
".panel :global([class*=\"code-block-module_header\"])" with
selector-pseudo-class-no-unknown; to fix, wrap that specific selector block with
local Stylelint directives: add a comment /* stylelint-disable
selector-pseudo-class-no-unknown */ immediately before the ".panel
:global([class*=\"code-block-module_header\"])" rule and /* stylelint-enable
selector-pseudo-class-no-unknown */ immediately after the rule so only this
selector is exempted and CI no longer fails.

In `@packages/chronicle/src/components/api/response-panel.tsx`:
- Around line 1-39: Replace the server-only import of STATUS_CODES (which is
causing client-side bundling failures) by using the existing description on each
response; update the ResponsePanel component so CodeBlock.LanguageSelectItem
renders "{resp.status} {resp.description ?? resp.status}" (and ensure the
fallback is resp.status) and remove any import or usage of STATUS_CODES; also
verify the defaultValue and CodeBlock.Code value keys still use resp.status and
that jsonExample is used as before in the CodeBlock.Code children.

In `@packages/chronicle/src/lib/openapi.ts`:
- Around line 65-85: deepResolveRefs incorrectly treats any repeated $ref as
circular because the single shared seen Set marks refs globally; change to track
refs that are currently being expanded with an inStack Set and use a
resolvedCache Map to store fully resolved schemas so repeated non-circular
references return the cached resolved value. Concretely, in deepResolveRefs use
an inStack (or visiting) Set to detect true recursion (return the circular
placeholder when a $ref is found in inStack), add the $ref to inStack before
resolving and remove it after recursion, store the result in resolvedCache keyed
by the $ref returned from resolveRef, and on encountering a $ref first check
resolvedCache to return the cached resolution immediately instead of treating it
as circular; keep function names resolveRef and deepResolveRefs unchanged.

In `@packages/chronicle/src/lib/schema.ts`:
- Around line 13-23: flattenSchema currently expands array items into separate
fields losing the array container; update the array branch in flattenSchema so
that when schema.type === 'array' and schema.items is present you return a
single SchemaField that represents the array container (e.g., type 'array' or
keep `${items.type}[]`) and set its children to the flattened result of the item
schema (call flattenSchema(items) for children), preserving requiredFields and
any metadata; reference flattenSchema, SchemaField, and schema.items to locate
and replace the current map-based expansion with this container-with-children
approach.

In `@packages/chronicle/src/themes/default/Layout.tsx`:
- Around line 24-27: The Layout component contains a debug console.log(config)
that leaks internal config and should be removed; open the Layout function
(export function Layout) and delete the console.log(config) statement (or guard
it behind a dev-only check if you must keep debugging output), ensuring no
client-side debug logging remains in
packages/chronicle/src/themes/default/Layout.tsx.

---

Duplicate comments:
In `@packages/chronicle/src/components/api/code-snippets.module.css`:
- Around line 5-6: The Stylelint complaint is caused by using :global(...)
inside the .snippets selector; apply the same fix used in
response-panel.module.css by either moving the
:global([class*="code-block-module_header"]) selector out to a top-level rule or
add a local stylelint suppression comment above the rule to allow the :global
usage; update the selector reference .snippets
:global([class*="code-block-module_header"]) accordingly (or add /*
stylelint-disable-next-line selector-class-pattern */ immediately before it) so
the lint error is resolved.

In `@packages/chronicle/src/lib/api-routes.ts`:
- Around line 10-23: buildApiRoutes currently pushes raw operationId into route
slugs; update it to use the same operationId encoding/slugging utility used in
the search route (import and call the canonical encoder, e.g.,
encodeOperationId) so slugs become { slug: [specSlug,
encodeOperationId(op.operationId)] } instead of raw op.operationId; apply the
same fix to the other blocks that push or build URLs from operationId (the other
occurrences around the later route-generation loops and the URL-building code
noted in the review) so all routing and lookup use the identical encode/decode
strategy.

---

Nitpick comments:
In `@examples/basic/petstore.json`:
- Line 1: The example OpenAPI JSON includes both "https" and "http" in the
"schemes" array which enables insecure HTTP; update the "schemes" array (the
JSON key "schemes") to remove the "http" entry so it contains only "https"
(i.e., change ["https","http"] to ["https"]) and run any example validations to
ensure no other code depends on plain-http in this spec.

In `@packages/chronicle/src/components/api/endpoint-page.tsx`:
- Around line 111-112: The catch block that currently calls setResponseBody({
status: 0, statusText: 'Error', body: 'Failed to send request' }) swallows the
original error; update that catch to capture the error (e.g., catch (err)) and
log it (e.g., console.error or the app logger) before calling setResponseBody so
developers can see the original exception when debugging; locate the try/catch
around the request/send logic where setResponseBody is used to identify the
exact spot to change.

In `@packages/chronicle/src/components/api/field-row.module.css`:
- Around line 73-94: Stylelint is flagging the CSS Modules :global()
pseudo-class used in selectors like :global([class*="accordion-header"]) and
:global([class*="accordion-content-inner"]); update the project's Stylelint
configuration to treat "global" as a known pseudo-class by adding the
selector-pseudo-class-no-unknown rule with ignorePseudoClasses including
"global" (i.e., modify your Stylelint config used by the repo to include
"selector-pseudo-class-no-unknown": [true, { "ignorePseudoClasses": ["global"]
}]). Ensure the updated config is in the same config file the linter uses so the
:global(...) selectors in field-row.module.css are no longer reported as
unknown.

In `@packages/chronicle/src/components/api/index.ts`:
- Line 8: The index re-export currently exposes KeyValueEditor but omits the
KeyValueEntry type used by consumers; update the export list in this module to
re-export the KeyValueEntry type (alongside KeyValueEditor) from
'./key-value-editor' so callers can import both KeyValueEditor and the
KeyValueEntry interface from this entry point.

In `@packages/chronicle/src/components/api/key-value-editor.tsx`:
- Around line 67-69: The IconButton rendering the TrashIcon in
key-value-editor.tsx lacks an accessible label, so add a clear accessible name
(e.g., aria-label="Remove entry" or aria-label={`Remove entry ${i + 1}`} or
aria-labelledby) to the IconButton that calls removeEntry(i) so screen readers
can announce its purpose; ensure the label is descriptive and unique enough for
list context and use the existing IconButton/TrashIcon elements (no visual text
required).
- Around line 49-50: The list is using the array index as a React key in the
entries.map (key={i}) which can break reconciliation when items are
inserted/removed; update the entry shape to include a stable id (e.g., add an
id: string field on the entry interface/type used by KeyValueEditor), change the
mapping to use entry.id as the key, and ensure any code paths that create new
entries (the add-entry handler or reducer used by the component) generate and
assign a unique id (UUID, nanoid, or a monotonic counter/timestamp) so keys
remain stable across reorders and edits.

In `@packages/chronicle/src/components/ui/search.tsx`:
- Around line 163-173: Add missing HTTP verbs HEAD and OPTIONS to the
API_METHODS set and update stripMethod to avoid returning a blank string when
content is only the method; specifically, in API_METHODS include "HEAD" and
"OPTIONS", and in stripMethod (which currently uses content.split and
content.slice) detect if the sliced remainder is empty or just whitespace and in
that case return the original first token (or the original content) instead of
an empty string so search results don't render blank; leave extractMethod
behavior as-is.

In `@packages/chronicle/src/lib/openapi.ts`:
- Around line 125-133: The v3Params mapping currently only copies type and
format, which loses array/items, enum, and default metadata; update the params
-> v3Params conversion (the mapping that builds OpenAPIV3.ParameterObject) to
build the schema by copying relevant fields from the original param (handle when
(p as JsonObject).type === 'array' to copy items, and always copy enum and
default if present), ensuring the resulting schema is a proper
OpenAPIV3.SchemaObject for the ParameterObject; modify the mapping in the
v3Params creation to merge type/format with items/enum/default from the source
param instead of only setting type/format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/chronicle/src/components/api/key-value-editor.tsx (1)

34-35: Avoid using array index as React key for dynamic lists.

Since entries can be added and removed, using the index as a key can cause React reconciliation issues—leading to unexpected focus loss or input state glitches when removing items from the middle of the list. Consider adding a unique id to each KeyValueEntry or generating one when entries are created.

♻️ Suggested approach

Update the interface:

 export interface KeyValueEntry {
+  id: string
   key: string
   value: string
 }

Then use entry.id as the key:

-        <Flex key={i} align="center" gap="small" className={styles.row}>
+        <Flex key={entry.id} align="center" gap="small" className={styles.row}>

And update addEntry to generate unique IDs (e.g., using crypto.randomUUID() or a simple counter).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/api/key-value-editor.tsx` around lines 34 -
35, The list is using the array index as the React key which breaks
reconciliation for dynamic edits; update the KeyValueEntry shape to include a
unique id (e.g., add an id field on the entry objects), change the map key from
the index to entry.id in the component where entries.map is used, and modify the
addEntry (and any entry creation logic) to assign a stable unique id (e.g.,
crypto.randomUUID() or an incrementing counter) when new entries are created so
keys remain stable across reorders/removals.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/src/components/api/field-row.tsx`:
- Line 76: The list rendering uses the array index as a React key (key={i} on
the <Flex ... className={styles.arrayItem}>), which can cause stale component
state when items are inserted/removed; change the data model and rendering to
use a stable unique id instead (e.g., store items as { id: string, ... }[] or
generate a UUID when adding an item) and replace key={i} with key={item.id};
also update any add/remove handlers and where items are created (inside the
component that maps items, and functions that push/splice items) to ensure every
new item receives a persistent id so keys remain stable across renders.

In `@packages/chronicle/src/components/api/key-value-editor.tsx`:
- Around line 52-54: The delete IconButton in the key-value editor is missing an
accessible label; update the IconButton (the element rendering the TrashIcon
that calls removeEntry(i)) to include an aria-label describing its action (e.g.,
"Delete entry" or a contextual label like `Delete entry ${i + 1}` or using the
entry key) so screen readers convey the button's purpose; keep the onClick
(removeEntry) and the TrashIcon unchanged and ensure the label is descriptive
and unique enough for assistive tech.

---

Duplicate comments:
In `@packages/chronicle/src/components/api/field-row.tsx`:
- Around line 151-166: The Select currently passes the raw string from
onValueChange to onChange, losing original enum types; update the handler in the
Select block (where onValueChange is defined) to map the selected string back to
the original typed enum value by finding the matching entry in field.enum (e.g.,
const selected = field.enum.find(opt => String(opt) === v) ?? v) and then call
onChange?.(field.name, selected); keep the Select value prop as String(value ??
'') and keep rendering Select.Item values as String(opt) to preserve display.
- Around line 93-103: Array items are being coerced to strings via String(item
?? '') and e.target.value in the InputField branch, breaking typed arrays
(number[]/boolean[]); replace this ad-hoc string handling by using the existing
EditableInput component (the same one used elsewhere for primitive fields) so it
preserves and parses the original type. Locate the block rendering InputField in
field-row (the InputField JSX with value={String(item ?? '')} and onChange
updating updated[i] = e.target.value) and swap it to render EditableInput
configured with the field schema/props and an onChange that writes back the
parsed/typed value into updated[i] before calling onChange?.(field.name,
updated), ensuring numbers/booleans are not left as strings.

---

Nitpick comments:
In `@packages/chronicle/src/components/api/key-value-editor.tsx`:
- Around line 34-35: The list is using the array index as the React key which
breaks reconciliation for dynamic edits; update the KeyValueEntry shape to
include a unique id (e.g., add an id field on the entry objects), change the map
key from the index to entry.id in the component where entries.map is used, and
modify the addEntry (and any entry creation logic) to assign a stable unique id
(e.g., crypto.randomUUID() or an incrementing counter) when new entries are
created so keys remain stable across reorders/removals.

- Propagate upstream HTTP status in proxy route
- Encode operationId in search URLs
- Remove Node http import from client component
- Fix $ref resolution with stack+cache for circular detection
- Sync JsonEditor value/readOnly/onChange props
- Use EditableInput for array items, preserve enum types
- Preserve array container in schema flattening
- Add PATCH method body support
- Render children in tabbed Fields view
- Add stylelint disable for :global selector
- Remove debug console.log from Layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/chronicle/src/themes/default/Layout.tsx (1)

97-105: Minor consideration: JSX element creation in render loop.

The as={<NextLink href={item.url ?? "#"} />} creates a new element on each render. For typical sidebar sizes this is fine, but if performance becomes a concern with large navigation trees, consider extracting this to a memoized component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/default/Layout.tsx` around lines 97 - 105, The
Sidebar.Item is recreating a JSX element each render via as={<NextLink
href={item.url ?? "#"} />}, which can hurt performance for large nav trees;
refactor by extracting a memoized Link component (e.g., create a MemoNextLink or
useMemo to produce the element) and pass that memoized component into
Sidebar.Item's as prop instead, referencing Sidebar.Item, NextLink and item.url
to locate the change so the link element is reused across renders.
packages/chronicle/src/lib/openapi.ts (1)

131-143: Consider handling missing type in v2 parameter schema.

Line 141 casts (p as JsonObject).type as string, but if a v2 parameter lacks an explicit type, this could result in undefined being cast to string. While rare, some v2 specs may omit type.

🛡️ Defensive fix
-      schema: { type: (p as JsonObject).type as string, format: (p as JsonObject).format as string | undefined } as OpenAPIV3.SchemaObject,
+      schema: { type: ((p as JsonObject).type as string) ?? 'string', format: (p as JsonObject).format as string | undefined } as OpenAPIV3.SchemaObject,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/lib/openapi.ts` around lines 131 - 143, The conversion
in convertV2Operation builds v3Params using (p as JsonObject).type cast to
string which can be undefined for some v2 params; update the mapping for
v3Params in convertV2Operation to defensively read the type and format from the
parameter (e.g., const t = (p as JsonObject).type ?? 'string' and const f = (p
as JsonObject).format ?? undefined) and use those values when constructing the
OpenAPIV3.SchemaObject, ensuring required fallbacks rather than casting possibly
undefined values; also keep the existing fields (name, in, required,
description) unchanged.
packages/chronicle/src/components/api/endpoint-page.tsx (1)

111-113: Consider logging the error for debugging.

The catch block swallows the actual error, making debugging difficult when requests fail. Logging the error would help identify issues like network failures or CORS problems.

🔧 Suggested fix
-    } catch {
+    } catch (err) {
+      console.error('API request failed:', err)
       setResponseBody({ status: 0, statusText: 'Error', body: 'Failed to send request' })
     } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/api/endpoint-page.tsx` around lines 111 -
113, The catch block that calls setResponseBody({ status: 0, statusText:
'Error', body: 'Failed to send request' }) is swallowing the thrown error;
update the catch to accept the error (e.g., catch (err)) and log it for
debugging (using console.error or the app's logger) so failures
(network/CORS/etc.) are visible; keep the existing setResponseBody behavior but
include error details in the log and optionally append short error info to the
response body for the EndpointPage/send request flow.
packages/chronicle/src/app/api/search/route.ts (1)

56-60: Synchronous file I/O in async context.

buildApiIndexes calls loadApiSpecs, which performs synchronous file reading. This is acceptable for build-time index generation but could block the event loop if called frequently or with many large specs. Consider async alternatives if this becomes a bottleneck.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/app/api/search/route.ts` around lines 56 - 60,
buildApiIndexes currently calls loadApiSpecs which does synchronous file I/O and
can block the event loop; change the implementation to use async file I/O:
create an async loadApiSpecsAsync that uses fs.promises (or other async readers)
and returns a Promise of specs, then make buildApiIndexes async (or add
buildApiIndexesAsync) so it awaits loadApiSpecsAsync and returns
Promise<AdvancedIndex[]>; update any callers of buildApiIndexes to await the new
async function (or adapt to promise-based usage) and ensure types/signatures
reflect the async change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/src/components/api/endpoint-page.tsx`:
- Around line 209-220: Remove the redundant hardcoded value="json" prop on the
outer CodeBlock in EndpointPage (the JSX block using CodeBlock,
CodeBlock.Header, CodeBlock.Content, CodeBlock.Code); keep only the
language="json" prop on CodeBlock.Code and remove the value prop from CodeBlock
so the language is driven by CodeBlock.Code (mirror the pattern used in
field-section.tsx). Ensure CodeBlock.Code retains language="json" and that
CopyButton/Content children remain unchanged.

---

Nitpick comments:
In `@packages/chronicle/src/app/api/search/route.ts`:
- Around line 56-60: buildApiIndexes currently calls loadApiSpecs which does
synchronous file I/O and can block the event loop; change the implementation to
use async file I/O: create an async loadApiSpecsAsync that uses fs.promises (or
other async readers) and returns a Promise of specs, then make buildApiIndexes
async (or add buildApiIndexesAsync) so it awaits loadApiSpecsAsync and returns
Promise<AdvancedIndex[]>; update any callers of buildApiIndexes to await the new
async function (or adapt to promise-based usage) and ensure types/signatures
reflect the async change.

In `@packages/chronicle/src/components/api/endpoint-page.tsx`:
- Around line 111-113: The catch block that calls setResponseBody({ status: 0,
statusText: 'Error', body: 'Failed to send request' }) is swallowing the thrown
error; update the catch to accept the error (e.g., catch (err)) and log it for
debugging (using console.error or the app's logger) so failures
(network/CORS/etc.) are visible; keep the existing setResponseBody behavior but
include error details in the log and optionally append short error info to the
response body for the EndpointPage/send request flow.

In `@packages/chronicle/src/lib/openapi.ts`:
- Around line 131-143: The conversion in convertV2Operation builds v3Params
using (p as JsonObject).type cast to string which can be undefined for some v2
params; update the mapping for v3Params in convertV2Operation to defensively
read the type and format from the parameter (e.g., const t = (p as
JsonObject).type ?? 'string' and const f = (p as JsonObject).format ??
undefined) and use those values when constructing the OpenAPIV3.SchemaObject,
ensuring required fallbacks rather than casting possibly undefined values; also
keep the existing fields (name, in, required, description) unchanged.

In `@packages/chronicle/src/themes/default/Layout.tsx`:
- Around line 97-105: The Sidebar.Item is recreating a JSX element each render
via as={<NextLink href={item.url ?? "#"} />}, which can hurt performance for
large nav trees; refactor by extracting a memoized Link component (e.g., create
a MemoNextLink or useMemo to produce the element) and pass that memoized
component into Sidebar.Item's as prop instead, referencing Sidebar.Item,
NextLink and item.url to locate the change so the link element is reused across
renders.

rsbh and others added 2 commits February 18, 2026 14:02
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Log caught errors in try-it-out handler
- Add defensive fallback for V2 param type
- Memoize NextLink in SidebarNode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/src/components/api/endpoint-page.tsx`:
- Around line 89-126: The request-building logic currently only attaches
Content-Type and includes bodyValues for methods POST/PUT/PATCH; change these
checks to instead use the presence of the OpenAPI requestBody (use the existing
body and/or bodyJsonStr variables) so bodies are sent whenever a requestBody
exists (e.g., DELETE with requestBody). Update the fetch payload to include
body: bodyValues when body or bodyJsonStr is present (not just method checks),
set reqHeaders['Content-Type'] = body?.contentType ?? 'application/json' when
body or bodyJsonStr exists, and do the same for snippetHeaders (replace the
method-based checks with a body/bodyJsonStr presence check) so transport,
headers, and snippets are consistent with the actual body variable.
- Around line 83-85: The loop that copies headerValues into reqHeaders (for
(const [key, value] of Object.entries(headerValues)) { ... }) currently uses a
truthy check which drops valid falsy values like 0 or false; change the
condition to explicitly filter out empty/undefined/null values (e.g. check value
!== undefined && value !== null && value !== '') before assigning
reqHeaders[key] = String(value) so boolean/number headers are preserved while
still excluding absent/empty headers.

rsbh and others added 2 commits February 18, 2026 14:06
- Add aria-label to TrashIcon button in key-value editor
- Use resp.status as fallback in response panel
- Encode operationId with encodeURIComponent in api-routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Body, Content-Type header, and snippets now check for requestBody
instead of hard-coded method list. Also fix falsy header value check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@rsbh rsbh merged commit 9d97cbd into main Feb 18, 2026
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 20, 2026
7 tasks
@rsbh rsbh deleted the feat_add_openapi_support branch March 16, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants