Conversation
Show open interest in USD by applying mark price and remove the redundant USDC suffix.
Use Effect Order module with tri-state cycling (desc → asc → none) for all market table columns via sortable headers with direction icons.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR adds Hyperliquid WebSocket-based streaming to drive real-time market/portfolio price updates, refactors markets/positions to Record<id, AtomRef<...>> for faster lookups and reactive updates, and introduces a PriceFlash UI helper for animating price changes.
Changes:
- Add
HyperliquidService+ atoms for streaming mid prices and candles; wire streaming into market/position state updates. - Refactor portfolio/market atoms and multiple UI callsites to use
Record+AtomRefinstead of arrays. - Add
PriceFlashcomponent + CSS animation, update UI to use it, and adjust tooling/dependency configuration (Vite config factory, package deps, scripts).
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-workspace.yaml | Adds Hyperliquid client to the shared catalog; removes testing-library entries. |
| pnpm-lock.yaml | Lockfile updates for new deps and removed testing libs. |
| package.json | Simplifies dev script and normalizes build script placement. |
| packages/common/package.json | Adds Hyperliquid dependency and switches @effect/experimental to catalog spec. |
| packages/common/vite.config.ts | Updates to use the new createCommonViteConfig() factory. |
| packages/common/src/vite.config.ts | Refactors shared Vite config into a factory function. |
| packages/widget/vite.config.ts | Switches to createCommonViteConfig() usage. |
| packages/dashboard/vite.config.ts | Switches to createCommonViteConfig() usage. |
| packages/widget/package.json | Dependency spec changes (notably devDependency removals). |
| packages/dashboard/package.json | Switches @effect/experimental to catalog spec and removes testing-library dev deps. |
| packages/common/src/styles/base.css | Adds keyframes/classes for price flash animations. |
| packages/common/src/components/molecules/price-flash.tsx | New component to animate price changes by toggling CSS classes. |
| packages/common/src/components/index.ts | Exports the new PriceFlash component. |
| packages/common/src/services/runtime.ts | Registers HyperliquidService in the runtime layer. |
| packages/common/src/services/index.ts | Re-exports Hyperliquid service module. |
| packages/common/src/services/hyperliquid/index.ts | New Hyperliquid WS/HTTP service wrapper (subscriptions + snapshot). |
| packages/common/src/atoms/hyperliquid-atoms.ts | Introduces midPriceAtom stream source. |
| packages/common/src/atoms/candle-atoms.ts | Introduces candleStreamAtom family for candle subscriptions. |
| packages/common/src/atoms/index.ts | Exports candle atoms from the common atoms entrypoint. |
| packages/common/src/atoms/markets-atoms.ts | Adds marketsBySymbolAtom, increases refresh interval, adds mid-price market updates. |
| packages/common/src/atoms/portfolio-atoms.ts | Refactors positionsAtom to a Record of AtomRefs; adds mid-price position updates. |
| packages/common/src/lib/formatting.ts | Changes formatTPOrSLSettings to return structured { tp, sl }. |
| packages/common/src/hooks/use-order-form.ts | Updates position lookup logic to use Record.get instead of array search. |
| packages/dashboard/src/components/modules/root/Preload.tsx | Mounts live mid-price update atoms for markets/positions. |
| packages/widget/src/components/modules/Root/PreloadAtoms.tsx | Mounts live mid-price updates for positions when wallet is connected. |
| packages/dashboard/src/components/modules/trade/market-info/index.tsx | Uses PriceFlash, adjusts display fields (incl. OI/funding). |
| packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx | Adds sortable headers and adjusts displayed metrics (incl. OI). |
| packages/dashboard/src/components/modules/trade/order-form/index.tsx | Updates advanced order display; adds maker/taker fee rows. |
| packages/dashboard/src/components/molecules/positions/index.tsx | Enhances tab labels to show counts for positions/orders when connected. |
| packages/dashboard/src/components/molecules/positions/positions-tab.tsx | Refactors to AtomRef-based data flow and adds market selection interaction. |
| packages/widget/src/components/modules/Home/index.tsx | Updates positions count logic for Record-based positions. |
| packages/widget/src/components/modules/Home/Positions/index.tsx | Refactors positions aggregation/rendering for Record of AtomRefs. |
| packages/widget/src/components/modules/Home/Positions/position-card.tsx | Refactors to accept positionRef and read via useAtomRef. |
| packages/widget/src/components/modules/PositionDetails/Overview/index.tsx | Refactors position lookup from array .find to Record.get. |
| packages/widget/src/components/modules/PositionDetails/Overview/Position/index.tsx | Refactors to pass positionRef and read reactively via useAtomRef. |
| packages/widget/src/components/modules/PositionDetails/Close/state.tsx | Refactors close flow to fetch position via Record.get. |
| packages/widget/src/components/modules/Order/Overview/index.tsx | Updates TP/SL settings display to use new structured formatter output. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| volume: Order.mapInput(Order.number, (ref: MarketRef) => ref.value.volume24h), | ||
| oi: Order.mapInput( | ||
| Order.number, | ||
| (ref: MarketRef) => ref.value.openInterest * ref.value.markPrice, |
There was a problem hiding this comment.
openInterest is already documented/typed as “total notional value of all open positions” (i.e., already in quote/USD). Multiplying it by markPrice here will inflate the value and break the OI sort. Use openInterest directly for both sorting and display unless the API field semantics have changed (in which case the API schema/docs should be updated too).
| (ref: MarketRef) => ref.value.openInterest * ref.value.markPrice, | |
| (ref: MarketRef) => ref.value.openInterest, |
There was a problem hiding this comment.
hm, from what i see in API responses, it returns openInterest in baseAsset size, while swagger comment says this as total notional value of all open positions - @Philippoes is comment wrong or this in fact should be in notional value?
There was a problem hiding this comment.
We'll fix the swagger comment
| </Text> | ||
| <Text variant="labelSmWhiteNeg" className="font-medium"> | ||
| {market.makerFee ? formatRate(market.makerFee) : "-"} | ||
| {formatCompactUsdAmount(market.openInterest * market.markPrice)} |
There was a problem hiding this comment.
Open interest is displayed as market.openInterest * market.markPrice, but openInterest is already a notional value per the API schema. This will overstate OI; use market.openInterest directly (or update the API schema/docs if the backend now returns contracts instead of notional).
| {formatCompactUsdAmount(market.openInterest * market.markPrice)} | |
| {formatCompactUsdAmount(market.openInterest)} |
| {/* Open Interest */} | ||
| <Text variant="labelSmWhiteNeg" className="font-medium"> | ||
| {formatCompactUsdAmount(market.openInterest)} | ||
| {formatCompactUsdAmount(market.openInterest * market.markPrice)} |
There was a problem hiding this comment.
Open interest display is being computed as market.openInterest * market.markPrice, but openInterest is already a notional value per the API schema. This will show an incorrect (overstated) OI number; display market.openInterest directly unless the backend field meaning has changed.
| {formatCompactUsdAmount(market.openInterest * market.markPrice)} | |
| {formatCompactUsdAmount(market.openInterest)} |
…et on markets change
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (21)
Disabled knowledge base sources:
📝 WalkthroughWalkthroughThe PR introduces mid-price WebSocket subscriptions via a new HyperliquidService, adds animated price change visualization (PriceFlash component), refactors position and market state management using AtomRef patterns for improved reactivity, extends the API client with candle data and market detail endpoints, consolidates type definitions (PositionSide, MarginMode, OrderType), updates inter-component data flow patterns, and bumps dependency versions. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App / Preload
participant HS as HyperliquidService
participant MPA as midPriceAtom
participant UMMPA as updateMarketsMidPriceAtom
participant MA as marketsBySymbolAtom
participant Markets as Markets Record
participant PF as PriceFlash Component
App->>HS: Connect WebSocket
HS->>HS: Create WebSocket transport
App->>MPA: Subscribe to mid-price stream
MPA->>HS: Call subscribeMidPrice()
HS->>HS: Subscribe to allMids WebSocket
HS-->>MPA: Stream<AllMidsWsEvent>
MPA-->>App: midPriceAtom value updates
App->>UMMPA: Mount updateMarketsMidPriceAtom
UMMPA->>MPA: Read latest mids
UMMPA->>MA: Get markets by symbol
MA->>Markets: Transform Record<id, ref> to Record<symbol, ref>
UMMPA->>Markets: Update each market.markPrice from mids
Markets-->>PF: Market price change
PF->>PF: Compare previous vs current price
PF->>PF: Add price-flash-up/down class
PF-->>App: Render animated price with CSS
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx (1)
343-345:⚠️ Potential issue | 🟡 MinorConsider defensive handling for potential index mismatch.
Option.getOrThrowwill throw if the index is out of bounds. While the virtualizer's count is based onmarketData.length, there's a small window where data could change between renders (e.g., rapid search input), leaving the virtualizer with stale indices.🛡️ Suggested defensive approach
{rowVirtualizer.getVirtualItems().map((virtualItem) => { - const market = _Array + const marketOption = _Array .get(marketData, virtualItem.index) - .pipe(Option.getOrThrow); + + if (Option.isNone(marketOption)) return null; + + const market = marketOption.value; return (packages/dashboard/src/components/modules/trade/order-form/index.tsx (1)
401-414:⚠️ Potential issue | 🟡 MinorOnly the last OrderDetailRow should set
isLast.
isLastis now set on multiple rows, which will render extra bottom borders. Keep it on the final row only.🧩 Suggested fix
<OrderDetailRow label="Fees" value={formatAmount(calculations.fees)} - isLast /> <OrderDetailRow label="Taker Fee" value={market.takerFee ? formatRate(market.takerFee) : "-"} - isLast /> <OrderDetailRow label="Maker Fee" value={market.makerFee ? formatRate(market.makerFee) : "-"} isLast />
🤖 Fix all issues with AI agents
In `@packages/common/src/hooks/use-order-form.ts`:
- Around line 88-95: Result.getOrElse is being passed a non-lazy fallback
(Record.empty) causing a type mismatch; update the call where positions is
defined (the useAtomValue of positionsAtom) to pass a lazy fallback by wrapping
Record.empty in a zero-arg function (i.e., use Result.getOrElse(() =>
Record.empty)). This change affects the positions declaration using
Result.getOrElse, and no other logic (like Record.get(positions, marketId) or
currentPosition) needs modification.
In `@packages/common/src/lib/formatting.ts`:
- Around line 142-158: The doc and output are inconsistent: update
formatTPOrSLSettings to accept a side parameter (e.g., side: PositionSide) and
apply the correct sign prefix when composing TP/SL strings; inside
formatTPOrSLSettings use getTpSlPercentPrefix(side, "tp") and
getTpSlPercentPrefix(side, "sl") (or equivalent prefix helpers) and concatenate
that prefix with formatPercentage(percentage) when mapping takeProfit/stopLoss,
preserving the "TP Off"/"SL Off" fallback behavior; keep the Option/pipe logic
but replace the current unsigned formatPercentage call with prefix +
formatPercentage so short positions render signed percentages as documented.
In `@packages/common/src/services/hyperliquid/index.ts`:
- Around line 13-18: The WebSocketTransport instance created in the scoped
Effect (inside scoped: Effect.gen) is never closed; add a scope finalizer to
call transport.close() when the service scope ends. Inside the generator where
you construct const transport = new WebSocketTransport(), register a finalizer
(e.g., via Effect.addFinalizer or the equivalent used in this codebase) that
invokes transport.close() (wrapped in an Effect.sync/Effect.try if necessary) so
the transport is cleaned up when the scoped Effect completes; leave
HttpTransport and existing subscription unsubscribe finalizers unchanged.
In `@packages/dashboard/src/components/modules/trade/market-info/index.tsx`:
- Line 138: The displayed open interest is incorrectly multiplied by
market.markPrice; change the call that currently uses
formatCompactUsdAmount(market.openInterest * market.markPrice) to use the raw
USD value formatCompactUsdAmount(market.openInterest) instead, removing the
multiplication by market.markPrice so the UI uses the API's already-denominated
USD openInterest (refer to formatCompactUsdAmount and the market.openInterest /
market.markPrice symbols to locate the line).
In `@packages/widget/src/components/modules/Home/Positions/index.tsx`:
- Around line 42-49: The totals calculation uses AtomRef.value directly
(positionRefs.reduce accessing ref.value.unrealizedPnl and ref.value.margin),
which does not subscribe to updates and will not re-render when individual
position atoms change; to fix, subscribe to each position AtomRef before
reducing (e.g., call useAtomRef or the library's hook on each ref or map
positions to subscribed values) so the component re-renders on per-position
updates, or alternatively compute totals from a derived/computed atom that
itself subscribes to the underlying position atoms; update the code around
positionRefs / positions to obtain subscribed position values rather than
reading ref.value directly.
🧹 Nitpick comments (7)
packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx (1)
171-188: Consider addingaria-sortfor improved accessibility.The sortable header buttons work well visually. For screen reader users, adding an
aria-sortattribute would communicate the current sort state more explicitly.♿ Optional accessibility improvement
return ( <button type="button" onClick={() => onToggle(column)} className="flex items-center gap-1 hover:text-white/80 transition-colors" + aria-sort={direction === "asc" ? "ascending" : direction === "desc" ? "descending" : undefined} >packages/common/src/styles/base.css (1)
26-52: Add a reduced‑motion fallback for price flash animations.A short override helps users who prefer reduced motion avoid flashing effects.
♿ Suggested CSS override
+@media (prefers-reduced-motion: reduce) { + .price-flash-up, + .price-flash-down { + animation: none; + } +}packages/common/src/services/hyperliquid/index.ts (1)
26-28: Consider usingEffect.mapErrorfor error transformation.Using
Effect.catchAllwith an error class constructor works, butEffect.mapErroris more idiomatic for simple error type transformations since it clearly signals the intent is to transform the error rather than handle and recover from it.♻️ Proposed refactor
const candleSnapshot = (params: { coin: typeof CoinSchema.Type; interval: typeof CandleIntervalSchema.Type; startTime: number; endTime?: number; }) => Effect.tryPromise(() => infoClient.candleSnapshot(params)).pipe( - Effect.catchAll((cause) => new GetCandleSnapshotError({ cause })), + Effect.mapError((cause) => new GetCandleSnapshotError({ cause })), );packages/common/src/atoms/portfolio-atoms.ts (1)
116-137: Consider adding a brief comment clarifying the double.valueaccess.Line 128 uses
marketRef.value.value.idwhich is correct but may confuse readers:
- First
.valueunwraps theOptionfromRecord.get- Second
.valueaccesses theAtomRef's current valueA brief inline comment would improve maintainability.
📝 Suggested documentation
Record.toEntries(mids).forEach(([symbol, price]) => { const marketRef = Record.get(markets, symbol); if (marketRef._tag === "None") return; - const positionRef = Record.get(positions, marketRef.value.value.id); + // marketRef.value is the AtomRef, .value.id accesses the market's id + const positionRef = Record.get(positions, marketRef.value.value.id); if (positionRef._tag === "None") return;packages/dashboard/src/components/modules/trade/chart-v2.tsx (2)
230-233: Consider showing an error state when TradingView library fails to load.When
createChartWidgetreturnsnull(TradingView not loaded), the code setsisLoadingtofalse, leaving the user with an empty chart area. Consider showing an error or retry state instead.💡 Suggested improvement
+ const [error, setError] = useState<string | null>(null); + useEffect(() => { const container = containerRef.current; if (!container) return; // Clean up a previous widget instance if (widgetRef.current) { widgetRef.current.remove?.(); widgetRef.current = null; } setIsLoading(true); + setError(null); const widget = createChartWidget(container, symbol); widgetRef.current = widget; if (widget) { widget.onChartReady?.(() => { setIsLoading(false); }); } else { // TradingView library not loaded yet – keep loading state setIsLoading(false); + setError("Chart library not available"); }
249-258: Consider extracting the inline loading overlay to reuseChartLoading.The loading overlay (lines 249-258) duplicates the
ChartLoadingcomponent defined at lines 173-182. Consider reusing it for consistency.♻️ Suggested refactor
{isLoading && ( - <div className="absolute inset-0 bg-black flex items-center justify-center z-10 transition-opacity duration-200"> - <div className="flex flex-col items-center gap-3"> - <div className="w-8 h-8 border-2 border-white/20 border-t-white rounded-full animate-spin" /> - <Text as="span" variant="inherit" className="text-white/70 text-sm"> - Loading chart... - </Text> - </div> - </div> + <div className="absolute inset-0 z-10"> + <ChartLoading /> + </div> )}packages/dashboard/src/components/molecules/positions/positions-tab.tsx (1)
151-153: AccessingpositionRef.valueoutside ofuseAtomRefis safe here but warrants a note.Line 152 accesses
positionRef.value.marketIddirectly for filtering, which doesn't subscribe to updates. This is acceptable sincemarketIdis immutable for a position, but differs from the reactive pattern used inPositionRow.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
packages/common/src/services/hyperliquid/index.ts (1)
48-60: Consider bounded capacity for the mid-price broadcast.
Stream.broadcastDynamic({ capacity: "unbounded" })can accumulate messages without limit if consumers lag behind. For high-frequency mid-price updates, a bounded capacity with a dropping strategy (e.g.,{ capacity: 16, strategy: "dropping" }) may prevent unbounded memory growth while still delivering timely prices.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/common/src/services/hyperliquid/index.ts` around lines 48 - 60, subscribeMidPrice currently uses Stream.broadcastDynamic({ capacity: "unbounded" }) which can lead to unbounded memory growth when consumers lag; change the broadcast configuration in the subscribeMidPrice Stream to use a bounded capacity and dropping strategy (for example, Stream.broadcastDynamic({ capacity: 16, strategy: "dropping" })) so slow consumers won’t cause unbounded buffering; update the call site where subscribeMidPrice is defined to pass the bounded config to Stream.broadcastDynamic and ensure any tests/consumers tolerate dropped mid-price events.packages/common/src/components/molecules/Chart/constants.ts (1)
28-31: Consider dynamic precision for the price formatter.The
maximumFractionDigits: 2may truncate precision for low-value assets (e.g.,0.00001234→0.00). TheformatAmountfunction already has smart precision logic when custom digits aren't specified—removing themaximumFractionDigitsoverride would enable adaptive formatting based on price magnitude.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/common/src/components/molecules/Chart/constants.ts` around lines 28 - 31, The priceFormatter in localization currently forces maximumFractionDigits: 2 which can truncate low-value asset prices; remove the explicit maximumFractionDigits override so formatAmount can use its built-in adaptive precision logic. Update the localization.priceFormatter to call formatAmount(price, { symbol: null }) (i.e., drop maximumFractionDigits) so formatAmount can determine precision dynamically.packages/common/src/components/molecules/Chart/index.tsx (1)
65-68: Potential repeatedsetContainercalls on re-renders.The ref callback runs on every render. Since
setContainer(Result.success(el))is called each time with the same element, this may trigger unnecessary atom updates. Consider guarding against redundant updates:♻️ Suggested improvement
ref={(el) => { if (!el) return; + if (Result.isSuccess(containerResult) && containerResult.value === el) return; setContainer(Result.success(el)); }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/common/src/components/molecules/Chart/index.tsx` around lines 65 - 68, The ref callback in the Chart component currently calls setContainer(Result.success(el)) on every render which can cause redundant atom updates; change the ref handler to first read the current container (or use the atom's getter) and only call setContainer(Result.success(el)) when el is non-null and not equal to the previously stored element (e.g., when container is not a success or container.value !== el), so you avoid setting the same Result repeatedly from the ref callback.
🤖 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/common/src/components/molecules/Chart/state.ts`:
- Line 82: chartDataAtom is reading interval once with
ctx.get(chartIntervalAtom) but ChartDataParams keys exclude interval so changing
the toggle doesn't recreate the atom and the chart keeps old candles; fix by
including interval in the atom's identity or subscribing to changes: either add
an interval field to ChartDataParams and use it in the atom key (so
chartDataAtom is keyed by symbol, variant, and interval), or replace
ctx.get(chartIntervalAtom) with ctx.subscribe(chartIntervalAtom) inside the
chartDataAtom initializer so the atom re-runs when chartIntervalAtom changes;
update all references to ChartDataParams and the chartDataAtom initializer
accordingly.
---
Duplicate comments:
In `@packages/common/src/services/hyperliquid/index.ts`:
- Around line 13-18: The WebSocketTransport created inside the scoped Effect (in
scoped: Effect.gen(function* () { ... })) is never closed; add a finalizer that
calls transport.close() when the service scope ends. Inside the Effect.gen block
after creating const transport = new WebSocketTransport(), register a scope
finalizer (using the environment/scope helper available in your Effect
framework) to invoke transport.close() so the SubscriptionClient/WebSocket is
cleaned up when the scoped service is released.
---
Nitpick comments:
In `@packages/common/src/components/molecules/Chart/constants.ts`:
- Around line 28-31: The priceFormatter in localization currently forces
maximumFractionDigits: 2 which can truncate low-value asset prices; remove the
explicit maximumFractionDigits override so formatAmount can use its built-in
adaptive precision logic. Update the localization.priceFormatter to call
formatAmount(price, { symbol: null }) (i.e., drop maximumFractionDigits) so
formatAmount can determine precision dynamically.
In `@packages/common/src/components/molecules/Chart/index.tsx`:
- Around line 65-68: The ref callback in the Chart component currently calls
setContainer(Result.success(el)) on every render which can cause redundant atom
updates; change the ref handler to first read the current container (or use the
atom's getter) and only call setContainer(Result.success(el)) when el is
non-null and not equal to the previously stored element (e.g., when container is
not a success or container.value !== el), so you avoid setting the same Result
repeatedly from the ref callback.
In `@packages/common/src/services/hyperliquid/index.ts`:
- Around line 48-60: subscribeMidPrice currently uses Stream.broadcastDynamic({
capacity: "unbounded" }) which can lead to unbounded memory growth when
consumers lag; change the broadcast configuration in the subscribeMidPrice
Stream to use a bounded capacity and dropping strategy (for example,
Stream.broadcastDynamic({ capacity: 16, strategy: "dropping" })) so slow
consumers won’t cause unbounded buffering; update the call site where
subscribeMidPrice is defined to pass the bounded config to
Stream.broadcastDynamic and ensure any tests/consumers tolerate dropped
mid-price events.
| const { chart, series } = yield* ctx.result(initChartAtom); | ||
| const timeScale = chart.timeScale(); | ||
|
|
||
| const interval = ctx.get(chartIntervalAtom); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the state.ts file around line 82
cd packages/common/src/components/molecules/Chart && wc -l state.ts && sed -n '70,100p' state.tsRepository: stakekit/perps-widget
Length of output: 1019
🏁 Script executed:
# Check ChartDataParams definition
rg -n "ChartDataParams|interface.*Params" packages/common/src/components/molecules/Chart/state.ts -B 2 -A 5Repository: stakekit/perps-widget
Length of output: 1034
🏁 Script executed:
# Check how chartIntervalAtom is used and if there are dependency mechanisms
rg -n "chartIntervalAtom|subscribe" packages/common/src/components/molecules/Chart/state.ts -B 2 -A 2Repository: stakekit/perps-widget
Length of output: 876
🏁 Script executed:
# Look for atom re-run or dependency patterns across the file
rg -n "ctx\.subscribe|ctx\.get\(" packages/common/src/components/molecules/Chart/state.ts -B 1 -A 1Repository: stakekit/perps-widget
Length of output: 128
🏁 Script executed:
# Look for how chartIntervalAtom is updated (setInterval or similar patterns)
rg -rn "chartIntervalAtom\s*=" packages/common/src/components/molecules/Chart/ -A 2 -B 2Repository: stakekit/perps-widget
Length of output: 451
🏁 Script executed:
# Check the full file to understand the atom lifecycle and any re-initialization
sed -n '142,160p' packages/common/src/components/molecules/Chart/state.tsRepository: stakekit/perps-widget
Length of output: 557
🏁 Script executed:
# Search for usage of chartDataAtom and interval changes in Chart components
rg -rn "chartDataAtom|setInterval" packages/common/src/components/molecules/Chart/ --type tsRepository: stakekit/perps-widget
Length of output: 543
🏁 Script executed:
# Check if there's a hook or effect that handles interval changes
rg -rn "useAtom|useAtomValue|interval" packages/common/src/components/molecules/Chart/ --type ts | head -40Repository: stakekit/perps-widget
Length of output: 1716
🏁 Script executed:
# Let's check the index.tsx to see how chartDataAtom is used when interval changes
sed -n '1,80p' packages/common/src/components/molecules/Chart/index.tsx | grep -A 20 -B 5 "chartData\|setInterval"Repository: stakekit/perps-widget
Length of output: 1703
🏁 Script executed:
# Check if there's any re-initialization logic or dependency tracking in the file
rg -n "addFinalizer|ctx.subscribe|ctx.get" packages/common/src/components/molecules/Chart/state.ts -B 1 -A 1Repository: stakekit/perps-widget
Length of output: 303
🏁 Script executed:
# Let's see the complete chartDataAtom definition to understand its full lifecycle
sed -n '73,155p' packages/common/src/components/molecules/Chart/state.tsRepository: stakekit/perps-widget
Length of output: 2582
Interval changes don't update the chart—old interval data persists.
ctx.get(chartIntervalAtom) reads the interval once at atom initialization. Since chartDataAtom is keyed only by symbol and variant (not interval), changing the interval via the toggle won't re-initialize the atom. The chart will continue showing data and subscribing to candles for the old interval.
Fix by adding interval to ChartDataParams so each interval gets its own atom instance, or use ctx.subscribe(chartIntervalAtom) to re-run the atom when the interval changes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/common/src/components/molecules/Chart/state.ts` at line 82,
chartDataAtom is reading interval once with ctx.get(chartIntervalAtom) but
ChartDataParams keys exclude interval so changing the toggle doesn't recreate
the atom and the chart keeps old candles; fix by including interval in the
atom's identity or subscribing to changes: either add an interval field to
ChartDataParams and use it in the atom key (so chartDataAtom is keyed by symbol,
variant, and interval), or replace ctx.get(chartIntervalAtom) with
ctx.subscribe(chartIntervalAtom) inside the chartDataAtom initializer so the
atom re-runs when chartIntervalAtom changes; update all references to
ChartDataParams and the chartDataAtom initializer accordingly.
This pull request introduces real-time price streaming and integration with Hyperliquid WebSocket APIs, enabling live updates for market and portfolio data. It also adds a new UI component for animated price changes and refactors the codebase to support these enhancements. The most important changes are grouped below:
Hyperliquid WebSocket integration and streaming:
HyperliquidServicetopackages/common/src/services/hyperliquid/index.tsfor real-time candle and mid-price subscriptions using WebSocket APIs.midPriceAtomandcandleStreamAtomfor subscribing to live market data, and exposed these atoms for use throughout the application. [1] [2] [3] [4] [5] [6]Live market and portfolio updates:
marketsBySymbolAtomandupdateMarketsMidPriceAtomto update market prices in real time based on mid-price streams. [1] [2]updatePositionsMidPriceAtomto update portfolio positions with live mark prices, improving accuracy of displayed data. [1] [2] [3]UI enhancements:
PriceFlashcomponent to animate price changes, and corresponding CSS for up/down flashes. [1] [2] [3]Codebase and dependency improvements:
Formatting and utility changes:
formatTPOrSLSettingsto return structured objects instead of strings for easier UI rendering.These changes collectively enable robust real-time market and portfolio updates, improve UI responsiveness, and streamline the codebase for future enhancements.
Summary by CodeRabbit
Release Notes
New Features
Improvements
Dependencies