Skip to content

Add async SSH connection management, reconnect logic, and security hardening to terminal screen#1

Merged
6m10cmUK merged 9 commits into
masterfrom
refactor/terminal-screen-review
Mar 6, 2026
Merged

Add async SSH connection management, reconnect logic, and security hardening to terminal screen#1
6m10cmUK merged 9 commits into
masterfrom
refactor/terminal-screen-review

Conversation

@6m10cmUK

@6m10cmUK 6m10cmUK commented Mar 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • SSH接続管理の修正(共有クライアント破壊防止、排他制御、isClosed判定)
  • 接続ライフサイクル改善(StreamSubscriptionリーク修正、リトライ上限+backoff、build副作用除去)
  • UI・セキュリティ(シェルインジェクション防止、スクロール制御改善、タップ領域拡大、定数化)

Test plan

  • タブを複数開いて1つの接続が切れても他タブが影響を受けないことを確認
  • SSH切断後の自動再接続が5回でリトライ停止し「再試行」ボタンが出ることを確認
  • スクロールが意図しないタイミングで発火しないことを確認
  • 不正なセッション名でのアタッチが拒否されることを確認

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Per-tab reconnect with capped retries, backoff, and clear retry UI
    • Session name validation with immediate user feedback
    • Pointer-based scroll gesture improvements
    • Per-tab terminal IO routing and lifecycle management
  • Bug Fixes

    • Safer connection lifecycle, subscription cleanup, and reconnect state handling
    • Prevents errors when no tabs exist
  • UI/UX

    • Larger close control and minor tab bar/accessory tweaks
  • Tests

    • Added unit tests for host config, session name validation, SSH parsing, terminal logic
  • Chores

    • CI workflow and repo config added

SSH接続管理:
- _reconnectTabが全タブの共有クライアントを破壊する問題を修正
- _getClientにCompleter排他制御を追加(競合状態防止)
- closedなSSHClientのisClosed判定を追加
- _showAddTabDialogで共有クライアントを再利用

接続ライフサイクル:
- build内のFuture.microtask副作用をsession.doneコールバックに移動
- StreamSubscriptionを保持しdispose時にcancel
- 再接続にリトライ上限(5回)+backoff導入、手動再試行ボタン表示
- _currentTabの空配列アクセスガード追加
- _reconnectingフラグをtry-finallyで保護

UI・セキュリティ:
- セッション名のバリデーション(シェルインジェクション防止)
- onPointerMoveスクロールに距離・方向しきい値追加
- タブ×ボタンのタップ領域拡大(32x32)
- マジックナンバー/マジックストリングを定数化
- _closeTab内のNavigator.popをaddPostFrameCallbackに移動

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

coderabbitai Bot commented Mar 6, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@6m10cmUK has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 8 seconds before requesting another review.

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a9a8ced-8c14-4484-82c8-d1b62f36bcde

📥 Commits

Reviewing files that changed from the base of the PR and between 3f13520 and 13b7807.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • test/terminal_logic_test.dart
📝 Walkthrough

Walkthrough

Refactors terminal SSH handling: introduces asynchronous shared client lifecycle, per-tab SSH sessions with subscription-managed stdout/stderr, reconnect with capped backoff, session-name validation, pointer-based scroll detection, improved cleanup/UI feedback, adds CI workflow, coderabbit config, and multiple unit tests.

Changes

Cohort / File(s) Summary
Terminal screen
lib/screens/terminal_screen.dart
Major functional changes: asynchronous shared SSH client orchestration; per-tab SSH sessions and per-tab stdout/stderr subscriptions with proper cancellation; reconnect/backoff (max 5 retries); session name validation on creation; pointer-based scroll gesture handling; sendKey/sendCtrlKey guards; tab lifecycle and cleanup updates; UI tweaks and error/retry UI.
CI workflow
.github/workflows/ci.yml
Adds GitHub Actions CI workflow to checkout, set up Flutter (stable), run flutter pub get, flutter analyze --no-fatal-infos, flutter test, and flutter build apk on specified branches and PRs.
Coderabbit config
.coderabbit.yaml
Adds project config enabling auto_review (language: ja) with path filters excluding build outputs, tooling, plugins, generated Dart files, and pubspec.lock.
Tests — HostConfig
test/host_config_test.dart
New unit tests for HostConfig: JSON round-trip, password handling, default port behavior, list serialization, UUID generation, and copyWith semantics.
Tests — Session name validation
test/session_name_validation_test.dart
Tests reproducing session-name regex behavior: validates allowed characters and rejects invalid names.
Tests — SshService
test/ssh_service_test.dart
Adds SshService tests for PATH prefix expectations and tmux session-list parsing and empty-output handling.
Tests — Terminal logic
test/terminal_logic_test.dart
Adds tests covering retry/backoff logic, tab index management on close, scroll accumulation thresholds, pointer-movement decision logic, and related internal behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Screen as TerminalScreen
    participant Client as SharedSSHClient
    participant Tab as Per-Tab Session
    participant Stream as Stdout/Stderr

    User->>Screen: Open tab / Connect
    activate Screen
    Screen->>Client: check/reuse or create client
    alt create
        Screen->>Client: establish connection (async)
        Client-->>Screen: connection ready
    end
    Screen->>Tab: create session bound to client
    Tab->>Stream: subscribe stdout/stderr
    Stream-->>Tab: deliver output
    Screen-->>User: display terminal
    deactivate Screen

    User->>Screen: Input / Resize
    activate Screen
    Screen->>Tab: route input / send resize
    Tab->>Client: write to SSH session
    Client->>Stream: emit stdout/stderr
    Stream-->>Tab: update display
    deactivate Screen

    Note over Screen,Tab: On disconnect/error
    Tab->>Screen: notify error
    alt attempt reconnect (<=5)
        Screen->>Screen: backoff + retry
        Screen->>Client: recreate/check client
        Screen->>Tab: recreate session & resubscribe
    else give up
        Screen->>Tab: cancel subscriptions & close session
        Screen->>Screen: if no tabs -> exit screen
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through sockets, thread by thread,
Tabs spawned, then whispered what I read.
Retries counted, scrolls felt by paw,
Subscriptions tidy — neatness in law.
A rabbit cheers: connections mend and spread.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: async SSH connection management, reconnect logic, and security hardening are all core themes evident in the terminal_screen.dart refactoring with per-tab reconnect logic, validation, and error handling improvements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/terminal-screen-review

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 and usage tips.

- GitHub Actions CI: analyze, test, build APK
- CodeRabbit: 日本語レビュー、auto_review有効
- 43 unit tests: HostConfig serialization, session name validation,
  SSH service path prefix and tmux output parsing

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/screens/terminal_screen.dart (2)

177-185: ⚠️ Potential issue | 🔴 Critical

Don't reconnect tabs that were intentionally closed.

Line 200 and Line 284 close the session during normal teardown, but the done handler on Lines 177-185 treats every completion as a dropped connection and calls _reconnectTab(tab). Closing a tab can therefore recreate an orphaned SSH session for a tab that's already been removed from _tabs, along with new listeners attached to it. Add a per-tab closed flag or a _tabs.contains(tab) guard before updating state/reconnecting, and re-check it after the backoff delay.

Also applies to: 200-210, 279-286

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

In `@lib/screens/terminal_screen.dart` around lines 177 - 185, The done handler
for tab.session (the block that calls setState and _reconnectTab(tab)) must not
treat intentional closes as dropped connections; add a per-tab closed flag
(e.g., tab.closed) or check `_tabs.contains(tab)` before mutating state and
before calling `_reconnectTab(tab)`, and set that flag when you intentionally
close the session (the paths that call session.close()/closeSession in the
teardown around the other handlers). Also re-check the same guard after the
backoff delay inside the reconnect logic so you never recreate sessions or
listeners for tabs that were intentionally removed.

59-60: ⚠️ Potential issue | 🟠 Major

Avoid accessing ScaffoldMessenger during the initState lifecycle phase.

Line 59 calls _addTab(widget.sessionName) from initState(). If the session name is invalid, lines 121-125 attempt to show a snackbar via ScaffoldMessenger.of(context) before the first build completes. This violates Flutter's widget lifecycle and can fail since inherited widgets may not be available during initialization.

Defer the validation feedback using addPostFrameCallback(), or return a validation result from _addTab() and display UI feedback from a post-build handler instead (as already shown in _closeTab() at line 208).

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

In `@lib/screens/terminal_screen.dart` around lines 59 - 60, _initState currently
calls _addTab(widget.sessionName) which may trigger
ScaffoldMessenger.of(context) to show a SnackBar before the first build; defer
any UI feedback by wrapping the call that shows the snackbar in
WidgetsBinding.instance.addPostFrameCallback or change _addTab to return a
validation result and then call ScaffoldMessenger.of(context).showSnackBar from
a post-frame callback (similar to _closeTab) instead of inside _addTab during
initState so inherited widgets are available.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/screens/terminal_screen.dart`:
- Around line 113-114: _reconnectTab resets tab._retryCount unconditionally
because _connectTab currently swallows all exceptions; change _connectTab (or
its callers) so failures are observable: either have _connectTab return a
success boolean or rethrow the caught exception after logging, and then update
_reconnectTab to only reset tab._retryCount when the connect call succeeded (or
catch the rethrown error to increment retry and schedule another attempt).
Target the _connectTab method and the _reconnectTab code path that awaits it,
and ensure tab._retryCount is only reset on a confirmed successful connection.
- Around line 201-205: When removing a tab in the setState block that mutates
_tabs (the code that calls _tabs.removeAt(index) and adjusts _currentIndex),
account for removals to the left of the active tab by decrementing _currentIndex
when index < previous _currentIndex; otherwise the selected tab will shift
incorrectly. Update the logic around _tabs.removeAt(index) to: capture the
previous _currentIndex, remove the tab, if the removed index is less than the
previous index decrement _currentIndex by 1, then clamp _currentIndex to 0 when
_tabs is empty or to _tabs.length - 1 if it’s out of range.

---

Outside diff comments:
In `@lib/screens/terminal_screen.dart`:
- Around line 177-185: The done handler for tab.session (the block that calls
setState and _reconnectTab(tab)) must not treat intentional closes as dropped
connections; add a per-tab closed flag (e.g., tab.closed) or check
`_tabs.contains(tab)` before mutating state and before calling
`_reconnectTab(tab)`, and set that flag when you intentionally close the session
(the paths that call session.close()/closeSession in the teardown around the
other handlers). Also re-check the same guard after the backoff delay inside the
reconnect logic so you never recreate sessions or listeners for tabs that were
intentionally removed.
- Around line 59-60: _initState currently calls _addTab(widget.sessionName)
which may trigger ScaffoldMessenger.of(context) to show a SnackBar before the
first build; defer any UI feedback by wrapping the call that shows the snackbar
in WidgetsBinding.instance.addPostFrameCallback or change _addTab to return a
validation result and then call ScaffoldMessenger.of(context).showSnackBar from
a post-frame callback (similar to _closeTab) instead of inside _addTab during
initState so inherited widgets are available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36d79722-66bc-4573-80b1-6c42d22f1f81

📥 Commits

Reviewing files that changed from the base of the PR and between 89e73f6 and f2bfe6b.

📒 Files selected for processing (1)
  • lib/screens/terminal_screen.dart

Comment thread lib/screens/terminal_screen.dart Outdated
Comment thread lib/screens/terminal_screen.dart
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@6m10cmUK 6m10cmUK changed the title Refactor terminal_screen.dart based on code review Add async SSH connection management, reconnect logic, and security hardening to terminal screen Mar 6, 2026
6m10cmUK and others added 2 commits March 6, 2026 22:40
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
test/session_name_validation_test.dart (1)

3-5: Pattern duplication creates a sync risk.

The regex is duplicated from lib/screens/terminal_screen.dart:15. If the production pattern changes, this test would still pass with the old pattern.

Consider exposing the pattern for testability, or at minimum, add a comment referencing the exact source location to ease future maintenance:

/// Mirrors _kSessionNamePattern from lib/screens/terminal_screen.dart:15
/// Keep in sync if the production pattern changes.
final _kSessionNamePattern = RegExp(r'^[a-zA-Z0-9_-]+$');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/session_name_validation_test.dart` around lines 3 - 5, The test
duplicates the production regex _kSessionNamePattern (from
lib/screens/terminal_screen.dart) causing a sync risk; either export the
constant from terminal_screen.dart (make _kSessionNamePattern public or provide
a getter) and import it in test_session_name_validation_test.dart, or if you
cannot change visibility, add a clear comment above the duplicated final
_kSessionNamePattern in the test that references "Mirrors _kSessionNamePattern
from lib/screens/terminal_screen.dart:15 — keep in sync if production pattern
changes" so reviewers/maintainers can find and update the source pattern.
test/ssh_service_test.dart (1)

12-29: Consider extracting parsing logic to a testable helper.

The test duplicates the parsing logic from listTmuxSessions (see lib/services/ssh_service.dart:17-29) rather than testing the actual method. While the comment explains the SSHClient dependency, this approach is fragile—if the production logic changes, these tests won't detect regressions.

Consider extracting the parsing logic into a separate static method that can be tested directly:

// In SshService:
static List<String> parseTmuxOutput(String output) {
  if (output.isEmpty || output.contains('no server running') || output.contains('not found')) {
    return [];
  }
  return output.split('\n')
      .where((s) => s.isNotEmpty)
      .where((l) => l.contains(':'))
      .map((l) => l.split(':').first.trim())
      .toList();
}

Then the test can call SshService.parseTmuxOutput(output) directly.

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

In `@test/ssh_service_test.dart` around lines 12 - 29, Extract the tmux-output
parsing out of the test into a new static helper on SshService (e.g.,
SshService.parseTmuxOutput) and update the test to call that helper instead of
duplicating logic; implement parseTmuxOutput to return [] for empty output or
when it contains "no server running" or "not found", otherwise split on '\n',
filter non-empty lines that contain ':', and map each line to the trimmed name
before the ':' so listTmuxSessions can delegate to this helper and the test
directly verifies SshService.parseTmuxOutput(output).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 24-27: The workflow uses the wrong GitHub Action name
"subosio/flutter-action" causing CI to fail; update the action reference in the
YAML to the correct repository "subosito/flutter-action" wherever
"subosio/flutter-action" appears (in the job step using
flutter-version-file/channel), preserving the existing inputs
(flutter-version-file and channel) so the step behavior remains unchanged.

---

Nitpick comments:
In `@test/session_name_validation_test.dart`:
- Around line 3-5: The test duplicates the production regex _kSessionNamePattern
(from lib/screens/terminal_screen.dart) causing a sync risk; either export the
constant from terminal_screen.dart (make _kSessionNamePattern public or provide
a getter) and import it in test_session_name_validation_test.dart, or if you
cannot change visibility, add a clear comment above the duplicated final
_kSessionNamePattern in the test that references "Mirrors _kSessionNamePattern
from lib/screens/terminal_screen.dart:15 — keep in sync if production pattern
changes" so reviewers/maintainers can find and update the source pattern.

In `@test/ssh_service_test.dart`:
- Around line 12-29: Extract the tmux-output parsing out of the test into a new
static helper on SshService (e.g., SshService.parseTmuxOutput) and update the
test to call that helper instead of duplicating logic; implement parseTmuxOutput
to return [] for empty output or when it contains "no server running" or "not
found", otherwise split on '\n', filter non-empty lines that contain ':', and
map each line to the trimmed name before the ':' so listTmuxSessions can
delegate to this helper and the test directly verifies
SshService.parseTmuxOutput(output).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b704454-d60d-4d31-b867-a9257df9e915

📥 Commits

Reviewing files that changed from the base of the PR and between f2bfe6b and 092e58b.

📒 Files selected for processing (5)
  • .coderabbit.yaml
  • .github/workflows/ci.yml
  • test/host_config_test.dart
  • test/session_name_validation_test.dart
  • test/ssh_service_test.dart
✅ Files skipped from review due to trivial changes (1)
  • .coderabbit.yaml

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +24 to +27
- uses: subosio/flutter-action@v2
with:
flutter-version-file: pubspec.yaml
channel: stable

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

subosito flutter-action GitHub official

💡 Result:

The official GitHub repository for subosito/flutter-action is:

https://github.com/subosito/flutter-action

[1]


Fix typo in Flutter action name — workflow will fail.

The action subosio/flutter-action is incorrect. The correct action is subosito/flutter-action (note: subosito, not subosio).

Proposed fix
-      - uses: subosio/flutter-action@v2
+      - uses: subosito/flutter-action@v2
         with:
           flutter-version-file: pubspec.yaml
           channel: stable
📝 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
- uses: subosio/flutter-action@v2
with:
flutter-version-file: pubspec.yaml
channel: stable
- uses: subosito/flutter-action@v2
with:
flutter-version-file: pubspec.yaml
channel: stable
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 24 - 27, The workflow uses the wrong
GitHub Action name "subosio/flutter-action" causing CI to fail; update the
action reference in the YAML to the correct repository "subosito/flutter-action"
wherever "subosio/flutter-action" appears (in the job step using
flutter-version-file/channel), preserving the existing inputs
(flutter-version-file and channel) so the step behavior remains unchanged.

6m10cmUK and others added 3 commits March 6, 2026 22:44
Local xterm package has deprecation infos that shouldn't fail CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Only reset _retryCount when connection actually succeeded (tab.connected)
- Adjust _currentIndex when closing a tab to the left of current

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Retry logic: max retries, backoff capping, reset on success only
- Tab index: left/right/self close, sequential close, bounds check
- Scroll: threshold, multi-event, reverse direction
- Pointer: min distance, axis locking

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/terminal_logic_test.dart (1)

52-59: Pipeline warning: Dead code is intentional here, but consider refactoring for clarity.

The CI flagged line 55 as dead code because the if (connected) block can never execute when connected = false. While this correctly tests that the retry count is not reset on failure, the dead code warning may cause confusion.

Consider simplifying the test to make the intent clearer:

Suggested refactor
     test('接続失敗時はリトライカウントがリセットされない', () {
       retryCount = 3;
-      final connected = false;
-      if (connected) {
-        retryCount = 0;
-      }
+      // Simulate failed connection - retryCount should NOT be reset
+      // (no reset logic executed when connection fails)
       expect(retryCount, 3);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/terminal_logic_test.dart` around lines 52 - 59, The test
'接続失敗時はリトライカウントがリセットされない' contains dead code inside the if (connected) block
because connected is explicitly false; simplify by removing the unreachable if
and directly asserting that retryCount remains 3 when connected is false: set
retryCount = 3, set connected = false (or omit if obvious), then call
expect(retryCount, 3) (or expect(retryCount, equals(3))) so the intent is clear;
update the test body in test(...) accordingly and keep the test name and
variables retryCount and connected for clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/terminal_logic_test.dart`:
- Around line 52-59: The test '接続失敗時はリトライカウントがリセットされない' contains dead code
inside the if (connected) block because connected is explicitly false; simplify
by removing the unreachable if and directly asserting that retryCount remains 3
when connected is false: set retryCount = 3, set connected = false (or omit if
obvious), then call expect(retryCount, 3) (or expect(retryCount, equals(3))) so
the intent is clear; update the test body in test(...) accordingly and keep the
test name and variables retryCount and connected for clarity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d235c0f2-e5ad-4175-a0e3-0c0c6977004f

📥 Commits

Reviewing files that changed from the base of the PR and between 092e58b and 3f13520.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • lib/screens/terminal_screen.dart
  • test/terminal_logic_test.dart

…age in CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@6m10cmUK
6m10cmUK merged commit 2a0bfa6 into master Mar 6, 2026
3 checks passed
@6m10cmUK
6m10cmUK deleted the refactor/terminal-screen-review branch March 6, 2026 14:19
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