19 Commits
Author SHA1 Message Date
rustdeskandClaude Opus 5.5 256dc9bc12 docs: scope the -p rule to macOS and Windows
Linux CI runs cargo test --workspace, so a Linux-gated test in another
crate already runs there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R
2026-10-14 16:39:57 +08:00
RustDeskandClaude Opus 5.5 b1761ee676 Ci run all dart tests (#16328)
* ci: run every Dart test, not three files

The step ran only the three cursor test files, so the other nine files
under flutter/test (terminal, file model, autocomplete, ...) never ran
on any pull request. With no path the runner takes the whole directory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* ci: run the Rust tests CI could not reach

cargo test --workspace runs with the default features on x86_64 Linux
only, so the tests behind `windows`, `not(target_os = "linux")`,
`unix-file-copy-paste` and drmtap_dl's `tests` module never ran: the
platform, portable-service and audio tests, the clipboard crate's file
copy-paste and Windows file-name tests, and the drmtap loader's.

The macOS and Windows build jobs now run `cargo test --lib` after the
build with the build's own feature set, plus the clipboard crate. The
Linux job's list of named test runs gains drmtap_dl and the clipboard
feature. AGENTS.md says where tests go and what CI runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

* ci: drop the skips for tests that no longer exist

#16539 removed test_get_cursor_pos and test_get_key_state, the only
tests these skips matched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 16:31:58 +08:00
0740063863 Keyboard shortcuts (#14933)
* fix web break introduced in 38f130071 fix(linux): enable mouse side buttons in remote sessions (#14848)

* feat(shortcuts): user-configurable keyboard shortcuts for session actions

  Adds a keyboard shortcut feature (Rust matcher + Dart UI + cross-language
  parity tests) that lets users bind combinations like Ctrl+Alt+Shift+P to
  session actions. Bindings are stored in LocalConfig under
  `keyboard-shortcuts`; the matcher gates dispatch on `enabled` and
  `pass_through` flags so flipping the master switch off is a hard stop.

  Wire-up summary:
  - src/keyboard/shortcuts.rs: matcher, default bindings, parity test against
    flutter/test/fixtures/default_keyboard_shortcuts.json
  - src/keyboard.rs: shortcut intercept in process_event{,_with_session},
    feature-gated to `flutter`; runs before key swapping so users bind to
    physical keys
  - src/flutter_ffi.rs: main_reload_keyboard_shortcuts +
    main_get_default_keyboard_shortcuts; reload_from_config seeded in main_init
  - flutter/lib/common/widgets/keyboard_shortcuts/: shared config page body,
    recording dialog, shortcut display formatter, action group registry
  - flutter/lib/desktop/pages/desktop_keyboard_shortcuts_page.dart and
    flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart: platform
    shells around the shared body
  - flutter/lib/models/shortcut_model.dart: per-session ShortcutModel +
    registerSessionShortcutActions for actions with no toolbar TToggleMenu /
    TRadioMenu (fullscreen, switch display/tab, close tab, voice call, etc.)
  - flutter/lib/common/widgets/toolbar.dart: optional `actionId` field on
    TToggleMenu / TRadioMenu, plus per-helper auto-register pass that wires
    tagged entries' existing onChanged into the ShortcutModel
  - flutter/test/keyboard_shortcuts_test.dart + fixtures: cross-language
    parity (default bindings, supported key vocabulary)

  Design principles applied during review:

  1. Additions are fine; modifications to original logic must be deliberate.
     Tagging an existing TToggleMenu entry with `actionId:` is an addition.
     Rewriting its onChanged to satisfy a new contract is a modification —
     and was reverted for every case where the original click behavior was
     working. Four closures were touched and then reverted (mobile View
     Mode, Privacy mode multi-impl, Relative mouse mode, Reverse mouse
     wheel); their shortcuts are wired via standalone closures in
     shortcut_model.dart instead.

  2. Toolbar auto-register is reserved for entries whose onChanged is
     inherently self-flipping — typically `sessionToggleOption(name)` where
     the named option is flipped in place and the input bool is unused. The
     register pass passes `!menu.value` from registration time, which is
     harmless under self-flipping but wrong for closures that consume the
     input bool directly. Tagging a non-self-flipping entry forces a closure
     rewrite; choose non-toolbar registration in that case.

  3. When shortcuts are disabled, toolbar behavior must be bit-for-bit
     unchanged. The matcher's `enabled`-gate already guarantees no
     dispatch; the auto-register pass is left unconditional (its only effect
     is HashMap operations on a separate ShortcutModel) so mid-session
     enable works without a reconnect. The trade-off is intentional and
     documented at the top of toolbarControls.

  4. Comments stay terse. Rationale lives in one place — the doc comment of
     the helper or registration site, not duplicated at every call site.

  5. Where an existing helper needs a new optional behavior (e.g.
     `_OptionCheckBox` gaining a tooltip slot), the new branch must reduce
     to byte-identical output for existing callers (`trailing == null`
     case → original `Expanded(Text)` layout). Verified.

  6. Action IDs and labels stay consistent. Renamed `reset_cursor` →
     `reset_canvas` so the action ID matches its user-facing label
     ("Reset canvas") and capability flag.

  Out-of-scope but included:
  - AGENTS.md: documents flutter_rust_bridge no-codegen workflow and the
    Web target's hand-written TS client, since both are load-bearing for
    any new FFI work.
  - remote_toolbar.dart: i18n fix for the per-monitor tooltip ("All
    monitors" / "Monitor #N"), unrelated to shortcuts but kept here.

* langs

* fix(keyboard): shortcuts, harden config and callback lifecycle

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat(keyboard): shortcuts, color of "Reset to defaults"

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat(keyboard): shortcuts, release keys before shortcut callback

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat(keyboard): shortcuts, debug web

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(keyboard): iPad, icon

Signed-off-by: fufesou <linlong1266@gmail.com>

* langs: add shortcut keys to az, gl, ur

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix(keyboard): shortcuts, keep local modifiers and release keys on the right session

When a chord fired, the shortcut path reused release_remote_keys(), which
replays synthetic releases through process_event(). That cleared the local
MODIFIERS_STATE while the user still held the modifiers, so the next key of
a held chord no longer matched and leaked to the remote, and it sent the
releases to the globally current session instead of the one that typed the
chord.

The shortcut path now releases the remote keys itself: it sends through the
caller's session, and restores the modifiers it released on the remote as
still held locally. The press that fired is remembered until its release,
so its auto repeats and its release are consumed instead of reaching the
remote.

Also state the contract for unavailable actions: a bound chord always
belongs to RustDesk and is a no-op when the session has no handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix(keyboard): shortcuts, record the physical key

The recording dialog and the Web Flutter matcher named keys by
LogicalKeyboardKey, while the native matcher sees the physical key through
its USB HID usage and the Web JS matcher through KeyboardEvent.code. On
AZERTY, QWERTZ, Dvorak and similar layouts a recorded binding then did not
fire, or fired from another key.

Name keys by their physical position everywhere: physicalKeyName maps the
USB HID usage to the stored name. A new fixture pins every name to its USB
HID usage, checked on the Dart side and against rdev::usb_hid_key_from_code
on the Rust side, and a test covers AZERTY and QWERTZ events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix(keyboard): shortcuts, keep an empty binding list on re-enable

setEnabled seeded the defaults whenever the binding list was empty, so a
user who cleared every binding got all defaults back after disable and
enable. Seed only when the config has never held a bindings list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix(keyboard): shortcuts, track every fired key until its release

FIRED_KEY held a single key, so overlapping shortcut keys broke it: with the
chord held, P then C overwrote P, P's release reached the remote, and a P
repeat could fire the screenshot again. A repeat after the modifiers were
released was also treated as a new press and leaked to the remote.

Keep a set of fired keys instead. Once a press fires, every event of that
key belongs to RustDesk until its release: repeats are consumed without
dispatching, whatever the modifiers do, and the release removes the key.
Entries whose release never arrives are dropped when a session view is
entered or a session is closed, not by guessing from the modifiers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix(keyboard): shortcuts, keep fired keys per session

FIRED_KEYS was process-global while dispatch is per session. Closing or
entering any session cleared the fired keys of all of them, so a key still
held in another session leaked its release to that remote, and a key fired
in one session made the same key in another session look like a repeat.

Key the fired set by session: try_dispatch resolves the session first and
only looks at that session's keys, and session_close clears only the
closing session. Entering a session view no longer clears anything: it is
a pointer enter, and a fired key may still be physically held across it.
A key whose release is missed otherwise heals itself: its next press is
consumed as a repeat and that release removes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix(shortcuts): preserve input state and refresh action availability

* fix(keyboard): shortcuts, keep a fired key owned across session close

Close tab is itself a shortcut, so the session that consumed the key press
is gone before the key is released. Both matchers dropped the key with the
session: the Flutter dispatcher removed its own entries in clear(), and the
Rust set was keyed by session and cleared in session_close. The release,
and the repeats while the key stayed held, then reached the next session's
remote as ordinary input.

Ownership of a fired key is physical, so it is now one set shared by every
session on each side, and closing a session no longer touches it: clear()
only drops the pending action and modifier replay, and the Rust session
state only tracks released modifiers. A key whose release is missed heals
itself on its next press. Entering view-only with the Flutter input source
still clears the Rust set, since that source stops routing key events to it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix(keyboard): shortcuts, drop the Toggle Input Source action

The action switches the key capture backend between key down and key up.
Fired-key ownership lives in whichever matcher saw the press, the Flutter
dispatcher or the Rust set, and there is no handoff between them: after the
switch the repeats and the release of the held key reach the other backend,
which forwards them to the remote and, while the chord is held, matches the
action again and toggles the source back. Remove the shortcut instead of
building a handoff for one action. The toolbar radio menu is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* perf(keyboard): shortcuts, parse the config once per stored value

The Flutter matcher read the local config three times and parsed its JSON
three times on every key press in legacy mode, on Web and in view-only.
Keep one parsed snapshot keyed by the raw stored string, so a key press
costs one config read and no parsing while the config is unchanged. The
existing readers are thin views of the same snapshot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix(keyboard): shortcuts, run focus-changing actions on key release

Close tab and tab switching move keyboard focus to another session before
the key that fired them is released. On Linux the Flutter input source
routes map-mode sessions through the Rust matcher and legacy-mode sessions
through the Dart matcher, and neither knows the other's fired keys, so
after such an action the repeats and the release of the held key reached
the next session's remote, and the key stayed recorded as fired in the
matcher that saw the press.

Routing Linux legacy mode through the Rust FFI path instead is not a drop-in
change: the raw key handler builds its event with no unicode, so Rust's
legacy mode would fall back to US key names and lose the layout-aware
characters the Dart path sends today, and it would take modifiers from the
X11 key state instead of Flutter's.

Run these three actions when the fired key is released. The matcher that
consumed the press then also sees the repeats and the release, the remote
modifiers are still released on the press, and a session that closes drops
the action it owed. Both matchers keep the same list of actions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* test(keyboard): shortcuts, pin the key-up action list on both sides

kShortcutActionsRunOnKeyUp and runs_on_release are two hand-kept lists of
the actions that run on key release. Add a shared fixture and check each
side against it, the way the key vocabulary and the default bindings are
already checked, so adding a focus-changing action on one side breaks a
test until the other side follows. The Dart test also checks that every
listed id is a configurable action.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D

* fix: prevent lost input after remote session shortcuts

Avoid synthetic Alt presses that activate the Windows system menu
and swallow the next character.

Trigger restart, switch-sides, and chat actions on key release so
focus changes cannot leave subsequent shortcut presses ignored.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(mobile): correct Chat keyboard and input helper behavior

- Keep soft keyboard and helper hidden when Chat opens via shortcut
- Restore soft input on touch and remote focus after Chat closes
- Handle modifier releases while Chat owns focus
- Clear hidden virtual modifiers while preserving held keys and Pin behavior

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(shortcuts): prevent recording non-simultaneous key combinations

Build new captures from currently held keys instead of reusing released
letters. Preserve the last valid chord after release so it can be saved.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(shortcuts): isolate window actions and prevent duplicate Alt presses

- Dispatch shortcut actions only to the originating session window.
- Clear local modifier state on focus loss without resending modifiers
  already released to Windows peers, preventing lost following input.
- Strengthen the regression assertion against extra Alt keydown events

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(shortcuts): empty default shortcuts for Cmd+Option+Shift+Del on macOS and iOS

Because Del may be ambiguous for new macOS users.
Cmd+Option+Shift+Backspace is "Empty Trash without confirmation" shortcut on macOS.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: use session peer platform for Ctrl+Alt+Del events

Fix Android-to-Linux Ctrl+Alt+Del delivery by using the session's peer
platform instead of the desktop global session, which defaults to Windows

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: hide relative mouse mode shortcuts on Wayland

Separate relative mouse mode availability from screenshot support.
Filter unsupported actions and defaults while preserving saved bindings

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: align shortcut availability with platform support

- Hide unsupported mobile and web shortcut actions
- Enable mobile scaling and Android voice call shortcuts
- Use openMonitorInTheSameTab() for display switching
- Preserve cursor position when switching displays on desktop and web

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: prevent held-key shortcut recording bypass

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: preserve keyboard input after shortcut dialogs

Run native clipboard shortcuts on key release and wait for remote modifier releases before dispatching.

Release remote modifiers when mobile local UI takes focus, and document the tested iPad Bluetooth Mute shortcut limitation.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: preserve remote key state during local shortcuts

Keep unrelated held keys pressed for session actions that retain input. Preserve key release when focus or input handling changes, and avoid synthetic Alt taps during shortcut dispatch.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: suspend saved relative mouse shortcuts on Wayland

Filter the native active binding cache without changing stored configuration, so the binding becomes available again under X11.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: exclude unsupported Wayland shortcuts from Flutter matching

Keep saved bindings intact while filtering the active config used by the Flutter matcher. Cover stored bindings and enable/pass-through flags in the existing shortcut tests.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: retain forwarded key ownership across shortcut repeats

Ignore repeat presses for keys already forwarded to the peer, and release ordinary held keys before Show My Cursor enables view-only. Cover both paths with converter-level regressions.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: release Flutter-held keys before view-only shortcuts

Track ordinary keys forwarded by the Dart matcher paths and await their releases before View Only or Show My Cursor disables input. Keep Mute key ownership and existing empty-cleanup timing unchanged.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: filter active Flutter shortcuts by platform capabilities

Reuse the Settings action filter for saved bindings while preserving storage, default-seeding behavior and configurable Apple Ctrl+Alt+Del bindings.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: filter native shortcut caches for mobile controllers

Suspend saved desktop-only actions on Android and iOS and hide unavailable iOS actions without changing persisted bindings. Keep native cache filtering aligned with Flutter platform capabilities.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: share filtered shortcut bindings with the Web matcher

Pass the active platform config to JavaScript at startup and after settings changes, preserving unsupported saved bindings in local storage.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: release Flutter-held keys before focus-changing shortcuts

Drain ordinary keys owned by the original session before run-on-key-up
actions and native clipboard callbacks. Preserve Mute holds, pending-action
cancellation and Web clipboard key-down activation.

Document the required Web matcher counterpart in the bridge:
rustdesk/rustdesk-web-js@94abe431d6

Validation: 61 native Flutter tests, 32 Chrome/HTML tests, targeted analysis
and Windows debug build.

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: remove the useless comments

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: prevent Windows menu activation from Alt-only shortcuts

Neutralize the forwarded Alt with a balanced temporary Ctrl press when a consumed local shortcut releases its only modifier in Windows Map mode. Keep physical modifier tracking and normal Alt pass-through unchanged.

Add a regression covering both Alt keys, trigger repeats and the eventual physical release. Verified with 36 native shortcut tests and real Windows menu/input checks.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: prevent Alt menu activation in all keyboard modes

Extend Windows Alt-only shortcut neutralization to Translate and Legacy. Keep the temporary Ctrl packets in Map mode so Legacy modifier synchronization cannot release Alt before Ctrl.

Extend the existing regression across keyboard modes and decode scan, virtual and control-key events. Verify 36 native tests and 34 Windows menu replay cases.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: initialize outgoing chat before shortcut dispatch

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: guard Windows Alt release in Flutter legacy shortcuts

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: coordinate Web shortcut dispatch and Alt modifier cleanup

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: distinguish AltGraph from ordinary Alt shortcuts

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: dispatch macOS Web shortcuts without waiting for keyup

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: support legacy peers in privacy mode shortcut

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: exclude AltGraph from Flutter shortcut matching

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: exclude GTK AltGr input from raw shortcut matching

Signed-off-by: fufesou <linlong1266@gmail.com>

* test: trim redundant keyboard shortcut coverage

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(keyboard): prevent Cmd shortcuts from opening remote Start

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(shortcuts): guard Windows menu keys after Ctrl/Cmd swapping

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(shortcuts): align recording and display routing

Reject non-Apple AltGraph captures to match runtime filtering. Honor individual-display windows for next, previous, and all-display shortcuts.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(shortcuts): reject GTK AltGr chords in recorder

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(android): restore keyboard focus for modifier-first input

Focus the existing FlutterView when a modifier is pressed without native view focus. This restores shortcut-first input after returning to RustDesk while preserving focused inputs and normal key dispatch.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(shortcuts): release held keys before keyboard mode changes

Drain Flutter-forwarded keys before keyboard-mode shortcut actions so releases use the original encoding. This prevents held keys from remaining down after a Web Input source 2 mode change.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(i18n): use sentence case for shortcut labels

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(mobile): preserve touch modifiers in the input helper

Keep touch-selected modifiers visible after hiding the keyboard. Clear physical modifier releases locally in view-only mode so they do not reopen the helper.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(web): preserve modifiers across Legacy-to-Map shortcuts

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(shortcuts): check support before registering all displays view

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:31:44 +08:00
rustdeskandClaude Opus 5.5 4812a9815b AGENTS.md: limit corner-case growth and test bloat
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:26:34 +08:00
RustDeskandClaude Opus 5.5 08affc69c4 Fix clipboard range overflow (#16329)
* fix(clipboard): unix file server, bound range reads

The Range request fields nPositionLow and cbRequested are UINT32s carried
in i32 fields, but were sign-extended to u64. A negative cbRequested
became a near-u64::MAX length, `offset + length` wrapped past the clamp,
and the server tried to allocate a u64::MAX buffer, aborting the process.
Sign-extending nPositionLow also rejected offsets whose low word is
>= 2 GiB, breaking reads past 2 GiB in large files.

- decode nPositionLow and cbRequested as u32
- clamp with `length > file.size - offset` (offset <= size is checked above)
- allocate the read buffer fallibly and return an error instead of aborting

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(enigo): win, scale absolute mouse moves without overflow

mouse_move_to multiplied the peer-supplied coordinate by 65535 in i32,
which overflows for large values, and divided by the virtual screen size,
which panics if the metrics come back as 0. Scale in i64, clamp to i32,
and skip the move when the virtual screen size is unavailable. Results
for inputs that did not overflow are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(agents): avoid unthrottled high-frequency debug logs

Debug output is written to the log file, so per-packet / per-event log sites
must use trace or hbb_common's throttled_log!.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): unix file server, reject range reads over the frame limit

A response larger than MAX_FRAME_LENGTH (1 GiB) cannot be encoded for
sending, yet the server allocated and read it first, then failed the send.
Reject such requests before allocating, so a peer can no longer make the
server allocate more than 1 GiB per request. Requests that work today are
unaffected.

Drop the extra allocation-failure log; the caller already logs the error.

Tests cover the wire decoding through read_file_contents (negative
cbRequested, nPositionLow past 2 GiB) and the frame-limit rejection. Tests
that use the global CLIP_FILES now take a shared lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): keep file transfers on the file list they started from

A file-contents request names a file only by its index, and the side that
owns the files resolved it against whatever it held at that moment. On
Windows 10+ Explorer's clipboard also offers FileContents, which the owner
re-read from the live clipboard for every chunk, so copying another file
mid-transfer spliced the new file into the one being pasted (#16332). A
second paste, or another connection's paste, replaced the list the same
way, and on macOS/Linux sync_files replaced CLIP_FILES on every host copy.

Each request now names its list in the existing clip_data_id field: an
FNV-1a hash of the file-descriptor PDU, which both sides already hold.
Owners keep the last 4 lists they sent and serve a request from the list
it names; an unknown id fails instead of reading another file. Peers that
send no id read the list last sent to their connection, and a Windows
owner stops reading the live clipboard once it has changed since the list
was built.

Tests cover a host copy and a second paste during a transfer, a list id
sent to another connection, and the hash's reference vectors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): unix file server, cap a single range read at 16 MiB

A peer could still make the server allocate and read up to the 1 GiB frame
limit per request. Cap the data a single range read returns at 16 MiB. The
read is clamped to the rest of the file first, and a larger one is refused
rather than shortened: a short response reads as end of file to IStream
callers on Windows and would silently truncate the copy.

macOS requests 4 MiB per range (BLOCK_SIZE) and FUSE reads are smaller. A
Windows client forwards the application's IStream::Read size, so only an
application reading more than 16 MiB in one call is affected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(enigo): win, warn (throttled) when a mouse move is skipped

A missing virtual screen size is a fault that should show up in a user's
log, but mouse_move_to runs per input event. Log it at warn at most once a
minute via throttled_log! instead of at trace.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): review fixes for the file lists kept per transfer

unix owner:
- A retired list closes its files and 8 MiB read buffers; they reopen,
  from offset 0, on the next read.
- The new list is built aside and swapped in only on success, so a failed
  rebuild leaves the current list in place.
- A rebuild with an unchanged id keeps its peers on the new list instead of
  retiring a duplicate, so re-copying a directory no longer pushes out a
  list that a transfer is still reading.

Windows:
- One served-list slot per list, recording every connection it was sent
  to, so pastes of the same list do not use up slots.
- A request for an earlier list is served before OleGetClipboard, so a busy
  clipboard cannot fail it.
- An IStream read over 16 MiB is split into requests the unix file server
  accepts. Reads up to 16 MiB take the unchanged path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): give each copied file list its own id

The list id hashed only the descriptors, so two copies with identical
names, sizes and times (the same file in two folders) shared an id and a
transfer could read the other copy's files. Each owner now writes a random
nonce into the first descriptor's clsid, inside the reserved bytes every
peer skips; no owner sets FD_CLSID. The paste side hashes what it received,
so it needs no change, and older peers ignore the bytes. Re-sending the same
paths with the same descriptors keeps the nonce, and the id.

Windows owner:
- Serve a request from its list before the ownership check when the list is
  no longer current or the host clipboard now holds files from a peer, so
  another controller copying onto the host no longer fails in-flight
  transfers. Only lists sent to that connection are served.
- Evict the list sent least recently instead of in insertion order.

unix owner: take everything from the crate in one use block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard,enigo): bound served-list reads, clamp absolute moves

wf_cliprdr: the served-list branch runs before the ownership check, yet it
allocated the peer's cbRequested (up to 4 GiB) before looking the list up.
Accept only FILECONTENTS_SIZE or a range of at most 16 MiB there, which is
all that clients sending an id ever request, and fail anything else before
allocating.

enigo: clamp the scaled absolute coordinate to 0..=65535, the range
MOUSEEVENTF_ABSOLUTE takes, instead of to the i32 range. Coordinates on the
virtual desktop give the same result as before; off-desktop ones now land on
its edge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): keep a file list while it is being read, throttle read errors

A unix paste side fetches every new file list as soon as the host copies,
so each copy retires the previous list, and four copies during one long
transfer evicted the list it was still reading. A kept list that serves a
read now becomes the most recent (unix: back of the retired queue, Windows:
last_served), so the next eviction takes a list nobody is reading.

A failed file-contents read logged an error per request, and peers can now
trigger new ones (an unknown list id, an over-limit range). Log it through
throttled_log! instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): unix, keep retired lists in the order they were sent

A peer that sends no list id is served the list last sent to its
connection, found by searching the retired queue from the back. That only
holds while the queue stays in retirement order, which is the order lists
were sent. The previous commit moved a list to the back on every read, so
another connection reading an older list could make a peer's next chunk
come from that list: a transfer of B continued with A's bytes.

Keep the queue in retirement order and track recency in a separate
last_used stamp, set on retirement and on each read. A full queue evicts the
list with the lowest stamp, so a list still being read is kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): unix, keep files being read open when their list is retired

Retiring a list closed every file handle, so the next read of a file part-way
through reopened it by path. If that path had been replaced meanwhile (an
editor saving over it), the rest of the transfer came from the replacement:
AAAABBBB arrived as AAAADDDD. Close only files that are not being read, such
as the preloaded next file; a file part-way through keeps its handle and
offset, and so its identity.

Tests read two files of one list alternately, retire the list, replace both
paths by rename, and check each file continues with its original bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): cap Windows ranges, expire idle kept lists, check file identity

Windows owner: refuse every FILECONTENTS_RANGE over 16 MiB before
allocating. Only the served-list branch was capped; requests without an id
still allocated whatever the peer asked for, up to 4 GiB.

Both owners: a kept (retired / non-current) file list that nobody has read
for 5 minutes is dropped, with its open files; its transfer is over or its
peer gone. The current list never expires, as before. On Windows, reads of
the current list through the existing path now also refresh its kept copy,
so a long transfer does not lose its list once a newer one is sent.

Windows owner: old-list reads compare the file's size and last-write time
with the descriptor that was sent, on the handle they read from, and fail
on a mismatch instead of reading whatever file now has that path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(clipboard): unix, check list expiry without backdating an Instant

an_idle_retired_list_expires backdated a timestamp with
Instant::now().checked_sub(TTL + 1s), which is None while the monotonic
clock is under about five minutes, as on a freshly booted CI machine; the
list then never expired and the test failed. expire_retired now takes the
current time, and the test passes a later one instead. Callers pass
Instant::now(), so behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(clipboard): drop read-time refresh, idle expiry and rebuild reuse; tighten checks

Remove the read-time recency and the 5-minute idle expiry on both owners,
and the unix reuse of an unchanged rebuild (has_same_files). They covered
corner cases, added state, and each brought regressions of its own. Kept
lists are again evicted oldest first (unix) or least recently sent
(Windows), 4 at most; a transfer that outlives 4 newer lists fails cleanly.

Windows: serve kept lists after the existing ownership check again, so a
request carrying a list id no longer bypasses it while the host clipboard
holds files from another session. Refuse request types other than size or
range before allocating.

unix: a retired list's file must still match the size and modified time it
was listed with, checked on the handle the read uses; a file replaced at
its path before its read now fails instead of being served.

Tests: drop the four tests of the removed behavior, add
a_retired_list_refuses_a_file_replaced_before_its_read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(agents): handle corner cases raised in review with small fixes or docs

A refactor added to cover a corner case rarely converges: new counters,
timestamps and expiry rules interact with state existing code relies on,
and the next review round finds the problems they introduced. Fix a corner
case when the fix is a few lines with no new state; otherwise document it
as a known limit, and ask before anything larger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 11:25:12 +08:00
rustdesk e5d473407e fix https://github.com/rustdesk/rustdesk/issues/16086 2026-09-08 12:33:10 +08:00
RustDeskandClaude Opus 5 b50fde6910 add the base crate and repoint the moved modules at it (#16107)
* add the base crate and repoint the moved modules at it

`libs/base` (crate `base`) takes the parts of hbb_common that only this app
uses: `fs`, `platform`, `keyboard`, `message.proto`, and 145 of the 177
`config::keys` constants. hbb_common keeps what the server names, and the 32
keys it reads itself are re-exported from `base::config::keys` so call sites
still see the full set through one path.

Sources move verbatim. The only edits inside them are `crate::` prefixes that
now have to say `hbb_common::`; `keyboard.rs` and `platform/windows.rs` are
byte-identical. The crate stays on edition 2018, the edition the moved code was
written under. `log`, `lazy_static` and `anyhow` become direct dependencies so
the bare paths in that code resolve exactly as before, and its winapi features
are spelled out rather than left to feature unification.

Two call sites outside Rust and Cargo had to follow the move: the Android
protobuf source dir, which still pointed at hbb_common/protos for message.proto,
and the three AGENTS.md entries that named hbb_common for options, protos and
file transfer.

`scrap`'s `drm` feature now forwards to `base/wayland_probe`. Left pointing at
hbb_common it would still have compiled, silently dropping the Wayland
socket-probe fallback, so that forward is verified by a build with and without
the feature.

`config::keys` carries a test asserting its names stay disjoint from the ones
hbb_common kept: the glob re-export and the local constants share a namespace,
and Rust prefers the local item silently, so a name added to both sides would
otherwise let client and server disagree with no diagnostic.

Verified: macOS and Linux, debug and release, `--all-targets`; the 177 key
constants diffed name-for-name and value-for-value; the generated protobuf types
compared before and after; every `#[cfg]` gate on a moved import checked against
its original; and every file that was `rustfmt`-clean before this change still
is, compared against master file by file. Windows is checked by inspection only
-- it cannot be compiled here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* one `use` per crate, and write the rule down

`fs.rs` came out of the move with two ungated `use hbb_common::` statements,
because the original single `use crate::{...}` had to give up `message_proto`
to the new crate and the rest was left in a second block. Fold it back into one.

A scan of the whole tree for the same shape finds nothing else: every other file
with more than one top-level `use base::` or `use hbb_common::` is split by a
`#[cfg]` that does not cover the whole block, or by `pub use` next to `use`.
Those are the cases that cannot merge, so AGENTS.md now states both the rule and
the exemption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:46:42 +08:00
RustDeskandClaude Fable 5.1 9a1c8da143 Agents regression surface (#16070)
* AGENTS.md: require a regression-surface check before a change is done

The minimal-invasiveness rules say what to prefer; nothing made an
agent check the final diff against them, so a feature could still route
the old path through its new code while every principle was "followed".
This adds the gate: audit every modified existing path, keep feature-off
on the old code, report the regression surface, and treat an
unnecessarily rewritten legacy path as a review finding whatever the
tests say.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* AGENTS.md: a scope check before shared code is touched

The minimal-invasiveness rules are principles; this adds the stop
condition that makes them mechanical. A fix for one path stays in that
path, and an unrelated caller needing a placeholder argument to satisfy
a changed signature is the signal that it did not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 19:50:43 +08:00
RustDeskandClaude Fable 5.1 d453a19601 AGENTS.md: require a regression-surface check before a change is done (#16068)
The minimal-invasiveness rules say what to prefer; nothing made an
agent check the final diff against them, so a feature could still route
the old path through its new code while every principle was "followed".
This adds the gate: audit every modified existing path, keep feature-off
on the old code, report the regression surface, and treat an
unnecessarily rewritten legacy path as a review finding whatever the
tests say.


Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 18:31:31 +08:00
cbf9440281 Prefer active X11 session display (#15933)
* Prefer active X11 session display

* Update linux.rs

* fix(linux): keep the logind display only when it is a local one

`get_display_from_session` returns the value pam_systemd was handed at session
creation, and logind never updates it afterwards. That value is not always a
usable local display: it can be qualified with this host (`myhost:0`), name an
X forwarding endpoint (`localhost:10.0`), or be a bare `:`.

Taking it unconditionally is worse than taking nothing, because a non-empty
`self.display` suppresses every fallback below it, `get_display_by_user` and the
`:0` default alike. The stripping at the end of `get_display_x11` does not save
the last two cases either: it leaves `:` as is and turns `localhost:10.0` into a
local looking `:10.0`, either of which is then exported as DISPLAY and leaves the
session unreachable, where before this PR the host got a working `:0`.

Strip this host so `myhost:0` is still accepted as `:0`, leave `localhost` in
place, and require a display number after the colon. Anything else falls through
to the existing chain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA

* docs(agents): prefer a little duplication over a restructure

The "Be minimally invasive" rules already ask for purely additive diffs, but not
in the case where the addition would otherwise reshape an existing function so
the two can share code. Repeating a few lines is the better diff there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:28:12 +08:00
rustdesk f1a06f6765 review rules 2026-08-20 19:22:36 +08:00
rustdesk 0c00d576dd improve comment rules 2026-08-18 12:31:59 +08:00
rustdesk dfca2c1b8f update agents.md 2026-08-12 17:28:59 +08:00
RustDeskandClaude Opus 5 9a81c8a138 Drm deb in release workflow (#15776)
* docs(agents): add a comment-length rule

Comments were growing to document rejected alternatives, past bugs and
measurements. That belongs in the commit message, not the source.

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

* ci(drm): build the unattended-wayland deb in the release workflow

The deb was built by a separate drm-capture workflow on a plain runner,
so it diverged from every other Linux deb: different base, different
vcpkg/ffmpeg, different toolchain. Move it into flutter-build.yml as
build-rustdesk-linux-drm, mirroring build-rustdesk-linux's x86_64 path --
same ubuntu18.04 container, same vcpkg install, same rust and flutter.
libdrmtap is built on the runner first and handed to the container via
DRMTAP_PREBUILT_DIR, because bionic's meson is too old to build it.

The job is ungated, so the --drm packaging path is exercised on every PR;
only publishing stays gated on upload-artifact. drm-capture.yml is
deleted along with docs/DRM_CAPTURE_SECURITY.md -- the 29 drm unit tests
that workflow ran are no longer executed by CI.

Three bugs the move exposed:

- build.py anchored the libdrmtap paths on abspath(__file__), which is
  only cwd-independent on Python >= 3.9 (bpo-20443). The packaging
  container runs 3.6 and chdir's into flutter/, so the ABI-gate
  cross-check resolved one directory off and every --drm packaging run
  would have died with FileNotFoundError. Captured as REPO_ROOT at
  import instead.
- DRMTAP_PREBUILT_DIR no longer needs DRMTAP_ALLOW_UNPINNED. A prebuilt
  dir inside the repo's own third_party/libdrmtap at the pinned sha is
  the pinned object, not an override, and is now verified as such.
- The variant's Depends carried a bare libdrm2. libdrmtap needs
  drmModeGetFB2, so it is libdrm2 (>= 2.4.95); below that the package
  installed and could never capture.

The loader also logs the dlerror now instead of discarding it, so a
soname or glibc mismatch is named rather than surfacing as a generic
"libdrmtap not available".

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

* fix(drm): declare the unattended-wayland deb's real libc6 and libdrm floors

libdrmtap is built on the ubuntu-22.04 runner while the rest of the deb comes
from the ubuntu18.04 container, so the package has a mixed glibc floor and
declared neither half. It installed happily on Ubuntu 20.04 / Debian 11
(glibc 2.31), then dlopen failed on GLIBC_2.34 and capture degraded to the
PipeWire portal -- the one thing this variant exists to avoid. Measure the
floor off the staged objects and put it in Depends, so apt refuses with a
reason instead of handing over a package that can never capture.

Measured rather than written down: the number moves whenever either base does,
and it lands exactly on RHEL/Rocky 9 (glibc 2.34), where one off-by-one decides
whether that whole family can install.

drmModeGetFB2 landed in libdrm 2.4.101, not 2.4.95 -- checked against the
libdrm tags, xf86drmMode.h first declares it in 2.4.101. The old floor admitted
Debian 10 (2.4.97), where the .so is linked -z now and dies on an undefined
symbol at dlopen. libdrmtap's own meson.build carries the same wrong number.

Upload the deb on always(): the run that fails the drm check is the one whose
artifact is most worth downloading. Publish stays gated on success, so an
unverified build still cannot reach a release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:31:09 +08:00
RustDeskandClaude Fable 5 85a5fefab8 fix(windows): prevent ghost and duplicate tray icons (#15689) (#15690)
* docs(agents): require minimally invasive, additive-first patches

Codify the review feedback from the tray ghost-icon fix: fixes should
add self-contained code around existing lines instead of restructuring
them, keep platform-specific logic in src/platform/ with fn-local
imports, and leave only thin one-line hooks in shared files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(windows): stop duplicate tray icons from piling up (#15689)

`check_process("--tray", ..)` is used to decide whether a tray process
needs to be spawned, but it can miss one that is already running: it
cannot read the command line of an elevated process from a non-elevated
one (the installer spawns the tray elevated), and wmic, used by 32-bit
builds since #11638, is gone from newer Windows 11. `connection.rs` runs
that check once per incoming connection, so every miss added another tray
icon and they kept piling up, which is the same blind spot behind #6692.

Hold a named mutex in the session namespace as the authoritative single
instance guard, so a redundant tray process exits before creating an
icon. `ERROR_ACCESS_DENIED` also counts as "already running", since it
means the mutex belongs to a tray we may not touch.

Also remove the icon before the tray menu's "Stop service" calls
uninstall_service(): on success it ends the process with
std::process::exit, which skips the destructor that would call
Shell_NotifyIcon(NIM_DELETE), so every click left a ghost icon behind.
The icon is shown again if stopping the service failed or was cancelled.

Ghost icons from the taskkill in the install/update/service flows are
left alone here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(windows): note that update_me's pid lookup can silently find nothing

The pids are matched by command line, which comes back empty for a 32-bit
build reading 64-bit processes (hence the `wmic` fallback of #11638, and
`wmic` is no longer installed by default since Windows 11 24H2) and for a
non-elevated process reading an elevated one. `taskkill` matches by image
name and still works, but the session lists are then empty, so the restore
guard silently restores nothing and the update leaves the user without a
tray icon and main window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(windows): record the confirmed cause of the duplicate tray icons

Process Explorer output in #15689 pinned it down: run_after_run_cmds()
spawns the tray in the caller's own context, so installing or toggling
the service from a RustDesk that was itself started elevated leaves a
high integrity tray behind, which a medium integrity main window cannot
inspect afterwards. Record where the detection fails exactly, so the next
reader doesn't have to rediscover that the executable path, not the
command line, is what comes back empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 12:18:57 +08:00
d6ea170061 Id whitelist (#15586)
* id whitelist

* hbb_common

* Update flutter/lib/common/widgets/dialog.dart

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* support wss:// for web client

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix: handle ID copying separately and remove whitelist logs

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix en translation

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix: check switch-side ID whitelist after login initialization

Signed-off-by: 21pages <sunboeasy@gmail.com>

* track pending 2FA challenge state

Signed-off-by: 21pages <sunboeasy@gmail.com>

* support Unicode IDs in whitelist settings

Signed-off-by: 21pages <sunboeasy@gmail.com>

* refactor: unify client ID resolution

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
2026-07-27 23:24:32 +08:00
rustdesk 0c86d46162 translate all 2026-06-02 23:33:40 +08:00
fufesouandRustDesk 3a1622e8b5 refact(AGENTS.md): code rules, tokio (#14911)
* refact(AGENTS.md): code rules, tokio

Signed-off-by: fufesou <linlong1266@gmail.com>

* Update AGENTS.md

* Update AGENTS.md

* Update AGENTS.md

* Update AGENTS.md

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-04-26 21:25:31 +08:00
rustdesk 0cf3e8ed40 improve agent md 2026-04-09 15:12:57 +08:00