Repository navigation
Fix ghost terminal surface rebind after close - #808
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds debug instrumentation and guarded portal-binding lifecycle checks across terminal surface/view, portal registry, panel/tab/workspace split flows, and exposes portal stats via v2; also adds a regression test verifying no portal-orphan rebinds after Ctrl‑D closes. Changes
Sequence Diagram(s)sequenceDiagram
participant TabMgr as TabManager
participant Panel as TerminalPanel / Window
participant PortalReg as TerminalWindowPortalRegistry
participant Surface as TerminalSurface / GhosttyView
participant Test as Test harness / Client
TabMgr->>Panel: request create split (direction)
Panel->>PortalReg: ensure surface/host wiring for new panel
Note over PortalReg,Surface: Binding attempt may include expectedSurfaceId & expectedGeneration
PortalReg->>Surface: bind(hostedView, anchorView, ..., expectedSurfaceId, expectedGeneration)
alt expected matches and surface live
PortalReg-->>Surface: bind success (map entry)
Surface-->>PortalReg: debugStats / generation confirmed
else mismatch or closing/closed
PortalReg--xSurface: bind blocked (increment blockedBindCount, log reason)
PortalReg->>PortalReg: cleanup stale mapping if needed
end
Test->>PortalReg: query debug.portal.stats()
PortalReg-->>Test: aggregated portal + blocked-bind stats
Note right of Surface: close lifecycle: beginPortalCloseLifecycle -> markPortalLifecycleClosed -> generation++
PortalReg->>Surface: further bind attempts with old generation will be blocked
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
Greptile SummaryFixes the ghost terminal bug where Ctrl-D closed terminals could briefly reappear in splits due to stale portal bind paths reattaching surfaces during close. Key Changes:
Validation: Confidence Score: 5/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[TerminalSurface Created] -->|generation=1| B[State: live]
B -->|Panel.close called| C[beginPortalCloseLifecycle]
C -->|generation=2| D[State: closing]
D -->|Surface deinit| E[markPortalLifecycleClosed]
E -->|generation=3| F[State: closed]
G[Portal Bind Request] --> H{canAcceptPortalBinding?}
H -->|state != live| I[REJECT - Block stale bind]
H -->|expectedSurfaceId mismatch| I
H -->|expectedGeneration mismatch| I
H -->|All checks pass| J[ACCEPT - Bind surface to window portal]
D -.->|Blocks any new binds| I
F -.->|Blocks any new binds| I
style B fill:#90EE90
style D fill:#FFD700
style F fill:#FF6B6B
style I fill:#FF6B6B
style J fill:#90EE90
Last reviewed commit: 56b5df0 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
Sources/TabManager.swift (1)
1869-1876: Deduplicate split direction label mapping.The same
SplitDirection→ string switch is repeated in three places (Lines 1869-1876, 1901-1908, 2038-2044). Extracting a small helper/computed property (e.g.,direction.debugLabel) would reduce drift risk in future log updates.Also applies to: 1901-1908, 2038-2044
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/TabManager.swift` around lines 1869 - 1876, The repeated switch-to-string mapping for SplitDirection should be extracted into a single computed property on the SplitDirection type (e.g., add var debugLabel: String { switch self { case .left: return "left" ... } }) and replace the three inline switches that assign directionLabel with a call to direction.debugLabel; update all usages where a debug label is needed (the places that currently switch on direction) to use this new property to avoid duplication and drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py`:
- Around line 131-142: The _portal_integrity_error function currently coerces
missing totals keys to 0 which can false-pass; instead, first assert that totals
contains the required keys ("orphan_terminal_subview_count",
"visible_orphan_terminal_subview_count", "stale_entry_count") and if any are
missing return a non-nil error stating which keys are absent, otherwise parse
the integer values and perform the existing orphan/stale checks (do not silently
default missing keys to 0); update the logic in _portal_integrity_error to
detect malformed stats payloads and fail closed by returning an error when
required counters are not present.
- Around line 181-214: The rebind detector incorrectly suppresses visible events
while deinit is in-progress because the RE_VISIBLE_ON handling checks "sid in
close_pending and sid not in deinit_started"; remove the deinit_started
exclusion so any visible event for a sid in close_pending is treated as a
violation (the RE_DEINIT_END handler already discards sids from close_pending on
completion). Update the RE_VISIBLE_ON branch in _find_close_rebind_violations to
append the line when sid is in close_pending (remove the "and sid not in
deinit_started" condition), keeping the existing RE_DEINIT_BEGIN/RE_DEINIT_END
logic and sets (deinit_started, close_pending) intact.
---
Nitpick comments:
In `@Sources/TabManager.swift`:
- Around line 1869-1876: The repeated switch-to-string mapping for
SplitDirection should be extracted into a single computed property on the
SplitDirection type (e.g., add var debugLabel: String { switch self { case
.left: return "left" ... } }) and replace the three inline switches that assign
directionLabel with a call to direction.debugLabel; update all usages where a
debug label is needed (the places that currently switch on direction) to use
this new property to avoid duplication and drift.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
Sources/AppDelegate.swiftSources/GhosttyTerminalView.swiftSources/Panels/TerminalPanel.swiftSources/TabManager.swiftSources/TerminalController.swiftSources/TerminalWindowPortal.swiftSources/Workspace.swifttests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py
There was a problem hiding this comment.
1 issue found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py">
<violation number="1" location="tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py:211">
P1: The regression detector stops flagging close→visible rebinds after `deinit.begin`, which can miss visibility rebinds that occur before `deinit.end` (still before deinit completion).</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
Addressed the review feedback in commit 7a0fb42:
@cubic-dev-ai please re-review. |
@lawrencecchen I have started the AI code review. It will take a few minutes to complete. |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py`:
- Around line 124-125: The RPC call in _portal_stats currently uses
c._call("debug.portal.stats") with the default 20s timeout and should be bounded
by the integrity loop deadline; update _portal_stats to accept a timeout
argument (e.g., timeout_s) and call c._call("debug.portal.stats",
timeout_s=timeout_s) (or similar param name used by cmux._call), then modify
callers such as _wait_for_portal_integrity(timeout_s=...) to compute the
remaining deadline and pass that remaining timeout into _portal_stats so each
RPC is never allowed to block longer than the integrity loop budget.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
Sources/TabManager.swifttests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py
🚧 Files skipped from review as they are similar to previous changes (1)
- Sources/TabManager.swift
There was a problem hiding this comment.
♻️ Duplicate comments (1)
tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py (1)
124-125:⚠️ Potential issue | 🟠 MajorBound
debug.portal.statsRPC timeout to the integrity-loop deadline.Line 125 still uses
c._call("debug.portal.stats")with the default 20s timeout (seetests_v2/cmux.py), so_wait_for_portal_integrity(timeout_s=...)can overrun its budget and hang/flap under RPC latency.💡 Suggested fix
-def _portal_stats(c: cmux) -> dict: - stats = c._call("debug.portal.stats") or {} +def _portal_stats(c: cmux, *, timeout_s: float) -> dict: + stats = c._call("debug.portal.stats", timeout_s=timeout_s) or {} if not isinstance(stats, dict): raise cmuxError(f"debug.portal.stats returned non-dict payload: {stats!r}") return stats @@ def _wait_for_portal_integrity(c: cmux, *, timeout_s: float, context: str) -> None: deadline = time.time() + timeout_s last = None error = None while time.time() < deadline: - last = _portal_stats(c) + remaining = deadline - time.time() + if remaining <= 0: + break + last = _portal_stats(c, timeout_s=min(remaining, 0.5)) error = _portal_integrity_error(last) if error is None: return time.sleep(POLL_S)Also applies to: 165-171
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py` around lines 124 - 125, _portal_stats currently calls c._call("debug.portal.stats") with the default RPC timeout which can exceed the overall _wait_for_portal_integrity deadline; change _portal_stats to accept a timeout/deadline parameter (e.g., timeout_s or remaining_s) and pass that through to c._call as the RPC timeout, and update callers (including _wait_for_portal_integrity and the other occurrences around lines 165-171) to compute and forward the remaining time so the RPC is bounded by the integrity-loop deadline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py`:
- Around line 124-125: _portal_stats currently calls
c._call("debug.portal.stats") with the default RPC timeout which can exceed the
overall _wait_for_portal_integrity deadline; change _portal_stats to accept a
timeout/deadline parameter (e.g., timeout_s or remaining_s) and pass that
through to c._call as the RPC timeout, and update callers (including
_wait_for_portal_integrity and the other occurrences around lines 165-171) to
compute and forward the remaining time so the RPC is bounded by the
integrity-loop deadline.
There was a problem hiding this comment.
2 issues found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Sources/TerminalController.swift">
<violation number="1" location="Sources/TerminalController.swift:8534">
P2: Avoid `DispatchQueue.main.sync` here; it can deadlock if the debug command is invoked on the main thread and it bypasses the existing `v2MainSync` guard. Use `v2MainSync` (or a `Thread.isMainThread` guard) to keep it safe and consistent.</violation>
</file>
<file name="tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py">
<violation number="1" location="tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py:125">
P2: The portal integrity poll is using an unbounded RPC timeout relative to the loop deadline. A slow `debug.portal.stats` call can block far longer than the integrity budget and make this test hang or become flaky. Pass an explicit short timeout for this RPC.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
Addressed follow-up review items in commit e536b86:
@cubic-dev-ai please re-review. |
|
I'll kick off a fresh incremental review now covering those changes. ✅ Actions performedReview triggered.
|
@lawrencecchen I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py (1)
247-279: Guarantee workspace cleanup withfinallyto avoid test state leakage.At Line 278,
c.close_workspace(workspace_id)only runs on the success path. If any earlier step raises, the workspace can remain open and affect subsequent runs.♻️ Suggested refactor
with cmux(SOCKET_PATH) as c: c.activate_app() workspace_id = c.new_workspace() - c.select_workspace(workspace_id) - time.sleep(0.2) - - c.new_split("right") - layout = _wait_for_panes(c, 2, timeout_s=PANE_TIMEOUT_S, context="initial right split") - _wait_for_portal_integrity(c, timeout_s=INTEGRITY_TIMEOUT_S, context="after initial right split") - - for iteration in range(1, ITERATIONS + 1): - right_panel_id = _rightmost_panel(layout) - c.focus_surface_by_panel(right_panel_id) - c.new_split("down") - layout = _wait_for_panes( - c, - 3, - timeout_s=PANE_TIMEOUT_S, - context=f"iter={iteration} after split down", - ) - - bottom_right_panel_id = _bottom_right_panel(layout) - layout = _close_bottom_right_via_ctrl_d( - c, - bottom_right_panel_id=bottom_right_panel_id, - context=f"iter={iteration}", - ) - _wait_for_portal_integrity(c, timeout_s=INTEGRITY_TIMEOUT_S, context=f"iter={iteration} integrity") - if POST_CLOSE_SETTLE_S > 0: - time.sleep(POST_CLOSE_SETTLE_S) - - c.close_workspace(workspace_id) + try: + c.select_workspace(workspace_id) + time.sleep(0.2) + + c.new_split("right") + layout = _wait_for_panes(c, 2, timeout_s=PANE_TIMEOUT_S, context="initial right split") + _wait_for_portal_integrity(c, timeout_s=INTEGRITY_TIMEOUT_S, context="after initial right split") + + for iteration in range(1, ITERATIONS + 1): + right_panel_id = _rightmost_panel(layout) + c.focus_surface_by_panel(right_panel_id) + c.new_split("down") + layout = _wait_for_panes( + c, + 3, + timeout_s=PANE_TIMEOUT_S, + context=f"iter={iteration} after split down", + ) + + bottom_right_panel_id = _bottom_right_panel(layout) + layout = _close_bottom_right_via_ctrl_d( + c, + bottom_right_panel_id=bottom_right_panel_id, + context=f"iter={iteration}", + ) + _wait_for_portal_integrity(c, timeout_s=INTEGRITY_TIMEOUT_S, context=f"iter={iteration} integrity") + if POST_CLOSE_SETTLE_S > 0: + time.sleep(POST_CLOSE_SETTLE_S) + finally: + c.close_workspace(workspace_id)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py` around lines 247 - 279, Wrap the test body that uses cmux and the created workspace in a try/finally so workspace cleanup always runs: after calling c.new_workspace() (workspace_id) and c.select_workspace(workspace_id) execute the split/iteration logic inside try and call c.close_workspace(workspace_id) in the finally block (guarding that workspace_id is set) to ensure the workspace is closed even on exceptions; reference cmux, c.new_workspace, c.select_workspace, and c.close_workspace to locate where to add the try/finally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py`:
- Around line 145-149: The test currently coerces
totals["orphan_terminal_subview_count"],
totals["visible_orphan_terminal_subview_count"], and totals["stale_entry_count"]
via int(...), which masks malformed values; change this to strict integer
validation instead of coercion: read the raw values from totals (the keys
referenced above), assert their types are exact integers (e.g., type(value) is
int or equivalently isinstance(value, int) and not isinstance(value, bool)), and
raise/propagate a TypeError when a value is not an exact int so the test fails
closed on non-integer counters.
---
Nitpick comments:
In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py`:
- Around line 247-279: Wrap the test body that uses cmux and the created
workspace in a try/finally so workspace cleanup always runs: after calling
c.new_workspace() (workspace_id) and c.select_workspace(workspace_id) execute
the split/iteration logic inside try and call c.close_workspace(workspace_id) in
the finally block (guarding that workspace_id is set) to ensure the workspace is
closed even on exceptions; reference cmux, c.new_workspace, c.select_workspace,
and c.close_workspace to locate where to add the try/finally.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
Sources/TerminalController.swifttests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py
🚧 Files skipped from review as they are similar to previous changes (1)
- Sources/TerminalController.swift
| try: | ||
| orphan = int(totals["orphan_terminal_subview_count"]) | ||
| visible_orphan = int(totals["visible_orphan_terminal_subview_count"]) | ||
| stale = int(totals["stale_entry_count"]) | ||
| except (TypeError, ValueError): |
There was a problem hiding this comment.
Fail closed on counter types; int(...) coercion can mask malformed portal stats.
At Line 146–148, using int(...) accepts/truncates non-integer values (0.7, "0", False), which can false-pass integrity checks that are supposed to reject non-integer counters.
💡 Suggested hardening
- try:
- orphan = int(totals["orphan_terminal_subview_count"])
- visible_orphan = int(totals["visible_orphan_terminal_subview_count"])
- stale = int(totals["stale_entry_count"])
- except (TypeError, ValueError):
+ orphan_raw = totals["orphan_terminal_subview_count"]
+ visible_orphan_raw = totals["visible_orphan_terminal_subview_count"]
+ stale_raw = totals["stale_entry_count"]
+ if type(orphan_raw) is not int or type(visible_orphan_raw) is not int or type(stale_raw) is not int:
return (
"portal totals contains non-integer counters "
- f"(orphan={totals.get('orphan_terminal_subview_count')!r}, "
- f"visible_orphan={totals.get('visible_orphan_terminal_subview_count')!r}, "
- f"stale={totals.get('stale_entry_count')!r})"
+ f"(orphan={orphan_raw!r}, visible_orphan={visible_orphan_raw!r}, stale={stale_raw!r})"
)
+ orphan = orphan_raw
+ visible_orphan = visible_orphan_raw
+ stale = stale_raw🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py` around lines 145
- 149, The test currently coerces totals["orphan_terminal_subview_count"],
totals["visible_orphan_terminal_subview_count"], and totals["stale_entry_count"]
via int(...), which masks malformed values; change this to strict integer
validation instead of coercion: read the raw values from totals (the keys
referenced above), assert their types are exact integers (e.g., type(value) is
int or equivalently isinstance(value, int) and not isinstance(value, bool)), and
raise/propagate a TypeError when a value is not an exact int so the test fails
closed on non-integer counters.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py (1)
145-149:⚠️ Potential issue | 🟠 MajorUse strict integer type checks for portal counters.
Line 145–149 still coerces with
int(...), which accepts values like"0",False, and0.7; that can false-pass malformeddebug.portal.statspayloads instead of failing closed.💡 Proposed hardening
- try: - orphan = int(totals["orphan_terminal_subview_count"]) - visible_orphan = int(totals["visible_orphan_terminal_subview_count"]) - stale = int(totals["stale_entry_count"]) - except (TypeError, ValueError): + orphan_raw = totals["orphan_terminal_subview_count"] + visible_orphan_raw = totals["visible_orphan_terminal_subview_count"] + stale_raw = totals["stale_entry_count"] + if ( + type(orphan_raw) is not int + or type(visible_orphan_raw) is not int + or type(stale_raw) is not int + ): return ( "portal totals contains non-integer counters " - f"(orphan={totals.get('orphan_terminal_subview_count')!r}, " - f"visible_orphan={totals.get('visible_orphan_terminal_subview_count')!r}, " - f"stale={totals.get('stale_entry_count')!r})" + f"(orphan={orphan_raw!r}, " + f"visible_orphan={visible_orphan_raw!r}, " + f"stale={stale_raw!r})" ) + orphan = orphan_raw + visible_orphan = visible_orphan_raw + stale = stale_raw#!/bin/bash set -euo pipefail # 1) Confirm current implementation still uses int(...) coercion. sed -n '131,160p' tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py # 2) Verify Python coercion behavior that can mask malformed counters. python - <<'PY' samples = ["0", False, 0.7, 0] for v in samples: try: print(f"value={v!r:<6} type={type(v).__name__:<7} int(v)={int(v)} strict_int={type(v) is int}") except Exception as e: print(f"value={v!r:<6} type={type(v).__name__:<7} int(v)=<error {type(e).__name__}> strict_int={type(v) is int}") PY🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py` around lines 145 - 149, The test currently coerces portal counters with int(...) which accepts strings, bools and floats; instead, fetch each value from totals (totals["orphan_terminal_subview_count"], totals["visible_orphan_terminal_subview_count"], totals["stale_entry_count"]) and perform a strict type check (require type(value) is int — not isinstance to exclude bool) before assigning to orphan, visible_orphan, and stale; if the check fails, raise or assert (e.g., ValueError/AssertionError) so malformed debug.portal.stats payloads fail the test rather than silently coerced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py`:
- Around line 145-149: The test currently coerces portal counters with int(...)
which accepts strings, bools and floats; instead, fetch each value from totals
(totals["orphan_terminal_subview_count"],
totals["visible_orphan_terminal_subview_count"], totals["stale_entry_count"])
and perform a strict type check (require type(value) is int — not isinstance to
exclude bool) before assigning to orphan, visible_orphan, and stale; if the
check fails, raise or assert (e.g., ValueError/AssertionError) so malformed
debug.portal.stats payloads fail the test rather than silently coerced.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
Sources/TerminalController.swifttests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.py
* Fix ghost terminal lifecycle rebind race * Address review feedback on portal regression checks * Address follow-up review feedback
Summary
Fixes the ghost terminal behavior where a Ctrl-D closed terminal can briefly reappear in a split (half-present/orphaned surface state).
Root Cause
A stale portal bind path could reattach a terminal surface after close had already started, so visibility/active flags could flip back before deinit completed.
Changes
TerminalSurfaceportal lifecycle transitions.debug.portal.statsCLI debug command and portal stats plumbing.tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.pythat catches close->visible rebind races during split churn.Validation
CMUX_SOCKET=/tmp/cmux-debug-ghost-surface-orphan-regression.sock python3 tests_v2/test_split_cmd_shift_d_ctrl_d_no_portal_orphans.pyPASS: no close->visible rebind races during split-down + ctrl-d churn (iters=16, closes=16)Summary by cubic
Prevents “ghost” terminals from briefly reappearing after Ctrl-D by guarding portal binds with surface lifecycle/generation and sealing the surface on close/deinit. Adds portal integrity stats and a regression test that fails on any close→visible rebind or portal orphan/stale state.
Bug Fixes
New Features
Written for commit e536b86. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes
Chores
Tests