Skip to content

refactor(frontend): shared UI primitives — PanelSection, split pane, context menu, composed tooltip - #5868

Closed
ardaerzin wants to merge 5 commits into
pkg/navigation-uifrom
pkg/ui-primitives
Closed

refactor(frontend): shared UI primitives — PanelSection, split pane, context menu, composed tooltip#5868
ardaerzin wants to merge 5 commits into
pkg/navigation-uifrom
pkg/ui-primitives

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Four primitives the extracted surfaces all need, hoisted into @agenta/ui: PanelSection, the
split pane, the context menu, and the composed tooltip that carries its own provider.

They were duplicated across the OSS components that later lanes move into packages; pulling them
up first is what lets those lanes be pure moves.
Not run in a browser — static gates only (pnpm lint-fix 24/24, tsc --noEmit clean
for @agenta/shared, ui, entities, entity-ui, settings-ui, oss, ee, mobile).

Stacked on pkg/navigation-ui; review only this lane's diff.

@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
agenta-documentation Error Error Aug 10, 2026 2:58pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 75857e33-cee6-459b-a294-9a5d29f7444d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added reusable chat UI elements, including message bubbles, typing indicators, avatars, action buttons, and attachment previews.
    • Added context menus with disabled, highlighted, and destructive actions.
    • Added resizable two-pane layouts with drag controls and configurable sizing.
    • Added simplified tooltips and expandable height transitions.
    • Expanded styling and component exports for broader UI customization.
  • Bug Fixes

    • Improved handling of missing timestamps.
    • Added fallback heading styles when theme values are unavailable.
    • Refined panel and rail layout behavior for more reliable sizing and overflow.

Walkthrough

The PR adds UI primitives, chat presentation components, package exports, layout fallbacks, and overflow updates. It also changes timeAgo to return an empty string for falsy timestamps.

Changes

UI component additions

Layer / File(s) Summary
UI primitives and package surface
web/packages/agenta-ui/package.json, web/packages/agenta-ui/src/components/ui/context-menu.tsx, web/packages/agenta-ui/src/components/ui/tooltip-composed.tsx, web/packages/agenta-ui/src/components/ui/index.ts, web/packages/agenta-ui/src/components/presentational/index.ts
The package exports additional CSS and components. It adds Radix context-menu components and the SimpleTooltip component.
Controlled split pane
web/packages/agenta-ui/src/components/ui/split-pane.tsx, web/packages/agenta-ui/src/components/ui/index.ts
SplitPane supports controlled sizing, pointer dragging, clamped dimensions, resize callbacks, pane ordering, and configurable divider behavior.
Chat presentation components
web/packages/agenta-ui/src/components/presentational/chat/index.tsx, web/packages/agenta-ui/src/components/presentational/index.ts
The package adds typing dots, chat bubbles, avatars, action buttons, and attachment cards with loading, preview, styling, and media-error handling.
Layout fallback and overflow behavior
web/packages/agenta-ui/src/components/PageLayout.tsx, web/packages/agenta-ui/src/components/presentational/layout/PanelSection.tsx
Heading styles now use local fallback values. Panel and rail containers update sizing and overflow classes.

Shared time utility

Layer / File(s) Summary
Timestamp validation
web/packages/agenta-shared/src/utils/timeAgo.ts
timeAgo now returns an empty string for any falsy timestamp before calculating relative time.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Pointer
  participant SplitPane
  participant ResizeCallbacks
  Pointer->>SplitPane: pointerdown on separator
  SplitPane->>ResizeCallbacks: onResizeStart()
  Pointer->>SplitPane: pointermove with captured pointer
  SplitPane->>SplitPane: clamp driven pane size
  SplitPane->>ResizeCallbacks: onResize(nextSize)
  Pointer->>SplitPane: pointerup
  SplitPane->>ResizeCallbacks: onResizeEnd()
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main shared UI primitive changes, including PanelSection, split pane, context menu, and composed tooltip.
Description check ✅ Passed The description explains the extraction of shared UI primitives and relates directly to the pull request changes and validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pkg/ui-primitives

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

🧹 Nitpick comments (3)
web/packages/agenta-ui/src/components/presentational/chat/index.tsx (1)

1-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shorten non-contract comments.

These multi-line comments describe ordinary component purpose and visual details. Replace them with one short comment, or remove them when the component name and types already describe the behavior.

As per coding guidelines, “Keep in-code comments to at most one short line; use longer comments only for genuinely surprising constraints such as bugs, races, or ordering requirements.”

Also applies to: 41-45, 150-154

Source: Coding guidelines

web/packages/agenta-ui/src/components/ui/context-menu.tsx (1)

7-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce multi-line implementation comments.

Keep one short in-code comment only when it documents a surprising constraint. Move migration guidance and component rationale to external documentation or a story.

  • web/packages/agenta-ui/src/components/ui/context-menu.tsx#L7-L17: reduce the migration and styling rationale to a short constraint comment.
  • web/packages/agenta-ui/src/components/ui/tooltip-composed.tsx#L14-L18: reduce the usage guidance to a short component comment.
  • web/packages/agenta-ui/src/components/ui/split-pane.tsx#L5-L17: reduce the implementation history and visual rationale to a short constraint comment.

Source: Coding guidelines

web/packages/agenta-ui/src/components/ui/split-pane.tsx (1)

127-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a supported semantic theme color token.

bg-[var(--ag-surface-gutter)] bypasses the approved theme color forms. Replace it with a semantic Tailwind color utility or a supported var(--ag-color*) token.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 91c27599-33f3-4fd3-9bee-d0590051d2a5

📥 Commits

Reviewing files that changed from the base of the PR and between 6d24e20 and aa9863a.

📒 Files selected for processing (10)
  • web/packages/agenta-shared/src/utils/timeAgo.ts
  • web/packages/agenta-ui/package.json
  • web/packages/agenta-ui/src/components/PageLayout.tsx
  • web/packages/agenta-ui/src/components/presentational/chat/index.tsx
  • web/packages/agenta-ui/src/components/presentational/index.ts
  • web/packages/agenta-ui/src/components/presentational/layout/PanelSection.tsx
  • web/packages/agenta-ui/src/components/ui/context-menu.tsx
  • web/packages/agenta-ui/src/components/ui/index.ts
  • web/packages/agenta-ui/src/components/ui/split-pane.tsx
  • web/packages/agenta-ui/src/components/ui/tooltip-composed.tsx

*/
export const timeAgo = (ts?: number): string => {
if (ts == null || !Number.isFinite(ts)) return ""
if (!ts) return ""

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve finite timestamp validation.

!ts treats the valid Unix timestamp 0 as missing and allows Infinity through. This can hide epoch timestamps and produce invalid output such as Infinityd ago. Keep the explicit null and Number.isFinite checks.

Proposed fix
-    if (!ts) return ""
+    if (ts == null || !Number.isFinite(ts)) return ""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!ts) return ""
if (ts == null || !Number.isFinite(ts)) return ""

Comment on lines +41 to +45
"./surfaces.css": "./src/styles/surfaces.css",
"./code-editor-styles.css": "./src/styles/code-editor-styles.css",
"./editor-theme.css": "./src/styles/editor-theme.css",
"./custom-resize-handle.css": "./src/styles/custom-resize-handle.css",
"./theme-variables.css": "./src/styles/theme-variables.css",

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Package file excerpt:\n'
cat -n web/packages/agenta-ui/package.json | sed -n '1,90p'

printf '\nRelevant files/exports locations:\n'
for f in \
  web/packages/agenta-ui/src/styles/surfaces.css \
  web/packages/agenta-ui/src/styles/code-editor-styles.css \
  web/packages/agenta-ui/src/styles/editor-theme.css \
  web/packages/agenta-ui/src/styles/custom-resize-handle.css \
  web/packages/agenta-ui/src/styles/theme-variables.css
do
  if [ -f "$f" ]; then
    printf '%s: %s\n' "$f" "$(wc -l < "$f") lines"
  else
    printf '%s: missing\n' "$f"
  fi
done

printf '\nSearch for sideEffects and CSS package imports:\n'
rg -n '"sideEffects"|"./.*\.css"' web/packages/agenta-ui/package.json web/packages/agenta-ui -g '*.json' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' | head -n 120

Repository: Agenta-AI/agenta

Length of output: 6381


🌐 Web query:

npm package.json sideEffects false CSS imports tree shaking documentation

💡 Result:

Setting "sideEffects": false in your package.json enables aggressive tree shaking by telling the bundler (like Webpack) that your package contains no side effects and can safely prune unused exports [1][2][3]. However, this often causes CSS imports to be dropped from your production bundle [4][3][5]. Because CSS imports (e.g., import './styles.css') do not export anything that the JavaScript code consumes, the bundler treats them as "unused" and removes them when "sideEffects": false is declared [4][6][7]. To fix this, you must explicitly mark your CSS files as having side effects so the bundler preserves them [1][2][3]. Do not set "sideEffects" to false globally if your package includes CSS; instead, use an array to specify which files have side effects [4][2][3]. Recommended Configuration: Use the following format in your package.json to preserve CSS while still enabling tree shaking for your JavaScript files [4][2][8]: { "sideEffects": [ ".css", ".scss", ".sass", ".less" ] } By providing this array, you instruct the bundler to skip tree shaking only for the specified file patterns, ensuring your styles are included in the final build while keeping your JavaScript code optimized [4][3].

Citations:


Preserve public CSS imports during tree-shaking.

"sideEffects": false can remove CSS package entry points from consuming production builds. Mark CSS files as side effects.

Proposed fix
-    "sideEffects": false,
+    "sideEffects": ["**/*.css"],

Comment on lines +192 to +200
if (kind === "video") {
return (
<video
src={src}
controls
onError={onVideoError}
className={cn(CARD_WIDTH, "rounded-md", className)}
/>
)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Render a placeholder for unloaded video attachments.

src is optional and loading represents media fetching. The video branch renders an empty <video> control when loading is true or src is absent. Return the same loading placeholder before rendering the video element.

Proposed fix
     if (kind === "video") {
+        if (loading || !src) {
+            return (
+                <div
+                    className={cn(
+                        CARD_WIDTH,
+                        "h-[140px] animate-pulse rounded-md bg-colorFillTertiary",
+                        className,
+                    )}
+                    aria-label={`Loading ${name}`}
+                />
+            )
+        }
         return (
             <video
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (kind === "video") {
return (
<video
src={src}
controls
onError={onVideoError}
className={cn(CARD_WIDTH, "rounded-md", className)}
/>
)
if (kind === "video") {
if (loading || !src) {
return (
<div
className={cn(
CARD_WIDTH,
"h-[140px] animate-pulse rounded-md bg-colorFillTertiary",
className,
)}
aria-label={`Loading ${name}`}
/>
)
}
return (
<video
src={src}
controls
onError={onVideoError}
className={cn(CARD_WIDTH, "rounded-md", className)}
/>
)

// `overflow-clip`, not `-hidden`: hidden makes this the sticky header's
// scroll ancestor, so it never pins against PanelScroll.
"shrink-0 overflow-clip rounded-xl border border-solid border-colorBorderSecondary bg-colorBgElevated"
"shrink-0 overflow-hidden rounded-xl border border-solid border-colorBorderSecondary bg-colorBgElevated"

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate PanelSection and consumers"
fd -a 'PanelSection\.tsx$|PanelSurface\.tsx$|PanelScroll\.tsx$' web || true

echo
echo "File outline"
ast-grep outline web/packages/agenta-ui/src/components/presentational/layout/PanelSection.tsx --view expanded || true

echo
echo "Relevant PanelSection source"
sed -n '1,180p' web/packages/agenta-ui/src/components/presentational/layout/PanelSection.tsx

echo
echo "Search PanelSection usages"
rg -n "PanelSection|panel" web/packages -g '*.{tsx,ts,jsx,js}' | head -n 200

Repository: Agenta-AI/agenta

Length of output: 37922


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Focused PanelSection imports/usages"
rg -n "from [\"']`@agenta/ui/components/presentational`[\"']|import \\{[^}]*PanelSection|<PanelSection" web/packages web/apps || true

echo
echo "Tailwind config overflows/packages"
fd -a 'tailwind|postcss|package\.json' web | while read -r f; do
  case "$f" in
    *tailwind*|*postcss*|*package.json*)
      echo "--- $f"
      rg -n "overflowHidden|overflowClip|container-type|overflow-clip|overflow-hidden|plugins|tailwindcss|`@tailwind`" "$f" || true
      ;;
  esac
done

echo
echo "Existing overflow-clip usage"
rg -n "overflow-clip|overflowClip" web/packages web/apps || true

echo
echo "PanelSection export barrel"
rg -n "PanelSection|presentational" web/packages/agenta-ui/src -g '*.{ts,tsx}' | head -n 120

Repository: Agenta-AI/agenta

Length of output: 17510


🌐 Web query:

MDN CSS sticky position element nearest ancestor with overflow clipping behavior overflow-clip overflow-hidden

💡 Result:

In CSS, a position: sticky element is anchored to its nearest scrolling ancestor [1][2]. A "scrolling ancestor" is defined as any ancestor element with an overflow value other than visible (e.g., hidden, scroll, or auto) [1][3][2]. When you set overflow: hidden on an ancestor, it becomes a scroll container—even if it has no visible scrollbars and cannot be scrolled by the user [4][3][2]. Because of this, the sticky element calculates its position relative to that ancestor rather than the viewport (or a parent that is actually intended to be the scroll container), often causing the sticky behavior to fail or appear broken [1][3][2]. The overflow: clip property provides a solution to this issue [4][3][2]. Like overflow: hidden, it clips content that extends beyond the element's box [5][4]. However, unlike hidden, overflow: clip specifically forbids all scrolling (including programmatic scrolling) and does not establish a scroll container [5][4][3]. Consequently, it does not become a sticky anchor, allowing the sticky element to "look past it" and anchor to the next available scrolling ancestor (typically the viewport) [6][4][3]. Key takeaway: If you need to hide overflow on an ancestor but want to maintain position: sticky behavior for a descendant, use overflow: clip instead of overflow: hidden [4][2].

Citations:


🌐 Web query:

Tailwind CSS overflow-clip utility class overflow clip

💡 Result:

The Tailwind CSS utility class overflow-clip applies the CSS property overflow: clip [1][2]. This utility is part of Tailwind's layout utilities for controlling how an element handles content that exceeds its container's bounds [1][3]. When applied, the element's content is clipped to the element's border box, and no scrollbars are provided [1]. Historical Note: In earlier versions of Tailwind CSS, the class name overflow-clip was used to represent text-overflow: clip [4]. To resolve a naming collision as the CSS overflow: clip property gained wider browser support, Tailwind renamed the text-related utility to text-clip and introduced the new overflow-clip utility for the overflow: clip property [4].

Citations:


Preserve sticky headers when clipping rail sections.

When isRail and sticky are true, the header uses sticky top-0. The rail section’s overflow-hidden makes it the nearest scrolling ancestor, so the header pins to the section instead of the intended outer scroll container. Use overflow-clip for sticky rail sections or move clipping below the sticky header.

Comment on lines +69 to +102
const lastRef = React.useRef<{size: number; total: number}>({size: paneSize, total: 0})

const clamp = React.useCallback(
(raw: number, total: number) =>
Math.max(paneMin, Math.min(raw, paneMax, Math.max(paneMin, total - fillMin))),
[fillMin, paneMax, paneMin],
)

const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
if (!resizable) return
e.preventDefault()
e.currentTarget.setPointerCapture(e.pointerId)
setDragging(true)
onResizeStart?.()
}
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (!dragging) return
const rect = rootRef.current?.getBoundingClientRect()
if (!rect) return
const total = rect.width - BAR_WIDTH
const raw =
paneSide === "start"
? e.clientX - rect.left - BAR_WIDTH / 2
: rect.right - e.clientX - BAR_WIDTH / 2
const size = clamp(Math.round(raw), total)
lastRef.current = {size, total}
onResize?.(size, total)
}
const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
if (!dragging) return
e.currentTarget.releasePointerCapture(e.pointerId)
setDragging(false)
onResizeEnd?.(lastRef.current.size, lastRef.current.total)
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Complete the resize lifecycle for clicks and cancelled pointers.

lastRef.current.total remains 0 until handlePointerMove runs. A press and release without movement calls onResizeEnd with an invalid total. A pointercancel also leaves dragging set and skips onResizeEnd.

Initialize lastRef from the root width in handlePointerDown. Handle pointer cancellation with the same completion path.

Comment on lines +119 to +131
<div
data-slot="split-pane-bar"
role="separator"
aria-orientation="vertical"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
className={cn(
"group relative z-[5] box-border h-full shrink-0 touch-none select-none bg-[var(--ag-surface-gutter)]",
resizable && "cursor-col-resize",
barClassName,
)}
style={{flexBasis: BAR_WIDTH, width: BAR_WIDTH}}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add keyboard resizing for the splitter control.

This separator changes pane width but has no tabIndex or keyboard handler. Keyboard users cannot resize the pane.

Make the resizable separator focusable. Support ArrowLeft and ArrowRight. Set aria-valuenow, aria-valuemin, and aria-valuemax from the active pane constraints.

…tful

The manifest claimed "sideEffects": false while exports publishes five stylesheets
(surfaces, editor-theme, code-editor-styles, custom-resize-handle, theme-variables).
That licenses a bundler to drop a bare `import "@agenta/ui/surfaces.css"` as a no-op.
Consumers reach these through a CSS @import today, so nothing is broken yet — the
declaration is simply untrue, and tree-shaking still applies to every JS module.
…an empty player

ChatAttachmentCard guarded only the image branch with `loading || !src`; video fell
straight through to <video src={undefined} controls />, which paints a dead player.
Both media kinds now share one guard ahead of the split, so there is one placeholder
rather than two copies of it.
…rollport

A rail PanelSection carried `overflow-hidden`, which makes the section a scroll
container — so its `sticky top-0` header pinned to a box that never scrolls and the
`sticky` prop did nothing (live on the agents strip and the agent list). `overflow-clip`
rounds the same corners but establishes no scrollport, so the header pins to PanelScroll
as intended. No call site relied on the section scrolling.
…he keyboard

Three gaps in one lifecycle. A press-and-release with no movement never reached
handlePointerMove, so onResizeEnd fired with the seed total of 0 and any ratio the
caller derived from it was wrong; the root is now measured on pointerdown. A cancelled
pointer (scroll takeover, browser gesture) fired nothing at all and left `dragging`
stuck true; pointercancel and pointerup now share one exit. And the separator had no
tabIndex, no key handler and no aria-value*, so it could not be resized by keyboard:
it is focusable while resizable, Arrow keys step it (Shift for a coarse step), Home/End
jump to the bounds, and it publishes valuenow/min/max from the same clamp the pointer
path uses. Pointer and keyboard both run onResizeStart -> onResize -> onResizeEnd, so
there is one callback contract, not two.
@ardaerzin

Copy link
Copy Markdown
Contributor Author

Superseded by #6065, which carries this work reconciled onto main. Verified before closing: merging this branch into #6065's head produces no change to the tree, so nothing here is lost. Closing without merging; the branch stays put.

@ardaerzin ardaerzin closed this Aug 21, 2026
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.

1 participant