镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(claude-plugin): draw ClickStack query results as terminal charts - #3400

Open
jordan-simonovski wants to merge 3 commits into
jordansimonovski/termchart-stacked-bar-legendfrom
jordansimonovski/claude-charts-plugin
Open

jordan-simonovski wants to merge 3 commits into
jordansimonovski/termchart-stacked-bar-legendfrom
jordansimonovski/claude-charts-plugin

Conversation

@jordan-simonovski

@jordan-simonovski jordan-simonovski commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds @hyperdx/claude-plugin, a Claude Code plugin that shows ClickStack MCP results as terminal charts. Today a clickstack_timeseries or clickstack_table call shows up in Claude Code as collapsed JSON, so the person in the session never sees the data Claude is reasoning about. With the plugin, that row becomes a line, stacked bar, bar, pie, number or table chart, drawn by the same renderer as hdx. Claude still reads the full result.

  • One install, /plugin install hyperdx-charts --marketplace hyperdxio/hyperdx, asks for the HyperDX URL and a personal API access key and connects the ClickStack MCP server, so there is no separate claude mcp add step. The key is kept in Claude Code's secure storage.
  • The repo root gains .claude-plugin/marketplace.json, which points at the npm package. Changesets publishes it with the other packages.
  • Chart text is stripped of control characters and bidi overrides, because labels carry user data such as service names.

Key decisions

  • Render from the tool call, not from a server-side chart spec. MCP servers can attach plugin-only data in _meta, but Claude Code only passes it to plugins behind a feature flag that is off. The plugin rebuilds the chart from the tool's input (shape, granularity, time range) and its result JSON, which is everything renderTileContent needs. A spec in _meta would be cleaner once that flag ships.
  • Capture at tool.call. Claude Code draws MCP calls as tool groups that don't carry the result, so the plugin keeps the last 20 charted results in session state and draws from there.
  • Bundle only the renderer. Claude Code compiles hooks/register.tsx itself and refuses hooks modules over 1 MiB. The renderer is bundled into hooks/chart.js, which is about 280 KB because the build reads common-utils' ESM source and drops unused code. Built from the CommonJS dist it was 5.5 MB.

Background

Claude Code plugins ("mods") are hooks modules loaded into a session. A ui.render hook can replace how a transcript row is drawn, and userConfig fields in plugin.json are prompted for at install and substituted into the bundled MCP server config. The plugin API is marked early access and may change between releases.

Impact

  • A new public npm package, @hyperdx/claude-plugin, published on the next release (0.1.0 via the changeset).
  • MCP.md points Claude Code users at the plugin as an alternative to claude mcp add.
  • Nothing changes for existing MCP users who don't install it.

How to reproduce

  1. yarn workspace @hyperdx/claude-plugin build
  2. claude --plugin-dir packages/claude-plugin, then enter your HyperDX URL and personal API access key when asked.
  3. Ask for something chartable, e.g. "error rate by service over the last hour". The clickstack_timeseries row shows a line chart instead of JSON.

How to test on Vercel preview

N/A, non-UI change.

References

Implementation detail
  • src/toTileResult.ts maps the tool input and MCP result onto the CLI's TileQueryResult: shape to display type, the requested window (or the server's 15-minute default), and granularity, inferred from bucket spacing when the call omitted it. Without a known bucket size it turns off zero-filling so it doesn't invent buckets.
  • src/ansiLines.ts parses termchart's ANSI output into styled Text segments. termchart runs at chalk level 1 here, so only basic SGR codes need handling.
  • Tools are matched by MCP name suffix, so the bundled server's mcp__plugin_hyperdx-charts_clickstack__* names and a hand-added mcp__clickstack__* both chart.
  • Tests: jest specs in src/__tests__/*.spec.ts run in CI. Engine tests in test/*.test.tsx run with claude plugin test, since they need Claude Code's test kit. claude plugin validate checks the manifest and hooks module. The hooks module and engine tests are typed against API declarations that Claude Code writes at load time, so they are excluded from the repo's tsc and eslint.

@changeset-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e3db061

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hyperdx/claude-plugin Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
hyperdx-oss Ignored Ignored Preview Oct 20, 2026 10:33am UTC
hyperdx-storybook Skipped Skipped Oct 20, 2026 10:33am UTC

Request Review

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Duplicate code analysis

No new duplicate code introduced by this PR.


jscpd compared this branch against origin/main
across app, api and common-utils. Only new clones are listed; the codebase
currently holds 124 in total (1.67%).
Run yarn dupes locally to reproduce, or yarn dupes:all for the full picture.

Comment thread packages/claude-plugin/hooks/chart.d.ts Outdated
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

5 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 5 minor

None could be anchored to changed lines; they are listed below.

Findings outside the changed lines

5 minor
  • 🔵 packages/claude-plugin/hooks/register.tsx:384 — The .catch fallback calls next(e) again, so a failed tool call runs a second time → The update call is already inside its own try/catch, so the only thing that can reach this .catch is await next(e) rejecting. The fallback then calls next(e) again, which sends the ClickStack query a second time on a transport or tool failure. Drop the .catch, or make it rethrow or return a non-retrying result, so a failed call is not repeated.
  • 🔵 packages/claude-plugin/src/renderChart.ts:906 — Each render parses the same MCP result JSON three times → toTileResult calls parseResponse, which runs parseBody. It calls isTrimmed for fillNulls, which runs parseBody again, and renderChart calls isTrimmed(call.output) a third time. That is three JSON.parse calls on results up to 50 KB, for every stored chart, every time a row re-renders. Parse the body once (e.g. have toTileResult return { result, trimmed }) and pass it down.
  • 🔵 packages/claude-plugin/src/__tests__/renderChart.spec.ts:582 — The untrimmed half of the test passes even when renderChart returns undefined → whole?.at(-1)?.[0]?.text ?? '' turns an undefined result into '', so the not.toContain('trimmed') check still passes if a regression stops the untrimmed result from charting. Assert expect(whole).toBeDefined() first. Better, compare whole with trimmed.slice(0, -1) so the test proves only the note differs.
  • 🔵 packages/claude-plugin/package.json:469 — Nothing in CI checks hooks/register.tsx or its engine tests → register.tsx is excluded from eslint and tsc. claude plugin test and claude plugin validate are not in ci:lint or ci:unit, and the Makefile ci-* targets only run those nx targets. So the storage, eviction (KEEP) and partial-group logic has no automated check. At minimum, move the pure parts (the eviction reducer and the row-selection logic) into src/ with jest specs, or add a CI job that runs claude plugin validate/test.
  • 🔵 packages/claude-plugin/src/ansiLines.ts:787 — The SGR regex repeats the CLI's ANSI_PATTERN → packages/cli/src/termchart/ansi.ts already defines ANSI_PATTERN = /\x1b\[[0-9;]*m/g for the same output this parser reads. The repo's DRY rule (REQUIRED) asks you to grep before adding a helper. Export the pattern from termchart, using a capture group or a shared source string, and reuse it so the two cannot drift.

Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with the earlier non-blocking chart-label issue still open.

Fix All in Claude CodeFindings

  1. P2 Labels break chart rows ▶

Summary

Adds a Claude Code plugin that draws ClickStack query results with the CLI’s chart renderers.

  • Claude Code shows supported ClickStack results as terminal charts.
  • Claude Code can install the plugin and connect it to ClickStack in one setup.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[ClickStack tool call] --> B[MCP result]
  B --> C[Keep recent results]
  C --> D[Rebuild chart settings]
  D --> E[CLI chart renderer]
  E --> F[Styled terminal row]
Loading

Reviews (4) · Last reviewed commit: "fix(claude-plugin): depend on common-uti..." · Reviewed by Greptile

Comment thread packages/claude-plugin/src/toTileResult.ts Outdated
Comment thread packages/claude-plugin/src/toTileResult.ts Outdated
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 440 passed • 1 skipped • 1441s

Status Count
✅ Passed 440
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

The two prior P1 review items are confirmed resolved in the current diff: clickstack_table multi-select now upgrades to a table exactly as the server does (toTileResult.ts:91-96, matching packages/api/src/mcp/tools/query/table.ts), and zero-fill is now suppressed unless an explicit granularity is given on an untrimmed result (fillNulls !== false semantics verified against shouldFillNullsWithZero). The findings below are all non-blocking.

🟡 P2 — recommended

  • packages/claude-plugin/src/ansiLines.ts:53 — stripControlChars runs only on slices between SGR matches, after split('\n') (line 49) and matchAll(SGR) (line 56), so attacker-influenced result data reaches the parser raw: a label containing ESC[32m is re-emitted as a real color/bold prop, and a label containing a literal \n injects extra chart rows beyond the computed height.
    • Fix: Sanitize user-derived cell and label values (strip ESC, C0/C1 controls, bidi overrides, and raw newlines) at the data boundary in toTileResult before they reach renderTileContent, leaving ansiLines to parse only the renderer's own line breaks and SGR colors.
    • previous-comments, security, correctness, adversarial
  • packages/claude-plugin/src/renderChart.ts:34 — table height is capped at MAX_TABLE_ROWS (20) but the trimmed note is appended only when the server set note, so an untrimmed result with more rows than fit is drawn truncated with no indication that rows were dropped.
    • Fix: Append a "showing N of M rows" note whenever result.data.data.length exceeds the rows actually drawn, independent of the server's trim note.
  • packages/claude-plugin/src/toTileResult.ts:109 — the as unknown as ChartConfigWithDateRange double cast erases the renderer's input contract, so if renderTileContent ever reads a config field this path does not set, it misrenders with zero compile-time signal.
    • Fix: Replace the double cast with a narrow type covering exactly the fields the renderer consumes (a Pick<> plus satisfies) so the compiler flags any newly-required field.
    • maintainability, kieran-typescript
  • packages/claude-plugin/hooks/register.tsx:107 — the two ui.render handlers lack the .catch(($, e, next) => next(e)) backstop that tool.call has (line 104), and renderChart runs toTileResult plus the height computation outside its try block, so a throw in read/chartTree/$.ui.resolve/toTileResult rejects the render hook rather than degrading to the engine row — undercutting the stated invariant that charting must never break the transcript.
    • Fix: Chain .catch(($, e, next) => next(e)) onto both ui.render registrations and move the toTileResult call and height computation inside renderChart's try block.
    • reliability, correctness
🔵 P3 nitpicks (9)
  • packages/claude-plugin/src/ansiLines.ts:48 — style is declared outside the per-line map, so an unterminated SGR run on one line bleeds styling into following rows; termchart resets per line today, so impact is limited to malformed/injected SGR.
    • Fix: Reset style = {} at the start of each line, or document that cross-line inheritance is intended.
  • packages/claude-plugin/src/ansiLines.ts:35 — the bidi filter covers overrides/embeddings/isolates but misses the bidi marks U+200E/U+200F/U+061C and invisible formatters U+2060/U+FEFF, which can still reorder or hide untrusted label text.
    • Fix: Extend the filtered code-point set to include these marks.
  • packages/claude-plugin/src/toTileResult.ts:9 — the clickstack_table shape map and the SINGLE_SERIES_SHAPES auto-upgrade rule are hand-copied from the server tool with no shared source or coupling test, so a server-side shape or threshold change silently desyncs the reconstructed chart.
    • Fix: Export a canonical shape map / upgrade helper from common-utils consumed by both the server tool and the plugin, or have the server echo the effective displayType.
  • packages/claude-plugin/src/toTileResult.ts:96 — the default shape comes from Object.keys(shapes)[0], which depends on the key insertion order of a map defined in common-utils; reordering that map silently changes the plugin's default chart type.
    • Fix: Define an explicit per-tool default constant matching the server's schema .default(...).
  • packages/claude-plugin/hooks/register.tsx:104 — the registration-level .catch(($, e, next) => next(e)) re-invokes next(e) if the awaited next(e) on line 84 itself rejects, which could re-run the underlying MCP tool (harmless today since the charted tools are read-only).
    • Fix: Wrap only the plugin's own logic in try/catch rather than re-calling next on any handler rejection.
  • packages/claude-plugin/src/renderChart.ts:40 — the MCP body is JSON-parsed three times per render (parseResponse, plus isTrimmed at toTileResult.ts:106 and renderChart.ts:40), and the trimmed-state logic lives at two call sites that can diverge.
    • Fix: Parse the body once and thread the parsed result plus an isTrimmed flag through toTileResult.
  • packages/claude-plugin/src/toTileResult.ts:65 — parseResponse validates only that meta and data are arrays, then asserts Record<string, string | number> cell types it never checked, handing downstream formatters a stronger type than verified.
    • Fix: Assert Record<string, unknown> and let the renderers coerce, or sample-check cell types before asserting.
  • packages/claude-plugin/hooks/register.tsx:89 — input: e stores the whole event and relies on tool args living at the event top level, an untyped contract read differently here than in the ui.render handlers (.input); if the event schema ever nests args, every field reads undefined and charts silently fall back to defaults.
    • Fix: Type the tool.call event (or a documented ToolInput view) at the hook boundary and align the two code paths on one input shape.
  • packages/claude-plugin/src/__tests__/renderChart.spec.ts:32 — the whole-result assertion whole?.at(-1)?.[0]?.text ?? '' passes vacuously if renderChart regresses to undefined.
    • Fix: Assert expect(whole).toBeDefined() and a minimum line count before checking the last line lacks the trimmed note.

Reviewers (9): correctness, testing, maintainability, project-standards, security, reliability, adversarial, kieran-typescript, previous-comments.

Testing gaps:

  • No test feeds a label/ServiceName/groupBy value containing an embedded ESC/SGR sequence or newline through renderTileContent → ansiLines (reinforces the P2 sanitization finding).
  • No test for an untrimmed table with more rows than MAX_TABLE_ROWS (reinforces the P2 truncation finding).
  • No test for the ran.isError/ran.deny early-return, the slice(-KEEP) eviction boundary (>20 calls), or a render-path throw degrading to the engine row.
  • Pie and number display types are exercised by no test (engine tests cover only line and bar).
  • hooks/register.tsx (130 lines of hook logic) is excluded from the package's eslint and tsc, so its types are checked only by claude plugin validate/test, not the repo's standard lint/type CI.

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

jordan-simonovski added a commit that referenced this pull request Oct 19, 2026
Address review feedback on #3400:

- Follow clickstack_table turning multi-select number, pie and bar into a
  table, so no metric is dropped.
- Stop inferring granularity from bucket gaps; empty buckets are filled
  only for an explicit granularity on an untrimmed result, so sparse or
  misaligned series no longer gain invented zeros.
- Mark results the server trimmed with a note under the chart.
- Take calledAt before the tool runs, as the server fixes its window then.
- Ship hand-written hooks/chart.d.ts types (the package has no src/), held
  in step with the source by a compile-time test.
- Reuse common-utils' time-series helpers and key mapped list elements.
@vercel
vercel Bot temporarily deployed to Preview – hyperdx-storybook October 19, 2026 07:59 Inactive
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 646 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 18
  • Production lines changed: 646 (+ 436 in test files, excluded from tier calculation)
  • Branch: jordansimonovski/claude-charts-plugin
  • Author: jordan-simonovski

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@jordan-simonovski

Copy link
Copy Markdown
Contributor Author

Replies to the review findings outside the changed lines, addressed in afac3ed unless noted:

Server-trimmed results are charted as complete, and missing tail buckets are zero-filled

Trimmed results (those with the server's note) are no longer zero-filled, and the chart ends with a dim line saying the server trimmed it.

Ignores the server's shape auto-upgrade

Fixed; see the inline thread.

Inferring granularity from the smallest gap adds made-up zero buckets to sparse series

Inference is removed. Empty buckets are filled only for an explicit granularity on an untrimmed result.

calledAt is taken after the tool returns

It's now taken before next(e).

Shape->DisplayType map duplicates existing maps

The time-series map now reuses TIME_SERIES_DISPLAY_TYPE_BY_NAME. ADHOC_DISPLAY_TYPES stays separate: it's private to adhocChart.ts and mixes time-series with table shapes, so the plugin keeps its own four-entry table map instead of widening the CLI's exports.

Hand-written time-series display check duplicates isTimeSeriesDisplayType

Now uses isTimeSeriesDisplayType.

List elements rendered without keys

Both lists are keyed.

Tests don't cover the cases where the chart diverges from what the server returned

Added tests for the multi-select upgrade and its single-select boundary, fill rules for inferred, trimmed and explicit granularity, the trimmed note line, and the ToolUse handler (engine test).

}
return ran;
}).catch(($, e, next) => next(e));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The tool.call .catch handler calls next(e) again, so a call that rejects can run twice

If await next(e) rejects (for example a transport error from the MCP server), the hook throws and .catch(($, e, next) => next(e)) runs the same ClickStack query a second time. The plugin's own bookkeeping already has a try/catch, so drop the outer .catch. If the API requires one, make it rethrow or return the original failure instead of calling next again.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond


const DEFAULT_WINDOW_MS = 15 * 60 * 1000;
// clickstack_table turns these into a table when given more than one select item.
const SINGLE_SERIES_SHAPES = ['number', 'pie', 'bar'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The server's shape auto-upgrade rule is copied by hand, giving it a second source of truth

SINGLE_SERIES_SHAPES and the select.length > 1 rewrite repeat the upgrade logic in packages/api/src/mcp/tools/query/table.ts:266-274. If the server rule changes, the plugin draws a chart type the server never returned. Have the tool response report the display type it actually used (for example a displayType field next to result) and read that. Otherwise, export the shape list and upgrade rule from one shared module and use it in both places.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

select,
groupBy: args.groupBy,
} as unknown as ChartConfigWithDateRange;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — as unknown as ChartConfigWithDateRange turns off type checking on the config handed to the renderer

This breaks the base-branch rule "Avoid as casts, use satisfies or type inference". It also hides that select holds raw MCP items, not internal select items, while renderTileContent passes them to isRatioChartConfig and resolveChartNumberFormats (packages/cli/src/shared/tileRender.ts:84,131). Build the object against a narrow Pick<…> of the fields the renderer reads, using satisfies, so a shape mismatch fails to compile.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

// Empty buckets are filled only when their alignment is known: an explicit
// granularity and every row present. Otherwise filling would invent zeros.
fillNulls: args.granularity && !isTrimmed(output) ? undefined : false,
select,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The args.granularity half of the fillNulls condition never changes the output, and granularity is not inferred as the PR says

formatResponseForTimeChart already skips empty-bucket filling when granularity == null (packages/cli/src/shared/chartData.ts:359), so forcing fillNulls: false for a missing granularity does nothing. Reduce the condition to isTrimmed(output) ? false : undefined. Also, the PR description says granularity is "inferred from bucket spacing" when omitted, but no code does that. Either implement it or correct the description.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

];
// Built from a string so the ESC character isn't a literal control char in a regex.
const SGR = new RegExp(`${String.fromCharCode(27)}\\[([0-9;]*)m`, 'g');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The SGR regex and color-code table duplicate termchart's ANSI helpers

packages/cli/src/termchart/ansi.ts already exports ANSI_PATTERN (the same \x1b\[[0-9;]*m match, used by stripAnsi) and PLOT_COLORS (color name → SGR code). The base-branch DRY rule is marked REQUIRED. Export the pattern from termchart and derive the code → name map from PLOT_COLORS/chalk names, so the parser and the emitter can't drift apart.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

<Box flexDirection="column" key={`chart-${row.id}`}>
<Text>
<Text bold>{stripControlChars(row.tool.split('__').pop() ?? '')}</Text>
{typeof reason === 'string' && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The header re-derives the tool suffix that chartedTool already computes

row.tool.split('__').pop() repeats the suffix logic in chartedTool (src/toTileResult.ts). Use chartedTool(row.tool) so the label and the matching rule always agree.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

isInterrupted: false,
},
],
isActive: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — No test covers a group with charted and uncharted calls, an expanded group, or eviction after 20 stored calls

The engine tests only mount single-call groups. Add cases for: a ToolGroup with one clickstack call and one other call (the engine row plus the chart should both render); isExpanded: true (should fall through to next); and the 21st call evicting the oldest stored chart. These are the branches in drawRows and the KEEP slice, and they also run outside CI (claude plugin test).

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

When Claude Code calls clickstack_timeseries or clickstack_table, the tool
row now shows the result as a line, stacked bar, bar, pie, number or table
chart instead of raw JSON, drawn by the hdx CLI's renderers. Claude still
reads the full result.

One /plugin install hyperdx-charts --marketplace hyperdxio/hyperdx asks for
the HyperDX URL and a personal API access key (kept in secure storage) and
connects the ClickStack MCP server, so there is no separate claude mcp add
step. Chart text is stripped of control characters and bidi overrides, since
labels carry user data that must not drive the terminal.
Address review feedback on #3400:

- Follow clickstack_table turning multi-select number, pie and bar into a
  table, so no metric is dropped.
- Stop inferring granularity from bucket gaps; empty buckets are filled
  only for an explicit granularity on an untrimmed result, so sparse or
  misaligned series no longer gain invented zeros.
- Mark results the server trimmed with a note under the chart.
- Take calledAt before the tool runs, as the server fixes its window then.
- Ship hand-written hooks/chart.d.ts types (the package has no src/), held
  in step with the source by a compile-time test.
- Reuse common-utils' time-series helpers and key mapped list elements.
@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/claude-charts-plugin branch from afac3ed to 15cb6ae Compare October 20, 2026 10:19
Comment thread packages/claude-plugin/package.json Outdated
},
"devDependencies": {
"@hyperdx/cli": "workspace:*",
"@hyperdx/common-utils": "^0.30.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 major — @hyperdx/common-utils: ^0.30.0 doesn't match the version in the repo (0.31.0), and yarn.lock has no entry for it

Change the range to ^0.31.0 (the same range as packages/cli, api and app) and run yarn install again. On a 0.x version a caret range is pinned to its minor version, so ^0.30.0 will not accept 0.31.0 and yarn cannot link it to the workspace copy. yarn.lock only has @hyperdx/common-utils@npm:^0.31.0, @hyperdx/common-utils@workspace:packages/common-utils (around line 4988) and nothing for npm:^0.30.0, so yarn install --immutable fails. If install is allowed to update the lockfile, it pulls the published 0.30.x from npm instead, and jest/tsc then run against that stale copy while tsup bundles the workspace source.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

Comment on lines +49 to +53
return ansi.split('\n').map(line => {
const segments: Segment[] = [];
let last = 0;
const push = (raw: string) => {
const text = stripControlChars(raw);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Labels break chart rows

ansiLines cleans user text too late. A table cell or service name containing \n has already become an extra chart row when stripControlChars runs. Embedded color codes are also read as chart styles instead of removed. This breaks table rows and legends or changes their colors.

Clean user strings before passing them to renderTileContent, while preserving the renderer’s own line breaks and colors.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

main moved @hyperdx/common-utils to 0.31.0, so yarn install could not resolve ^0.30.0.
@vercel
vercel Bot temporarily deployed to Preview – hyperdx-storybook October 20, 2026 10:32 Inactive

This branch was previously deployed

1 inactive deployment
Preview – hyperdx-storybook — e3db0612 Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant