{"owner":"telegramdesktop","repo":"tdesktop","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md"],"skills":{"AGENTS.md":"# Agent Guide for Telegram Desktop\n\nThis guide defines repository-wide instructions for coding agents working with the Telegram Desktop codebase.\n\n## Working from Codex on Windows + WSL\n\nThis checkout may be opened in Codex Desktop through the Windows UNC path `\\\\wsl.localhost\\{distro}\\home\\{user}\\Telegram\\tdesktop`, while the real Linux path is `/home/{user}/Telegram/tdesktop`. Treat it as a WSL/Linux checkout first, not as a native Windows checkout.\n\n- Prefer running repository-aware commands through WSL:\n\n```powershell\nwsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- <command>\n```\n\n- PowerShell can read and write files through the UNC path, but native Windows tools may see different ownership, path, executable, or line-ending behavior than Linux tools.\n- Git from PowerShell over `\\\\wsl.localhost\\...` can fail with `detected dubious ownership`. Use WSL Git instead. Do not change global Git `safe.directory` settings unless the user explicitly asks for that.\n- Keep path styles matched to the shell. Use `/home/{user}/Telegram/tdesktop/...` with WSL commands, and quoted `\\\\wsl.localhost\\{distro}\\home\\{user}\\Telegram\\tdesktop\\...` paths with native Windows commands. Avoid passing UNC paths to Linux tools or Linux paths to native Windows tools unless the tool explicitly supports them.\n- If a command behaves strangely from the PowerShell UNC working directory, retry the same command through `wsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- ...` before concluding the repository or command is broken.\n- Recursive searches and repo inspection are usually faster and more faithful through WSL, for example `wsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- rg ...`.\n- Do not assume the WSL host has the build toolchain installed directly. In this setup, WSL may not have `cmake`, while Windows may have `cmake`, and the configured `out/` tree may still target the Linux Docker toolchain. Do not run native Windows `cmake --build out` against a Linux/Docker build tree.\n- For WSL/Linux builds, use the Docker build entry point from the repository root: `Telegram/build/docker/centos_env/build_debug.sh`. The Docker daemon must be reachable from WSL; checking `docker info` is fine, but do not start a build unless the user asked for one.\n- Existing build outputs may be Linux binaries, for example `out/Debug/Telegram` as an ELF executable, not `Telegram.exe`. Verify the build tree before assuming which platform produced it.\n- Be careful with text file line endings. In a WSL/Linux checkout, files should remain LF-only unless the file already uses another convention. CRLF finishing applies only to native, non-WSL Windows runs/checkouts. Do not let PowerShell or Windows tools silently rewrite WSL files to CRLF. If a file becomes mixed, normalize it back to the convention appropriate for the current checkout, without adding a UTF-8 BOM.\n- When using the local `perform-task` skill from this WSL checkout, keep external AI task artifacts and edited project text files LF-only. Treat its Windows text-normalization phase as not applicable to WSL, except to record that line endings were checked and kept LF/no-BOM. Run CRLF normalization only in a native, non-WSL Windows checkout.\n\n## Build System Structure\n\nThe build system expects this directory layout:\n\n```text\nL:\\Telegram\\                    # BuildPath\nL:\\Telegram\\tdesktop\\           # Repository (you work here)\nL:\\Telegram\\Libraries\\          # 32-bit dependencies (Linux/macOS)\nL:\\Telegram\\win64\\Libraries\\    # 64-bit dependencies (Windows)\nL:\\Telegram\\ThirdParty\\         # Build tools (NuGet, Python, etc.)\n```\n\nDependencies are located relative to the repository: `../Libraries`, `../win64/Libraries`, or `../ThirdParty`.\n\n## Build Configuration\n\n### Build Commands\n\n**From repository root, run:**\n\n```bash\ncmake --build out --config Debug --target Telegram\n```\n\nThat's it. The `out/` directory is already configured. The executable will be at `out/Debug/Telegram.exe`.\n\n**From WSL, run through the Linux Docker build environment:**\n\n```bash\nTelegram/build/docker/centos_env/build_debug.sh\n```\n\n**Important:** When running cmake from a shell that doesn't support `cd`, use quoted absolute paths:\n```bash\ncmake --build \"l:\\Telegram\\tx64\\out\" --config Debug --target Telegram\n```\n\n**Never build Release** - it's extremely heavy and not needed for testing changes.\n\n## Platform-Specific Requirements\n\n### Windows\n- Requires Visual Studio 2022\n- Must run from appropriate Native Tools Command Prompt:\n  - \"x64 Native Tools Command Prompt\" for `win64`\n  - \"x86 Native Tools Command Prompt\" for `win`\n  - \"ARM64 Native Tools Command Prompt\" for `winarm`\n- Dependencies: `../win64/Libraries` (64-bit) or `../Libraries` (32-bit)\n\n### macOS\n- Requires Xcode\n- Dependencies: `../Libraries/local/Qt-*`\n- Set `QT` environment variable: `export QT=6.8`\n\n### Linux\n- Build dependencies in `../Libraries`\n- Set `QT` environment variable if needed\n\n## Key Files\n\n- **`Telegram/build/version`** - Version information\n- **`out/`** - Build output directory\n\n## Troubleshooting\n\n### \"Libraries not found\"\nEnsure the repository is in `L:\\Telegram\\tdesktop`. The build system requires `../win64/Libraries` to exist.\n\n### Build fails with \"wrong command prompt\"\nOn Windows, use the correct Visual Studio Native Tools Command Prompt matching your target (x64/x86/ARM64).\n\n### macOS crashes while reading the cached language pack\n\nAfter an incremental Xcode build that regenerated `lang.strings` outputs, the\napp can link a new generated key lookup with stale objects that still use an\nolder `kKeysCount`. The characteristic failure is:\n\n- the Debug log stops immediately after\n  `Lang Info: Loaded cached, keys: ...`;\n- stderr and `tdata/working` may be empty;\n- a fresh `~/Library/Logs/DiagnosticReports/Telegram-*.ips` shows `SIGABRT`\n  from `std::vector<unsigned char>::operator[]`, then\n  `Lang::Instance::applyValue()`, `fillFromSerialized()`, and\n  `Local::readLangPack()`.\n\nIf this exact startup failure repeats twice, do not change the implementation,\ntest overlay, or portable account. Stop only this checkout's exact Telegram\nprocess. Because Xcode's `CONFIGURATION_BUILD_DIR` is `out/Debug`, make a\nsafety copy of every existing portable folder outside `out/` before cleaning:\n\n```bash\nportable_backup_root=\"$(mktemp -d \"${TMPDIR:-/tmp}/tdesktop-portable-clean.XXXXXX\")\"\nfor portable_name in \\\n  TelegramForcePortable \\\n  test_TelegramForcePortable \\\n  real_TelegramForcePortable; do\n  if [ -d \"out/Debug/$portable_name\" ]; then\n    ditto \"out/Debug/$portable_name\" \"$portable_backup_root/$portable_name\"\n  fi\ndone\n```\n\nRequire every expected backup copy to exist before continuing. Then perform\none full Xcode Debug clean and rebuild:\n\n```bash\ncmake --build out --config Debug --target clean\ncmake --build out --config Debug --target Telegram\n```\n\nAfterward, restore a portable folder from the backup only when its original\npath is missing; never overwrite a folder that survived the clean. Verify all\nthree original folder names that existed before the clean are present, keep\nthe backup until the rebuilt app completes one successful launch, and record\nits path if the run stops before verification. Then rerun the same test once.\nIf the signature persists after that clean rebuild, continue normal crash\ndiagnosis or report the blocker. Do not loop clean rebuilds.\n\n### Build output locks\n\nFor builds owned by the autonomous `continue` / `perform-task` workflow, read\nand follow `.agents/shared/build-lock-recovery.md`. PDB, EXE, OBJ, and other\nbuild-output lock errors are recoverable: stop only the exact checkout\nexecutable or verified build-tree holders, delete only exact named artifacts\ninside that checkout's build tree, and retry within the bounded recovery\nbudget. Never stop an installed Telegram client, another checkout, an IDE, or\nan unknown process.\n\nOutside that autonomous workflow, an exact checkout executable may be running\nbecause the user is testing it. Do not terminate it or delete locked build\noutputs without explicit permission. Report the exact locked path and ask the\nuser to close that checkout's Telegram/debugger before rebuilding.\n\n## Best Practices\n\n1. **Always use Debug builds** - Release builds are extremely heavy\n2. **Don't build Release configuration** - it's too heavy for testing\n\n## Text File Format\n\n- On Windows, keep project text files with CRLF line endings.\n- Do not save source, header, build/config, style, or localization files as UTF-8 with BOM. Use UTF-8 without BOM.\n- When rewriting project text files for normalization, preserve file content otherwise and do not introduce a BOM.\n\n## Commits\n\n- Subject: one concise, plain-language line summarizing the change, ~50-60 characters, matching the style of recent `git log` subjects. This is usually the entire message.\n- For an `ai-tdesktop` task, start the subject with exactly `[ai] ` when the\n  retained task implementation changes permanent test-helper code, the agent\n  harness, or agent documentation in any way. This includes\n  `Telegram/SourceFiles/test/`, `.agents/`, `.claude/`, `AGENTS.md`,\n  `CLAUDE.md`, and files whose sole role is supporting those systems. Do not\n  count the disposable test overlay or external AI task artifacts. For every\n  other task, the subject must not contain `[ai]` anywhere.\n- For ordinary work not associated with an AI task, add a short plain-language body only when the subject can't carry it (what was done, not the technical how) — a line or two at most.\n- Never add a `Co-Authored-By:` line or any tool/assistant attribution trailer.\n- Never add `Autotask:`/attempt or other internal run markers. A commit owned by\n  an `ai-tdesktop` task has exactly three lines: the concise subject, a blank\n  line, and `Task: <task-id>`. Do not add a body. Keep rationale and\n  implementation notes out of the commit message; put a short durable note\n  under `tasks/<task-id>.md` only when useful. Do not copy commit hashes into\n  that note or any AI task artifact; the task id is the cross-repository link.\n\n## Local Storage Serialization\n\nBoth app-level (`Core::Settings`) and session-level (`Main::SessionSettings`) use sequential binary serialization via `QDataStream`. Key rules:\n\n- New fields must ALWAYS be appended at the **end** of the stream, never inserted in the middle\n- Reading new fields must be guarded with `!stream.atEnd()` and provide a meaningful default/fallback\n- Inserting in the middle breaks reading of data saved by older versions (the new read code consumes bytes that belong to subsequent fields)\n- For simple flags and values, prefer using the generic KV prefs facility (`writePref<Type>` / `readPref<Type>`) instead of adding to the binary stream -- this avoids serialization ordering issues entirely\n\n---\n\n# Development Guidelines\n\n## Coding Style\n\n**Do NOT write comments in code:**\n\nThis is important! Do not write single-line comments that describe what the next line does - they are bloat. Comments are allowed ONLY to describe complex algorithms in detail, when the explanation requires at least 4-5 lines. Self-documenting code with clear variable and function names is preferred.\n\n```cpp\n// BAD - don't do this:\n// Get the user's name\nauto name = user->name();\n// Check if premium\nif (user->isPremium()) {\n\n// GOOD - no comments needed, code is self-explanatory:\nauto name = user->name();\nif (user->isPremium()) {\n\n// ACCEPTABLE - complex algorithm explanation (4+ lines):\n// The algorithm works by first collecting all visible messages\n// in the viewport, then calculating their intersection with\n// the clip rectangle. Messages are grouped by date headers,\n// and we need to account for sticky headers that may overlap\n// with the first message in each group.\n```\n\n**Style and formatting rules** are in `REVIEW.md` — see that file for empty-line-before-closing-brace, operator placement in multi-line expressions, if-with-initializer, and other mechanical style rules.\n\n**Use `auto` for type deduction:**\n\nPrefer `auto` (or `const auto`, `const auto &`) instead of explicit types:\n\n```cpp\n// Prefer this:\nauto currentTitle = tr::lng_settings_title(tr::now);\nauto nameProducer = GetNameProducer();\n\n// Instead of this:\nQString currentTitle = tr::lng_settings_title(tr::now);\nrpl::producer<QString> nameProducer = GetNameProducer();\n```\n\n**Use trailing return types only when the normal form is too long:**\n\nPrefer the normal return type form when the opening line fits comfortably, roughly around 77 characters or less:\n\n```cpp\n// GOOD:\n[[nodiscard]] TextWithEntities FlattenSummaryBlocks(\n\tconst std::vector<Block> &blocks);\n```\n\nDo not use one-line trailing return types, or put the trailing return type after `)` on the same line. If it fits on one line with trailing syntax, the normal form would be shorter and easier to read:\n\n```cpp\n// BAD:\nauto ComputeTitle() -> QString;\n\n// BAD:\n[[nodiscard]] auto FlattenSummaryBlocks(\n\tconst std::vector<Block> &blocks) -> TextWithEntities;\n```\n\nUse `auto` with a trailing return type only when the normal opening line\n`{attributes} {return-type} {class-name::}{function-name(}` would be too long, or would force the return type onto its own line. Put the arrow and return type on the next line so the return type remains easy to find:\n\n```cpp\n// BAD:\nnot_null<HistoryView::Controls::ComposeAiButton*>\nHistoryView::Controls::SetupCaptionAiButton(SetupCaptionAiButtonArgs &&args);\n```\n\n```cpp\n// GOOD:\nauto HistoryView::Controls::SetupCaptionAiButton(\n\t\tSetupCaptionAiButtonArgs &&args)\n-> not_null<HistoryView::Controls::ComposeAiButton*>;\n```\n\nThis applies to both declarations and definitions.\n\n**Use `_q` for QString literals:**\n\nPrefer the project literal `u\"...\"_q` instead of the verbose `QStringLiteral(\"...\")` macro when creating `QString` values:\n\n```cpp\n// Prefer this:\nauto text = u\"Settings\"_q;\n\n// Instead of this:\nauto text = QStringLiteral(\"Settings\");\n```\n\n**Never use `Q_OS_LINUX` for platform checks in new code:**\n\nTelegram Desktop distinguishes at most three platforms: Windows / macOS / all-other. The \"all-other\" branch covers Linux, the BSD variants and more — and this is almost always the branch you want. `Q_OS_LINUX` narrows it to Linux alone, silently excluding the non-Linux Unix platforms, which is almost never intended. For the all-other branch use `!defined Q_OS_WIN && !defined Q_OS_MAC` at compile time, or its runtime equivalent `Platform::IsLinux()` — which, despite the name, means exactly `!defined Q_OS_WIN && !defined Q_OS_MAC` (\"everything except Windows and macOS\"), not Linux specifically:\n\n```cpp\n// BAD - excludes FreeBSD and other non-Linux Unix:\n#ifdef Q_OS_LINUX\nUnixSpecificCode();\n#endif // Q_OS_LINUX\n\n// GOOD - the all-other branch, compile time:\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\nUnixSpecificCode();\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\n// GOOD - the all-other branch, runtime (same meaning, NOT Linux-only):\nif (Platform::IsLinux()) {\n\tUnixSpecificCode();\n}\n```\n\n`Q_OS_LINUX` is only for the rare case where you genuinely want exactly Linux and not the other Unix-like systems — usually you don't. The few existing uses (`Telegram/SourceFiles/core/sandbox.cpp`, `Telegram/SourceFiles/platform/linux/specific_linux.cpp`) are such genuinely Linux-only code paths and stay as-is.\n\n**Treat CMake `LINUX` as the all-other platform:**\n\nIn this project, `cmake/validate_special_target.cmake` sets `LINUX` in the\nfinal `else()` after checking `WIN32` and `APPLE`. It therefore means\n`NOT WIN32 AND NOT APPLE`, including non-Linux Unix platforms; it does not\nmean exactly Linux. For the usual three-way platform split, write:\n\n```cmake\nif (WIN32)\n    set(platform_source platform/win.cpp)\nelseif (APPLE)\n    set(platform_source platform/mac.mm)\nelse()\n    set(platform_source platform/linux.cpp)\nendif()\ntarget_sources(my_target PRIVATE ${platform_source})\n```\n\nDo not add a separate fallback branch after `if (LINUX)` as though `LINUX`\nwere one platform among several remaining platforms. There are no remaining\nplatforms in this project's CMake platform model.\n\n**Prefer cppgir wrappers over the GLib C API:**\n\nWhen implementing all-other-platform code with GLib, GObject, or GIO, use the\ngenerated cppgir C++ bindings under `gi::repository` as much as possible.\nPrefer their `GLib`, `GObject`, and `Gio` types, ownership handling, results,\nand callbacks over raw `g_*`, `g_object_*`, and `g_io_*` APIs. Use the C API\nonly when cppgir does not expose the required functionality or at a narrow\ninterop boundary that genuinely requires raw GLib types, and keep that raw\nAPI surface as small as possible.\n\n**Generate typed D-Bus bindings from introspection XML:**\n\nFor a D-Bus interface known at build time, prefer the CMake `generate_dbus`\nfunction from `cmake/external/glib/generate_dbus.cmake` over handwritten\n`GDBusProxy` calls, stringly typed method and signal names, or manually\nmaintained C wrappers. Its signature is:\n\n```cmake\ngenerate_dbus(\n    target_name\n    interface_prefix\n    namespace\n    interface_file)\n```\n\n`target_name` is the existing target that will use the bindings,\n`interface_prefix` is the common D-Bus interface prefix passed to\n`gdbus-codegen`, `namespace` names the generated API, and `interface_file` is\nthe D-Bus introspection XML file. Include the helper and call it inside the\nall-other-platform branch:\n\n```cmake\ninclude(${cmake_helpers_loc}/external/glib/generate_dbus.cmake)\ngenerate_dbus(\n    my_target\n    org.example.\n    Example\n    ${src_loc}/platform/linux/org.example.Service.xml)\n```\n\nThe helper runs `gdbus-codegen`, generates proxy, skeleton, and object-manager\ntypes, produces GIR metadata, wraps that metadata with cppgir, and links the\nresult into `target_name`. Consume the resulting typed API from\n`gi::repository::Example` (using the namespace argument from the example);\ndo not edit or separately list files under the build `gen` directory. Use\ngeneric GLib D-Bus calls only when the interface is genuinely dynamic or\ncannot be represented by suitable introspection XML.\n\n## API Usage\n\n### API Schema Files\n\nAPI definitions use [TL Language](https://core.telegram.org/mtproto/TL):\n\n1. **`Telegram/SourceFiles/mtproto/scheme/mtproto.tl`** - MTProto protocol (encryption, auth, etc.)\n2. **`Telegram/SourceFiles/mtproto/scheme/api.tl`** - Telegram API (messages, users, chats, etc.)\n\n### Making API Requests\n\nStandard pattern using `api()`, generated `MTP...` types, and callbacks:\n\n```cpp\napi().request(MTPnamespace_MethodName(\n    MTP_flags(flags_value),\n    MTP_inputPeer(peer),\n    MTP_string(messageText),\n    MTP_long(randomId),\n    MTP_vector<MTPMessageEntity>()\n)).done([=](const MTPResponseType &result) {\n    // Handle successful response\n\n    // Multiple constructors - use .match() or check type:\n    result.match([&](const MTPDuser &data) {\n        // use data.vfirst_name().v\n    }, [&](const MTPDuserEmpty &data) {\n        // handle empty user\n    });\n\n    // Single constructor - use .data() shortcut:\n    const auto &data = result.data();\n    // use data.vmessages().v\n\n}).fail([=](const MTP::Error &error) {\n    // Handle API error\n    if (error.type() == u\"FLOOD_WAIT_X\"_q) {\n        // Handle flood wait\n    }\n}).handleFloodErrors().send();\n```\n\n**Key points:**\n- Always refer to `api.tl` for method signatures and return types\n- Use generated `MTP...` types for parameters (`MTP_int`, `MTP_string`, etc.)\n- For multiple constructors, use `.match()` or check `.type()` against `mtpc_` constants then call `.c_constructorName()`:\n  ```cpp\n  // Using match:\n  result.match([&](const MTPDuser &data) { ... }, [&](const MTPDuserEmpty &data) { ... });\n  // Or explicit type check:\n  if (result.type() == mtpc_user) {\n      const auto &data = result.c_user(); // asserts on type mismatch\n  }\n  ```\n- For single constructors, use `.data()` shortcut\n- Include `.handleFloodErrors()` before `.send()` in rare cases where you want special case flood error handling\n- Silently ignore HTTP 406 errors in UI: the server uses 406 to mean \"show nothing to the user\". Guard toasts with `MTP::IgnoreError(error)` or use `MTP::ShowErrorFallback(show, error)` (both in `mtproto/mtproto_response.h`) which shows `error.type()` as a toast unless the error should be ignored.\n\n### API Request Callback Lifetime\n\n`api().request(...)` callbacks are owned by the session, not by whatever created\nthem. A `.done()` / `.fail()` handler stays alive for the whole session lifetime,\nso a handler that captured a widget, a box, a controller, or any shorter-lived\nstate still runs after that state is gone. A plain `[=]` capture warns about\nnothing, which makes this one of the easiest ways to write a use-after-free here.\n\nCapturing only plain values or session-owned objects is fine. When anything\ncaptured can die before the session does, pick one of three:\n\n**1. Guard the callback with `crl::guard`.** The request is always sent; the\nhandler is skipped when the context is gone. Use when the call itself must reach\nthe server and only the local reaction is optional.\n\n```cpp\napi().request(MTPmethod(\n\t...\n)).done(crl::guard(this, [=](const MTPResult &result) {\n\t// runs only while `this` is still alive\n})).send();\n```\n\nAccepted guards, in rough order of how often they are used: a raw pointer or\n`not_null` to any `QObject`-derived type — widgets, boxes, controllers — where the\n`QPointer` is created on the spot, so passing `this` is the normal case; a raw\npointer or `not_null` to a `base::has_weak_ptr` type; `QPointer`, `QWeakPointer`,\n`QSharedPointer`; `base::weak_ptr`, `base::weak_qptr`; `std::weak_ptr`,\n`std::shared_ptr`; and `base::binary_guard`.\n\n**2. Remember the `mtpRequestId` and cancel it.** Cancel when the result stops\nbeing relevant, and in the destructor. The request may never reach the server —\nif it is still queued when cancelled, or connectivity dies first, it is simply\ndropped — so never use this when the call itself has to happen.\n\n```cpp\n_requestId = api().request(MTPmethod(\n\t...\n)).done([=](const MTPResult &result) {\n\t_requestId = 0;\n\t...\n}).send();\n\n// when the result is no longer relevant, and in the destructor:\napi().request(base::take(_requestId)).cancel();\n```\n\n**3. Own an `MTP::Sender`.** Its destructor cancels everything it sent that is\nstill in flight, so request lifetime follows the owner with no bookkeeping. Same\ndelivery caveat as (2). Prefer this for a widget, box, or controller that issues\nmore than a request or two.\n\n```cpp\n// header\n\tMTP::Sender _api;\n\n// constructor initializer list\n, _api(&session->mtp())\n\n// requests sent through it die with the owner\n_api.request(MTPmethod(\n\t...\n)).done([=](const MTPResult &result) {\n\t...\n}).send();\n```\n\nChoosing between them: if the server must see the request, use (1). If it only\nmatters while its owner is alive, use (3) — or (2) when a single request does not\njustify a `Sender` member.\n\n## UI Styling\n\n### Style Files\n\nUI styles are defined in `.style` files using custom syntax:\n\n```style\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\n\nMyButtonStyle {\n    textPadding: margins;\n    icon: icon;\n    height: pixels;\n}\n\ndefaultButton: MyButtonStyle {\n    textPadding: margins(10px, 15px, 10px, 15px);\n    icon: icon{{ \"gui/icons/search\", iconColor }};\n    height: 30px;\n}\n\nprimaryButton: MyButtonStyle(defaultButton) {\n    icon: icon{{ \"gui/icons/check\", iconColor }};\n}\n```\n\n**Built-in types:**\n- `int` - Integer numbers (e.g., `maxLines: 3;`)\n- `bool` - Boolean values (e.g., `useShadow: true;`)\n- `pixels` - Pixel values with `px` suffix (e.g., `10px`)\n- `color` - Named colors from `ui/colors.palette`\n- `icon` - Inline icon definition: `icon{{ \"path/stem\", color }}`\n- `margins` - Four values: `margins(left, top, right, bottom)`\n- `size` - Two values: `size(width, height)`\n- `point` - Two values: `point(x, y)`\n- `align` - Alignment: `align(center)`, `align(left)`\n- `font` - Font: `font(14px semibold)`\n- `double` - Floating point\n\n**Multi-part icons** (layers drawn bottom-up):\n```style\nmyComplexIcon: icon{\n  { \"gui/icons/background\", iconBgColor },\n  { \"gui/icons/foreground\", iconFgColor }\n};\n```\n\n**Borders** are typically separate fields, not a single property:\n```style\nchatInput {\n  border: 1px;                       // width\n  borderFg: defaultInputFieldBorder; // color\n}\n```\n\n**Never hardcode sizes in code:**\n\nThe app supports different interface scale options. Style `px` values are automatically scaled at runtime, but raw integer constants in code are not. Never use hardcoded numbers for margins, paddings, spacing, sizes, coordinates, or any other dimensional values. Always define them in `.style` files and reference via `st::`.\n\n```cpp\n// BAD - breaks at non-100% interface scale:\np.drawText(10, 20, text);\nwidget->setFixedHeight(48);\nauto margin = 8;\nauto iconSize = QSize(24, 24);\n\n// GOOD - define in .style file and reference:\np.drawText(st::myWidgetTextLeft, st::myWidgetTextTop, text);\nwidget->setFixedHeight(st::myWidgetHeight);\nauto margin = st::myWidgetMargin;\nauto iconSize = st::myWidgetIconSize;\n```\n\n**Duration constants**: Animation durations should NOT go in `.style` files, this is a legacy approach. Prefer `constexpr auto kName = crl::time(N)` in an anonymous namespace in the relevant `.cpp` file.\n\n### Usage in Code\n\n```cpp\n#include \"styles/style_widgets.h\"\n\n// Access style members\nint height = st::primaryButton.height;\nconst style::icon &icon = st::primaryButton.icon;\nstyle::margins padding = st::primaryButton.textPadding;\n\n// Use in painting\nvoid MyWidget::paintEvent(QPaintEvent *e) {\n    Painter p(this);\n    p.fillRect(rect(), st::chatInput.backgroundColor);\n}\n```\n\n## Localization\n\n### String Definitions\n\nStrings are defined in `Telegram/Resources/langs/lang.strings`:\n\n```\n\"lng_settings_title\" = \"Settings\";\n\"lng_confirm_delete_item\" = \"Are you sure you want to delete {item_name}?\";\n\"lng_files_selected#one\" = \"{count} file selected\";\n\"lng_files_selected#other\" = \"{count} files selected\";\n```\n\n### Usage in Code\n\n**Immediate (current value):**\n\n```cpp\nauto currentTitle = tr::lng_settings_title(tr::now);\n\nauto currentConfirmation = tr::lng_confirm_delete_item(\n    tr::now,\n    lt_item_name, currentItemName);\n\nauto filesText = tr::lng_files_selected(tr::now, lt_count, count);\n```\n\n**Reactive (rpl::producer):**\n\n```cpp\nauto titleProducer = tr::lng_settings_title();\n\nauto confirmationProducer = tr::lng_confirm_delete_item(\n    lt_item_name,\n    std::move(itemNameProducer));\n\nauto filesTextProducer = tr::lng_files_selected(\n    lt_count,\n    countProducer | tr::to_count());\n```\n\n**Key points:**\n- Pass `tr::now` as first argument for immediate `QString`\n- Omit `tr::now` for reactive `rpl::producer<QString>`\n- Placeholders use `lt_tag_name, value` pattern\n- For `{count}`: immediate uses `int`, reactive uses `rpl::producer<float64>` with `| tr::to_count()`\n- Move producers with `std::move` when passing to placeholders\n- Rich text projectors — these `tr::` helpers serve double duty: as the **last argument** (projector) they set the return type to `TextWithEntities`, and as **placeholder values** they wrap individual substitutions in formatting. Always prefer them over `Ui::Text::Bold()`, `Ui::Text::RichLangValue`, etc. — see REVIEW.md for the full mapping.\n  - `tr::marked` — basic projection, converts `QString` to `TextWithEntities`\n  - `tr::rich` — interprets `**bold**`/`__italic__` markup in the string\n  - `tr::bold`, `tr::italic`, `tr::underline` — wrap text in that formatting\n  - `tr::link` — wrap as a clickable link\n  - `tr::url(u\"https://...\"_q)` — returns a projection that converts text to a link pointing to the given URL; can be passed to `rpl::map` or directly to a `tr::lng_...` call\n  ```cpp\n  // As last argument (projector):\n  auto title = tr::lng_export_progress_title(tr::now, tr::bold);\n  auto text = tr::lng_proxy_incorrect_secret(tr::now, tr::rich);\n  // As placeholder value wrapper + projector:\n  auto desc = tr::lng_some_key(\n      tr::now,\n      lt_name,\n      tr::bold(userName),\n      lt_group,\n      tr::bold(groupName),\n      tr::rich);\n  // Nested tr::lng as placeholder:\n  auto linked = tr::lng_settings_birthday_contacts(\n      lt_link,\n      tr::lng_settings_birthday_contacts_link(tr::url(link)),\n      tr::marked);\n  ```\n\n## RPL (Reactive Programming Library)\n\n### Core Concepts\n\n**Producers** represent streams of values over time:\n\n```cpp\nauto intProducer = rpl::single(123);  // Emits single value\nauto lifetime = rpl::lifetime();       // Manages subscription lifetime\n```\n\n### Starting Pipelines\n\n```cpp\nstd::move(counter) | rpl::on_next([=](int value) {\n    qDebug() << \"Received: \" << value;\n}, lifetime);\n\n// Without lifetime parameter - MUST store returned lifetime:\nauto subscriptionLifetime = std::move(counter) | rpl::on_next([=](int value) {\n    // process value\n});\n```\n\n### Transforming Producers\n\n```cpp\nauto strings = std::move(ints) | rpl::map([](int value) {\n    return QString::number(value * 2);\n});\n\nauto evenInts = std::move(ints) | rpl::filter([](int value) {\n    return (value % 2 == 0);\n});\n```\n\n### Combining Producers\n\n**`rpl::combine`** - combines latest values (lambdas receive unpacked arguments):\n\n```cpp\nauto combined = rpl::combine(countProducer, textProducer);\n\nstd::move(combined) | rpl::on_next([=](int count, const QString &text) {\n    qDebug() << \"Count=\" << count << \", Text=\" << text;\n}, lifetime);\n```\n\n**`rpl::merge`** - merges producers of same type:\n\n```cpp\nauto merged = rpl::merge(sourceA, sourceB);\n\nstd::move(merged) | rpl::on_next([=](QString &&value) {\n    qDebug() << \"Merged value: \" << value;\n}, lifetime);\n```\n\n**Other pipeline starters** — besides `rpl::on_next`, there are:\n- `rpl::on_error([=](Error &&e) { ... }, lifetime)` — handle errors\n- `rpl::on_done([=] { ... }, lifetime)` — handle stream completion\n- `rpl::on_next_error_done(nextCb, errorCb, doneCb, lifetime)` — handle all three\n\nThe `Error` template parameter defaults to `rpl::no_error`: `rpl::producer<Type, Error = no_error>`.\n\n**Key points:**\n- Explicitly `std::move` producers when starting pipelines\n- Pass `rpl::lifetime` to `on_...` methods or store returned lifetime\n- Use `rpl::duplicate(producer)` to reuse a producer multiple times\n- Combined producers automatically unpack tuples in lambdas (works with `rpl::map`, `rpl::filter`, and `rpl::on_next`)\n","CLAUDE.md":"# Claude Code Pointer\n\nRead `AGENTS.md` and treat it as the canonical repository-wide instructions.\n"},"files":{"AGENTS.md":"# Agent Guide for Telegram Desktop\n\nThis guide defines repository-wide instructions for coding agents working with the Telegram Desktop codebase.\n\n## Working from Codex on Windows + WSL\n\nThis checkout may be opened in Codex Desktop through the Windows UNC path `\\\\wsl.localhost\\{distro}\\home\\{user}\\Telegram\\tdesktop`, while the real Linux path is `/home/{user}/Telegram/tdesktop`. Treat it as a WSL/Linux checkout first, not as a native Windows checkout.\n\n- Prefer running repository-aware commands through WSL:\n\n```powershell\nwsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- <command>\n```\n\n- PowerShell can read and write files through the UNC path, but native Windows tools may see different ownership, path, executable, or line-ending behavior than Linux tools.\n- Git from PowerShell over `\\\\wsl.localhost\\...` can fail with `detected dubious ownership`. Use WSL Git instead. Do not change global Git `safe.directory` settings unless the user explicitly asks for that.\n- Keep path styles matched to the shell. Use `/home/{user}/Telegram/tdesktop/...` with WSL commands, and quoted `\\\\wsl.localhost\\{distro}\\home\\{user}\\Telegram\\tdesktop\\...` paths with native Windows commands. Avoid passing UNC paths to Linux tools or Linux paths to native Windows tools unless the tool explicitly supports them.\n- If a command behaves strangely from the PowerShell UNC working directory, retry the same command through `wsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- ...` before concluding the repository or command is broken.\n- Recursive searches and repo inspection are usually faster and more faithful through WSL, for example `wsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- rg ...`.\n- Do not assume the WSL host has the build toolchain installed directly. In this setup, WSL may not have `cmake`, while Windows may have `cmake`, and the configured `out/` tree may still target the Linux Docker toolchain. Do not run native Windows `cmake --build out` against a Linux/Docker build tree.\n- For WSL/Linux builds, use the Docker build entry point from the repository root: `Telegram/build/docker/centos_env/build_debug.sh`. The Docker daemon must be reachable from WSL; checking `docker info` is fine, but do not start a build unless the user asked for one.\n- Existing build outputs may be Linux binaries, for example `out/Debug/Telegram` as an ELF executable, not `Telegram.exe`. Verify the build tree before assuming which platform produced it.\n- Be careful with text file line endings. In a WSL/Linux checkout, files should remain LF-only unless the file already uses another convention. CRLF finishing applies only to native, non-WSL Windows runs/checkouts. Do not let PowerShell or Windows tools silently rewrite WSL files to CRLF. If a file becomes mixed, normalize it back to the convention appropriate for the current checkout, without adding a UTF-8 BOM.\n- When using the local `perform-task` skill from this WSL checkout, keep external AI task artifacts and edited project text files LF-only. Treat its Windows text-normalization phase as not applicable to WSL, except to record that line endings were checked and kept LF/no-BOM. Run CRLF normalization only in a native, non-WSL Windows checkout.\n\n## Build System Structure\n\nThe build system expects this directory layout:\n\n```text\nL:\\Telegram\\                    # BuildPath\nL:\\Telegram\\tdesktop\\           # Repository (you work here)\nL:\\Telegram\\Libraries\\          # 32-bit dependencies (Linux/macOS)\nL:\\Telegram\\win64\\Libraries\\    # 64-bit dependencies (Windows)\nL:\\Telegram\\ThirdParty\\         # Build tools (NuGet, Python, etc.)\n```\n\nDependencies are located relative to the repository: `../Libraries`, `../win64/Libraries`, or `../ThirdParty`.\n\n## Build Configuration\n\n### Build Commands\n\n**From repository root, run:**\n\n```bash\ncmake --build out --config Debug --target Telegram\n```\n\nThat's it. The `out/` directory is already configured. The executable will be at `out/Debug/Telegram.exe`.\n\n**From WSL, run through the Linux Docker build environment:**\n\n```bash\nTelegram/build/docker/centos_env/build_debug.sh\n```\n\n**Important:** When running cmake from a shell that doesn't support `cd`, use quoted absolute paths:\n```bash\ncmake --build \"l:\\Telegram\\tx64\\out\" --config Debug --target Telegram\n```\n\n**Never build Release** - it's extremely heavy and not needed for testing changes.\n\n## Platform-Specific Requirements\n\n### Windows\n- Requires Visual Studio 2022\n- Must run from appropriate Native Tools Command Prompt:\n  - \"x64 Native Tools Command Prompt\" for `win64`\n  - \"x86 Native Tools Command Prompt\" for `win`\n  - \"ARM64 Native Tools Command Prompt\" for `winarm`\n- Dependencies: `../win64/Libraries` (64-bit) or `../Libraries` (32-bit)\n\n### macOS\n- Requires Xcode\n- Dependencies: `../Libraries/local/Qt-*`\n- Set `QT` environment variable: `export QT=6.8`\n\n### Linux\n- Build dependencies in `../Libraries`\n- Set `QT` environment variable if needed\n\n## Key Files\n\n- **`Telegram/build/version`** - Version information\n- **`out/`** - Build output directory\n\n## Troubleshooting\n\n### \"Libraries not found\"\nEnsure the repository is in `L:\\Telegram\\tdesktop`. The build system requires `../win64/Libraries` to exist.\n\n### Build fails with \"wrong command prompt\"\nOn Windows, use the correct Visual Studio Native Tools Command Prompt matching your target (x64/x86/ARM64).\n\n### macOS crashes while reading the cached language pack\n\nAfter an incremental Xcode build that regenerated `lang.strings` outputs, the\napp can link a new generated key lookup with stale objects that still use an\nolder `kKeysCount`. The characteristic failure is:\n\n- the Debug log stops immediately after\n  `Lang Info: Loaded cached, keys: ...`;\n- stderr and `tdata/working` may be empty;\n- a fresh `~/Library/Logs/DiagnosticReports/Telegram-*.ips` shows `SIGABRT`\n  from `std::vector<unsigned char>::operator[]`, then\n  `Lang::Instance::applyValue()`, `fillFromSerialized()`, and\n  `Local::readLangPack()`.\n\nIf this exact startup failure repeats twice, do not change the implementation,\ntest overlay, or portable account. Stop only this checkout's exact Telegram\nprocess. Because Xcode's `CONFIGURATION_BUILD_DIR` is `out/Debug`, make a\nsafety copy of every existing portable folder outside `out/` before cleaning:\n\n```bash\nportable_backup_root=\"$(mktemp -d \"${TMPDIR:-/tmp}/tdesktop-portable-clean.XXXXXX\")\"\nfor portable_name in \\\n  TelegramForcePortable \\\n  test_TelegramForcePortable \\\n  real_TelegramForcePortable; do\n  if [ -d \"out/Debug/$portable_name\" ]; then\n    ditto \"out/Debug/$portable_name\" \"$portable_backup_root/$portable_name\"\n  fi\ndone\n```\n\nRequire every expected backup copy to exist before continuing. Then perform\none full Xcode Debug clean and rebuild:\n\n```bash\ncmake --build out --config Debug --target clean\ncmake --build out --config Debug --target Telegram\n```\n\nAfterward, restore a portable folder from the backup only when its original\npath is missing; never overwrite a folder that survived the clean. Verify all\nthree original folder names that existed before the clean are present, keep\nthe backup until the rebuilt app completes one successful launch, and record\nits path if the run stops before verification. Then rerun the same test once.\nIf the signature persists after that clean rebuild, continue normal crash\ndiagnosis or report the blocker. Do not loop clean rebuilds.\n\n### Build output locks\n\nFor builds owned by the autonomous `continue` / `perform-task` workflow, read\nand follow `.agents/shared/build-lock-recovery.md`. PDB, EXE, OBJ, and other\nbuild-output lock errors are recoverable: stop only the exact checkout\nexecutable or verified build-tree holders, delete only exact named artifacts\ninside that checkout's build tree, and retry within the bounded recovery\nbudget. Never stop an installed Telegram client, another checkout, an IDE, or\nan unknown process.\n\nOutside that autonomous workflow, an exact checkout executable may be running\nbecause the user is testing it. Do not terminate it or delete locked build\noutputs without explicit permission. Report the exact locked path and ask the\nuser to close that checkout's Telegram/debugger before rebuilding.\n\n## Best Practices\n\n1. **Always use Debug builds** - Release builds are extremely heavy\n2. **Don't build Release configuration** - it's too heavy for testing\n\n## Text File Format\n\n- On Windows, keep project text files with CRLF line endings.\n- Do not save source, header, build/config, style, or localization files as UTF-8 with BOM. Use UTF-8 without BOM.\n- When rewriting project text files for normalization, preserve file content otherwise and do not introduce a BOM.\n\n## Commits\n\n- Subject: one concise, plain-language line summarizing the change, ~50-60 characters, matching the style of recent `git log` subjects. This is usually the entire message.\n- For an `ai-tdesktop` task, start the subject with exactly `[ai] ` when the\n  retained task implementation changes permanent test-helper code, the agent\n  harness, or agent documentation in any way. This includes\n  `Telegram/SourceFiles/test/`, `.agents/`, `.claude/`, `AGENTS.md`,\n  `CLAUDE.md`, and files whose sole role is supporting those systems. Do not\n  count the disposable test overlay or external AI task artifacts. For every\n  other task, the subject must not contain `[ai]` anywhere.\n- For ordinary work not associated with an AI task, add a short plain-language body only when the subject can't carry it (what was done, not the technical how) — a line or two at most.\n- Never add a `Co-Authored-By:` line or any tool/assistant attribution trailer.\n- Never add `Autotask:`/attempt or other internal run markers. A commit owned by\n  an `ai-tdesktop` task has exactly three lines: the concise subject, a blank\n  line, and `Task: <task-id>`. Do not add a body. Keep rationale and\n  implementation notes out of the commit message; put a short durable note\n  under `tasks/<task-id>.md` only when useful. Do not copy commit hashes into\n  that note or any AI task artifact; the task id is the cross-repository link.\n\n## Local Storage Serialization\n\nBoth app-level (`Core::Settings`) and session-level (`Main::SessionSettings`) use sequential binary serialization via `QDataStream`. Key rules:\n\n- New fields must ALWAYS be appended at the **end** of the stream, never inserted in the middle\n- Reading new fields must be guarded with `!stream.atEnd()` and provide a meaningful default/fallback\n- Inserting in the middle breaks reading of data saved by older versions (the new read code consumes bytes that belong to subsequent fields)\n- For simple flags and values, prefer using the generic KV prefs facility (`writePref<Type>` / `readPref<Type>`) instead of adding to the binary stream -- this avoids serialization ordering issues entirely\n\n---\n\n# Development Guidelines\n\n## Coding Style\n\n**Do NOT write comments in code:**\n\nThis is important! Do not write single-line comments that describe what the next line does - they are bloat. Comments are allowed ONLY to describe complex algorithms in detail, when the explanation requires at least 4-5 lines. Self-documenting code with clear variable and function names is preferred.\n\n```cpp\n// BAD - don't do this:\n// Get the user's name\nauto name = user->name();\n// Check if premium\nif (user->isPremium()) {\n\n// GOOD - no comments needed, code is self-explanatory:\nauto name = user->name();\nif (user->isPremium()) {\n\n// ACCEPTABLE - complex algorithm explanation (4+ lines):\n// The algorithm works by first collecting all visible messages\n// in the viewport, then calculating their intersection with\n// the clip rectangle. Messages are grouped by date headers,\n// and we need to account for sticky headers that may overlap\n// with the first message in each group.\n```\n\n**Style and formatting rules** are in `REVIEW.md` — see that file for empty-line-before-closing-brace, operator placement in multi-line expressions, if-with-initializer, and other mechanical style rules.\n\n**Use `auto` for type deduction:**\n\nPrefer `auto` (or `const auto`, `const auto &`) instead of explicit types:\n\n```cpp\n// Prefer this:\nauto currentTitle = tr::lng_settings_title(tr::now);\nauto nameProducer = GetNameProducer();\n\n// Instead of this:\nQString currentTitle = tr::lng_settings_title(tr::now);\nrpl::producer<QString> nameProducer = GetNameProducer();\n```\n\n**Use trailing return types only when the normal form is too long:**\n\nPrefer the normal return type form when the opening line fits comfortably, roughly around 77 characters or less:\n\n```cpp\n// GOOD:\n[[nodiscard]] TextWithEntities FlattenSummaryBlocks(\n\tconst std::vector<Block> &blocks);\n```\n\nDo not use one-line trailing return types, or put the trailing return type after `)` on the same line. If it fits on one line with trailing syntax, the normal form would be shorter and easier to read:\n\n```cpp\n// BAD:\nauto ComputeTitle() -> QString;\n\n// BAD:\n[[nodiscard]] auto FlattenSummaryBlocks(\n\tconst std::vector<Block> &blocks) -> TextWithEntities;\n```\n\nUse `auto` with a trailing return type only when the normal opening line\n`{attributes} {return-type} {class-name::}{function-name(}` would be too long, or would force the return type onto its own line. Put the arrow and return type on the next line so the return type remains easy to find:\n\n```cpp\n// BAD:\nnot_null<HistoryView::Controls::ComposeAiButton*>\nHistoryView::Controls::SetupCaptionAiButton(SetupCaptionAiButtonArgs &&args);\n```\n\n```cpp\n// GOOD:\nauto HistoryView::Controls::SetupCaptionAiButton(\n\t\tSetupCaptionAiButtonArgs &&args)\n-> not_null<HistoryView::Controls::ComposeAiButton*>;\n```\n\nThis applies to both declarations and definitions.\n\n**Use `_q` for QString literals:**\n\nPrefer the project literal `u\"...\"_q` instead of the verbose `QStringLiteral(\"...\")` macro when creating `QString` values:\n\n```cpp\n// Prefer this:\nauto text = u\"Settings\"_q;\n\n// Instead of this:\nauto text = QStringLiteral(\"Settings\");\n```\n\n**Never use `Q_OS_LINUX` for platform checks in new code:**\n\nTelegram Desktop distinguishes at most three platforms: Windows / macOS / all-other. The \"all-other\" branch covers Linux, the BSD variants and more — and this is almost always the branch you want. `Q_OS_LINUX` narrows it to Linux alone, silently excluding the non-Linux Unix platforms, which is almost never intended. For the all-other branch use `!defined Q_OS_WIN && !defined Q_OS_MAC` at compile time, or its runtime equivalent `Platform::IsLinux()` — which, despite the name, means exactly `!defined Q_OS_WIN && !defined Q_OS_MAC` (\"everything except Windows and macOS\"), not Linux specifically:\n\n```cpp\n// BAD - excludes FreeBSD and other non-Linux Unix:\n#ifdef Q_OS_LINUX\nUnixSpecificCode();\n#endif // Q_OS_LINUX\n\n// GOOD - the all-other branch, compile time:\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\nUnixSpecificCode();\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\n// GOOD - the all-other branch, runtime (same meaning, NOT Linux-only):\nif (Platform::IsLinux()) {\n\tUnixSpecificCode();\n}\n```\n\n`Q_OS_LINUX` is only for the rare case where you genuinely want exactly Linux and not the other Unix-like systems — usually you don't. The few existing uses (`Telegram/SourceFiles/core/sandbox.cpp`, `Telegram/SourceFiles/platform/linux/specific_linux.cpp`) are such genuinely Linux-only code paths and stay as-is.\n\n**Treat CMake `LINUX` as the all-other platform:**\n\nIn this project, `cmake/validate_special_target.cmake` sets `LINUX` in the\nfinal `else()` after checking `WIN32` and `APPLE`. It therefore means\n`NOT WIN32 AND NOT APPLE`, including non-Linux Unix platforms; it does not\nmean exactly Linux. For the usual three-way platform split, write:\n\n```cmake\nif (WIN32)\n    set(platform_source platform/win.cpp)\nelseif (APPLE)\n    set(platform_source platform/mac.mm)\nelse()\n    set(platform_source platform/linux.cpp)\nendif()\ntarget_sources(my_target PRIVATE ${platform_source})\n```\n\nDo not add a separate fallback branch after `if (LINUX)` as though `LINUX`\nwere one platform among several remaining platforms. There are no remaining\nplatforms in this project's CMake platform model.\n\n**Prefer cppgir wrappers over the GLib C API:**\n\nWhen implementing all-other-platform code with GLib, GObject, or GIO, use the\ngenerated cppgir C++ bindings under `gi::repository` as much as possible.\nPrefer their `GLib`, `GObject`, and `Gio` types, ownership handling, results,\nand callbacks over raw `g_*`, `g_object_*`, and `g_io_*` APIs. Use the C API\nonly when cppgir does not expose the required functionality or at a narrow\ninterop boundary that genuinely requires raw GLib types, and keep that raw\nAPI surface as small as possible.\n\n**Generate typed D-Bus bindings from introspection XML:**\n\nFor a D-Bus interface known at build time, prefer the CMake `generate_dbus`\nfunction from `cmake/external/glib/generate_dbus.cmake` over handwritten\n`GDBusProxy` calls, stringly typed method and signal names, or manually\nmaintained C wrappers. Its signature is:\n\n```cmake\ngenerate_dbus(\n    target_name\n    interface_prefix\n    namespace\n    interface_file)\n```\n\n`target_name` is the existing target that will use the bindings,\n`interface_prefix` is the common D-Bus interface prefix passed to\n`gdbus-codegen`, `namespace` names the generated API, and `interface_file` is\nthe D-Bus introspection XML file. Include the helper and call it inside the\nall-other-platform branch:\n\n```cmake\ninclude(${cmake_helpers_loc}/external/glib/generate_dbus.cmake)\ngenerate_dbus(\n    my_target\n    org.example.\n    Example\n    ${src_loc}/platform/linux/org.example.Service.xml)\n```\n\nThe helper runs `gdbus-codegen`, generates proxy, skeleton, and object-manager\ntypes, produces GIR metadata, wraps that metadata with cppgir, and links the\nresult into `target_name`. Consume the resulting typed API from\n`gi::repository::Example` (using the namespace argument from the example);\ndo not edit or separately list files under the build `gen` directory. Use\ngeneric GLib D-Bus calls only when the interface is genuinely dynamic or\ncannot be represented by suitable introspection XML.\n\n## API Usage\n\n### API Schema Files\n\nAPI definitions use [TL Language](https://core.telegram.org/mtproto/TL):\n\n1. **`Telegram/SourceFiles/mtproto/scheme/mtproto.tl`** - MTProto protocol (encryption, auth, etc.)\n2. **`Telegram/SourceFiles/mtproto/scheme/api.tl`** - Telegram API (messages, users, chats, etc.)\n\n### Making API Requests\n\nStandard pattern using `api()`, generated `MTP...` types, and callbacks:\n\n```cpp\napi().request(MTPnamespace_MethodName(\n    MTP_flags(flags_value),\n    MTP_inputPeer(peer),\n    MTP_string(messageText),\n    MTP_long(randomId),\n    MTP_vector<MTPMessageEntity>()\n)).done([=](const MTPResponseType &result) {\n    // Handle successful response\n\n    // Multiple constructors - use .match() or check type:\n    result.match([&](const MTPDuser &data) {\n        // use data.vfirst_name().v\n    }, [&](const MTPDuserEmpty &data) {\n        // handle empty user\n    });\n\n    // Single constructor - use .data() shortcut:\n    const auto &data = result.data();\n    // use data.vmessages().v\n\n}).fail([=](const MTP::Error &error) {\n    // Handle API error\n    if (error.type() == u\"FLOOD_WAIT_X\"_q) {\n        // Handle flood wait\n    }\n}).handleFloodErrors().send();\n```\n\n**Key points:**\n- Always refer to `api.tl` for method signatures and return types\n- Use generated `MTP...` types for parameters (`MTP_int`, `MTP_string`, etc.)\n- For multiple constructors, use `.match()` or check `.type()` against `mtpc_` constants then call `.c_constructorName()`:\n  ```cpp\n  // Using match:\n  result.match([&](const MTPDuser &data) { ... }, [&](const MTPDuserEmpty &data) { ... });\n  // Or explicit type check:\n  if (result.type() == mtpc_user) {\n      const auto &data = result.c_user(); // asserts on type mismatch\n  }\n  ```\n- For single constructors, use `.data()` shortcut\n- Include `.handleFloodErrors()` before `.send()` in rare cases where you want special case flood error handling\n- Silently ignore HTTP 406 errors in UI: the server uses 406 to mean \"show nothing to the user\". Guard toasts with `MTP::IgnoreError(error)` or use `MTP::ShowErrorFallback(show, error)` (both in `mtproto/mtproto_response.h`) which shows `error.type()` as a toast unless the error should be ignored.\n\n### API Request Callback Lifetime\n\n`api().request(...)` callbacks are owned by the session, not by whatever created\nthem. A `.done()` / `.fail()` handler stays alive for the whole session lifetime,\nso a handler that captured a widget, a box, a controller, or any shorter-lived\nstate still runs after that state is gone. A plain `[=]` capture warns about\nnothing, which makes this one of the easiest ways to write a use-after-free here.\n\nCapturing only plain values or session-owned objects is fine. When anything\ncaptured can die before the session does, pick one of three:\n\n**1. Guard the callback with `crl::guard`.** The request is always sent; the\nhandler is skipped when the context is gone. Use when the call itself must reach\nthe server and only the local reaction is optional.\n\n```cpp\napi().request(MTPmethod(\n\t...\n)).done(crl::guard(this, [=](const MTPResult &result) {\n\t// runs only while `this` is still alive\n})).send();\n```\n\nAccepted guards, in rough order of how often they are used: a raw pointer or\n`not_null` to any `QObject`-derived type — widgets, boxes, controllers — where the\n`QPointer` is created on the spot, so passing `this` is the normal case; a raw\npointer or `not_null` to a `base::has_weak_ptr` type; `QPointer`, `QWeakPointer`,\n`QSharedPointer`; `base::weak_ptr`, `base::weak_qptr`; `std::weak_ptr`,\n`std::shared_ptr`; and `base::binary_guard`.\n\n**2. Remember the `mtpRequestId` and cancel it.** Cancel when the result stops\nbeing relevant, and in the destructor. The request may never reach the server —\nif it is still queued when cancelled, or connectivity dies first, it is simply\ndropped — so never use this when the call itself has to happen.\n\n```cpp\n_requestId = api().request(MTPmethod(\n\t...\n)).done([=](const MTPResult &result) {\n\t_requestId = 0;\n\t...\n}).send();\n\n// when the result is no longer relevant, and in the destructor:\napi().request(base::take(_requestId)).cancel();\n```\n\n**3. Own an `MTP::Sender`.** Its destructor cancels everything it sent that is\nstill in flight, so request lifetime follows the owner with no bookkeeping. Same\ndelivery caveat as (2). Prefer this for a widget, box, or controller that issues\nmore than a request or two.\n\n```cpp\n// header\n\tMTP::Sender _api;\n\n// constructor initializer list\n, _api(&session->mtp())\n\n// requests sent through it die with the owner\n_api.request(MTPmethod(\n\t...\n)).done([=](const MTPResult &result) {\n\t...\n}).send();\n```\n\nChoosing between them: if the server must see the request, use (1). If it only\nmatters while its owner is alive, use (3) — or (2) when a single request does not\njustify a `Sender` member.\n\n## UI Styling\n\n### Style Files\n\nUI styles are defined in `.style` files using custom syntax:\n\n```style\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\n\nMyButtonStyle {\n    textPadding: margins;\n    icon: icon;\n    height: pixels;\n}\n\ndefaultButton: MyButtonStyle {\n    textPadding: margins(10px, 15px, 10px, 15px);\n    icon: icon{{ \"gui/icons/search\", iconColor }};\n    height: 30px;\n}\n\nprimaryButton: MyButtonStyle(defaultButton) {\n    icon: icon{{ \"gui/icons/check\", iconColor }};\n}\n```\n\n**Built-in types:**\n- `int` - Integer numbers (e.g., `maxLines: 3;`)\n- `bool` - Boolean values (e.g., `useShadow: true;`)\n- `pixels` - Pixel values with `px` suffix (e.g., `10px`)\n- `color` - Named colors from `ui/colors.palette`\n- `icon` - Inline icon definition: `icon{{ \"path/stem\", color }}`\n- `margins` - Four values: `margins(left, top, right, bottom)`\n- `size` - Two values: `size(width, height)`\n- `point` - Two values: `point(x, y)`\n- `align` - Alignment: `align(center)`, `align(left)`\n- `font` - Font: `font(14px semibold)`\n- `double` - Floating point\n\n**Multi-part icons** (layers drawn bottom-up):\n```style\nmyComplexIcon: icon{\n  { \"gui/icons/background\", iconBgColor },\n  { \"gui/icons/foreground\", iconFgColor }\n};\n```\n\n**Borders** are typically separate fields, not a single property:\n```style\nchatInput {\n  border: 1px;                       // width\n  borderFg: defaultInputFieldBorder; // color\n}\n```\n\n**Never hardcode sizes in code:**\n\nThe app supports different interface scale options. Style `px` values are automatically scaled at runtime, but raw integer constants in code are not. Never use hardcoded numbers for margins, paddings, spacing, sizes, coordinates, or any other dimensional values. Always define them in `.style` files and reference via `st::`.\n\n```cpp\n// BAD - breaks at non-100% interface scale:\np.drawText(10, 20, text);\nwidget->setFixedHeight(48);\nauto margin = 8;\nauto iconSize = QSize(24, 24);\n\n// GOOD - define in .style file and reference:\np.drawText(st::myWidgetTextLeft, st::myWidgetTextTop, text);\nwidget->setFixedHeight(st::myWidgetHeight);\nauto margin = st::myWidgetMargin;\nauto iconSize = st::myWidgetIconSize;\n```\n\n**Duration constants**: Animation durations should NOT go in `.style` files, this is a legacy approach. Prefer `constexpr auto kName = crl::time(N)` in an anonymous namespace in the relevant `.cpp` file.\n\n### Usage in Code\n\n```cpp\n#include \"styles/style_widgets.h\"\n\n// Access style members\nint height = st::primaryButton.height;\nconst style::icon &icon = st::primaryButton.icon;\nstyle::margins padding = st::primaryButton.textPadding;\n\n// Use in painting\nvoid MyWidget::paintEvent(QPaintEvent *e) {\n    Painter p(this);\n    p.fillRect(rect(), st::chatInput.backgroundColor);\n}\n```\n\n## Localization\n\n### String Definitions\n\nStrings are defined in `Telegram/Resources/langs/lang.strings`:\n\n```\n\"lng_settings_title\" = \"Settings\";\n\"lng_confirm_delete_item\" = \"Are you sure you want to delete {item_name}?\";\n\"lng_files_selected#one\" = \"{count} file selected\";\n\"lng_files_selected#other\" = \"{count} files selected\";\n```\n\n### Usage in Code\n\n**Immediate (current value):**\n\n```cpp\nauto currentTitle = tr::lng_settings_title(tr::now);\n\nauto currentConfirmation = tr::lng_confirm_delete_item(\n    tr::now,\n    lt_item_name, currentItemName);\n\nauto filesText = tr::lng_files_selected(tr::now, lt_count, count);\n```\n\n**Reactive (rpl::producer):**\n\n```cpp\nauto titleProducer = tr::lng_settings_title();\n\nauto confirmationProducer = tr::lng_confirm_delete_item(\n    lt_item_name,\n    std::move(itemNameProducer));\n\nauto filesTextProducer = tr::lng_files_selected(\n    lt_count,\n    countProducer | tr::to_count());\n```\n\n**Key points:**\n- Pass `tr::now` as first argument for immediate `QString`\n- Omit `tr::now` for reactive `rpl::producer<QString>`\n- Placeholders use `lt_tag_name, value` pattern\n- For `{count}`: immediate uses `int`, reactive uses `rpl::producer<float64>` with `| tr::to_count()`\n- Move producers with `std::move` when passing to placeholders\n- Rich text projectors — these `tr::` helpers serve double duty: as the **last argument** (projector) they set the return type to `TextWithEntities`, and as **placeholder values** they wrap individual substitutions in formatting. Always prefer them over `Ui::Text::Bold()`, `Ui::Text::RichLangValue`, etc. — see REVIEW.md for the full mapping.\n  - `tr::marked` — basic projection, converts `QString` to `TextWithEntities`\n  - `tr::rich` — interprets `**bold**`/`__italic__` markup in the string\n  - `tr::bold`, `tr::italic`, `tr::underline` — wrap text in that formatting\n  - `tr::link` — wrap as a clickable link\n  - `tr::url(u\"https://...\"_q)` — returns a projection that converts text to a link pointing to the given URL; can be passed to `rpl::map` or directly to a `tr::lng_...` call\n  ```cpp\n  // As last argument (projector):\n  auto title = tr::lng_export_progress_title(tr::now, tr::bold);\n  auto text = tr::lng_proxy_incorrect_secret(tr::now, tr::rich);\n  // As placeholder value wrapper + projector:\n  auto desc = tr::lng_some_key(\n      tr::now,\n      lt_name,\n      tr::bold(userName),\n      lt_group,\n      tr::bold(groupName),\n      tr::rich);\n  // Nested tr::lng as placeholder:\n  auto linked = tr::lng_settings_birthday_contacts(\n      lt_link,\n      tr::lng_settings_birthday_contacts_link(tr::url(link)),\n      tr::marked);\n  ```\n\n## RPL (Reactive Programming Library)\n\n### Core Concepts\n\n**Producers** represent streams of values over time:\n\n```cpp\nauto intProducer = rpl::single(123);  // Emits single value\nauto lifetime = rpl::lifetime();       // Manages subscription lifetime\n```\n\n### Starting Pipelines\n\n```cpp\nstd::move(counter) | rpl::on_next([=](int value) {\n    qDebug() << \"Received: \" << value;\n}, lifetime);\n\n// Without lifetime parameter - MUST store returned lifetime:\nauto subscriptionLifetime = std::move(counter) | rpl::on_next([=](int value) {\n    // process value\n});\n```\n\n### Transforming Producers\n\n```cpp\nauto strings = std::move(ints) | rpl::map([](int value) {\n    return QString::number(value * 2);\n});\n\nauto evenInts = std::move(ints) | rpl::filter([](int value) {\n    return (value % 2 == 0);\n});\n```\n\n### Combining Producers\n\n**`rpl::combine`** - combines latest values (lambdas receive unpacked arguments):\n\n```cpp\nauto combined = rpl::combine(countProducer, textProducer);\n\nstd::move(combined) | rpl::on_next([=](int count, const QString &text) {\n    qDebug() << \"Count=\" << count << \", Text=\" << text;\n}, lifetime);\n```\n\n**`rpl::merge`** - merges producers of same type:\n\n```cpp\nauto merged = rpl::merge(sourceA, sourceB);\n\nstd::move(merged) | rpl::on_next([=](QString &&value) {\n    qDebug() << \"Merged value: \" << value;\n}, lifetime);\n```\n\n**Other pipeline starters** — besides `rpl::on_next`, there are:\n- `rpl::on_error([=](Error &&e) { ... }, lifetime)` — handle errors\n- `rpl::on_done([=] { ... }, lifetime)` — handle stream completion\n- `rpl::on_next_error_done(nextCb, errorCb, doneCb, lifetime)` — handle all three\n\nThe `Error` template parameter defaults to `rpl::no_error`: `rpl::producer<Type, Error = no_error>`.\n\n**Key points:**\n- Explicitly `std::move` producers when starting pipelines\n- Pass `rpl::lifetime` to `on_...` methods or store returned lifetime\n- Use `rpl::duplicate(producer)` to reuse a producer multiple times\n- Combined producers automatically unpack tuples in lambdas (works with `rpl::map`, `rpl::filter`, and `rpl::on_next`)\n","CLAUDE.md":"# Claude Code Pointer\n\nRead `AGENTS.md` and treat it as the canonical repository-wide instructions.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# Agent Guide for Telegram Desktop\n\nThis guide defines repository-wide instructions for coding agents working with the Telegram Desktop codebase.\n\n## Working from Codex on Windows + WSL\n\nThis checkout may be opened in Codex Desktop through the Windows UNC path `\\\\wsl.localhost\\{distro}\\home\\{user}\\Telegram\\tdesktop`, while the real Linux path is `/home/{user}/Telegram/tdesktop`. Treat it as a WSL/Linux checkout first, not as a native Windows checkout.\n\n- Prefer running repository-aware commands through WSL:\n\n```powershell\nwsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- <command>\n```\n\n- PowerShell can read and write files through the UNC path, but native Windows tools may see different ownership, path, executable, or line-ending behavior than Linux tools.\n- Git from PowerShell over `\\\\wsl.localhost\\...` can fail with `detected dubious ownership`. Use WSL Git instead. Do not change global Git `safe.directory` settings unless the user explicitly asks for that.\n- Keep path styles matched to the shell. Use `/home/{user}/Telegram/tdesktop/...` with WSL commands, and quoted `\\\\wsl.localhost\\{distro}\\home\\{user}\\Telegram\\tdesktop\\...` paths with native Windows commands. Avoid passing UNC paths to Linux tools or Linux paths to native Windows tools unless the tool explicitly supports them.\n- If a command behaves strangely from the PowerShell UNC working directory, retry the same command through `wsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- ...` before concluding the repository or command is broken.\n- Recursive searches and repo inspection are usually faster and more faithful through WSL, for example `wsl.exe -d {distro} --cd /home/{user}/Telegram/tdesktop -- rg ...`.\n- Do not assume the WSL host has the build toolchain installed directly. In this setup, WSL may not have `cmake`, while Windows may have `cmake`, and the configured `out/` tree may still target the Linux Docker toolchain. Do not run native Windows `cmake --build out` against a Linux/Docker build tree.\n- For WSL/Linux builds, use the Docker build entry point from the repository root: `Telegram/build/docker/centos_env/build_debug.sh`. The Docker daemon must be reachable from WSL; checking `docker info` is fine, but do not start a build unless the user asked for one.\n- Existing build outputs may be Linux binaries, for example `out/Debug/Telegram` as an ELF executable, not `Telegram.exe`. Verify the build tree before assuming which platform produced it.\n- Be careful with text file line endings. In a WSL/Linux checkout, files should remain LF-only unless the file already uses another convention. CRLF finishing applies only to native, non-WSL Windows runs/checkouts. Do not let PowerShell or Windows tools silently rewrite WSL files to CRLF. If a file becomes mixed, normalize it back to the convention appropriate for the current checkout, without adding a UTF-8 BOM.\n- When using the local `perform-task` skill from this WSL checkout, keep external AI task artifacts and edited project text files LF-only. Treat its Windows text-normalization phase as not applicable to WSL, except to record that line endings were checked and kept LF/no-BOM. Run CRLF normalization only in a native, non-WSL Windows checkout.\n\n## Build System Structure\n\nThe build system expects this directory layout:\n\n```text\nL:\\Telegram\\                    # BuildPath\nL:\\Telegram\\tdesktop\\           # Repository (you work here)\nL:\\Telegram\\Libraries\\          # 32-bit dependencies (Linux/macOS)\nL:\\Telegram\\win64\\Libraries\\    # 64-bit dependencies (Windows)\nL:\\Telegram\\ThirdParty\\         # Build tools (NuGet, Python, etc.)\n```\n\nDependencies are located relative to the repository: `../Libraries`, `../win64/Libraries`, or `../ThirdParty`.\n\n## Build Configuration\n\n### Build Commands\n\n**From repository root, run:**\n\n```bash\ncmake --build out --config Debug --target Telegram\n```\n\nThat's it. The `out/` directory is already configured. The executable will be at `out/Debug/Telegram.exe`.\n\n**From WSL, run through the Linux Docker build environment:**\n\n```bash\nTelegram/build/docker/centos_env/build_debug.sh\n```\n\n**Important:** When running cmake from a shell that doesn't support `cd`, use quoted absolute paths:\n```bash\ncmake --build \"l:\\Telegram\\tx64\\out\" --config Debug --target Telegram\n```\n\n**Never build Release** - it's extremely heavy and not needed for testing changes.\n\n## Platform-Specific Requirements\n\n### Windows\n- Requires Visual Studio 2022\n- Must run from appropriate Native Tools Command Prompt:\n  - \"x64 Native Tools Command Prompt\" for `win64`\n  - \"x86 Native Tools Command Prompt\" for `win`\n  - \"ARM64 Native Tools Command Prompt\" for `winarm`\n- Dependencies: `../win64/Libraries` (64-bit) or `../Libraries` (32-bit)\n\n### macOS\n- Requires Xcode\n- Dependencies: `../Libraries/local/Qt-*`\n- Set `QT` environment variable: `export QT=6.8`\n\n### Linux\n- Build dependencies in `../Libraries`\n- Set `QT` environment variable if needed\n\n## Key Files\n\n- **`Telegram/build/version`** - Version information\n- **`out/`** - Build output directory\n\n## Troubleshooting\n\n### \"Libraries not found\"\nEnsure the repository is in `L:\\Telegram\\tdesktop`. The build system requires `../win64/Libraries` to exist.\n\n### Build fails with \"wrong command prompt\"\nOn Windows, use the correct Visual Studio Native Tools Command Prompt matching your target (x64/x86/ARM64).\n\n### macOS crashes while reading the cached language pack\n\nAfter an incremental Xcode build that regenerated `lang.strings` outputs, the\napp can link a new generated key lookup with stale objects that still use an\nolder `kKeysCount`. The characteristic failure is:\n\n- the Debug log stops immediately after\n  `Lang Info: Loaded cached, keys: ...`;\n- stderr and `tdata/working` may be empty;\n- a fresh `~/Library/Logs/DiagnosticReports/Telegram-*.ips` shows `SIGABRT`\n  from `std::vector<unsigned char>::operator[]`, then\n  `Lang::Instance::applyValue()`, `fillFromSerialized()`, and\n  `Local::readLangPack()`.\n\nIf this exact startup failure repeats twice, do not change the implementation,\ntest overlay, or portable account. Stop only this checkout's exact Telegram\nprocess. Because Xcode's `CONFIGURATION_BUILD_DIR` is `out/Debug`, make a\nsafety copy of every existing portable folder outside `out/` before cleaning:\n\n```bash\nportable_backup_root=\"$(mktemp -d \"${TMPDIR:-/tmp}/tdesktop-portable-clean.XXXXXX\")\"\nfor portable_name in \\\n  TelegramForcePortable \\\n  test_TelegramForcePortable \\\n  real_TelegramForcePortable; do\n  if [ -d \"out/Debug/$portable_name\" ]; then\n    ditto \"out/Debug/$portable_name\" \"$portable_backup_root/$portable_name\"\n  fi\ndone\n```\n\nRequire every expected backup copy to exist before continuing. Then perform\none full Xcode Debug clean and rebuild:\n\n```bash\ncmake --build out --config Debug --target clean\ncmake --build out --config Debug --target Telegram\n```\n\nAfterward, restore a portable folder from the backup only when its original\npath is missing; never overwrite a folder that survived the clean. Verify all\nthree original folder names that existed before the clean are present, keep\nthe backup until the rebuilt app completes one successful launch, and record\nits path if the run stops before verification. Then rerun the same test once.\nIf the signature persists after that clean rebuild, continue normal crash\ndiagnosis or report the blocker. Do not loop clean rebuilds.\n\n### Build output locks\n\nFor builds owned by the autonomous `continue` / `perform-task` workflow, read\nand follow `.agents/shared/build-lock-recovery.md`. PDB, EXE, OBJ, and other\nbuild-output lock errors are recoverable: stop only the exact checkout\nexecutable or verified build-tree holders, delete only exact named artifacts\ninside that checkout's build tree, and retry within the bounded recovery\nbudget. Never stop an installed Telegram client, another checkout, an IDE, or\nan unknown process.\n\nOutside that autonomous workflow, an exact checkout executable may be running\nbecause the user is testing it. Do not terminate it or delete locked build\noutputs without explicit permission. Report the exact locked path and ask the\nuser to close that checkout's Telegram/debugger before rebuilding.\n\n## Best Practices\n\n1. **Always use Debug builds** - Release builds are extremely heavy\n2. **Don't build Release configuration** - it's too heavy for testing\n\n## Text File Format\n\n- On Windows, keep project text files with CRLF line endings.\n- Do not save source, header, build/config, style, or localization files as UTF-8 with BOM. Use UTF-8 without BOM.\n- When rewriting project text files for normalization, preserve file content otherwise and do not introduce a BOM.\n\n## Commits\n\n- Subject: one concise, plain-language line summarizing the change, ~50-60 characters, matching the style of recent `git log` subjects. This is usually the entire message.\n- For an `ai-tdesktop` task, start the subject with exactly `[ai] ` when the\n  retained task implementation changes permanent test-helper code, the agent\n  harness, or agent documentation in any way. This includes\n  `Telegram/SourceFiles/test/`, `.agents/`, `.claude/`, `AGENTS.md`,\n  `CLAUDE.md`, and files whose sole role is supporting those systems. Do not\n  count the disposable test overlay or external AI task artifacts. For every\n  other task, the subject must not contain `[ai]` anywhere.\n- For ordinary work not associated with an AI task, add a short plain-language body only when the subject can't carry it (what was done, not the technical how) — a line or two at most.\n- Never add a `Co-Authored-By:` line or any tool/assistant attribution trailer.\n- Never add `Autotask:`/attempt or other internal run markers. A commit owned by\n  an `ai-tdesktop` task has exactly three lines: the concise subject, a blank\n  line, and `Task: <task-id>`. Do not add a body. Keep rationale and\n  implementation notes out of the commit message; put a short durable note\n  under `tasks/<task-id>.md` only when useful. Do not copy commit hashes into\n  that note or any AI task artifact; the task id is the cross-repository link.\n\n## Local Storage Serialization\n\nBoth app-level (`Core::Settings`) and session-level (`Main::SessionSettings`) use sequential binary serialization via `QDataStream`. Key rules:\n\n- New fields must ALWAYS be appended at the **end** of the stream, never inserted in the middle\n- Reading new fields must be guarded with `!stream.atEnd()` and provide a meaningful default/fallback\n- Inserting in the middle breaks reading of data saved by older versions (the new read code consumes bytes that belong to subsequent fields)\n- For simple flags and values, prefer using the generic KV prefs facility (`writePref<Type>` / `readPref<Type>`) instead of adding to the binary stream -- this avoids serialization ordering issues entirely\n\n---\n\n# Development Guidelines\n\n## Coding Style\n\n**Do NOT write comments in code:**\n\nThis is important! Do not write single-line comments that describe what the next line does - they are bloat. Comments are allowed ONLY to describe complex algorithms in detail, when the explanation requires at least 4-5 lines. Self-documenting code with clear variable and function names is preferred.\n\n```cpp\n// BAD - don't do this:\n// Get the user's name\nauto name = user->name();\n// Check if premium\nif (user->isPremium()) {\n\n// GOOD - no comments needed, code is self-explanatory:\nauto name = user->name();\nif (user->isPremium()) {\n\n// ACCEPTABLE - complex algorithm explanation (4+ lines):\n// The algorithm works by first collecting all visible messages\n// in the viewport, then calculating their intersection with\n// the clip rectangle. Messages are grouped by date headers,\n// and we need to account for sticky headers that may overlap\n// with the first message in each group.\n```\n\n**Style and formatting rules** are in `REVIEW.md` — see that file for empty-line-before-closing-brace, operator placement in multi-line expressions, if-with-initializer, and other mechanical style rules.\n\n**Use `auto` for type deduction:**\n\nPrefer `auto` (or `const auto`, `const auto &`) instead of explicit types:\n\n```cpp\n// Prefer this:\nauto currentTitle = tr::lng_settings_title(tr::now);\nauto nameProducer = GetNameProducer();\n\n// Instead of this:\nQString currentTitle = tr::lng_settings_title(tr::now);\nrpl::producer<QString> nameProducer = GetNameProducer();\n```\n\n**Use trailing return types only when the normal form is too long:**\n\nPrefer the normal return type form when the opening line fits comfortably, roughly around 77 characters or less:\n\n```cpp\n// GOOD:\n[[nodiscard]] TextWithEntities FlattenSummaryBlocks(\n\tconst std::vector<Block> &blocks);\n```\n\nDo not use one-line trailing return types, or put the trailing return type after `)` on the same line. If it fits on one line with trailing syntax, the normal form would be shorter and easier to read:\n\n```cpp\n// BAD:\nauto ComputeTitle() -> QString;\n\n// BAD:\n[[nodiscard]] auto FlattenSummaryBlocks(\n\tconst std::vector<Block> &blocks) -> TextWithEntities;\n```\n\nUse `auto` with a trailing return type only when the normal opening line\n`{attributes} {return-type} {class-name::}{function-name(}` would be too long, or would force the return type onto its own line. Put the arrow and return type on the next line so the return type remains easy to find:\n\n```cpp\n// BAD:\nnot_null<HistoryView::Controls::ComposeAiButton*>\nHistoryView::Controls::SetupCaptionAiButton(SetupCaptionAiButtonArgs &&args);\n```\n\n```cpp\n// GOOD:\nauto HistoryView::Controls::SetupCaptionAiButton(\n\t\tSetupCaptionAiButtonArgs &&args)\n-> not_null<HistoryView::Controls::ComposeAiButton*>;\n```\n\nThis applies to both declarations and definitions.\n\n**Use `_q` for QString literals:**\n\nPrefer the project literal `u\"...\"_q` instead of the verbose `QStringLiteral(\"...\")` macro when creating `QString` values:\n\n```cpp\n// Prefer this:\nauto text = u\"Settings\"_q;\n\n// Instead of this:\nauto text = QStringLiteral(\"Settings\");\n```\n\n**Never use `Q_OS_LINUX` for platform checks in new code:**\n\nTelegram Desktop distinguishes at most three platforms: Windows / macOS / all-other. The \"all-other\" branch covers Linux, the BSD variants and more — and this is almost always the branch you want. `Q_OS_LINUX` narrows it to Linux alone, silently excluding the non-Linux Unix platforms, which is almost never intended. For the all-other branch use `!defined Q_OS_WIN && !defined Q_OS_MAC` at compile time, or its runtime equivalent `Platform::IsLinux()` — which, despite the name, means exactly `!defined Q_OS_WIN && !defined Q_OS_MAC` (\"everything except Windows and macOS\"), not Linux specifically:\n\n```cpp\n// BAD - excludes FreeBSD and other non-Linux Unix:\n#ifdef Q_OS_LINUX\nUnixSpecificCode();\n#endif // Q_OS_LINUX\n\n// GOOD - the all-other branch, compile time:\n#if !defined Q_OS_WIN && !defined Q_OS_MAC\nUnixSpecificCode();\n#endif // !Q_OS_WIN && !Q_OS_MAC\n\n// GOOD - the all-other branch, runtime (same meaning, NOT Linux-only):\nif (Platform::IsLinux()) {\n\tUnixSpecificCode();\n}\n```\n\n`Q_OS_LINUX` is only for the rare case where you genuinely want exactly Linux and not the other Unix-like systems — usually you don't. The few existing uses (`Telegram/SourceFiles/core/sandbox.cpp`, `Telegram/SourceFiles/platform/linux/specific_linux.cpp`) are such genuinely Linux-only code paths and stay as-is.\n\n**Treat CMake `LINUX` as the all-other platform:**\n\nIn this project, `cmake/validate_special_target.cmake` sets `LINUX` in the\nfinal `else()` after checking `WIN32` and `APPLE`. It therefore means\n`NOT WIN32 AND NOT APPLE`, including non-Linux Unix platforms; it does not\nmean exactly Linux. For the usual three-way platform split, write:\n\n```cmake\nif (WIN32)\n    set(platform_source platform/win.cpp)\nelseif (APPLE)\n    set(platform_source platform/mac.mm)\nelse()\n    set(platform_source platform/linux.cpp)\nendif()\ntarget_sources(my_target PRIVATE ${platform_source})\n```\n\nDo not add a separate fallback branch after `if (LINUX)` as though `LINUX`\nwere one platform among several remaining platforms. There are no remaining\nplatforms in this project's CMake platform model.\n\n**Prefer cppgir wrappers over the GLib C API:**\n\nWhen implementing all-other-platform code with GLib, GObject, or GIO, use the\ngenerated cppgir C++ bindings under `gi::repository` as much as possible.\nPrefer their `GLib`, `GObject`, and `Gio` types, ownership handling, results,\nand callbacks over raw `g_*`, `g_object_*`, and `g_io_*` APIs. Use the C API\nonly when cppgir does not expose the required functionality or at a narrow\ninterop boundary that genuinely requires raw GLib types, and keep that raw\nAPI surface as small as possible.\n\n**Generate typed D-Bus bindings from introspection XML:**\n\nFor a D-Bus interface known at build time, prefer the CMake `generate_dbus`\nfunction from `cmake/external/glib/generate_dbus.cmake` over handwritten\n`GDBusProxy` calls, stringly typed method and signal names, or manually\nmaintained C wrappers. Its signature is:\n\n```cmake\ngenerate_dbus(\n    target_name\n    interface_prefix\n    namespace\n    interface_file)\n```\n\n`target_name` is the existing target that will use the bindings,\n`interface_prefix` is the common D-Bus interface prefix passed to\n`gdbus-codegen`, `namespace` names the generated API, and `interface_file` is\nthe D-Bus introspection XML file. Include the helper and call it inside the\nall-other-platform branch:\n\n```cmake\ninclude(${cmake_helpers_loc}/external/glib/generate_dbus.cmake)\ngenerate_dbus(\n    my_target\n    org.example.\n    Example\n    ${src_loc}/platform/linux/org.example.Service.xml)\n```\n\nThe helper runs `gdbus-codegen`, generates proxy, skeleton, and object-manager\ntypes, produces GIR metadata, wraps that metadata with cppgir, and links the\nresult into `target_name`. Consume the resulting typed API from\n`gi::repository::Example` (using the namespace argument from the example);\ndo not edit or separately list files under the build `gen` directory. Use\ngeneric GLib D-Bus calls only when the interface is genuinely dynamic or\ncannot be represented by suitable introspection XML.\n\n## API Usage\n\n### API Schema Files\n\nAPI definitions use [TL Language](https://core.telegram.org/mtproto/TL):\n\n1. **`Telegram/SourceFiles/mtproto/scheme/mtproto.tl`** - MTProto protocol (encryption, auth, etc.)\n2. **`Telegram/SourceFiles/mtproto/scheme/api.tl`** - Telegram API (messages, users, chats, etc.)\n\n### Making API Requests\n\nStandard pattern using `api()`, generated `MTP...` types, and callbacks:\n\n```cpp\napi().request(MTPnamespace_MethodName(\n    MTP_flags(flags_value),\n    MTP_inputPeer(peer),\n    MTP_string(messageText),\n    MTP_long(randomId),\n    MTP_vector<MTPMessageEntity>()\n)).done([=](const MTPResponseType &result) {\n    // Handle successful response\n\n    // Multiple constructors - use .match() or check type:\n    result.match([&](const MTPDuser &data) {\n        // use data.vfirst_name().v\n    }, [&](const MTPDuserEmpty &data) {\n        // handle empty user\n    });\n\n    // Single constructor - use .data() shortcut:\n    const auto &data = result.data();\n    // use data.vmessages().v\n\n}).fail([=](const MTP::Error &error) {\n    // Handle API error\n    if (error.type() == u\"FLOOD_WAIT_X\"_q) {\n        // Handle flood wait\n    }\n}).handleFloodErrors().send();\n```\n\n**Key points:**\n- Always refer to `api.tl` for method signatures and return types\n- Use generated `MTP...` types for parameters (`MTP_int`, `MTP_string`, etc.)\n- For multiple constructors, use `.match()` or check `.type()` against `mtpc_` constants then call `.c_constructorName()`:\n  ```cpp\n  // Using match:\n  result.match([&](const MTPDuser &data) { ... }, [&](const MTPDuserEmpty &data) { ... });\n  // Or explicit type check:\n  if (result.type() == mtpc_user) {\n      const auto &data = result.c_user(); // asserts on type mismatch\n  }\n  ```\n- For single constructors, use `.data()` shortcut\n- Include `.handleFloodErrors()` before `.send()` in rare cases where you want special case flood error handling\n- Silently ignore HTTP 406 errors in UI: the server uses 406 to mean \"show nothing to the user\". Guard toasts with `MTP::IgnoreError(error)` or use `MTP::ShowErrorFallback(show, error)` (both in `mtproto/mtproto_response.h`) which shows `error.type()` as a toast unless the error should be ignored.\n\n### API Request Callback Lifetime\n\n`api().request(...)` callbacks are owned by the session, not by whatever created\nthem. A `.done()` / `.fail()` handler stays alive for the whole session lifetime,\nso a handler that captured a widget, a box, a controller, or any shorter-lived\nstate still runs after that state is gone. A plain `[=]` capture warns about\nnothing, which makes this one of the easiest ways to write a use-after-free here.\n\nCapturing only plain values or session-owned objects is fine. When anything\ncaptured can die before the session does, pick one of three:\n\n**1. Guard the callback with `crl::guard`.** The request is always sent; the\nhandler is skipped when the context is gone. Use when the call itself must reach\nthe server and only the local reaction is optional.\n\n```cpp\napi().request(MTPmethod(\n\t...\n)).done(crl::guard(this, [=](const MTPResult &result) {\n\t// runs only while `this` is still alive\n})).send();\n```\n\nAccepted guards, in rough order of how often they are used: a raw pointer or\n`not_null` to any `QObject`-derived type — widgets, boxes, controllers — where the\n`QPointer` is created on the spot, so passing `this` is the normal case; a raw\npointer or `not_null` to a `base::has_weak_ptr` type; `QPointer`, `QWeakPointer`,\n`QSharedPointer`; `base::weak_ptr`, `base::weak_qptr`; `std::weak_ptr`,\n`std::shared_ptr`; and `base::binary_guard`.\n\n**2. Remember the `mtpRequestId` and cancel it.** Cancel when the result stops\nbeing relevant, and in the destructor. The request may never reach the server —\nif it is still queued when cancelled, or connectivity dies first, it is simply\ndropped — so never use this when the call itself has to happen.\n\n```cpp\n_requestId = api().request(MTPmethod(\n\t...\n)).done([=](const MTPResult &result) {\n\t_requestId = 0;\n\t...\n}).send();\n\n// when the result is no longer relevant, and in the destructor:\napi().request(base::take(_requestId)).cancel();\n```\n\n**3. Own an `MTP::Sender`.** Its destructor cancels everything it sent that is\nstill in flight, so request lifetime follows the owner with no bookkeeping. Same\ndelivery caveat as (2). Prefer this for a widget, box, or controller that issues\nmore than a request or two.\n\n```cpp\n// header\n\tMTP::Sender _api;\n\n// constructor initializer list\n, _api(&session->mtp())\n\n// requests sent through it die with the owner\n_api.request(MTPmethod(\n\t...\n)).done([=](const MTPResult &result) {\n\t...\n}).send();\n```\n\nChoosing between them: if the server must see the request, use (1). If it only\nmatters while its owner is alive, use (3) — or (2) when a single request does not\njustify a `Sender` member.\n\n## UI Styling\n\n### Style Files\n\nUI styles are defined in `.style` files using custom syntax:\n\n```style\nusing \"ui/basic.style\";\nusing \"ui/widgets/widgets.style\";\n\nMyButtonStyle {\n    textPadding: margins;\n    icon: icon;\n    height: pixels;\n}\n\ndefaultButton: MyButtonStyle {\n    textPadding: margins(10px, 15px, 10px, 15px);\n    icon: icon{{ \"gui/icons/search\", iconColor }};\n    height: 30px;\n}\n\nprimaryButton: MyButtonStyle(defaultButton) {\n    icon: icon{{ \"gui/icons/check\", iconColor }};\n}\n```\n\n**Built-in types:**\n- `int` - Integer numbers (e.g., `maxLines: 3;`)\n- `bool` - Boolean values (e.g., `useShadow: true;`)\n- `pixels` - Pixel values with `px` suffix (e.g., `10px`)\n- `color` - Named colors from `ui/colors.palette`\n- `icon` - Inline icon definition: `icon{{ \"path/stem\", color }}`\n- `margins` - Four values: `margins(left, top, right, bottom)`\n- `size` - Two values: `size(width, height)`\n- `point` - Two values: `point(x, y)`\n- `align` - Alignment: `align(center)`, `align(left)`\n- `font` - Font: `font(14px semibold)`\n- `double` - Floating point\n\n**Multi-part icons** (layers drawn bottom-up):\n```style\nmyComplexIcon: icon{\n  { \"gui/icons/background\", iconBgColor },\n  { \"gui/icons/foreground\", iconFgColor }\n};\n```\n\n**Borders** are typically separate fields, not a single property:\n```style\nchatInput {\n  border: 1px;                       // width\n  borderFg: defaultInputFieldBorder; // color\n}\n```\n\n**Never hardcode sizes in code:**\n\nThe app supports different interface scale options. Style `px` values are automatically scaled at runtime, but raw integer constants in code are not. Never use hardcoded numbers for margins, paddings, spacing, sizes, coordinates, or any other dimensional values. Always define them in `.style` files and reference via `st::`.\n\n```cpp\n// BAD - breaks at non-100% interface scale:\np.drawText(10, 20, text);\nwidget->setFixedHeight(48);\nauto margin = 8;\nauto iconSize = QSize(24, 24);\n\n// GOOD - define in .style file and reference:\np.drawText(st::myWidgetTextLeft, st::myWidgetTextTop, text);\nwidget->setFixedHeight(st::myWidgetHeight);\nauto margin = st::myWidgetMargin;\nauto iconSize = st::myWidgetIconSize;\n```\n\n**Duration constants**: Animation durations should NOT go in `.style` files, this is a legacy approach. Prefer `constexpr auto kName = crl::time(N)` in an anonymous namespace in the relevant `.cpp` file.\n\n### Usage in Code\n\n```cpp\n#include \"styles/style_widgets.h\"\n\n// Access style members\nint height = st::primaryButton.height;\nconst style::icon &icon = st::primaryButton.icon;\nstyle::margins padding = st::primaryButton.textPadding;\n\n// Use in painting\nvoid MyWidget::paintEvent(QPaintEvent *e) {\n    Painter p(this);\n    p.fillRect(rect(), st::chatInput.backgroundColor);\n}\n```\n\n## Localization\n\n### String Definitions\n\nStrings are defined in `Telegram/Resources/langs/lang.strings`:\n\n```\n\"lng_settings_title\" = \"Settings\";\n\"lng_confirm_delete_item\" = \"Are you sure you want to delete {item_name}?\";\n\"lng_files_selected#one\" = \"{count} file selected\";\n\"lng_files_selected#other\" = \"{count} files selected\";\n```\n\n### Usage in Code\n\n**Immediate (current value):**\n\n```cpp\nauto currentTitle = tr::lng_settings_title(tr::now);\n\nauto currentConfirmation = tr::lng_confirm_delete_item(\n    tr::now,\n    lt_item_name, currentItemName);\n\nauto filesText = tr::lng_files_selected(tr::now, lt_count, count);\n```\n\n**Reactive (rpl::producer):**\n\n```cpp\nauto titleProducer = tr::lng_settings_title();\n\nauto confirmationProducer = tr::lng_confirm_delete_item(\n    lt_item_name,\n    std::move(itemNameProducer));\n\nauto filesTextProducer = tr::lng_files_selected(\n    lt_count,\n    countProducer | tr::to_count());\n```\n\n**Key points:**\n- Pass `tr::now` as first argument for immediate `QString`\n- Omit `tr::now` for reactive `rpl::producer<QString>`\n- Placeholders use `lt_tag_name, value` pattern\n- For `{count}`: immediate uses `int`, reactive uses `rpl::producer<float64>` with `| tr::to_count()`\n- Move producers with `std::move` when passing to placeholders\n- Rich text projectors — these `tr::` helpers serve double duty: as the **last argument** (projector) they set the return type to `TextWithEntities`, and as **placeholder values** they wrap individual substitutions in formatting. Always prefer them over `Ui::Text::Bold()`, `Ui::Text::RichLangValue`, etc. — see REVIEW.md for the full mapping.\n  - `tr::marked` — basic projection, converts `QString` to `TextWithEntities`\n  - `tr::rich` — interprets `**bold**`/`__italic__` markup in the string\n  - `tr::bold`, `tr::italic`, `tr::underline` — wrap text in that formatting\n  - `tr::link` — wrap as a clickable link\n  - `tr::url(u\"https://...\"_q)` — returns a projection that converts text to a link pointing to the given URL; can be passed to `rpl::map` or directly to a `tr::lng_...` call\n  ```cpp\n  // As last argument (projector):\n  auto title = tr::lng_export_progress_title(tr::now, tr::bold);\n  auto text = tr::lng_proxy_incorrect_secret(tr::now, tr::rich);\n  // As placeholder value wrapper + projector:\n  auto desc = tr::lng_some_key(\n      tr::now,\n      lt_name,\n      tr::bold(userName),\n      lt_group,\n      tr::bold(groupName),\n      tr::rich);\n  // Nested tr::lng as placeholder:\n  auto linked = tr::lng_settings_birthday_contacts(\n      lt_link,\n      tr::lng_settings_birthday_contacts_link(tr::url(link)),\n      tr::marked);\n  ```\n\n## RPL (Reactive Programming Library)\n\n### Core Concepts\n\n**Producers** represent streams of values over time:\n\n```cpp\nauto intProducer = rpl::single(123);  // Emits single value\nauto lifetime = rpl::lifetime();       // Manages subscription lifetime\n```\n\n### Starting Pipelines\n\n```cpp\nstd::move(counter) | rpl::on_next([=](int value) {\n    qDebug() << \"Received: \" << value;\n}, lifetime);\n\n// Without lifetime parameter - MUST store returned lifetime:\nauto subscriptionLifetime = std::move(counter) | rpl::on_next([=](int value) {\n    // process value\n});\n```\n\n### Transforming Producers\n\n```cpp\nauto strings = std::move(ints) | rpl::map([](int value) {\n    return QString::number(value * 2);\n});\n\nauto evenInts = std::move(ints) | rpl::filter([](int value) {\n    return (value % 2 == 0);\n});\n```\n\n### Combining Producers\n\n**`rpl::combine`** - combines latest values (lambdas receive unpacked arguments):\n\n```cpp\nauto combined = rpl::combine(countProducer, textProducer);\n\nstd::move(combined) | rpl::on_next([=](int count, const QString &text) {\n    qDebug() << \"Count=\" << count << \", Text=\" << text;\n}, lifetime);\n```\n\n**`rpl::merge`** - merges producers of same type:\n\n```cpp\nauto merged = rpl::merge(sourceA, sourceB);\n\nstd::move(merged) | rpl::on_next([=](QString &&value) {\n    qDebug() << \"Merged value: \" << value;\n}, lifetime);\n```\n\n**Other pipeline starters** — besides `rpl::on_next`, there are:\n- `rpl::on_error([=](Error &&e) { ... }, lifetime)` — handle errors\n- `rpl::on_done([=] { ... }, lifetime)` — handle stream completion\n- `rpl::on_next_error_done(nextCb, errorCb, doneCb, lifetime)` — handle all three\n\nThe `Error` template parameter defaults to `rpl::no_error`: `rpl::producer<Type, Error = no_error>`.\n\n**Key points:**\n- Explicitly `std::move` producers when starting pipelines\n- Pass `rpl::lifetime` to `on_...` methods or store returned lifetime\n- Use `rpl::duplicate(producer)` to reuse a producer multiple times\n- Combined producers automatically unpack tuples in lambdas (works with `rpl::map`, `rpl::filter`, and `rpl::on_next`)\n","category":"root","tokens":7560},{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# Claude Code Pointer\n\nRead `AGENTS.md` and treat it as the canonical repository-wide instructions.\n","category":"root","tokens":25}]}