{"owner":"telegramdesktop","repo":"tdesktop","hasSkills":true,"totalSkillsCount":4,"totalTokensCount":16951,"categories":["root-instruction","command-prompt","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md",".claude/commands/release.md",".claude/commands/icon.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## AI Tasks\n\nIn this repository \"task\" is a specific term. It always means one work record in\nthe sibling `ai-tdesktop` repository, never a `TODO` comment, a checklist item,\nor a unit of work invented during the current conversation. \"The tasks\", \"the\nqueue\", \"the board\", \"what's most pressing\", and a bare task slug all refer to\nthat queue.\n\n- The queue lives in `../ai-tdesktop`, a sibling of this checkout, with one\n  record per directory under `tasks/YYYY/MM/DD/<slug>/`. A task id is that dated\n  path, for example `2026/07/18/fix-community-forward`, and it is the only\n  durable link between a source commit and its task. Linked slot worktrees live\n  in `../ai-tdesktop-worktrees`. Read `../ai-tdesktop/AGENTS.md` before doing\n  anything inside that repository.\n- Each record holds `task.md` — one `# ` title line, then one self-contained\n  paragraph that is the task's description wherever it is summarized — and\n  `state.yaml` with `status`, `type`, `depends_on`, `claimed_by`, and dates.\n  Open statuses are `todo`, `in-progress`, `blocked`, and `split-required`;\n  `approved` is completed history, and `split.yaml` / `superseded.yaml` mark\n  retired records.\n- To browse the queue, read those files directly. Do not run `python3 ai.py`: it\n  is a full-screen browser for the user's own terminal and refuses to run in a\n  pipe. A compact open listing:\n\n```bash\ncd ../ai-tdesktop && for state in tasks/*/*/*/*/state.yaml; do\n  status=$(sed -n 's/^status: //p' \"$state\")\n  case \"$status\" in todo|in-progress|blocked|split-required)\n    dir=$(dirname \"$state\")\n    printf '%-14s %-70s %s\\n' \"$status\" \"${dir#tasks/}\" \\\n      \"$(sed -n '1s/^# //p' \"$dir/task.md\")\";;\n  esac\ndone | sort\n```\n\n- When ranking open work by urgency, use `in-progress`, then `blocked`, then\n  `split-required`, then ready `todo` (no `claimed_by`, and every `depends_on`\n  already `approved`), then `todo` still waiting on a dependency. Say who owns\n  claimed work. This checkout's tag is the `Telegram/build/ai-machine-tag` value\n  plus the checkout folder name, such as `macbook-tdesktop`, and work claimed by\n  another checkout is never taken over without an explicit human reassignment.\n- Browsing is read-only. Listing, summarizing, comparing, or recommending tasks\n  never edits `state.yaml`, publishes a lifecycle commit, or starts\n  implementation, no matter how small the task looks.\n- Acting on an existing queue task goes through the workflow skills instead of\n  by hand: `perform-task <slug or full id>` starts or resumes and performs\n  exactly one known task, and `continue <batch specification>` runs the batch\n  of tasks the specification selects. Source commits owned by a task use the\n  three-line form described under `## Commits`.\n- A request the user makes in conversation is ordinary work: do it directly in\n  this checkout. Do not write it to the ignored `../ai-tdesktop/inbox/inbox.md`,\n  and do not run `process-inbox` or `continue` over it, unless the user asks for\n  that. The queue's planning, review and evidence campaign costs hours, so\n  spending it on a request the user expected you to just do is a real error, not\n  thoroughness.\n- You may *offer* the queue when the work genuinely earns it: a large or\n  ambiguous change, one whose correctness needs a real testing campaign, or one\n  that is safety-, security- or data-safety-critical. Make the offer in one\n  line, say why, and route it only after the user approves. Absent approval,\n  implement it directly and say what you skipped.\n- Never guess between similarly named tasks. Report the matching full ids and\n  let the user choose.\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## Dependency backports\n\nFor explicitly requested dependency updates and backports, use the dependency\nrepositories' `master` branches directly. Do not create `codex/*` or other topic\nbranches in `Libraries/patches`, `Libraries/tg_owt`, or `Libraries/tg_angle`.\nThe scheduled dependency monitor remains advisory and does not implement fixes.\n\n- Before editing each dependency repository, inspect its working tree and\n  current branch, fetch `origin`, and verify that the checkout is on `master`,\n  tracks `origin/master`, and is synchronized with the fetched remote branch.\n  Switch to `master` when necessary and fast-forward it when possible. Review\n  divergence before integrating it; preserve unrelated edits, untracked library\n  checkouts and local commits. Do not reset them away or publish unrelated local\n  commits implicitly.\n- Commit fixes to our copied dependencies, including WebRTC and ANGLE, directly\n  in `tg_owt` and `tg_angle`. For external dependencies such as Qt and FFmpeg,\n  commit patch files to `Libraries/patches` and apply them consistently through\n  the applicable prepare, Docker and Snap recipes. Keep intentional differences\n  between platform versions and do not add patch copies inside tdesktop.\n- Keep each independent fix in a separate commit and retain its upstream\n  provenance. Review applicability, prerequisites and patch interactions before\n  committing. Respect the requested validation scope; do not rebuild dependencies\n  unless the user explicitly requests it.\n- Push `master` to `origin/master` without force, fetch again and verify that\n  every new dependency commit is reachable from the published `origin/master`.\n  Only then update the applicable tdesktop revisions to those published SHAs.\n  If integration changes a SHA, update every recipe that consumed the old one.\n  A commit available only on a local or topic branch is not a finished update.\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**On macOS, never run an Xcode clean against `out/`** - not `--target clean`,\nnot `--clean-first` with any target, not `xcodebuild ... clean` and not a\nClean in the Xcode app. Xcode's build-products folder for `Telegram` is\n`out/Debug`, which also holds the portable account folders. When `out/Debug`\ncarries the `com.apple.xcode.CreatedByBuildSystem` extended attribute,\n`--target clean` deletes `out/Debug` with every folder in it, and\n`--target Telegram --clean-first` does the same before it builds. Without the\nattribute, `--target clean` refuses to delete `out/Debug` and ends\n`** CLEAN FAILED **` (exit 65). It ends the same way when it refuses only\nanother, unmarked products folder and deletes a marked `out/Debug`, so exit 65\nalone does not prove that `out/Debug` survived. To force a full recompile,\nfollow the `out/build` deletion in \"### Forcing a full recompile on macOS\"\nunder \"## Troubleshooting\" exactly as written there, its precondition and its\nchecks before and after the rebuild included. Never mark `out/Debug` deletable\nwith `xattr -w com.apple.xcode.CreatedByBuildSystem true`, as the note in\nXcode's refusal suggests, and never move, rename or remove any `out/Debug`\nentry to make a clean succeed.\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- First-time configure of a new `out/` tree: set the `QT` environment variable\n  to the Qt version this checkout actually has, for example `export QT=6.11.1`\n  when `../Libraries/local/Qt-6.11.1` is the installed one. The value names a\n  directory, so it is the checkout's version, not a constant.\n- Reconfiguring an existing `out/` tree: do **not** export `QT`. Run\n  `env -u QT cmake -S . -B out` instead. `cmake/external/qt/package.cmake`\n  writes an exported `QT` into the `qt_requested` cache entry with `FORCE`, so\n  it overwrites the version the tree was already configured with instead of\n  being ignored. A value that disagrees with the configured tree then fails the\n  regeneration, and the generated project keeps the source list it was last\n  generated with: newly added sources are never compiled — which surfaces later\n  as undefined-symbol link errors rather than as a configure error — and newly\n  added `.style` modules leave the generated `style_*.h` includes stale.\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. Rebuild every object once by following the\n`out/build` deletion in \"### Forcing a full recompile on macOS\" exactly as\nwritten there, its precondition and checks included, never with an Xcode clean\n(see the macOS statement in \"### Build Commands\" under\n\"## Build Configuration\"). Then rerun the same test once. If the signature\npersists after that clean rebuild, continue normal crash diagnosis or report\nthe blocker. Do not loop clean rebuilds.\n\n### Forcing a full recompile on macOS\n\nStart only when no build of this checkout is running and this checkout's exact\nTelegram process is stopped. Never stop any other process for this procedure.\nOutside the autonomous `continue` / `perform-task` workflow, stopping that\nprocess follows \"### Build output locks\": the user may be testing it, so ask\nthe user first and never terminate it without explicit permission. Then\nrebuild every object by deleting the intermediate tree `out/build`, never with\nan Xcode clean (see the macOS statement in \"### Build Commands\" under\n\"## Build Configuration\").\n\nThe generated project keeps every intermediate (object files, precompiled\nheaders and Xcode's build database) under `out/build`, its `SYMROOT`, and\nonly build products in `out/Debug` and the other per-target `Debug` folders.\nDeleting `out/build` therefore works the same whether or not `out/Debug`\ncarries the `com.apple.xcode.CreatedByBuildSystem` attribute, and it touches\nnothing in `out/Debug`. From the repository root, record what `out/Debug`\nholds, delete the intermediates and confirm that `out/build` is gone:\n\n```bash\ntest -d out/Telegram.xcodeproj &&\n  ls -A out/Debug | sort > out/language-rebuild-mark.txt &&\n  rm -rf out/build &&\n  test ! -e out/build &&\n  echo 'out/build is gone'\n```\n\nIt must print `out/build is gone`: no intermediate object survived. Object\nfiles elsewhere in `out/`, such as CMake's compiler checks or a release\nbuild's tree, belong to other builds; leave them alone. This step has no\naccepted failure: an error, other output or no output stops the procedure.\nThen run one full Debug rebuild, which has to compile every object again:\n\n```bash\ncmake --build out --config Debug --target Telegram\n```\n\nIt must end `** BUILD SUCCEEDED **`. Then check that the rebuild compiled every\nobject and that nothing in `out/Debug` was lost:\n\n```bash\ntest -f out/language-rebuild-mark.txt &&\n  find out/build -name '*.o' ! -newer out/language-rebuild-mark.txt | wc -l &&\n  find out/build -name '*.o' | wc -l &&\n  ls -A out/Debug | sort | comm -23 out/language-rebuild-mark.txt -\n```\n\nIt must print `0` (no object file is older than the mark written before\n`out/build` was deleted), then a nonzero object count, and nothing else (every\nentry `out/Debug` held before is still there). Anything else stops the\nprocedure; report what it printed.\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## Debug-Only Code\n\nProduction translation units stay free of debug machinery. The permanent test\nharness lives in `Telegram/SourceFiles/test/`, and the disposable `-testagent`\noverlay owns per-task instrumentation; production code carries at most a thin\nsingle-call seam (a `Test::Fire()`-style waitpoint or a live-object\npublication at a construction seam).\n\n- Do not add `#ifdef _DEBUG` blocks, debug-only types, debug state, mutexes,\n  counters, or observation structs to production headers or sources. When a\n  behavior cannot be observed without such machinery, that is a harness gap:\n  extend the `test/` helpers or the overlay instead.\n- Never commit debugging machinery interleaved with working code in the same\n  translation unit. A permanent helper belongs under `test/`; a temporary one\n  belongs in the overlay and is never retained.\n- Exceptions exist — a few `#ifdef _DEBUG` hooks are deliberately kept in\n  production files — but they are exceptions and each new one needs a solid,\n  stated reason. \"The test needed it\" is not one.\n\n## Harness documentation\n\nKeep `Telegram/SourceFiles/test/README.md` a short mandatory guide, under\n8 KiB. When adding or changing a harness helper, follow\n`Telegram/SourceFiles/test/docs/adding-helpers.md`: add or update one helper\nindex row and its on-demand reference, and keep the source header focused on\nits callable contract. Detailed examples, platform caveats and diagnostic\nhistory belong in the references, not in the mandatory README. Overlay work\nreads that guide, the references selected by the task and their headers;\nnever require reading every helper page.\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- Decide the `[ai] ` prefix separately for each commit. Use it only when every\n  retained change in that commit, and the commit's purpose, are exclusively\n  about the AI workflow: the agent harness, skills, prompts, custom commands,\n  agent documentation, or AI testing infrastructure. Typical qualifying paths\n  include `Telegram/SourceFiles/test/`, `.agents/`, `.claude/`, `.grok/`,\n  `AGENTS.md`, `CLAUDE.md`, and `GROK.md`, but paths alone do not decide the\n  prefix. Product-specific test seams, app code, and build-system integration do\n  not qualify merely because agents use them for verification. Split mixed\n  workflow and product work into separate commits when practical; otherwise the\n  mixed commit must not use `[ai] `. Do not count the disposable test overlay or\n  external AI task artifacts. Every other commit must not contain `[ai]`\n  anywhere.\n- The `[ai] ` prefix marks the commit's scope, never its authorship. It does\n  not mean \"authored by an AI\": an AI-authored product fix takes a plain\n  subject, and a workflow-only commit takes the prefix no matter who wrote it.\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- Never create backup branches or tags before a rebase, reorder, squash or any\n  other history rewrite. The branch reflog keeps the old tip; report its sha\n  as the undo path instead.\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## Area maps\n\n`Telegram/SourceFiles/AGENTS.md` is the source atlas: every area of the\ntree with its paths and responsibility. A directory may also\nhave its own `AGENTS.md`: the map of that area's modules, placement rules,\ninvariants, and things to leave alone. Before adding code, read the atlas\nentry of the area you touch and the nearest map, and put new code where they\nsay. A change that needs a different shape amends the map in the same task.\nHow the atlas and maps are written and how changes are checked against them\nis in `.agents/skills/code-health/SKILL.md`.\n\n## Coding Style\n\n**Comments are rationed:**\n\nA comment is one line; two or three only when the block opens with `// WHY:`. A commit may add two comment lines plus one such exception, and a trailing comment is a line too; only `} // namespace X` closers and `#endif // X` labels are free. Say why, never what. Hooks enforce it, and `Telegram/SourceFiles/test/` is exempt.\n\nDo not remove existing comments just to satisfy this rule. Preserve comments unless your change makes them incorrect or truly obsolete; when moving or refactoring code, move the useful comment with it. Inline comments that label positional arguments for generated or schema-driven APIs (for example TL/MTP constructors) are useful because the field names are not visible in the call itself.\n\n**Never cite line numbers** in comments, `README.md` files or other checked-in text. Name the function, member, constant or type, and its file when that helps: write `Runner::finish()` in `test_runner.cpp`, not `test_runner.cpp:NNN` or `:NNN-MMM`. Line numbers drift with every edit and nothing updates them. This covers submodule, Qt and other third-party sources too. Quoted log or crash text stays verbatim.\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 - one line, and it carries a reason:\n_limit = kDefaultLimit; // the server rejects anything larger\n\n// ACCEPTABLE - the exception: opens with WHY, three lines at most,\n// once per commit.\n// WHY: the server sends the id before the peer exists, so the row is\n// created empty and filled on the next update, or the list flickers\n// on every reconnect.\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**Never discard a result with a cast:** `static_cast<void>(...)` and `(void)expr` are banned; instead of silencing `[[nodiscard]]`, fix the design.\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## Input Method Hints\n\nA field for typed content tells the input method what it is for: call `setInputMethodHints` right after creating it (`Ui::InputField` forwards the hints to its inner editor; `Ui::MaskedInputField` subclasses set them in the constructor). Combinations already used in the code:\n\n- URL: `Qt::ImhUrlCharactersOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- E-mail: `Qt::ImhEmailCharactersOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- Phone: `Qt::ImhDialableCharactersOnly | Qt::ImhNoPredictiveText`\n- Digits only (codes, date and time parts): `Qt::ImhDigitsOnly | Qt::ImhNoPredictiveText`\n- Numbers with separators (amounts, timestamps): `Qt::ImhFormattedNumbersOnly | Qt::ImhNoPredictiveText`\n- Latin identifiers (usernames, file extensions, keys): `Qt::ImhLatinOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- Add `Qt::ImhSensitiveData` to one-time codes and to secrets shown in clear text, such as recovery phrase words\n- Free text (names, titles, comments, search) gets no hints; `Ui::PasswordInput` sets its own\n\nPrefer the hint that says what the field is for, as long as it works or at least doesn't conflict with what works. To see what works, check every consumer on every platform tdesktop ships at the Qt version in use: the Qt platform plugins (windows, cocoa, xcb, wayland) and the input context plugins, including the bundled ones (fcitx5-qt). If the semantic hint conflicts, pick what works. Hints don't filter input: the field still drops unwanted characters itself.\n\nAs of 2026-10-20, `Qt::ImhDate` and `Qt::ImhTime` do nothing anywhere tdesktop runs. Qt on Wayland sends them as the date/time content purpose, but no Wayland on-screen keyboard implements those purposes (Qt Virtual Keyboard, which Plasma Keyboard is built on, GNOME Shell, Squeekboard). Qt reads them on none of tdesktop's other platforms (Windows, macOS, X11), and neither does the bundled fcitx5-qt. Used instead of the digit hints, they lose the digit layouts, so date and time fields use the digit hints above.\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- Every plural form, `#one` included, keeps `{count}`: many languages put more than 1 into their \"one\" category (Russian: 21, 31, ...), so a `#one` phrase written for exactly 1 is wrong for them. When exactly 1 deserves different wording, add a separate non-plural key and pick it in code, so every language gets that special case\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\n### Per-subscription state\n\n`rpl::on_next` stores the handler by value. Each emission invokes a *copy* of that stored handler, so a nested emit does not re-enter the running callable. Writes a `mutable` lambda makes to its own captures land on the copy and are discarded, so they never become per-subscription state.\n\n```cpp\n// BAD - compiles, runs, and grew is just value > 0 every time:\nevents | rpl::on_next([last = 0](int value) mutable {\n    const auto grew = (value > last);\n    last = value;\n    if (grew) {\n        // ...\n    }\n}, lifetime);\n\n// GOOD - state lives beside the handler:\nconst auto last = lifetime.make_state<int>(0);\nevents | rpl::on_next([=](int value) {\n    const auto grew = (value > *last);\n    *last = value;\n    if (grew) {\n        // ...\n    }\n}, lifetime);\n```\n\nKeep per-subscription state in `lifetime.make_state<T>()`, behind a `shared_ptr`, or on the object that owns the subscription. A `mutable` lambda that is one-shot — `rpl::take(1)`, a self-destroying subscription, a `crl::on_main` or `crl::async` callback, a `done` or `error` callback — is not this trap.\n",".claude/commands/release.md":"---\ndescription: Prepare changelog, set version, and commit a new release\nallowed-tools: Read, Bash, Edit, Grep, AskUserQuestion\n---\n\n# Release — Changelog, Set Version, Commit\n\nFull release flow: generate changelog entry, run `set_version`, and commit.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nParse `$ARGUMENTS` for two optional parts (in any order):\n- A **version number** like `6.7` or `6.7.0` — if provided, use it as the new version exactly.\n- The word **\"beta\"** — if present, mark the release as beta.\n\nIf no version number is given, auto-increment the patch component (see step 2).\n\n## Steps\n\n### 1. Check git status is clean\n\nRun `git status --porcelain`. If there are any uncommitted changes, **stop** and ask the user to commit or discard them before proceeding. Do not continue until status is clean.\n\n### 2. Read the current changelog\n\nRead `changelog.txt` from the repository root. Note the **latest version number** on the first line (e.g. `6.6.3 beta (12.03.26)`). Parse its major.minor.patch components.\n\n### 3. Determine the new version number\n\n**Important:** version numbers are shared across the beta and stable tracks — the sequence advances through both. The same `major.minor.patch` cannot be released as both beta and stable. A beta release \"uses up\" that number; the next stable must bump to a new patch.\n\n- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given). If the latest changelog entry already used this exact number (regardless of beta/stable), warn the user — they likely want a bumped patch.\n- **If no version was provided**, auto-increment from the latest changelog version:\n  - If it was a beta, and the new release is **not** beta, bump the patch component by 1 (do **not** reuse the beta's number for stable).\n  - If the new release is beta and the latest was also beta with the same major.minor, bump patch.\n  - Otherwise bump the patch component by 1.\n- Present the chosen version to the user and ask for confirmation before proceeding. If the user suggests a different version, use that instead.\n\n### 4. Fetch tags and determine the last release tag\n\nRun `git fetch origin --tags` first to ensure all tags from the public repository are available locally. Then run `git tag --sort=-v:refname` and find the most recent `v*` tag. This is the baseline for the diff.\n\n### 5. Collect commits\n\nRun `git log <last-tag>..HEAD --oneline` to get all commits since the last release.\n\n### 6. Write the changelog entry\n\nAnalyze every commit message. Group them mentally into features, improvements, and bug fixes. Then produce **brief, user-facing bullet points** following these rules:\n\n- **Style:** Match the existing changelog tone exactly — short, imperative sentences starting with a verb (Fix, Add, Allow, Show, Improve, Support…). Keep the trailing periods (the existing changelog uses them).\n- **Brevity:** Each bullet should be one short sentence, around 80 characters when possible. No implementation details. No commit hashes.\n- **Selection:** Only include changes that matter to end users. Skip CI, build infra, submodule bumps, code style, refactors, and intermediate WIP commits. Collapse many related commits (e.g. a dozen image-editor commits) into one or two bullets.\n- **Ordering:** Features first, then improvements, then bug fixes.\n- **Quantity:** Aim for 4-12 bullets total depending on the amount of changes.\n\n### 7. Format and insert into changelog.txt\n\nUse this exact format (date is today in DD.MM.YY):\n\n```\n<version> [beta ](DD.MM.YY)\n\n- Bullet one.\n- Bullet two.\n```\n\nPrepend the new entry at the very top of `changelog.txt`, separated by a blank line from the previous first entry. Use the Edit tool.\n\n**Never delete or edit existing entries**, even if the new stable entry merges bullets from prior beta(s). Prior beta blocks remain as historical record for beta-track users.\n\n**For stable releases spanning prior beta(s):** the entry should cover everything since the last **stable** release (not just since the last beta), including noteworthy beta-shipped features, trimmed as needed to stay within 4–12 bullets.\n\n### 8. Wait for approval\n\nAfter writing the entry to `changelog.txt` (step 7), tell the user the changelog has been updated and ask them to review it in the IDE. They can edit it directly and tell you to continue, or tell you what to change in chat. Do **not** print the full entry in chat — the file itself is the review surface.\n\n**Do NOT proceed until the user explicitly approves.**\n\n### 9. Run set_version\n\nOnce approved, run the `set_version` script from the repository root. On Windows:\n\n```\n.\\Telegram\\build\\set_version.bat <version_arg>\n```\n\nWhere `<version_arg>` is formatted as the `set_version` script expects:\n- Stable: `6.7.0` or `6.7`\n- Beta: `6.7.0.beta`\n\nVerify the script exits successfully (exit code 0). If it fails, show the error and stop.\n\n### 10. Commit\n\nStage all changes and create a commit. The commit message format:\n\n**First line:**\n- For stable: `Version <major>.<minor>.` if patch is 0, otherwise `Version <major>.<minor>.<patch>.`\n- For beta: `Beta version <major>.<minor>.<patch>.`\n\n**Then an empty line, then the changelog bullets.** Copy bullet lines from the changelog as-is. Only wrap lines that exceed 72 characters; shorter lines must stay on a single line. When wrapping is needed, break at logically correct places (between words/phrases) and indent continuation lines with two spaces.\n\nExample commit message:\n```\nBeta version 6.6.3.\n\n- Drawing tools in image editor\n  (brush, marker, eraser, arrow).\n- Draw-to-reply button in media viewer.\n- Trim recorded voice messages.\n- Fix reorder freeze in chats list.\n```\n\nUse a HEREDOC to pass the message to `git commit -a`.\n\n### 11. Done\n\nRun `git log -1` to show the resulting commit and confirm success.\n",".claude/commands/icon.md":"---\ndescription: Generate an SVG icon from a design mockup using vectosolve vectorization\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion, TodoWrite, mcp__vectosolve__vectorize\n---\n\n# Icon - SVG Icon Generation from Design Mockup\n\nYou generate production-quality SVG icons for Telegram Desktop by vectorizing design mockup screenshots using the vectosolve MCP service, then post-processing the result to match the Telegram icon format.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf `$ARGUMENTS` is empty, ask the user to describe the icon they want and paste a cropped screenshot of it.\n\n## Overview\n\nThe workflow takes a cropped screenshot of an icon from a design mockup (grabbed from the clipboard), vectorizes it via the vectosolve MCP, then post-processes the SVG (recolor to white-on-transparent, restructure to minimal format, set 24x24 output size).\n\nWorking directory: `.ai/icon_{name}/` with iterations labeled by letter (`a/`, `b/`, ...), each containing `source.png`. Output SVGs are in the icon root: `a.svg`, `b.svg`, etc.\n\nFollow-ups are supported: `/icon {icon_name} <description>` continues from where the previous run left off.\n\n## Phase 0: Setup\n\n**Record the current time** (using `date` or equivalent) as `$START_TIME`.\n\n### Step 0a: Clipboard grab (MUST be the VERY FIRST action)\n\nIf there is an image attached to the user's message:\n\n1. Generate a random 8-character hex string for `HASH` (use `openssl rand -hex 4` or similar).\n2. **IMMEDIATELY** — before any other processing — run this Bash command to save the clipboard image:\n   ```bash\n   HASH=$(openssl rand -hex 4) && if [[ \"$OSTYPE\" == darwin* ]]; then bash .claude/grab_clipboard.sh \".ai/icon_${HASH}.png\"; else powershell -ExecutionPolicy Bypass -File .claude/grab_clipboard.ps1 \".ai/icon_${HASH}.png\"; fi\n   ```\n   On macOS `.claude/grab_clipboard.sh` is used; on Windows `.claude/grab_clipboard.ps1`. Both grab the current clipboard image and save it to the specified path.\n\n3. If the command fails (exit 1 / no image on clipboard):\n   - Tell the user: **\"Clipboard doesn't contain an image. Please copy the icon area first, then retry.\"** (On macOS: Cmd+Ctrl+Shift+4 to snip to clipboard; on Windows: Win+Shift+S.)\n   - **STOP IMMEDIATELY. Do NOT continue.** You cannot use the image pasted in the conversation — it exists only as pixels in the chat, not as a file you can send to vectosolve. The clipboard grab is the ONLY way to get the image to disk. Do not attempt any workaround.\n\n4. Read back the saved `.ai/icon_HASH.png` using the Read tool.\n5. Compare it visually with the image pasted in the conversation. They should depict the same thing.\n   - If they look **completely different**: delete `.ai/icon_HASH.png` and fail:\n     > \"The clipboard image doesn't match what you pasted. Please re-copy and retry.\"\n   - If they look the same (or close enough): proceed. Store the temp path.\n\nIf NO image is attached to the message, skip this step entirely.\n\n### Step 0b: Fail-fast — verify vectosolve MCP\n\nCheck that the `mcp__vectosolve__vectorize` tool is available by looking at your available tools list. If it is NOT available, fail immediately with:\n\n> vectosolve MCP is not configured. Set it up with:\n> ```\n> claude mcp add vectosolve --scope user -e VECTOSOLVE_API_KEY=vs_xxx -- npx @vectosolve/mcp\n> ```\n> Then restart Claude Code.\n\n### Step 0c: Follow-up detection\n\nExtract the first word/token from `$ARGUMENTS` (everything before the first space or newline). Call it `FIRST_TOKEN`.\n\nRun these TWO commands using the Bash tool, **IN PARALLEL**:\n1. `ls .ai/` — to see all existing icon project names\n2. `ls .ai/icon_{FIRST_TOKEN}/context.md` — to check if this specific icon project exists\n\n**Evaluate the results:**\n- If command 2 **succeeds** (context.md exists): this is a **follow-up**. The icon name is `FIRST_TOKEN`. The follow-up description is everything in `$ARGUMENTS` after `FIRST_TOKEN`.\n- If command 2 **fails** (not found): this is a **new icon**. The full `$ARGUMENTS` is the icon description.\n\n### Step 0d: New icon setup\n\n1. Parse `$ARGUMENTS` to determine:\n   - **Icon description**: what the icon should depict\n   - **Icon type**: default is `menu` (24x24 menu/button icon). User may specify otherwise.\n   - **Target subfolder**: `menu/` by default, or another subfolder if specified.\n\n2. Choose an icon file name:\n   - Lowercase letters and underscores only — **NO hyphens**\n   - Match existing naming conventions (check `Telegram/Resources/icons/{subfolder}/`)\n   - Must NOT conflict with existing icons\n   - Must NOT collide with existing `.ai/icon_{name}/` directories\n\n3. Create `.ai/icon_{name}/` and `.ai/icon_{name}/a/`.\n\n4. Write `.ai/icon_{name}/context.md` with:\n   ```\n   ## Icon: {icon_name}\n   Type: {menu/other}\n   Target: Telegram/Resources/icons/{subfolder}/{icon_name}.svg\n\n   ## Original Request\n   {full $ARGUMENTS text}\n\n   ## Follow-ups\n   (none yet)\n   ```\n\n5. Set `LETTER` to `a`.\n\n### Step 0e: Follow-up setup\n\n1. Read `.ai/icon_{name}/context.md` to get the icon type, subfolder, and full history.\n2. Find the latest existing letter folder in `.ai/icon_{name}/` (highest letter).\n3. Set `LETTER` to the next letter after the latest.\n4. Create `.ai/icon_{name}/{LETTER}/`.\n5. Update `.ai/icon_{name}/context.md` — append the follow-up description to the `## Follow-ups` section:\n   ```\n   ### Follow-up (starting at letter {LETTER})\n   {follow-up description}\n   ```\n\n### Step 0f: Place source image\n\nIf a clipboard image was grabbed in Step 0a:\n1. Copy (or move) `.ai/icon_HASH.png` → `.ai/icon_{name}/source.png` (overwrite if exists — this is always the latest source).\n2. Copy it to `.ai/icon_{name}/{LETTER}/source.png` (archive per-iteration source).\n3. Delete the temp `.ai/icon_HASH.png` if it was copied (not moved).\n\nIf NO image was grabbed:\n- **New icon with no image**: Ask the user to provide a screenshot. STOP.\n- **Follow-up with no image**: The existing `source.png` in the icon root carries forward. Copy it to `.ai/icon_{name}/{LETTER}/source.png`. If no source.png exists at all, ask the user for an image.\n\n### Step 0g: Verify renderer\n\nLocate the render tool (`codegen_style` with `--render-svg` mode):\n\n```bash\nif [[ \"$OSTYPE\" == darwin* ]]; then\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style\nelse\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe\nfi\n```\n\nIf missing, build it: `cmake --build out --config Debug --target codegen_style`\n\nTest on a known good SVG (use the appropriate binary path for the OS):\n```bash\nCODEGEN=$(if [[ \"$OSTYPE\" == darwin* ]]; then echo out/Telegram/codegen/codegen/style/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)\n$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512\n```\n\nIf works → delete test render, set `RENDER_AVAILABLE = true`. If fails → `RENDER_AVAILABLE = false`.\n\n## Phase 1: Vectorize & Post-process\n\n### Step 1a: Call vectosolve\n\nUse the `mcp__vectosolve__vectorize` tool with `file_path` set to the **absolute path** of `.ai/icon_{name}/{LETTER}/source.png`.\n\n**If this fails, STOP IMMEDIATELY.** Do NOT try to generate the SVG manually or by any other means. Report the error to the user and let them fix the issue (bad API key, no credits, network error, etc.).\n\nSave the returned SVG content to `.ai/icon_{name}/{LETTER}/raw_vectosolve.svg`.\n\nThe MCP tool calls the vectosolve API ($0.20/call). The API key is stored in `~/.claude.json` MCP config (never in the repository).\n\n### Step 1b: Post-process the SVG\n\nThe vectosolve SVG will have colors from the mockup, arbitrary dimensions, and possibly a non-square aspect ratio from a non-square screenshot crop. Post-processing fixes this by adjusting the **viewBox** — leave path coordinates untouched.\n\n**Do NOT transform path coordinates.** Vectosolve's paths are correct — the only thing wrong is the framing. All geometry adjustments are done by manipulating the `viewBox` and the `width`/`height` attributes.\n\n#### Sub-step 1: Read the request and determine parameters\n\nBefore touching the SVG, determine these from the user's request and context.md:\n\n1. **Output size** (`OUT_W × OUT_H`): default is `24px × 24px` for menu icons. The user may request different dimensions (e.g., 36×36, 48×48, or non-square). Always check the request.\n2. **Content padding**: default is ~2px equivalent on each side at the output scale (so content fills roughly (OUT_W-4) × (OUT_H-4)). The user may request different padding or edge-to-edge.\n3. **Centering**: default is centered both horizontally and vertically. The user may request specific alignment (e.g., \"align to bottom\").\n\n#### Sub-step 2: Parse the raw SVG\n\n1. Extract the `viewBox`: `viewBox=\"VB_X VB_Y VB_W VB_H\"` (typically `0 0 W H`).\n2. Identify ALL paths. Classify each:\n   - **Background**: a rect or path spanning the full viewBox (first path that's a simple rectangle matching the viewBox bounds). **Remove it entirely.**\n   - **Content**: the actual icon shapes. **Keep these, paths unchanged.**\n3. If paths have `transform=\"translate(TX,TY)\"` attributes, that's fine — keep them as-is. The viewBox framing will work regardless.\n\n#### Sub-step 3: Compute the content bounding box\n\nEstimate the bounding box of the content paths (after removing the background). You can either:\n- Eyeball it from the path coordinates (look at first/last M commands and extremes of curves)\n- Or for precision, write a quick script to parse the paths and find min/max X/Y\n\nCall the result: `CX_MIN, CY_MIN, CX_MAX, CY_MAX`. Content dimensions: `CW = CX_MAX - CX_MIN`, `CH = CY_MAX - CY_MIN`.\n\n#### Sub-step 4: Compute the new viewBox\n\nThe viewBox determines what part of the SVG coordinate space maps to the output rectangle. By expanding the viewBox beyond the content bounds, we add padding. By making the viewBox aspect ratio match the output aspect ratio, we prevent stretching.\n\n1. **Output aspect ratio**: `OUT_AR = OUT_W / OUT_H` (for 24×24 this is 1.0).\n2. **Padding in SVG coordinates**: we want ~2px padding at output scale. The scale factor is `OUT_W / VB_CONTENT_W` approximately, so padding in SVG coords = `2 * (CW / (OUT_W - 4))` (or similar — the exact formula depends on which dimension is dominant). Simpler approach: aim for content to occupy ~83% of the viewBox (≈ 20/24), so:\n   - `PADDED_W = CW / 0.83`\n   - `PADDED_H = CH / 0.83`\n3. **Match output aspect ratio**: the viewBox aspect ratio must equal `OUT_AR` to avoid stretching.\n   - If `PADDED_W / PADDED_H > OUT_AR`: width is dominant → `VB_W = PADDED_W`, `VB_H = VB_W / OUT_AR`\n   - If `PADDED_W / PADDED_H < OUT_AR`: height is dominant → `VB_H = PADDED_H`, `VB_W = VB_H * OUT_AR`\n   - If equal: `VB_W = PADDED_W`, `VB_H = PADDED_H`\n4. **Center the content** in the new viewBox:\n   - `VB_X = CX_MIN - (VB_W - CW) / 2`\n   - `VB_Y = CY_MIN - (VB_H - CH) / 2`\n   - (Adjust if the user requested non-centered alignment)\n\nThe new viewBox is: `viewBox=\"VB_X VB_Y VB_W VB_H\"`.\n\n#### Sub-step 5: Recolor to white-on-transparent\n\n- Replace ALL `fill` color values (anything that isn't `none`) with `#FFFFFF`.\n- Remove ALL `stroke` and `stroke-width` attributes entirely.\n- Remove `opacity` attributes if present.\n\n#### Sub-step 6: Determine path composition\n\nLook at the icon's visual structure and decide how paths should combine:\n- **Outlined shape** (e.g., circle outline with something inside): combine outer + inner cutout into one `<path>` with `fill-rule=\"evenodd\"`.\n- **Separate distinct parts** (e.g., magnifying glass + checkmark): keep as separate `<path>` elements.\n- **Filled shape with cutout** (e.g., filled circle with checkmark punched out): combine into one path with `fill-rule=\"evenodd\"`.\n\n#### Sub-step 7: Assemble final SVG\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg width=\"{OUT_W}px\" height=\"{OUT_H}px\" viewBox=\"{VB_X} {VB_Y} {VB_W} {VB_H}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g stroke=\"none\" fill=\"none\" fill-rule=\"evenodd\">\n        <path d=\"...\" fill=\"#FFFFFF\"></path>\n    </g>\n</svg>\n```\n\n- `width`/`height` = the output size from the request (default `24px`/`24px`).\n- `viewBox` = the computed viewBox from Sub-step 4. The SVG renderer maps this coordinate region to the output size.\n- Path `d` attributes are **unchanged** from vectosolve output (just background removed, colors replaced).\n- No `<title>`, `id`, `xmlns:xlink`, `version`, `class`, `style`, XML comments, `<metadata>`, or `preserveAspectRatio`.\n- No `<circle>`, `<rect>`, `<line>` — only `<path>`.\n\nWrite the final SVG to `.ai/icon_{name}/{LETTER}.svg`.\n\n### Step 1c: Render\n\nIf `RENDER_AVAILABLE`:\n```bash\n$CODEGEN --render-svg \".ai/icon_{name}/{LETTER}.svg\" \".ai/icon_{name}/render_{LETTER}.png\" 512\n```\n\nRead the render to visually verify the result.\n\n## Phase 2: Review\n\nAfter rendering, assess the result:\n\n1. **Recognizable?** The icon should be clearly identifiable as the intended symbol.\n2. **Scale reasonable?** Should fill the space appropriately with ~2-3px padding.\n3. **Clean lines?** No broken paths, artifacts, or unwanted elements.\n4. **Correct colors?** All white on transparent (no leftover colors from the mockup).\n\nIf the result looks good → proceed to Phase 3 (Output).\n\nIf there are fixable issues (stray element, missed color, etc.) → fix the SVG directly, re-render, and re-check.\n\nIf the result is poor (vectosolve couldn't handle the input well) → report to the user and suggest:\n- Trying a cleaner/larger crop of the icon\n- Providing a different screenshot\n- Following up: `/icon {icon_name} <description of what to change>`\n\n## Phase 3: Output\n\n1. Read the `Target:` line from `.ai/icon_{name}/context.md` to get the output path.\n\n2. Copy the final SVG to that target path (e.g., `Telegram/Resources/icons/menu/{icon_name}.svg`).\n\n3. Update `.ai/icon_{name}/context.md` — append to the end:\n   ```\n   ## Latest Output\n   Letter: {LETTER}\n   Written to: {target_path}\n   ```\n\n4. Report to the user:\n   - Final icon file path\n   - Number of vectosolve calls made (cost at $0.20/call)\n   - Suggest verifying visually\n   - Working directory `.ai/icon_{name}/` has all iterations\n   - Elapsed time since `$START_TIME` (format `Xm Ys`)\n   - Follow-up: `/icon {icon_name} <description of what to change>`\n\n## Text-only Follow-ups (no new image)\n\nWhen a follow-up has no attached image, the user wants to refine the existing SVG based on text feedback. In this case:\n\n1. Skip Phase 1 (no vectosolve call needed).\n2. Read the latest SVG (`.ai/icon_{name}/{prev_letter}.svg`).\n3. Read the latest render if available.\n4. Apply the user's requested changes by editing the SVG directly.\n5. Save as `.ai/icon_{name}/{LETTER}.svg`.\n6. Render, review, and output as normal (Phases 1c → 3).\n\nIf the changes are too complex for manual SVG editing, suggest the user provide a new screenshot instead.\n\n## Error Handling\n\n- If clipboard grab fails → tell user to re-copy and retry.\n- If vectosolve returns an error → report it and suggest a different/cleaner screenshot.\n- If vectosolve returns SVG that can't be parsed → save raw output for debugging, report to user.\n- If the render helper fails → set `RENDER_AVAILABLE = false`, continue with SVG-only review.\n- If post-processing produces a broken SVG → fall back to the raw vectosolve output and do lighter cleanup.\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## AI Tasks\n\nIn this repository \"task\" is a specific term. It always means one work record in\nthe sibling `ai-tdesktop` repository, never a `TODO` comment, a checklist item,\nor a unit of work invented during the current conversation. \"The tasks\", \"the\nqueue\", \"the board\", \"what's most pressing\", and a bare task slug all refer to\nthat queue.\n\n- The queue lives in `../ai-tdesktop`, a sibling of this checkout, with one\n  record per directory under `tasks/YYYY/MM/DD/<slug>/`. A task id is that dated\n  path, for example `2026/07/18/fix-community-forward`, and it is the only\n  durable link between a source commit and its task. Linked slot worktrees live\n  in `../ai-tdesktop-worktrees`. Read `../ai-tdesktop/AGENTS.md` before doing\n  anything inside that repository.\n- Each record holds `task.md` — one `# ` title line, then one self-contained\n  paragraph that is the task's description wherever it is summarized — and\n  `state.yaml` with `status`, `type`, `depends_on`, `claimed_by`, and dates.\n  Open statuses are `todo`, `in-progress`, `blocked`, and `split-required`;\n  `approved` is completed history, and `split.yaml` / `superseded.yaml` mark\n  retired records.\n- To browse the queue, read those files directly. Do not run `python3 ai.py`: it\n  is a full-screen browser for the user's own terminal and refuses to run in a\n  pipe. A compact open listing:\n\n```bash\ncd ../ai-tdesktop && for state in tasks/*/*/*/*/state.yaml; do\n  status=$(sed -n 's/^status: //p' \"$state\")\n  case \"$status\" in todo|in-progress|blocked|split-required)\n    dir=$(dirname \"$state\")\n    printf '%-14s %-70s %s\\n' \"$status\" \"${dir#tasks/}\" \\\n      \"$(sed -n '1s/^# //p' \"$dir/task.md\")\";;\n  esac\ndone | sort\n```\n\n- When ranking open work by urgency, use `in-progress`, then `blocked`, then\n  `split-required`, then ready `todo` (no `claimed_by`, and every `depends_on`\n  already `approved`), then `todo` still waiting on a dependency. Say who owns\n  claimed work. This checkout's tag is the `Telegram/build/ai-machine-tag` value\n  plus the checkout folder name, such as `macbook-tdesktop`, and work claimed by\n  another checkout is never taken over without an explicit human reassignment.\n- Browsing is read-only. Listing, summarizing, comparing, or recommending tasks\n  never edits `state.yaml`, publishes a lifecycle commit, or starts\n  implementation, no matter how small the task looks.\n- Acting on an existing queue task goes through the workflow skills instead of\n  by hand: `perform-task <slug or full id>` starts or resumes and performs\n  exactly one known task, and `continue <batch specification>` runs the batch\n  of tasks the specification selects. Source commits owned by a task use the\n  three-line form described under `## Commits`.\n- A request the user makes in conversation is ordinary work: do it directly in\n  this checkout. Do not write it to the ignored `../ai-tdesktop/inbox/inbox.md`,\n  and do not run `process-inbox` or `continue` over it, unless the user asks for\n  that. The queue's planning, review and evidence campaign costs hours, so\n  spending it on a request the user expected you to just do is a real error, not\n  thoroughness.\n- You may *offer* the queue when the work genuinely earns it: a large or\n  ambiguous change, one whose correctness needs a real testing campaign, or one\n  that is safety-, security- or data-safety-critical. Make the offer in one\n  line, say why, and route it only after the user approves. Absent approval,\n  implement it directly and say what you skipped.\n- Never guess between similarly named tasks. Report the matching full ids and\n  let the user choose.\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## Dependency backports\n\nFor explicitly requested dependency updates and backports, use the dependency\nrepositories' `master` branches directly. Do not create `codex/*` or other topic\nbranches in `Libraries/patches`, `Libraries/tg_owt`, or `Libraries/tg_angle`.\nThe scheduled dependency monitor remains advisory and does not implement fixes.\n\n- Before editing each dependency repository, inspect its working tree and\n  current branch, fetch `origin`, and verify that the checkout is on `master`,\n  tracks `origin/master`, and is synchronized with the fetched remote branch.\n  Switch to `master` when necessary and fast-forward it when possible. Review\n  divergence before integrating it; preserve unrelated edits, untracked library\n  checkouts and local commits. Do not reset them away or publish unrelated local\n  commits implicitly.\n- Commit fixes to our copied dependencies, including WebRTC and ANGLE, directly\n  in `tg_owt` and `tg_angle`. For external dependencies such as Qt and FFmpeg,\n  commit patch files to `Libraries/patches` and apply them consistently through\n  the applicable prepare, Docker and Snap recipes. Keep intentional differences\n  between platform versions and do not add patch copies inside tdesktop.\n- Keep each independent fix in a separate commit and retain its upstream\n  provenance. Review applicability, prerequisites and patch interactions before\n  committing. Respect the requested validation scope; do not rebuild dependencies\n  unless the user explicitly requests it.\n- Push `master` to `origin/master` without force, fetch again and verify that\n  every new dependency commit is reachable from the published `origin/master`.\n  Only then update the applicable tdesktop revisions to those published SHAs.\n  If integration changes a SHA, update every recipe that consumed the old one.\n  A commit available only on a local or topic branch is not a finished update.\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**On macOS, never run an Xcode clean against `out/`** - not `--target clean`,\nnot `--clean-first` with any target, not `xcodebuild ... clean` and not a\nClean in the Xcode app. Xcode's build-products folder for `Telegram` is\n`out/Debug`, which also holds the portable account folders. When `out/Debug`\ncarries the `com.apple.xcode.CreatedByBuildSystem` extended attribute,\n`--target clean` deletes `out/Debug` with every folder in it, and\n`--target Telegram --clean-first` does the same before it builds. Without the\nattribute, `--target clean` refuses to delete `out/Debug` and ends\n`** CLEAN FAILED **` (exit 65). It ends the same way when it refuses only\nanother, unmarked products folder and deletes a marked `out/Debug`, so exit 65\nalone does not prove that `out/Debug` survived. To force a full recompile,\nfollow the `out/build` deletion in \"### Forcing a full recompile on macOS\"\nunder \"## Troubleshooting\" exactly as written there, its precondition and its\nchecks before and after the rebuild included. Never mark `out/Debug` deletable\nwith `xattr -w com.apple.xcode.CreatedByBuildSystem true`, as the note in\nXcode's refusal suggests, and never move, rename or remove any `out/Debug`\nentry to make a clean succeed.\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- First-time configure of a new `out/` tree: set the `QT` environment variable\n  to the Qt version this checkout actually has, for example `export QT=6.11.1`\n  when `../Libraries/local/Qt-6.11.1` is the installed one. The value names a\n  directory, so it is the checkout's version, not a constant.\n- Reconfiguring an existing `out/` tree: do **not** export `QT`. Run\n  `env -u QT cmake -S . -B out` instead. `cmake/external/qt/package.cmake`\n  writes an exported `QT` into the `qt_requested` cache entry with `FORCE`, so\n  it overwrites the version the tree was already configured with instead of\n  being ignored. A value that disagrees with the configured tree then fails the\n  regeneration, and the generated project keeps the source list it was last\n  generated with: newly added sources are never compiled — which surfaces later\n  as undefined-symbol link errors rather than as a configure error — and newly\n  added `.style` modules leave the generated `style_*.h` includes stale.\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. Rebuild every object once by following the\n`out/build` deletion in \"### Forcing a full recompile on macOS\" exactly as\nwritten there, its precondition and checks included, never with an Xcode clean\n(see the macOS statement in \"### Build Commands\" under\n\"## Build Configuration\"). Then rerun the same test once. If the signature\npersists after that clean rebuild, continue normal crash diagnosis or report\nthe blocker. Do not loop clean rebuilds.\n\n### Forcing a full recompile on macOS\n\nStart only when no build of this checkout is running and this checkout's exact\nTelegram process is stopped. Never stop any other process for this procedure.\nOutside the autonomous `continue` / `perform-task` workflow, stopping that\nprocess follows \"### Build output locks\": the user may be testing it, so ask\nthe user first and never terminate it without explicit permission. Then\nrebuild every object by deleting the intermediate tree `out/build`, never with\nan Xcode clean (see the macOS statement in \"### Build Commands\" under\n\"## Build Configuration\").\n\nThe generated project keeps every intermediate (object files, precompiled\nheaders and Xcode's build database) under `out/build`, its `SYMROOT`, and\nonly build products in `out/Debug` and the other per-target `Debug` folders.\nDeleting `out/build` therefore works the same whether or not `out/Debug`\ncarries the `com.apple.xcode.CreatedByBuildSystem` attribute, and it touches\nnothing in `out/Debug`. From the repository root, record what `out/Debug`\nholds, delete the intermediates and confirm that `out/build` is gone:\n\n```bash\ntest -d out/Telegram.xcodeproj &&\n  ls -A out/Debug | sort > out/language-rebuild-mark.txt &&\n  rm -rf out/build &&\n  test ! -e out/build &&\n  echo 'out/build is gone'\n```\n\nIt must print `out/build is gone`: no intermediate object survived. Object\nfiles elsewhere in `out/`, such as CMake's compiler checks or a release\nbuild's tree, belong to other builds; leave them alone. This step has no\naccepted failure: an error, other output or no output stops the procedure.\nThen run one full Debug rebuild, which has to compile every object again:\n\n```bash\ncmake --build out --config Debug --target Telegram\n```\n\nIt must end `** BUILD SUCCEEDED **`. Then check that the rebuild compiled every\nobject and that nothing in `out/Debug` was lost:\n\n```bash\ntest -f out/language-rebuild-mark.txt &&\n  find out/build -name '*.o' ! -newer out/language-rebuild-mark.txt | wc -l &&\n  find out/build -name '*.o' | wc -l &&\n  ls -A out/Debug | sort | comm -23 out/language-rebuild-mark.txt -\n```\n\nIt must print `0` (no object file is older than the mark written before\n`out/build` was deleted), then a nonzero object count, and nothing else (every\nentry `out/Debug` held before is still there). Anything else stops the\nprocedure; report what it printed.\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## Debug-Only Code\n\nProduction translation units stay free of debug machinery. The permanent test\nharness lives in `Telegram/SourceFiles/test/`, and the disposable `-testagent`\noverlay owns per-task instrumentation; production code carries at most a thin\nsingle-call seam (a `Test::Fire()`-style waitpoint or a live-object\npublication at a construction seam).\n\n- Do not add `#ifdef _DEBUG` blocks, debug-only types, debug state, mutexes,\n  counters, or observation structs to production headers or sources. When a\n  behavior cannot be observed without such machinery, that is a harness gap:\n  extend the `test/` helpers or the overlay instead.\n- Never commit debugging machinery interleaved with working code in the same\n  translation unit. A permanent helper belongs under `test/`; a temporary one\n  belongs in the overlay and is never retained.\n- Exceptions exist — a few `#ifdef _DEBUG` hooks are deliberately kept in\n  production files — but they are exceptions and each new one needs a solid,\n  stated reason. \"The test needed it\" is not one.\n\n## Harness documentation\n\nKeep `Telegram/SourceFiles/test/README.md` a short mandatory guide, under\n8 KiB. When adding or changing a harness helper, follow\n`Telegram/SourceFiles/test/docs/adding-helpers.md`: add or update one helper\nindex row and its on-demand reference, and keep the source header focused on\nits callable contract. Detailed examples, platform caveats and diagnostic\nhistory belong in the references, not in the mandatory README. Overlay work\nreads that guide, the references selected by the task and their headers;\nnever require reading every helper page.\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- Decide the `[ai] ` prefix separately for each commit. Use it only when every\n  retained change in that commit, and the commit's purpose, are exclusively\n  about the AI workflow: the agent harness, skills, prompts, custom commands,\n  agent documentation, or AI testing infrastructure. Typical qualifying paths\n  include `Telegram/SourceFiles/test/`, `.agents/`, `.claude/`, `.grok/`,\n  `AGENTS.md`, `CLAUDE.md`, and `GROK.md`, but paths alone do not decide the\n  prefix. Product-specific test seams, app code, and build-system integration do\n  not qualify merely because agents use them for verification. Split mixed\n  workflow and product work into separate commits when practical; otherwise the\n  mixed commit must not use `[ai] `. Do not count the disposable test overlay or\n  external AI task artifacts. Every other commit must not contain `[ai]`\n  anywhere.\n- The `[ai] ` prefix marks the commit's scope, never its authorship. It does\n  not mean \"authored by an AI\": an AI-authored product fix takes a plain\n  subject, and a workflow-only commit takes the prefix no matter who wrote it.\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- Never create backup branches or tags before a rebase, reorder, squash or any\n  other history rewrite. The branch reflog keeps the old tip; report its sha\n  as the undo path instead.\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## Area maps\n\n`Telegram/SourceFiles/AGENTS.md` is the source atlas: every area of the\ntree with its paths and responsibility. A directory may also\nhave its own `AGENTS.md`: the map of that area's modules, placement rules,\ninvariants, and things to leave alone. Before adding code, read the atlas\nentry of the area you touch and the nearest map, and put new code where they\nsay. A change that needs a different shape amends the map in the same task.\nHow the atlas and maps are written and how changes are checked against them\nis in `.agents/skills/code-health/SKILL.md`.\n\n## Coding Style\n\n**Comments are rationed:**\n\nA comment is one line; two or three only when the block opens with `// WHY:`. A commit may add two comment lines plus one such exception, and a trailing comment is a line too; only `} // namespace X` closers and `#endif // X` labels are free. Say why, never what. Hooks enforce it, and `Telegram/SourceFiles/test/` is exempt.\n\nDo not remove existing comments just to satisfy this rule. Preserve comments unless your change makes them incorrect or truly obsolete; when moving or refactoring code, move the useful comment with it. Inline comments that label positional arguments for generated or schema-driven APIs (for example TL/MTP constructors) are useful because the field names are not visible in the call itself.\n\n**Never cite line numbers** in comments, `README.md` files or other checked-in text. Name the function, member, constant or type, and its file when that helps: write `Runner::finish()` in `test_runner.cpp`, not `test_runner.cpp:NNN` or `:NNN-MMM`. Line numbers drift with every edit and nothing updates them. This covers submodule, Qt and other third-party sources too. Quoted log or crash text stays verbatim.\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 - one line, and it carries a reason:\n_limit = kDefaultLimit; // the server rejects anything larger\n\n// ACCEPTABLE - the exception: opens with WHY, three lines at most,\n// once per commit.\n// WHY: the server sends the id before the peer exists, so the row is\n// created empty and filled on the next update, or the list flickers\n// on every reconnect.\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**Never discard a result with a cast:** `static_cast<void>(...)` and `(void)expr` are banned; instead of silencing `[[nodiscard]]`, fix the design.\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## Input Method Hints\n\nA field for typed content tells the input method what it is for: call `setInputMethodHints` right after creating it (`Ui::InputField` forwards the hints to its inner editor; `Ui::MaskedInputField` subclasses set them in the constructor). Combinations already used in the code:\n\n- URL: `Qt::ImhUrlCharactersOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- E-mail: `Qt::ImhEmailCharactersOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- Phone: `Qt::ImhDialableCharactersOnly | Qt::ImhNoPredictiveText`\n- Digits only (codes, date and time parts): `Qt::ImhDigitsOnly | Qt::ImhNoPredictiveText`\n- Numbers with separators (amounts, timestamps): `Qt::ImhFormattedNumbersOnly | Qt::ImhNoPredictiveText`\n- Latin identifiers (usernames, file extensions, keys): `Qt::ImhLatinOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- Add `Qt::ImhSensitiveData` to one-time codes and to secrets shown in clear text, such as recovery phrase words\n- Free text (names, titles, comments, search) gets no hints; `Ui::PasswordInput` sets its own\n\nPrefer the hint that says what the field is for, as long as it works or at least doesn't conflict with what works. To see what works, check every consumer on every platform tdesktop ships at the Qt version in use: the Qt platform plugins (windows, cocoa, xcb, wayland) and the input context plugins, including the bundled ones (fcitx5-qt). If the semantic hint conflicts, pick what works. Hints don't filter input: the field still drops unwanted characters itself.\n\nAs of 2026-10-20, `Qt::ImhDate` and `Qt::ImhTime` do nothing anywhere tdesktop runs. Qt on Wayland sends them as the date/time content purpose, but no Wayland on-screen keyboard implements those purposes (Qt Virtual Keyboard, which Plasma Keyboard is built on, GNOME Shell, Squeekboard). Qt reads them on none of tdesktop's other platforms (Windows, macOS, X11), and neither does the bundled fcitx5-qt. Used instead of the digit hints, they lose the digit layouts, so date and time fields use the digit hints above.\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- Every plural form, `#one` included, keeps `{count}`: many languages put more than 1 into their \"one\" category (Russian: 21, 31, ...), so a `#one` phrase written for exactly 1 is wrong for them. When exactly 1 deserves different wording, add a separate non-plural key and pick it in code, so every language gets that special case\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\n### Per-subscription state\n\n`rpl::on_next` stores the handler by value. Each emission invokes a *copy* of that stored handler, so a nested emit does not re-enter the running callable. Writes a `mutable` lambda makes to its own captures land on the copy and are discarded, so they never become per-subscription state.\n\n```cpp\n// BAD - compiles, runs, and grew is just value > 0 every time:\nevents | rpl::on_next([last = 0](int value) mutable {\n    const auto grew = (value > last);\n    last = value;\n    if (grew) {\n        // ...\n    }\n}, lifetime);\n\n// GOOD - state lives beside the handler:\nconst auto last = lifetime.make_state<int>(0);\nevents | rpl::on_next([=](int value) {\n    const auto grew = (value > *last);\n    *last = value;\n    if (grew) {\n        // ...\n    }\n}, lifetime);\n```\n\nKeep per-subscription state in `lifetime.make_state<T>()`, behind a `shared_ptr`, or on the object that owns the subscription. A `mutable` lambda that is one-shot — `rpl::take(1)`, a self-destroying subscription, a `crl::on_main` or `crl::async` callback, a `done` or `error` callback — is not this trap.\n",".claude/commands/release.md":"---\ndescription: Prepare changelog, set version, and commit a new release\nallowed-tools: Read, Bash, Edit, Grep, AskUserQuestion\n---\n\n# Release — Changelog, Set Version, Commit\n\nFull release flow: generate changelog entry, run `set_version`, and commit.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nParse `$ARGUMENTS` for two optional parts (in any order):\n- A **version number** like `6.7` or `6.7.0` — if provided, use it as the new version exactly.\n- The word **\"beta\"** — if present, mark the release as beta.\n\nIf no version number is given, auto-increment the patch component (see step 2).\n\n## Steps\n\n### 1. Check git status is clean\n\nRun `git status --porcelain`. If there are any uncommitted changes, **stop** and ask the user to commit or discard them before proceeding. Do not continue until status is clean.\n\n### 2. Read the current changelog\n\nRead `changelog.txt` from the repository root. Note the **latest version number** on the first line (e.g. `6.6.3 beta (12.03.26)`). Parse its major.minor.patch components.\n\n### 3. Determine the new version number\n\n**Important:** version numbers are shared across the beta and stable tracks — the sequence advances through both. The same `major.minor.patch` cannot be released as both beta and stable. A beta release \"uses up\" that number; the next stable must bump to a new patch.\n\n- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given). If the latest changelog entry already used this exact number (regardless of beta/stable), warn the user — they likely want a bumped patch.\n- **If no version was provided**, auto-increment from the latest changelog version:\n  - If it was a beta, and the new release is **not** beta, bump the patch component by 1 (do **not** reuse the beta's number for stable).\n  - If the new release is beta and the latest was also beta with the same major.minor, bump patch.\n  - Otherwise bump the patch component by 1.\n- Present the chosen version to the user and ask for confirmation before proceeding. If the user suggests a different version, use that instead.\n\n### 4. Fetch tags and determine the last release tag\n\nRun `git fetch origin --tags` first to ensure all tags from the public repository are available locally. Then run `git tag --sort=-v:refname` and find the most recent `v*` tag. This is the baseline for the diff.\n\n### 5. Collect commits\n\nRun `git log <last-tag>..HEAD --oneline` to get all commits since the last release.\n\n### 6. Write the changelog entry\n\nAnalyze every commit message. Group them mentally into features, improvements, and bug fixes. Then produce **brief, user-facing bullet points** following these rules:\n\n- **Style:** Match the existing changelog tone exactly — short, imperative sentences starting with a verb (Fix, Add, Allow, Show, Improve, Support…). Keep the trailing periods (the existing changelog uses them).\n- **Brevity:** Each bullet should be one short sentence, around 80 characters when possible. No implementation details. No commit hashes.\n- **Selection:** Only include changes that matter to end users. Skip CI, build infra, submodule bumps, code style, refactors, and intermediate WIP commits. Collapse many related commits (e.g. a dozen image-editor commits) into one or two bullets.\n- **Ordering:** Features first, then improvements, then bug fixes.\n- **Quantity:** Aim for 4-12 bullets total depending on the amount of changes.\n\n### 7. Format and insert into changelog.txt\n\nUse this exact format (date is today in DD.MM.YY):\n\n```\n<version> [beta ](DD.MM.YY)\n\n- Bullet one.\n- Bullet two.\n```\n\nPrepend the new entry at the very top of `changelog.txt`, separated by a blank line from the previous first entry. Use the Edit tool.\n\n**Never delete or edit existing entries**, even if the new stable entry merges bullets from prior beta(s). Prior beta blocks remain as historical record for beta-track users.\n\n**For stable releases spanning prior beta(s):** the entry should cover everything since the last **stable** release (not just since the last beta), including noteworthy beta-shipped features, trimmed as needed to stay within 4–12 bullets.\n\n### 8. Wait for approval\n\nAfter writing the entry to `changelog.txt` (step 7), tell the user the changelog has been updated and ask them to review it in the IDE. They can edit it directly and tell you to continue, or tell you what to change in chat. Do **not** print the full entry in chat — the file itself is the review surface.\n\n**Do NOT proceed until the user explicitly approves.**\n\n### 9. Run set_version\n\nOnce approved, run the `set_version` script from the repository root. On Windows:\n\n```\n.\\Telegram\\build\\set_version.bat <version_arg>\n```\n\nWhere `<version_arg>` is formatted as the `set_version` script expects:\n- Stable: `6.7.0` or `6.7`\n- Beta: `6.7.0.beta`\n\nVerify the script exits successfully (exit code 0). If it fails, show the error and stop.\n\n### 10. Commit\n\nStage all changes and create a commit. The commit message format:\n\n**First line:**\n- For stable: `Version <major>.<minor>.` if patch is 0, otherwise `Version <major>.<minor>.<patch>.`\n- For beta: `Beta version <major>.<minor>.<patch>.`\n\n**Then an empty line, then the changelog bullets.** Copy bullet lines from the changelog as-is. Only wrap lines that exceed 72 characters; shorter lines must stay on a single line. When wrapping is needed, break at logically correct places (between words/phrases) and indent continuation lines with two spaces.\n\nExample commit message:\n```\nBeta version 6.6.3.\n\n- Drawing tools in image editor\n  (brush, marker, eraser, arrow).\n- Draw-to-reply button in media viewer.\n- Trim recorded voice messages.\n- Fix reorder freeze in chats list.\n```\n\nUse a HEREDOC to pass the message to `git commit -a`.\n\n### 11. Done\n\nRun `git log -1` to show the resulting commit and confirm success.\n",".claude/commands/icon.md":"---\ndescription: Generate an SVG icon from a design mockup using vectosolve vectorization\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion, TodoWrite, mcp__vectosolve__vectorize\n---\n\n# Icon - SVG Icon Generation from Design Mockup\n\nYou generate production-quality SVG icons for Telegram Desktop by vectorizing design mockup screenshots using the vectosolve MCP service, then post-processing the result to match the Telegram icon format.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf `$ARGUMENTS` is empty, ask the user to describe the icon they want and paste a cropped screenshot of it.\n\n## Overview\n\nThe workflow takes a cropped screenshot of an icon from a design mockup (grabbed from the clipboard), vectorizes it via the vectosolve MCP, then post-processes the SVG (recolor to white-on-transparent, restructure to minimal format, set 24x24 output size).\n\nWorking directory: `.ai/icon_{name}/` with iterations labeled by letter (`a/`, `b/`, ...), each containing `source.png`. Output SVGs are in the icon root: `a.svg`, `b.svg`, etc.\n\nFollow-ups are supported: `/icon {icon_name} <description>` continues from where the previous run left off.\n\n## Phase 0: Setup\n\n**Record the current time** (using `date` or equivalent) as `$START_TIME`.\n\n### Step 0a: Clipboard grab (MUST be the VERY FIRST action)\n\nIf there is an image attached to the user's message:\n\n1. Generate a random 8-character hex string for `HASH` (use `openssl rand -hex 4` or similar).\n2. **IMMEDIATELY** — before any other processing — run this Bash command to save the clipboard image:\n   ```bash\n   HASH=$(openssl rand -hex 4) && if [[ \"$OSTYPE\" == darwin* ]]; then bash .claude/grab_clipboard.sh \".ai/icon_${HASH}.png\"; else powershell -ExecutionPolicy Bypass -File .claude/grab_clipboard.ps1 \".ai/icon_${HASH}.png\"; fi\n   ```\n   On macOS `.claude/grab_clipboard.sh` is used; on Windows `.claude/grab_clipboard.ps1`. Both grab the current clipboard image and save it to the specified path.\n\n3. If the command fails (exit 1 / no image on clipboard):\n   - Tell the user: **\"Clipboard doesn't contain an image. Please copy the icon area first, then retry.\"** (On macOS: Cmd+Ctrl+Shift+4 to snip to clipboard; on Windows: Win+Shift+S.)\n   - **STOP IMMEDIATELY. Do NOT continue.** You cannot use the image pasted in the conversation — it exists only as pixels in the chat, not as a file you can send to vectosolve. The clipboard grab is the ONLY way to get the image to disk. Do not attempt any workaround.\n\n4. Read back the saved `.ai/icon_HASH.png` using the Read tool.\n5. Compare it visually with the image pasted in the conversation. They should depict the same thing.\n   - If they look **completely different**: delete `.ai/icon_HASH.png` and fail:\n     > \"The clipboard image doesn't match what you pasted. Please re-copy and retry.\"\n   - If they look the same (or close enough): proceed. Store the temp path.\n\nIf NO image is attached to the message, skip this step entirely.\n\n### Step 0b: Fail-fast — verify vectosolve MCP\n\nCheck that the `mcp__vectosolve__vectorize` tool is available by looking at your available tools list. If it is NOT available, fail immediately with:\n\n> vectosolve MCP is not configured. Set it up with:\n> ```\n> claude mcp add vectosolve --scope user -e VECTOSOLVE_API_KEY=vs_xxx -- npx @vectosolve/mcp\n> ```\n> Then restart Claude Code.\n\n### Step 0c: Follow-up detection\n\nExtract the first word/token from `$ARGUMENTS` (everything before the first space or newline). Call it `FIRST_TOKEN`.\n\nRun these TWO commands using the Bash tool, **IN PARALLEL**:\n1. `ls .ai/` — to see all existing icon project names\n2. `ls .ai/icon_{FIRST_TOKEN}/context.md` — to check if this specific icon project exists\n\n**Evaluate the results:**\n- If command 2 **succeeds** (context.md exists): this is a **follow-up**. The icon name is `FIRST_TOKEN`. The follow-up description is everything in `$ARGUMENTS` after `FIRST_TOKEN`.\n- If command 2 **fails** (not found): this is a **new icon**. The full `$ARGUMENTS` is the icon description.\n\n### Step 0d: New icon setup\n\n1. Parse `$ARGUMENTS` to determine:\n   - **Icon description**: what the icon should depict\n   - **Icon type**: default is `menu` (24x24 menu/button icon). User may specify otherwise.\n   - **Target subfolder**: `menu/` by default, or another subfolder if specified.\n\n2. Choose an icon file name:\n   - Lowercase letters and underscores only — **NO hyphens**\n   - Match existing naming conventions (check `Telegram/Resources/icons/{subfolder}/`)\n   - Must NOT conflict with existing icons\n   - Must NOT collide with existing `.ai/icon_{name}/` directories\n\n3. Create `.ai/icon_{name}/` and `.ai/icon_{name}/a/`.\n\n4. Write `.ai/icon_{name}/context.md` with:\n   ```\n   ## Icon: {icon_name}\n   Type: {menu/other}\n   Target: Telegram/Resources/icons/{subfolder}/{icon_name}.svg\n\n   ## Original Request\n   {full $ARGUMENTS text}\n\n   ## Follow-ups\n   (none yet)\n   ```\n\n5. Set `LETTER` to `a`.\n\n### Step 0e: Follow-up setup\n\n1. Read `.ai/icon_{name}/context.md` to get the icon type, subfolder, and full history.\n2. Find the latest existing letter folder in `.ai/icon_{name}/` (highest letter).\n3. Set `LETTER` to the next letter after the latest.\n4. Create `.ai/icon_{name}/{LETTER}/`.\n5. Update `.ai/icon_{name}/context.md` — append the follow-up description to the `## Follow-ups` section:\n   ```\n   ### Follow-up (starting at letter {LETTER})\n   {follow-up description}\n   ```\n\n### Step 0f: Place source image\n\nIf a clipboard image was grabbed in Step 0a:\n1. Copy (or move) `.ai/icon_HASH.png` → `.ai/icon_{name}/source.png` (overwrite if exists — this is always the latest source).\n2. Copy it to `.ai/icon_{name}/{LETTER}/source.png` (archive per-iteration source).\n3. Delete the temp `.ai/icon_HASH.png` if it was copied (not moved).\n\nIf NO image was grabbed:\n- **New icon with no image**: Ask the user to provide a screenshot. STOP.\n- **Follow-up with no image**: The existing `source.png` in the icon root carries forward. Copy it to `.ai/icon_{name}/{LETTER}/source.png`. If no source.png exists at all, ask the user for an image.\n\n### Step 0g: Verify renderer\n\nLocate the render tool (`codegen_style` with `--render-svg` mode):\n\n```bash\nif [[ \"$OSTYPE\" == darwin* ]]; then\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style\nelse\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe\nfi\n```\n\nIf missing, build it: `cmake --build out --config Debug --target codegen_style`\n\nTest on a known good SVG (use the appropriate binary path for the OS):\n```bash\nCODEGEN=$(if [[ \"$OSTYPE\" == darwin* ]]; then echo out/Telegram/codegen/codegen/style/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)\n$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512\n```\n\nIf works → delete test render, set `RENDER_AVAILABLE = true`. If fails → `RENDER_AVAILABLE = false`.\n\n## Phase 1: Vectorize & Post-process\n\n### Step 1a: Call vectosolve\n\nUse the `mcp__vectosolve__vectorize` tool with `file_path` set to the **absolute path** of `.ai/icon_{name}/{LETTER}/source.png`.\n\n**If this fails, STOP IMMEDIATELY.** Do NOT try to generate the SVG manually or by any other means. Report the error to the user and let them fix the issue (bad API key, no credits, network error, etc.).\n\nSave the returned SVG content to `.ai/icon_{name}/{LETTER}/raw_vectosolve.svg`.\n\nThe MCP tool calls the vectosolve API ($0.20/call). The API key is stored in `~/.claude.json` MCP config (never in the repository).\n\n### Step 1b: Post-process the SVG\n\nThe vectosolve SVG will have colors from the mockup, arbitrary dimensions, and possibly a non-square aspect ratio from a non-square screenshot crop. Post-processing fixes this by adjusting the **viewBox** — leave path coordinates untouched.\n\n**Do NOT transform path coordinates.** Vectosolve's paths are correct — the only thing wrong is the framing. All geometry adjustments are done by manipulating the `viewBox` and the `width`/`height` attributes.\n\n#### Sub-step 1: Read the request and determine parameters\n\nBefore touching the SVG, determine these from the user's request and context.md:\n\n1. **Output size** (`OUT_W × OUT_H`): default is `24px × 24px` for menu icons. The user may request different dimensions (e.g., 36×36, 48×48, or non-square). Always check the request.\n2. **Content padding**: default is ~2px equivalent on each side at the output scale (so content fills roughly (OUT_W-4) × (OUT_H-4)). The user may request different padding or edge-to-edge.\n3. **Centering**: default is centered both horizontally and vertically. The user may request specific alignment (e.g., \"align to bottom\").\n\n#### Sub-step 2: Parse the raw SVG\n\n1. Extract the `viewBox`: `viewBox=\"VB_X VB_Y VB_W VB_H\"` (typically `0 0 W H`).\n2. Identify ALL paths. Classify each:\n   - **Background**: a rect or path spanning the full viewBox (first path that's a simple rectangle matching the viewBox bounds). **Remove it entirely.**\n   - **Content**: the actual icon shapes. **Keep these, paths unchanged.**\n3. If paths have `transform=\"translate(TX,TY)\"` attributes, that's fine — keep them as-is. The viewBox framing will work regardless.\n\n#### Sub-step 3: Compute the content bounding box\n\nEstimate the bounding box of the content paths (after removing the background). You can either:\n- Eyeball it from the path coordinates (look at first/last M commands and extremes of curves)\n- Or for precision, write a quick script to parse the paths and find min/max X/Y\n\nCall the result: `CX_MIN, CY_MIN, CX_MAX, CY_MAX`. Content dimensions: `CW = CX_MAX - CX_MIN`, `CH = CY_MAX - CY_MIN`.\n\n#### Sub-step 4: Compute the new viewBox\n\nThe viewBox determines what part of the SVG coordinate space maps to the output rectangle. By expanding the viewBox beyond the content bounds, we add padding. By making the viewBox aspect ratio match the output aspect ratio, we prevent stretching.\n\n1. **Output aspect ratio**: `OUT_AR = OUT_W / OUT_H` (for 24×24 this is 1.0).\n2. **Padding in SVG coordinates**: we want ~2px padding at output scale. The scale factor is `OUT_W / VB_CONTENT_W` approximately, so padding in SVG coords = `2 * (CW / (OUT_W - 4))` (or similar — the exact formula depends on which dimension is dominant). Simpler approach: aim for content to occupy ~83% of the viewBox (≈ 20/24), so:\n   - `PADDED_W = CW / 0.83`\n   - `PADDED_H = CH / 0.83`\n3. **Match output aspect ratio**: the viewBox aspect ratio must equal `OUT_AR` to avoid stretching.\n   - If `PADDED_W / PADDED_H > OUT_AR`: width is dominant → `VB_W = PADDED_W`, `VB_H = VB_W / OUT_AR`\n   - If `PADDED_W / PADDED_H < OUT_AR`: height is dominant → `VB_H = PADDED_H`, `VB_W = VB_H * OUT_AR`\n   - If equal: `VB_W = PADDED_W`, `VB_H = PADDED_H`\n4. **Center the content** in the new viewBox:\n   - `VB_X = CX_MIN - (VB_W - CW) / 2`\n   - `VB_Y = CY_MIN - (VB_H - CH) / 2`\n   - (Adjust if the user requested non-centered alignment)\n\nThe new viewBox is: `viewBox=\"VB_X VB_Y VB_W VB_H\"`.\n\n#### Sub-step 5: Recolor to white-on-transparent\n\n- Replace ALL `fill` color values (anything that isn't `none`) with `#FFFFFF`.\n- Remove ALL `stroke` and `stroke-width` attributes entirely.\n- Remove `opacity` attributes if present.\n\n#### Sub-step 6: Determine path composition\n\nLook at the icon's visual structure and decide how paths should combine:\n- **Outlined shape** (e.g., circle outline with something inside): combine outer + inner cutout into one `<path>` with `fill-rule=\"evenodd\"`.\n- **Separate distinct parts** (e.g., magnifying glass + checkmark): keep as separate `<path>` elements.\n- **Filled shape with cutout** (e.g., filled circle with checkmark punched out): combine into one path with `fill-rule=\"evenodd\"`.\n\n#### Sub-step 7: Assemble final SVG\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg width=\"{OUT_W}px\" height=\"{OUT_H}px\" viewBox=\"{VB_X} {VB_Y} {VB_W} {VB_H}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g stroke=\"none\" fill=\"none\" fill-rule=\"evenodd\">\n        <path d=\"...\" fill=\"#FFFFFF\"></path>\n    </g>\n</svg>\n```\n\n- `width`/`height` = the output size from the request (default `24px`/`24px`).\n- `viewBox` = the computed viewBox from Sub-step 4. The SVG renderer maps this coordinate region to the output size.\n- Path `d` attributes are **unchanged** from vectosolve output (just background removed, colors replaced).\n- No `<title>`, `id`, `xmlns:xlink`, `version`, `class`, `style`, XML comments, `<metadata>`, or `preserveAspectRatio`.\n- No `<circle>`, `<rect>`, `<line>` — only `<path>`.\n\nWrite the final SVG to `.ai/icon_{name}/{LETTER}.svg`.\n\n### Step 1c: Render\n\nIf `RENDER_AVAILABLE`:\n```bash\n$CODEGEN --render-svg \".ai/icon_{name}/{LETTER}.svg\" \".ai/icon_{name}/render_{LETTER}.png\" 512\n```\n\nRead the render to visually verify the result.\n\n## Phase 2: Review\n\nAfter rendering, assess the result:\n\n1. **Recognizable?** The icon should be clearly identifiable as the intended symbol.\n2. **Scale reasonable?** Should fill the space appropriately with ~2-3px padding.\n3. **Clean lines?** No broken paths, artifacts, or unwanted elements.\n4. **Correct colors?** All white on transparent (no leftover colors from the mockup).\n\nIf the result looks good → proceed to Phase 3 (Output).\n\nIf there are fixable issues (stray element, missed color, etc.) → fix the SVG directly, re-render, and re-check.\n\nIf the result is poor (vectosolve couldn't handle the input well) → report to the user and suggest:\n- Trying a cleaner/larger crop of the icon\n- Providing a different screenshot\n- Following up: `/icon {icon_name} <description of what to change>`\n\n## Phase 3: Output\n\n1. Read the `Target:` line from `.ai/icon_{name}/context.md` to get the output path.\n\n2. Copy the final SVG to that target path (e.g., `Telegram/Resources/icons/menu/{icon_name}.svg`).\n\n3. Update `.ai/icon_{name}/context.md` — append to the end:\n   ```\n   ## Latest Output\n   Letter: {LETTER}\n   Written to: {target_path}\n   ```\n\n4. Report to the user:\n   - Final icon file path\n   - Number of vectosolve calls made (cost at $0.20/call)\n   - Suggest verifying visually\n   - Working directory `.ai/icon_{name}/` has all iterations\n   - Elapsed time since `$START_TIME` (format `Xm Ys`)\n   - Follow-up: `/icon {icon_name} <description of what to change>`\n\n## Text-only Follow-ups (no new image)\n\nWhen a follow-up has no attached image, the user wants to refine the existing SVG based on text feedback. In this case:\n\n1. Skip Phase 1 (no vectosolve call needed).\n2. Read the latest SVG (`.ai/icon_{name}/{prev_letter}.svg`).\n3. Read the latest render if available.\n4. Apply the user's requested changes by editing the SVG directly.\n5. Save as `.ai/icon_{name}/{LETTER}.svg`.\n6. Render, review, and output as normal (Phases 1c → 3).\n\nIf the changes are too complex for manual SVG editing, suggest the user provide a new screenshot instead.\n\n## Error Handling\n\n- If clipboard grab fails → tell user to re-copy and retry.\n- If vectosolve returns an error → report it and suggest a different/cleaner screenshot.\n- If vectosolve returns SVG that can't be parsed → save raw output for debugging, report to user.\n- If the render helper fails → set `RENDER_AVAILABLE = false`, continue with SVG-only review.\n- If post-processing produces a broken SVG → fall back to the raw vectosolve output and do lighter cleanup.\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","rawUrl":"https://raw.githubusercontent.com/telegramdesktop/tdesktop/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Agent Guide for Telegram Desktop\n\nThis guide defines repository-wide instructions for coding agents working with the Telegram Desktop codebase.\n\n## AI Tasks\n\nIn this repository \"task\" is a specific term. It always means one work record in\nthe sibling `ai-tdesktop` repository, never a `TODO` comment, a checklist item,\nor a unit of work invented during the current conversation. \"The tasks\", \"the\nqueue\", \"the board\", \"what's most pressing\", and a bare task slug all refer to\nthat queue.\n\n- The queue lives in `../ai-tdesktop`, a sibling of this checkout, with one\n  record per directory under `tasks/YYYY/MM/DD/<slug>/`. A task id is that dated\n  path, for example `2026/07/18/fix-community-forward`, and it is the only\n  durable link between a source commit and its task. Linked slot worktrees live\n  in `../ai-tdesktop-worktrees`. Read `../ai-tdesktop/AGENTS.md` before doing\n  anything inside that repository.\n- Each record holds `task.md` — one `# ` title line, then one self-contained\n  paragraph that is the task's description wherever it is summarized — and\n  `state.yaml` with `status`, `type`, `depends_on`, `claimed_by`, and dates.\n  Open statuses are `todo`, `in-progress`, `blocked`, and `split-required`;\n  `approved` is completed history, and `split.yaml` / `superseded.yaml` mark\n  retired records.\n- To browse the queue, read those files directly. Do not run `python3 ai.py`: it\n  is a full-screen browser for the user's own terminal and refuses to run in a\n  pipe. A compact open listing:\n\n```bash\ncd ../ai-tdesktop && for state in tasks/*/*/*/*/state.yaml; do\n  status=$(sed -n 's/^status: //p' \"$state\")\n  case \"$status\" in todo|in-progress|blocked|split-required)\n    dir=$(dirname \"$state\")\n    printf '%-14s %-70s %s\\n' \"$status\" \"${dir#tasks/}\" \\\n      \"$(sed -n '1s/^# //p' \"$dir/task.md\")\";;\n  esac\ndone | sort\n```\n\n- When ranking open work by urgency, use `in-progress`, then `blocked`, then\n  `split-required`, then ready `todo` (no `claimed_by`, and every `depends_on`\n  already `approved`), then `todo` still waiting on a dependency. Say who owns\n  claimed work. This checkout's tag is the `Telegram/build/ai-machine-tag` value\n  plus the checkout folder name, such as `macbook-tdesktop`, and work claimed by\n  another checkout is never taken over without an explicit human reassignment.\n- Browsing is read-only. Listing, summarizing, comparing, or recommending tasks\n  never edits `state.yaml`, publishes a lifecycle commit, or starts\n  implementation, no matter how small the task looks.\n- Acting on an existing queue task goes through the workflow skills instead of\n  by hand: `perform-task <slug or full id>` starts or resumes and performs\n  exactly one known task, and `continue <batch specification>` runs the batch\n  of tasks the specification selects. Source commits owned by a task use the\n  three-line form described under `## Commits`.\n- A request the user makes in conversation is ordinary work: do it directly in\n  this checkout. Do not write it to the ignored `../ai-tdesktop/inbox/inbox.md`,\n  and do not run `process-inbox` or `continue` over it, unless the user asks for\n  that. The queue's planning, review and evidence campaign costs hours, so\n  spending it on a request the user expected you to just do is a real error, not\n  thoroughness.\n- You may *offer* the queue when the work genuinely earns it: a large or\n  ambiguous change, one whose correctness needs a real testing campaign, or one\n  that is safety-, security- or data-safety-critical. Make the offer in one\n  line, say why, and route it only after the user approves. Absent approval,\n  implement it directly and say what you skipped.\n- Never guess between similarly named tasks. Report the matching full ids and\n  let the user choose.\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## Dependency backports\n\nFor explicitly requested dependency updates and backports, use the dependency\nrepositories' `master` branches directly. Do not create `codex/*` or other topic\nbranches in `Libraries/patches`, `Libraries/tg_owt`, or `Libraries/tg_angle`.\nThe scheduled dependency monitor remains advisory and does not implement fixes.\n\n- Before editing each dependency repository, inspect its working tree and\n  current branch, fetch `origin`, and verify that the checkout is on `master`,\n  tracks `origin/master`, and is synchronized with the fetched remote branch.\n  Switch to `master` when necessary and fast-forward it when possible. Review\n  divergence before integrating it; preserve unrelated edits, untracked library\n  checkouts and local commits. Do not reset them away or publish unrelated local\n  commits implicitly.\n- Commit fixes to our copied dependencies, including WebRTC and ANGLE, directly\n  in `tg_owt` and `tg_angle`. For external dependencies such as Qt and FFmpeg,\n  commit patch files to `Libraries/patches` and apply them consistently through\n  the applicable prepare, Docker and Snap recipes. Keep intentional differences\n  between platform versions and do not add patch copies inside tdesktop.\n- Keep each independent fix in a separate commit and retain its upstream\n  provenance. Review applicability, prerequisites and patch interactions before\n  committing. Respect the requested validation scope; do not rebuild dependencies\n  unless the user explicitly requests it.\n- Push `master` to `origin/master` without force, fetch again and verify that\n  every new dependency commit is reachable from the published `origin/master`.\n  Only then update the applicable tdesktop revisions to those published SHAs.\n  If integration changes a SHA, update every recipe that consumed the old one.\n  A commit available only on a local or topic branch is not a finished update.\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**On macOS, never run an Xcode clean against `out/`** - not `--target clean`,\nnot `--clean-first` with any target, not `xcodebuild ... clean` and not a\nClean in the Xcode app. Xcode's build-products folder for `Telegram` is\n`out/Debug`, which also holds the portable account folders. When `out/Debug`\ncarries the `com.apple.xcode.CreatedByBuildSystem` extended attribute,\n`--target clean` deletes `out/Debug` with every folder in it, and\n`--target Telegram --clean-first` does the same before it builds. Without the\nattribute, `--target clean` refuses to delete `out/Debug` and ends\n`** CLEAN FAILED **` (exit 65). It ends the same way when it refuses only\nanother, unmarked products folder and deletes a marked `out/Debug`, so exit 65\nalone does not prove that `out/Debug` survived. To force a full recompile,\nfollow the `out/build` deletion in \"### Forcing a full recompile on macOS\"\nunder \"## Troubleshooting\" exactly as written there, its precondition and its\nchecks before and after the rebuild included. Never mark `out/Debug` deletable\nwith `xattr -w com.apple.xcode.CreatedByBuildSystem true`, as the note in\nXcode's refusal suggests, and never move, rename or remove any `out/Debug`\nentry to make a clean succeed.\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- First-time configure of a new `out/` tree: set the `QT` environment variable\n  to the Qt version this checkout actually has, for example `export QT=6.11.1`\n  when `../Libraries/local/Qt-6.11.1` is the installed one. The value names a\n  directory, so it is the checkout's version, not a constant.\n- Reconfiguring an existing `out/` tree: do **not** export `QT`. Run\n  `env -u QT cmake -S . -B out` instead. `cmake/external/qt/package.cmake`\n  writes an exported `QT` into the `qt_requested` cache entry with `FORCE`, so\n  it overwrites the version the tree was already configured with instead of\n  being ignored. A value that disagrees with the configured tree then fails the\n  regeneration, and the generated project keeps the source list it was last\n  generated with: newly added sources are never compiled — which surfaces later\n  as undefined-symbol link errors rather than as a configure error — and newly\n  added `.style` modules leave the generated `style_*.h` includes stale.\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. Rebuild every object once by following the\n`out/build` deletion in \"### Forcing a full recompile on macOS\" exactly as\nwritten there, its precondition and checks included, never with an Xcode clean\n(see the macOS statement in \"### Build Commands\" under\n\"## Build Configuration\"). Then rerun the same test once. If the signature\npersists after that clean rebuild, continue normal crash diagnosis or report\nthe blocker. Do not loop clean rebuilds.\n\n### Forcing a full recompile on macOS\n\nStart only when no build of this checkout is running and this checkout's exact\nTelegram process is stopped. Never stop any other process for this procedure.\nOutside the autonomous `continue` / `perform-task` workflow, stopping that\nprocess follows \"### Build output locks\": the user may be testing it, so ask\nthe user first and never terminate it without explicit permission. Then\nrebuild every object by deleting the intermediate tree `out/build`, never with\nan Xcode clean (see the macOS statement in \"### Build Commands\" under\n\"## Build Configuration\").\n\nThe generated project keeps every intermediate (object files, precompiled\nheaders and Xcode's build database) under `out/build`, its `SYMROOT`, and\nonly build products in `out/Debug` and the other per-target `Debug` folders.\nDeleting `out/build` therefore works the same whether or not `out/Debug`\ncarries the `com.apple.xcode.CreatedByBuildSystem` attribute, and it touches\nnothing in `out/Debug`. From the repository root, record what `out/Debug`\nholds, delete the intermediates and confirm that `out/build` is gone:\n\n```bash\ntest -d out/Telegram.xcodeproj &&\n  ls -A out/Debug | sort > out/language-rebuild-mark.txt &&\n  rm -rf out/build &&\n  test ! -e out/build &&\n  echo 'out/build is gone'\n```\n\nIt must print `out/build is gone`: no intermediate object survived. Object\nfiles elsewhere in `out/`, such as CMake's compiler checks or a release\nbuild's tree, belong to other builds; leave them alone. This step has no\naccepted failure: an error, other output or no output stops the procedure.\nThen run one full Debug rebuild, which has to compile every object again:\n\n```bash\ncmake --build out --config Debug --target Telegram\n```\n\nIt must end `** BUILD SUCCEEDED **`. Then check that the rebuild compiled every\nobject and that nothing in `out/Debug` was lost:\n\n```bash\ntest -f out/language-rebuild-mark.txt &&\n  find out/build -name '*.o' ! -newer out/language-rebuild-mark.txt | wc -l &&\n  find out/build -name '*.o' | wc -l &&\n  ls -A out/Debug | sort | comm -23 out/language-rebuild-mark.txt -\n```\n\nIt must print `0` (no object file is older than the mark written before\n`out/build` was deleted), then a nonzero object count, and nothing else (every\nentry `out/Debug` held before is still there). Anything else stops the\nprocedure; report what it printed.\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## Debug-Only Code\n\nProduction translation units stay free of debug machinery. The permanent test\nharness lives in `Telegram/SourceFiles/test/`, and the disposable `-testagent`\noverlay owns per-task instrumentation; production code carries at most a thin\nsingle-call seam (a `Test::Fire()`-style waitpoint or a live-object\npublication at a construction seam).\n\n- Do not add `#ifdef _DEBUG` blocks, debug-only types, debug state, mutexes,\n  counters, or observation structs to production headers or sources. When a\n  behavior cannot be observed without such machinery, that is a harness gap:\n  extend the `test/` helpers or the overlay instead.\n- Never commit debugging machinery interleaved with working code in the same\n  translation unit. A permanent helper belongs under `test/`; a temporary one\n  belongs in the overlay and is never retained.\n- Exceptions exist — a few `#ifdef _DEBUG` hooks are deliberately kept in\n  production files — but they are exceptions and each new one needs a solid,\n  stated reason. \"The test needed it\" is not one.\n\n## Harness documentation\n\nKeep `Telegram/SourceFiles/test/README.md` a short mandatory guide, under\n8 KiB. When adding or changing a harness helper, follow\n`Telegram/SourceFiles/test/docs/adding-helpers.md`: add or update one helper\nindex row and its on-demand reference, and keep the source header focused on\nits callable contract. Detailed examples, platform caveats and diagnostic\nhistory belong in the references, not in the mandatory README. Overlay work\nreads that guide, the references selected by the task and their headers;\nnever require reading every helper page.\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- Decide the `[ai] ` prefix separately for each commit. Use it only when every\n  retained change in that commit, and the commit's purpose, are exclusively\n  about the AI workflow: the agent harness, skills, prompts, custom commands,\n  agent documentation, or AI testing infrastructure. Typical qualifying paths\n  include `Telegram/SourceFiles/test/`, `.agents/`, `.claude/`, `.grok/`,\n  `AGENTS.md`, `CLAUDE.md`, and `GROK.md`, but paths alone do not decide the\n  prefix. Product-specific test seams, app code, and build-system integration do\n  not qualify merely because agents use them for verification. Split mixed\n  workflow and product work into separate commits when practical; otherwise the\n  mixed commit must not use `[ai] `. Do not count the disposable test overlay or\n  external AI task artifacts. Every other commit must not contain `[ai]`\n  anywhere.\n- The `[ai] ` prefix marks the commit's scope, never its authorship. It does\n  not mean \"authored by an AI\": an AI-authored product fix takes a plain\n  subject, and a workflow-only commit takes the prefix no matter who wrote it.\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- Never create backup branches or tags before a rebase, reorder, squash or any\n  other history rewrite. The branch reflog keeps the old tip; report its sha\n  as the undo path instead.\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## Area maps\n\n`Telegram/SourceFiles/AGENTS.md` is the source atlas: every area of the\ntree with its paths and responsibility. A directory may also\nhave its own `AGENTS.md`: the map of that area's modules, placement rules,\ninvariants, and things to leave alone. Before adding code, read the atlas\nentry of the area you touch and the nearest map, and put new code where they\nsay. A change that needs a different shape amends the map in the same task.\nHow the atlas and maps are written and how changes are checked against them\nis in `.agents/skills/code-health/SKILL.md`.\n\n## Coding Style\n\n**Comments are rationed:**\n\nA comment is one line; two or three only when the block opens with `// WHY:`. A commit may add two comment lines plus one such exception, and a trailing comment is a line too; only `} // namespace X` closers and `#endif // X` labels are free. Say why, never what. Hooks enforce it, and `Telegram/SourceFiles/test/` is exempt.\n\nDo not remove existing comments just to satisfy this rule. Preserve comments unless your change makes them incorrect or truly obsolete; when moving or refactoring code, move the useful comment with it. Inline comments that label positional arguments for generated or schema-driven APIs (for example TL/MTP constructors) are useful because the field names are not visible in the call itself.\n\n**Never cite line numbers** in comments, `README.md` files or other checked-in text. Name the function, member, constant or type, and its file when that helps: write `Runner::finish()` in `test_runner.cpp`, not `test_runner.cpp:NNN` or `:NNN-MMM`. Line numbers drift with every edit and nothing updates them. This covers submodule, Qt and other third-party sources too. Quoted log or crash text stays verbatim.\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 - one line, and it carries a reason:\n_limit = kDefaultLimit; // the server rejects anything larger\n\n// ACCEPTABLE - the exception: opens with WHY, three lines at most,\n// once per commit.\n// WHY: the server sends the id before the peer exists, so the row is\n// created empty and filled on the next update, or the list flickers\n// on every reconnect.\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**Never discard a result with a cast:** `static_cast<void>(...)` and `(void)expr` are banned; instead of silencing `[[nodiscard]]`, fix the design.\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## Input Method Hints\n\nA field for typed content tells the input method what it is for: call `setInputMethodHints` right after creating it (`Ui::InputField` forwards the hints to its inner editor; `Ui::MaskedInputField` subclasses set them in the constructor). Combinations already used in the code:\n\n- URL: `Qt::ImhUrlCharactersOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- E-mail: `Qt::ImhEmailCharactersOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- Phone: `Qt::ImhDialableCharactersOnly | Qt::ImhNoPredictiveText`\n- Digits only (codes, date and time parts): `Qt::ImhDigitsOnly | Qt::ImhNoPredictiveText`\n- Numbers with separators (amounts, timestamps): `Qt::ImhFormattedNumbersOnly | Qt::ImhNoPredictiveText`\n- Latin identifiers (usernames, file extensions, keys): `Qt::ImhLatinOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- Add `Qt::ImhSensitiveData` to one-time codes and to secrets shown in clear text, such as recovery phrase words\n- Free text (names, titles, comments, search) gets no hints; `Ui::PasswordInput` sets its own\n\nPrefer the hint that says what the field is for, as long as it works or at least doesn't conflict with what works. To see what works, check every consumer on every platform tdesktop ships at the Qt version in use: the Qt platform plugins (windows, cocoa, xcb, wayland) and the input context plugins, including the bundled ones (fcitx5-qt). If the semantic hint conflicts, pick what works. Hints don't filter input: the field still drops unwanted characters itself.\n\nAs of 2026-10-20, `Qt::ImhDate` and `Qt::ImhTime` do nothing anywhere tdesktop runs. Qt on Wayland sends them as the date/time content purpose, but no Wayland on-screen keyboard implements those purposes (Qt Virtual Keyboard, which Plasma Keyboard is built on, GNOME Shell, Squeekboard). Qt reads them on none of tdesktop's other platforms (Windows, macOS, X11), and neither does the bundled fcitx5-qt. Used instead of the digit hints, they lose the digit layouts, so date and time fields use the digit hints above.\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- Every plural form, `#one` included, keeps `{count}`: many languages put more than 1 into their \"one\" category (Russian: 21, 31, ...), so a `#one` phrase written for exactly 1 is wrong for them. When exactly 1 deserves different wording, add a separate non-plural key and pick it in code, so every language gets that special case\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\n### Per-subscription state\n\n`rpl::on_next` stores the handler by value. Each emission invokes a *copy* of that stored handler, so a nested emit does not re-enter the running callable. Writes a `mutable` lambda makes to its own captures land on the copy and are discarded, so they never become per-subscription state.\n\n```cpp\n// BAD - compiles, runs, and grew is just value > 0 every time:\nevents | rpl::on_next([last = 0](int value) mutable {\n    const auto grew = (value > last);\n    last = value;\n    if (grew) {\n        // ...\n    }\n}, lifetime);\n\n// GOOD - state lives beside the handler:\nconst auto last = lifetime.make_state<int>(0);\nevents | rpl::on_next([=](int value) {\n    const auto grew = (value > *last);\n    *last = value;\n    if (grew) {\n        // ...\n    }\n}, lifetime);\n```\n\nKeep per-subscription state in `lifetime.make_state<T>()`, behind a `shared_ptr`, or on the object that owns the subscription. A `mutable` lambda that is one-shot — `rpl::take(1)`, a self-destroying subscription, a `crl::on_main` or `crl::async` callback, a `done` or `error` callback — is not this trap.\n","isInternal":false,"tokens":11504,"sizeBytes":46923},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/telegramdesktop/tdesktop/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Claude Code Pointer\n\nRead `AGENTS.md` and treat it as the canonical repository-wide instructions.\n","isInternal":false,"tokens":21,"sizeBytes":100},{"name":"icon.md","path":".claude/commands/icon.md","rawUrl":"https://raw.githubusercontent.com/telegramdesktop/tdesktop/HEAD/.claude/commands/icon.md","title":"Command: /icon","category":"command-prompt","format":"markdown","content":"---\ndescription: Generate an SVG icon from a design mockup using vectosolve vectorization\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion, TodoWrite, mcp__vectosolve__vectorize\n---\n\n# Icon - SVG Icon Generation from Design Mockup\n\nYou generate production-quality SVG icons for Telegram Desktop by vectorizing design mockup screenshots using the vectosolve MCP service, then post-processing the result to match the Telegram icon format.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf `$ARGUMENTS` is empty, ask the user to describe the icon they want and paste a cropped screenshot of it.\n\n## Overview\n\nThe workflow takes a cropped screenshot of an icon from a design mockup (grabbed from the clipboard), vectorizes it via the vectosolve MCP, then post-processes the SVG (recolor to white-on-transparent, restructure to minimal format, set 24x24 output size).\n\nWorking directory: `.ai/icon_{name}/` with iterations labeled by letter (`a/`, `b/`, ...), each containing `source.png`. Output SVGs are in the icon root: `a.svg`, `b.svg`, etc.\n\nFollow-ups are supported: `/icon {icon_name} <description>` continues from where the previous run left off.\n\n## Phase 0: Setup\n\n**Record the current time** (using `date` or equivalent) as `$START_TIME`.\n\n### Step 0a: Clipboard grab (MUST be the VERY FIRST action)\n\nIf there is an image attached to the user's message:\n\n1. Generate a random 8-character hex string for `HASH` (use `openssl rand -hex 4` or similar).\n2. **IMMEDIATELY** — before any other processing — run this Bash command to save the clipboard image:\n   ```bash\n   HASH=$(openssl rand -hex 4) && if [[ \"$OSTYPE\" == darwin* ]]; then bash .claude/grab_clipboard.sh \".ai/icon_${HASH}.png\"; else powershell -ExecutionPolicy Bypass -File .claude/grab_clipboard.ps1 \".ai/icon_${HASH}.png\"; fi\n   ```\n   On macOS `.claude/grab_clipboard.sh` is used; on Windows `.claude/grab_clipboard.ps1`. Both grab the current clipboard image and save it to the specified path.\n\n3. If the command fails (exit 1 / no image on clipboard):\n   - Tell the user: **\"Clipboard doesn't contain an image. Please copy the icon area first, then retry.\"** (On macOS: Cmd+Ctrl+Shift+4 to snip to clipboard; on Windows: Win+Shift+S.)\n   - **STOP IMMEDIATELY. Do NOT continue.** You cannot use the image pasted in the conversation — it exists only as pixels in the chat, not as a file you can send to vectosolve. The clipboard grab is the ONLY way to get the image to disk. Do not attempt any workaround.\n\n4. Read back the saved `.ai/icon_HASH.png` using the Read tool.\n5. Compare it visually with the image pasted in the conversation. They should depict the same thing.\n   - If they look **completely different**: delete `.ai/icon_HASH.png` and fail:\n     > \"The clipboard image doesn't match what you pasted. Please re-copy and retry.\"\n   - If they look the same (or close enough): proceed. Store the temp path.\n\nIf NO image is attached to the message, skip this step entirely.\n\n### Step 0b: Fail-fast — verify vectosolve MCP\n\nCheck that the `mcp__vectosolve__vectorize` tool is available by looking at your available tools list. If it is NOT available, fail immediately with:\n\n> vectosolve MCP is not configured. Set it up with:\n> ```\n> claude mcp add vectosolve --scope user -e VECTOSOLVE_API_KEY=vs_xxx -- npx @vectosolve/mcp\n> ```\n> Then restart Claude Code.\n\n### Step 0c: Follow-up detection\n\nExtract the first word/token from `$ARGUMENTS` (everything before the first space or newline). Call it `FIRST_TOKEN`.\n\nRun these TWO commands using the Bash tool, **IN PARALLEL**:\n1. `ls .ai/` — to see all existing icon project names\n2. `ls .ai/icon_{FIRST_TOKEN}/context.md` — to check if this specific icon project exists\n\n**Evaluate the results:**\n- If command 2 **succeeds** (context.md exists): this is a **follow-up**. The icon name is `FIRST_TOKEN`. The follow-up description is everything in `$ARGUMENTS` after `FIRST_TOKEN`.\n- If command 2 **fails** (not found): this is a **new icon**. The full `$ARGUMENTS` is the icon description.\n\n### Step 0d: New icon setup\n\n1. Parse `$ARGUMENTS` to determine:\n   - **Icon description**: what the icon should depict\n   - **Icon type**: default is `menu` (24x24 menu/button icon). User may specify otherwise.\n   - **Target subfolder**: `menu/` by default, or another subfolder if specified.\n\n2. Choose an icon file name:\n   - Lowercase letters and underscores only — **NO hyphens**\n   - Match existing naming conventions (check `Telegram/Resources/icons/{subfolder}/`)\n   - Must NOT conflict with existing icons\n   - Must NOT collide with existing `.ai/icon_{name}/` directories\n\n3. Create `.ai/icon_{name}/` and `.ai/icon_{name}/a/`.\n\n4. Write `.ai/icon_{name}/context.md` with:\n   ```\n   ## Icon: {icon_name}\n   Type: {menu/other}\n   Target: Telegram/Resources/icons/{subfolder}/{icon_name}.svg\n\n   ## Original Request\n   {full $ARGUMENTS text}\n\n   ## Follow-ups\n   (none yet)\n   ```\n\n5. Set `LETTER` to `a`.\n\n### Step 0e: Follow-up setup\n\n1. Read `.ai/icon_{name}/context.md` to get the icon type, subfolder, and full history.\n2. Find the latest existing letter folder in `.ai/icon_{name}/` (highest letter).\n3. Set `LETTER` to the next letter after the latest.\n4. Create `.ai/icon_{name}/{LETTER}/`.\n5. Update `.ai/icon_{name}/context.md` — append the follow-up description to the `## Follow-ups` section:\n   ```\n   ### Follow-up (starting at letter {LETTER})\n   {follow-up description}\n   ```\n\n### Step 0f: Place source image\n\nIf a clipboard image was grabbed in Step 0a:\n1. Copy (or move) `.ai/icon_HASH.png` → `.ai/icon_{name}/source.png` (overwrite if exists — this is always the latest source).\n2. Copy it to `.ai/icon_{name}/{LETTER}/source.png` (archive per-iteration source).\n3. Delete the temp `.ai/icon_HASH.png` if it was copied (not moved).\n\nIf NO image was grabbed:\n- **New icon with no image**: Ask the user to provide a screenshot. STOP.\n- **Follow-up with no image**: The existing `source.png` in the icon root carries forward. Copy it to `.ai/icon_{name}/{LETTER}/source.png`. If no source.png exists at all, ask the user for an image.\n\n### Step 0g: Verify renderer\n\nLocate the render tool (`codegen_style` with `--render-svg` mode):\n\n```bash\nif [[ \"$OSTYPE\" == darwin* ]]; then\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style\nelse\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe\nfi\n```\n\nIf missing, build it: `cmake --build out --config Debug --target codegen_style`\n\nTest on a known good SVG (use the appropriate binary path for the OS):\n```bash\nCODEGEN=$(if [[ \"$OSTYPE\" == darwin* ]]; then echo out/Telegram/codegen/codegen/style/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)\n$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512\n```\n\nIf works → delete test render, set `RENDER_AVAILABLE = true`. If fails → `RENDER_AVAILABLE = false`.\n\n## Phase 1: Vectorize & Post-process\n\n### Step 1a: Call vectosolve\n\nUse the `mcp__vectosolve__vectorize` tool with `file_path` set to the **absolute path** of `.ai/icon_{name}/{LETTER}/source.png`.\n\n**If this fails, STOP IMMEDIATELY.** Do NOT try to generate the SVG manually or by any other means. Report the error to the user and let them fix the issue (bad API key, no credits, network error, etc.).\n\nSave the returned SVG content to `.ai/icon_{name}/{LETTER}/raw_vectosolve.svg`.\n\nThe MCP tool calls the vectosolve API ($0.20/call). The API key is stored in `~/.claude.json` MCP config (never in the repository).\n\n### Step 1b: Post-process the SVG\n\nThe vectosolve SVG will have colors from the mockup, arbitrary dimensions, and possibly a non-square aspect ratio from a non-square screenshot crop. Post-processing fixes this by adjusting the **viewBox** — leave path coordinates untouched.\n\n**Do NOT transform path coordinates.** Vectosolve's paths are correct — the only thing wrong is the framing. All geometry adjustments are done by manipulating the `viewBox` and the `width`/`height` attributes.\n\n#### Sub-step 1: Read the request and determine parameters\n\nBefore touching the SVG, determine these from the user's request and context.md:\n\n1. **Output size** (`OUT_W × OUT_H`): default is `24px × 24px` for menu icons. The user may request different dimensions (e.g., 36×36, 48×48, or non-square). Always check the request.\n2. **Content padding**: default is ~2px equivalent on each side at the output scale (so content fills roughly (OUT_W-4) × (OUT_H-4)). The user may request different padding or edge-to-edge.\n3. **Centering**: default is centered both horizontally and vertically. The user may request specific alignment (e.g., \"align to bottom\").\n\n#### Sub-step 2: Parse the raw SVG\n\n1. Extract the `viewBox`: `viewBox=\"VB_X VB_Y VB_W VB_H\"` (typically `0 0 W H`).\n2. Identify ALL paths. Classify each:\n   - **Background**: a rect or path spanning the full viewBox (first path that's a simple rectangle matching the viewBox bounds). **Remove it entirely.**\n   - **Content**: the actual icon shapes. **Keep these, paths unchanged.**\n3. If paths have `transform=\"translate(TX,TY)\"` attributes, that's fine — keep them as-is. The viewBox framing will work regardless.\n\n#### Sub-step 3: Compute the content bounding box\n\nEstimate the bounding box of the content paths (after removing the background). You can either:\n- Eyeball it from the path coordinates (look at first/last M commands and extremes of curves)\n- Or for precision, write a quick script to parse the paths and find min/max X/Y\n\nCall the result: `CX_MIN, CY_MIN, CX_MAX, CY_MAX`. Content dimensions: `CW = CX_MAX - CX_MIN`, `CH = CY_MAX - CY_MIN`.\n\n#### Sub-step 4: Compute the new viewBox\n\nThe viewBox determines what part of the SVG coordinate space maps to the output rectangle. By expanding the viewBox beyond the content bounds, we add padding. By making the viewBox aspect ratio match the output aspect ratio, we prevent stretching.\n\n1. **Output aspect ratio**: `OUT_AR = OUT_W / OUT_H` (for 24×24 this is 1.0).\n2. **Padding in SVG coordinates**: we want ~2px padding at output scale. The scale factor is `OUT_W / VB_CONTENT_W` approximately, so padding in SVG coords = `2 * (CW / (OUT_W - 4))` (or similar — the exact formula depends on which dimension is dominant). Simpler approach: aim for content to occupy ~83% of the viewBox (≈ 20/24), so:\n   - `PADDED_W = CW / 0.83`\n   - `PADDED_H = CH / 0.83`\n3. **Match output aspect ratio**: the viewBox aspect ratio must equal `OUT_AR` to avoid stretching.\n   - If `PADDED_W / PADDED_H > OUT_AR`: width is dominant → `VB_W = PADDED_W`, `VB_H = VB_W / OUT_AR`\n   - If `PADDED_W / PADDED_H < OUT_AR`: height is dominant → `VB_H = PADDED_H`, `VB_W = VB_H * OUT_AR`\n   - If equal: `VB_W = PADDED_W`, `VB_H = PADDED_H`\n4. **Center the content** in the new viewBox:\n   - `VB_X = CX_MIN - (VB_W - CW) / 2`\n   - `VB_Y = CY_MIN - (VB_H - CH) / 2`\n   - (Adjust if the user requested non-centered alignment)\n\nThe new viewBox is: `viewBox=\"VB_X VB_Y VB_W VB_H\"`.\n\n#### Sub-step 5: Recolor to white-on-transparent\n\n- Replace ALL `fill` color values (anything that isn't `none`) with `#FFFFFF`.\n- Remove ALL `stroke` and `stroke-width` attributes entirely.\n- Remove `opacity` attributes if present.\n\n#### Sub-step 6: Determine path composition\n\nLook at the icon's visual structure and decide how paths should combine:\n- **Outlined shape** (e.g., circle outline with something inside): combine outer + inner cutout into one `<path>` with `fill-rule=\"evenodd\"`.\n- **Separate distinct parts** (e.g., magnifying glass + checkmark): keep as separate `<path>` elements.\n- **Filled shape with cutout** (e.g., filled circle with checkmark punched out): combine into one path with `fill-rule=\"evenodd\"`.\n\n#### Sub-step 7: Assemble final SVG\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg width=\"{OUT_W}px\" height=\"{OUT_H}px\" viewBox=\"{VB_X} {VB_Y} {VB_W} {VB_H}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g stroke=\"none\" fill=\"none\" fill-rule=\"evenodd\">\n        <path d=\"...\" fill=\"#FFFFFF\"></path>\n    </g>\n</svg>\n```\n\n- `width`/`height` = the output size from the request (default `24px`/`24px`).\n- `viewBox` = the computed viewBox from Sub-step 4. The SVG renderer maps this coordinate region to the output size.\n- Path `d` attributes are **unchanged** from vectosolve output (just background removed, colors replaced).\n- No `<title>`, `id`, `xmlns:xlink`, `version`, `class`, `style`, XML comments, `<metadata>`, or `preserveAspectRatio`.\n- No `<circle>`, `<rect>`, `<line>` — only `<path>`.\n\nWrite the final SVG to `.ai/icon_{name}/{LETTER}.svg`.\n\n### Step 1c: Render\n\nIf `RENDER_AVAILABLE`:\n```bash\n$CODEGEN --render-svg \".ai/icon_{name}/{LETTER}.svg\" \".ai/icon_{name}/render_{LETTER}.png\" 512\n```\n\nRead the render to visually verify the result.\n\n## Phase 2: Review\n\nAfter rendering, assess the result:\n\n1. **Recognizable?** The icon should be clearly identifiable as the intended symbol.\n2. **Scale reasonable?** Should fill the space appropriately with ~2-3px padding.\n3. **Clean lines?** No broken paths, artifacts, or unwanted elements.\n4. **Correct colors?** All white on transparent (no leftover colors from the mockup).\n\nIf the result looks good → proceed to Phase 3 (Output).\n\nIf there are fixable issues (stray element, missed color, etc.) → fix the SVG directly, re-render, and re-check.\n\nIf the result is poor (vectosolve couldn't handle the input well) → report to the user and suggest:\n- Trying a cleaner/larger crop of the icon\n- Providing a different screenshot\n- Following up: `/icon {icon_name} <description of what to change>`\n\n## Phase 3: Output\n\n1. Read the `Target:` line from `.ai/icon_{name}/context.md` to get the output path.\n\n2. Copy the final SVG to that target path (e.g., `Telegram/Resources/icons/menu/{icon_name}.svg`).\n\n3. Update `.ai/icon_{name}/context.md` — append to the end:\n   ```\n   ## Latest Output\n   Letter: {LETTER}\n   Written to: {target_path}\n   ```\n\n4. Report to the user:\n   - Final icon file path\n   - Number of vectosolve calls made (cost at $0.20/call)\n   - Suggest verifying visually\n   - Working directory `.ai/icon_{name}/` has all iterations\n   - Elapsed time since `$START_TIME` (format `Xm Ys`)\n   - Follow-up: `/icon {icon_name} <description of what to change>`\n\n## Text-only Follow-ups (no new image)\n\nWhen a follow-up has no attached image, the user wants to refine the existing SVG based on text feedback. In this case:\n\n1. Skip Phase 1 (no vectosolve call needed).\n2. Read the latest SVG (`.ai/icon_{name}/{prev_letter}.svg`).\n3. Read the latest render if available.\n4. Apply the user's requested changes by editing the SVG directly.\n5. Save as `.ai/icon_{name}/{LETTER}.svg`.\n6. Render, review, and output as normal (Phases 1c → 3).\n\nIf the changes are too complex for manual SVG editing, suggest the user provide a new screenshot instead.\n\n## Error Handling\n\n- If clipboard grab fails → tell user to re-copy and retry.\n- If vectosolve returns an error → report it and suggest a different/cleaner screenshot.\n- If vectosolve returns SVG that can't be parsed → save raw output for debugging, report to user.\n- If the render helper fails → set `RENDER_AVAILABLE = false`, continue with SVG-only review.\n- If post-processing produces a broken SVG → fall back to the raw vectosolve output and do lighter cleanup.\n","frontmatter":{"description":"Generate an SVG icon from a design mockup using vectosolve vectorization","allowed-tools":"Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion, TodoWrite, mcp__vectosolve__vectorize","allowedTools":["Read,","Write,","Edit,","Glob,","Grep,","Bash,","Agent,","AskUserQuestion,","TodoWrite,","mcp__vectosolve__vectorize"]},"allowedTools":["Read,","Write,","Edit,","Glob,","Grep,","Bash,","Agent,","AskUserQuestion,","TodoWrite,","mcp__vectosolve__vectorize"],"isInternal":false,"tokens":4019,"sizeBytes":15382},{"name":"release.md","path":".claude/commands/release.md","rawUrl":"https://raw.githubusercontent.com/telegramdesktop/tdesktop/HEAD/.claude/commands/release.md","title":"Command: /release","category":"command-prompt","format":"markdown","content":"---\ndescription: Prepare changelog, set version, and commit a new release\nallowed-tools: Read, Bash, Edit, Grep, AskUserQuestion\n---\n\n# Release — Changelog, Set Version, Commit\n\nFull release flow: generate changelog entry, run `set_version`, and commit.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nParse `$ARGUMENTS` for two optional parts (in any order):\n- A **version number** like `6.7` or `6.7.0` — if provided, use it as the new version exactly.\n- The word **\"beta\"** — if present, mark the release as beta.\n\nIf no version number is given, auto-increment the patch component (see step 2).\n\n## Steps\n\n### 1. Check git status is clean\n\nRun `git status --porcelain`. If there are any uncommitted changes, **stop** and ask the user to commit or discard them before proceeding. Do not continue until status is clean.\n\n### 2. Read the current changelog\n\nRead `changelog.txt` from the repository root. Note the **latest version number** on the first line (e.g. `6.6.3 beta (12.03.26)`). Parse its major.minor.patch components.\n\n### 3. Determine the new version number\n\n**Important:** version numbers are shared across the beta and stable tracks — the sequence advances through both. The same `major.minor.patch` cannot be released as both beta and stable. A beta release \"uses up\" that number; the next stable must bump to a new patch.\n\n- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given). If the latest changelog entry already used this exact number (regardless of beta/stable), warn the user — they likely want a bumped patch.\n- **If no version was provided**, auto-increment from the latest changelog version:\n  - If it was a beta, and the new release is **not** beta, bump the patch component by 1 (do **not** reuse the beta's number for stable).\n  - If the new release is beta and the latest was also beta with the same major.minor, bump patch.\n  - Otherwise bump the patch component by 1.\n- Present the chosen version to the user and ask for confirmation before proceeding. If the user suggests a different version, use that instead.\n\n### 4. Fetch tags and determine the last release tag\n\nRun `git fetch origin --tags` first to ensure all tags from the public repository are available locally. Then run `git tag --sort=-v:refname` and find the most recent `v*` tag. This is the baseline for the diff.\n\n### 5. Collect commits\n\nRun `git log <last-tag>..HEAD --oneline` to get all commits since the last release.\n\n### 6. Write the changelog entry\n\nAnalyze every commit message. Group them mentally into features, improvements, and bug fixes. Then produce **brief, user-facing bullet points** following these rules:\n\n- **Style:** Match the existing changelog tone exactly — short, imperative sentences starting with a verb (Fix, Add, Allow, Show, Improve, Support…). Keep the trailing periods (the existing changelog uses them).\n- **Brevity:** Each bullet should be one short sentence, around 80 characters when possible. No implementation details. No commit hashes.\n- **Selection:** Only include changes that matter to end users. Skip CI, build infra, submodule bumps, code style, refactors, and intermediate WIP commits. Collapse many related commits (e.g. a dozen image-editor commits) into one or two bullets.\n- **Ordering:** Features first, then improvements, then bug fixes.\n- **Quantity:** Aim for 4-12 bullets total depending on the amount of changes.\n\n### 7. Format and insert into changelog.txt\n\nUse this exact format (date is today in DD.MM.YY):\n\n```\n<version> [beta ](DD.MM.YY)\n\n- Bullet one.\n- Bullet two.\n```\n\nPrepend the new entry at the very top of `changelog.txt`, separated by a blank line from the previous first entry. Use the Edit tool.\n\n**Never delete or edit existing entries**, even if the new stable entry merges bullets from prior beta(s). Prior beta blocks remain as historical record for beta-track users.\n\n**For stable releases spanning prior beta(s):** the entry should cover everything since the last **stable** release (not just since the last beta), including noteworthy beta-shipped features, trimmed as needed to stay within 4–12 bullets.\n\n### 8. Wait for approval\n\nAfter writing the entry to `changelog.txt` (step 7), tell the user the changelog has been updated and ask them to review it in the IDE. They can edit it directly and tell you to continue, or tell you what to change in chat. Do **not** print the full entry in chat — the file itself is the review surface.\n\n**Do NOT proceed until the user explicitly approves.**\n\n### 9. Run set_version\n\nOnce approved, run the `set_version` script from the repository root. On Windows:\n\n```\n.\\Telegram\\build\\set_version.bat <version_arg>\n```\n\nWhere `<version_arg>` is formatted as the `set_version` script expects:\n- Stable: `6.7.0` or `6.7`\n- Beta: `6.7.0.beta`\n\nVerify the script exits successfully (exit code 0). If it fails, show the error and stop.\n\n### 10. Commit\n\nStage all changes and create a commit. The commit message format:\n\n**First line:**\n- For stable: `Version <major>.<minor>.` if patch is 0, otherwise `Version <major>.<minor>.<patch>.`\n- For beta: `Beta version <major>.<minor>.<patch>.`\n\n**Then an empty line, then the changelog bullets.** Copy bullet lines from the changelog as-is. Only wrap lines that exceed 72 characters; shorter lines must stay on a single line. When wrapping is needed, break at logically correct places (between words/phrases) and indent continuation lines with two spaces.\n\nExample commit message:\n```\nBeta version 6.6.3.\n\n- Drawing tools in image editor\n  (brush, marker, eraser, arrow).\n- Draw-to-reply button in media viewer.\n- Trim recorded voice messages.\n- Fix reorder freeze in chats list.\n```\n\nUse a HEREDOC to pass the message to `git commit -a`.\n\n### 11. Done\n\nRun `git log -1` to show the resulting commit and confirm success.\n","frontmatter":{"description":"Prepare changelog, set version, and commit a new release","allowed-tools":"Read, Bash, Edit, Grep, AskUserQuestion","allowedTools":["Read,","Bash,","Edit,","Grep,","AskUserQuestion"]},"allowedTools":["Read,","Bash,","Edit,","Grep,","AskUserQuestion"],"isInternal":false,"tokens":1407,"sizeBytes":5840}],"systemPromptSnippet":"<yakaai_skills repo=\"telegramdesktop/tdesktop\">\n<!-- File: AGENTS.md (Tokens: ~11504 | Category: root-instruction) -->\n# Agent Guide for Telegram Desktop\n\nThis guide defines repository-wide instructions for coding agents working with the Telegram Desktop codebase.\n\n## AI Tasks\n\nIn this repository \"task\" is a specific term. It always means one work record in\nthe sibling `ai-tdesktop` repository, never a `TODO` comment, a checklist item,\nor a unit of work invented during the current conversation. \"The tasks\", \"the\nqueue\", \"the board\", \"what's most pressing\", and a bare task slug all refer to\nthat queue.\n\n- The queue lives in `../ai-tdesktop`, a sibling of this checkout, with one\n  record per directory under `tasks/YYYY/MM/DD/<slug>/`. A task id is that dated\n  path, for example `2026/07/18/fix-community-forward`, and it is the only\n  durable link between a source commit and its task. Linked slot worktrees live\n  in `../ai-tdesktop-worktrees`. Read `../ai-tdesktop/AGENTS.md` before doing\n  anything inside that repository.\n- Each record holds `task.md` — one `# ` title line, then one self-contained\n  paragraph that is the task's description wherever it is summarized — and\n  `state.yaml` with `status`, `type`, `depends_on`, `claimed_by`, and dates.\n  Open statuses are `todo`, `in-progress`, `blocked`, and `split-required`;\n  `approved` is completed history, and `split.yaml` / `superseded.yaml` mark\n  retired records.\n- To browse the queue, read those files directly. Do not run `python3 ai.py`: it\n  is a full-screen browser for the user's own terminal and refuses to run in a\n  pipe. A compact open listing:\n\n```bash\ncd ../ai-tdesktop && for state in tasks/*/*/*/*/state.yaml; do\n  status=$(sed -n 's/^status: //p' \"$state\")\n  case \"$status\" in todo|in-progress|blocked|split-required)\n    dir=$(dirname \"$state\")\n    printf '%-14s %-70s %s\\n' \"$status\" \"${dir#tasks/}\" \\\n      \"$(sed -n '1s/^# //p' \"$dir/task.md\")\";;\n  esac\ndone | sort\n```\n\n- When ranking open work by urgency, use `in-progress`, then `blocked`, then\n  `split-required`, then ready `todo` (no `claimed_by`, and every `depends_on`\n  already `approved`), then `todo` still waiting on a dependency. Say who owns\n  claimed work. This checkout's tag is the `Telegram/build/ai-machine-tag` value\n  plus the checkout folder name, such as `macbook-tdesktop`, and work claimed by\n  another checkout is never taken over without an explicit human reassignment.\n- Browsing is read-only. Listing, summarizing, comparing, or recommending tasks\n  never edits `state.yaml`, publishes a lifecycle commit, or starts\n  implementation, no matter how small the task looks.\n- Acting on an existing queue task goes through the workflow skills instead of\n  by hand: `perform-task <slug or full id>` starts or resumes and performs\n  exactly one known task, and `continue <batch specification>` runs the batch\n  of tasks the specification selects. Source commits owned by a task use the\n  three-line form described under `## Commits`.\n- A request the user makes in conversation is ordinary work: do it directly in\n  this checkout. Do not write it to the ignored `../ai-tdesktop/inbox/inbox.md`,\n  and do not run `process-inbox` or `continue` over it, unless the user asks for\n  that. The queue's planning, review and evidence campaign costs hours, so\n  spending it on a request the user expected you to just do is a real error, not\n  thoroughness.\n- You may *offer* the queue when the work genuinely earns it: a large or\n  ambiguous change, one whose correctness needs a real testing campaign, or one\n  that is safety-, security- or data-safety-critical. Make the offer in one\n  line, say why, and route it only after the user approves. Absent approval,\n  implement it directly and say what you skipped.\n- Never guess between similarly named tasks. Report the matching full ids and\n  let the user choose.\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## Dependency backports\n\nFor explicitly requested dependency updates and backports, use the dependency\nrepositories' `master` branches directly. Do not create `codex/*` or other topic\nbranches in `Libraries/patches`, `Libraries/tg_owt`, or `Libraries/tg_angle`.\nThe scheduled dependency monitor remains advisory and does not implement fixes.\n\n- Before editing each dependency repository, inspect its working tree and\n  current branch, fetch `origin`, and verify that the checkout is on `master`,\n  tracks `origin/master`, and is synchronized with the fetched remote branch.\n  Switch to `master` when necessary and fast-forward it when possible. Review\n  divergence before integrating it; preserve unrelated edits, untracked library\n  checkouts and local commits. Do not reset them away or publish unrelated local\n  commits implicitly.\n- Commit fixes to our copied dependencies, including WebRTC and ANGLE, directly\n  in `tg_owt` and `tg_angle`. For external dependencies such as Qt and FFmpeg,\n  commit patch files to `Libraries/patches` and apply them consistently through\n  the applicable prepare, Docker and Snap recipes. Keep intentional differences\n  between platform versions and do not add patch copies inside tdesktop.\n- Keep each independent fix in a separate commit and retain its upstream\n  provenance. Review applicability, prerequisites and patch interactions before\n  committing. Respect the requested validation scope; do not rebuild dependencies\n  unless the user explicitly requests it.\n- Push `master` to `origin/master` without force, fetch again and verify that\n  every new dependency commit is reachable from the published `origin/master`.\n  Only then update the applicable tdesktop revisions to those published SHAs.\n  If integration changes a SHA, update every recipe that consumed the old one.\n  A commit available only on a local or topic branch is not a finished update.\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**On macOS, never run an Xcode clean against `out/`** - not `--target clean`,\nnot `--clean-first` with any target, not `xcodebuild ... clean` and not a\nClean in the Xcode app. Xcode's build-products folder for `Telegram` is\n`out/Debug`, which also holds the portable account folders. When `out/Debug`\ncarries the `com.apple.xcode.CreatedByBuildSystem` extended attribute,\n`--target clean` deletes `out/Debug` with every folder in it, and\n`--target Telegram --clean-first` does the same before it builds. Without the\nattribute, `--target clean` refuses to delete `out/Debug` and ends\n`** CLEAN FAILED **` (exit 65). It ends the same way when it refuses only\nanother, unmarked products folder and deletes a marked `out/Debug`, so exit 65\nalone does not prove that `out/Debug` survived. To force a full recompile,\nfollow the `out/build` deletion in \"### Forcing a full recompile on macOS\"\nunder \"## Troubleshooting\" exactly as written there, its precondition and its\nchecks before and after the rebuild included. Never mark `out/Debug` deletable\nwith `xattr -w com.apple.xcode.CreatedByBuildSystem true`, as the note in\nXcode's refusal suggests, and never move, rename or remove any `out/Debug`\nentry to make a clean succeed.\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- First-time configure of a new `out/` tree: set the `QT` environment variable\n  to the Qt version this checkout actually has, for example `export QT=6.11.1`\n  when `../Libraries/local/Qt-6.11.1` is the installed one. The value names a\n  directory, so it is the checkout's version, not a constant.\n- Reconfiguring an existing `out/` tree: do **not** export `QT`. Run\n  `env -u QT cmake -S . -B out` instead. `cmake/external/qt/package.cmake`\n  writes an exported `QT` into the `qt_requested` cache entry with `FORCE`, so\n  it overwrites the version the tree was already configured with instead of\n  being ignored. A value that disagrees with the configured tree then fails the\n  regeneration, and the generated project keeps the source list it was last\n  generated with: newly added sources are never compiled — which surfaces later\n  as undefined-symbol link errors rather than as a configure error — and newly\n  added `.style` modules leave the generated `style_*.h` includes stale.\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. Rebuild every object once by following the\n`out/build` deletion in \"### Forcing a full recompile on macOS\" exactly as\nwritten there, its precondition and checks included, never with an Xcode clean\n(see the macOS statement in \"### Build Commands\" under\n\"## Build Configuration\"). Then rerun the same test once. If the signature\npersists after that clean rebuild, continue normal crash diagnosis or report\nthe blocker. Do not loop clean rebuilds.\n\n### Forcing a full recompile on macOS\n\nStart only when no build of this checkout is running and this checkout's exact\nTelegram process is stopped. Never stop any other process for this procedure.\nOutside the autonomous `continue` / `perform-task` workflow, stopping that\nprocess follows \"### Build output locks\": the user may be testing it, so ask\nthe user first and never terminate it without explicit permission. Then\nrebuild every object by deleting the intermediate tree `out/build`, never with\nan Xcode clean (see the macOS statement in \"### Build Commands\" under\n\"## Build Configuration\").\n\nThe generated project keeps every intermediate (object files, precompiled\nheaders and Xcode's build database) under `out/build`, its `SYMROOT`, and\nonly build products in `out/Debug` and the other per-target `Debug` folders.\nDeleting `out/build` therefore works the same whether or not `out/Debug`\ncarries the `com.apple.xcode.CreatedByBuildSystem` attribute, and it touches\nnothing in `out/Debug`. From the repository root, record what `out/Debug`\nholds, delete the intermediates and confirm that `out/build` is gone:\n\n```bash\ntest -d out/Telegram.xcodeproj &&\n  ls -A out/Debug | sort > out/language-rebuild-mark.txt &&\n  rm -rf out/build &&\n  test ! -e out/build &&\n  echo 'out/build is gone'\n```\n\nIt must print `out/build is gone`: no intermediate object survived. Object\nfiles elsewhere in `out/`, such as CMake's compiler checks or a release\nbuild's tree, belong to other builds; leave them alone. This step has no\naccepted failure: an error, other output or no output stops the procedure.\nThen run one full Debug rebuild, which has to compile every object again:\n\n```bash\ncmake --build out --config Debug --target Telegram\n```\n\nIt must end `** BUILD SUCCEEDED **`. Then check that the rebuild compiled every\nobject and that nothing in `out/Debug` was lost:\n\n```bash\ntest -f out/language-rebuild-mark.txt &&\n  find out/build -name '*.o' ! -newer out/language-rebuild-mark.txt | wc -l &&\n  find out/build -name '*.o' | wc -l &&\n  ls -A out/Debug | sort | comm -23 out/language-rebuild-mark.txt -\n```\n\nIt must print `0` (no object file is older than the mark written before\n`out/build` was deleted), then a nonzero object count, and nothing else (every\nentry `out/Debug` held before is still there). Anything else stops the\nprocedure; report what it printed.\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## Debug-Only Code\n\nProduction translation units stay free of debug machinery. The permanent test\nharness lives in `Telegram/SourceFiles/test/`, and the disposable `-testagent`\noverlay owns per-task instrumentation; production code carries at most a thin\nsingle-call seam (a `Test::Fire()`-style waitpoint or a live-object\npublication at a construction seam).\n\n- Do not add `#ifdef _DEBUG` blocks, debug-only types, debug state, mutexes,\n  counters, or observation structs to production headers or sources. When a\n  behavior cannot be observed without such machinery, that is a harness gap:\n  extend the `test/` helpers or the overlay instead.\n- Never commit debugging machinery interleaved with working code in the same\n  translation unit. A permanent helper belongs under `test/`; a temporary one\n  belongs in the overlay and is never retained.\n- Exceptions exist — a few `#ifdef _DEBUG` hooks are deliberately kept in\n  production files — but they are exceptions and each new one needs a solid,\n  stated reason. \"The test needed it\" is not one.\n\n## Harness documentation\n\nKeep `Telegram/SourceFiles/test/README.md` a short mandatory guide, under\n8 KiB. When adding or changing a harness helper, follow\n`Telegram/SourceFiles/test/docs/adding-helpers.md`: add or update one helper\nindex row and its on-demand reference, and keep the source header focused on\nits callable contract. Detailed examples, platform caveats and diagnostic\nhistory belong in the references, not in the mandatory README. Overlay work\nreads that guide, the references selected by the task and their headers;\nnever require reading every helper page.\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- Decide the `[ai] ` prefix separately for each commit. Use it only when every\n  retained change in that commit, and the commit's purpose, are exclusively\n  about the AI workflow: the agent harness, skills, prompts, custom commands,\n  agent documentation, or AI testing infrastructure. Typical qualifying paths\n  include `Telegram/SourceFiles/test/`, `.agents/`, `.claude/`, `.grok/`,\n  `AGENTS.md`, `CLAUDE.md`, and `GROK.md`, but paths alone do not decide the\n  prefix. Product-specific test seams, app code, and build-system integration do\n  not qualify merely because agents use them for verification. Split mixed\n  workflow and product work into separate commits when practical; otherwise the\n  mixed commit must not use `[ai] `. Do not count the disposable test overlay or\n  external AI task artifacts. Every other commit must not contain `[ai]`\n  anywhere.\n- The `[ai] ` prefix marks the commit's scope, never its authorship. It does\n  not mean \"authored by an AI\": an AI-authored product fix takes a plain\n  subject, and a workflow-only commit takes the prefix no matter who wrote it.\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- Never create backup branches or tags before a rebase, reorder, squash or any\n  other history rewrite. The branch reflog keeps the old tip; report its sha\n  as the undo path instead.\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## Area maps\n\n`Telegram/SourceFiles/AGENTS.md` is the source atlas: every area of the\ntree with its paths and responsibility. A directory may also\nhave its own `AGENTS.md`: the map of that area's modules, placement rules,\ninvariants, and things to leave alone. Before adding code, read the atlas\nentry of the area you touch and the nearest map, and put new code where they\nsay. A change that needs a different shape amends the map in the same task.\nHow the atlas and maps are written and how changes are checked against them\nis in `.agents/skills/code-health/SKILL.md`.\n\n## Coding Style\n\n**Comments are rationed:**\n\nA comment is one line; two or three only when the block opens with `// WHY:`. A commit may add two comment lines plus one such exception, and a trailing comment is a line too; only `} // namespace X` closers and `#endif // X` labels are free. Say why, never what. Hooks enforce it, and `Telegram/SourceFiles/test/` is exempt.\n\nDo not remove existing comments just to satisfy this rule. Preserve comments unless your change makes them incorrect or truly obsolete; when moving or refactoring code, move the useful comment with it. Inline comments that label positional arguments for generated or schema-driven APIs (for example TL/MTP constructors) are useful because the field names are not visible in the call itself.\n\n**Never cite line numbers** in comments, `README.md` files or other checked-in text. Name the function, member, constant or type, and its file when that helps: write `Runner::finish()` in `test_runner.cpp`, not `test_runner.cpp:NNN` or `:NNN-MMM`. Line numbers drift with every edit and nothing updates them. This covers submodule, Qt and other third-party sources too. Quoted log or crash text stays verbatim.\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 - one line, and it carries a reason:\n_limit = kDefaultLimit; // the server rejects anything larger\n\n// ACCEPTABLE - the exception: opens with WHY, three lines at most,\n// once per commit.\n// WHY: the server sends the id before the peer exists, so the row is\n// created empty and filled on the next update, or the list flickers\n// on every reconnect.\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**Never discard a result with a cast:** `static_cast<void>(...)` and `(void)expr` are banned; instead of silencing `[[nodiscard]]`, fix the design.\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## Input Method Hints\n\nA field for typed content tells the input method what it is for: call `setInputMethodHints` right after creating it (`Ui::InputField` forwards the hints to its inner editor; `Ui::MaskedInputField` subclasses set them in the constructor). Combinations already used in the code:\n\n- URL: `Qt::ImhUrlCharactersOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- E-mail: `Qt::ImhEmailCharactersOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- Phone: `Qt::ImhDialableCharactersOnly | Qt::ImhNoPredictiveText`\n- Digits only (codes, date and time parts): `Qt::ImhDigitsOnly | Qt::ImhNoPredictiveText`\n- Numbers with separators (amounts, timestamps): `Qt::ImhFormattedNumbersOnly | Qt::ImhNoPredictiveText`\n- Latin identifiers (usernames, file extensions, keys): `Qt::ImhLatinOnly | Qt::ImhNoAutoUppercase | Qt::ImhNoPredictiveText`\n- Add `Qt::ImhSensitiveData` to one-time codes and to secrets shown in clear text, such as recovery phrase words\n- Free text (names, titles, comments, search) gets no hints; `Ui::PasswordInput` sets its own\n\nPrefer the hint that says what the field is for, as long as it works or at least doesn't conflict with what works. To see what works, check every consumer on every platform tdesktop ships at the Qt version in use: the Qt platform plugins (windows, cocoa, xcb, wayland) and the input context plugins, including the bundled ones (fcitx5-qt). If the semantic hint conflicts, pick what works. Hints don't filter input: the field still drops unwanted characters itself.\n\nAs of 2026-10-20, `Qt::ImhDate` and `Qt::ImhTime` do nothing anywhere tdesktop runs. Qt on Wayland sends them as the date/time content purpose, but no Wayland on-screen keyboard implements those purposes (Qt Virtual Keyboard, which Plasma Keyboard is built on, GNOME Shell, Squeekboard). Qt reads them on none of tdesktop's other platforms (Windows, macOS, X11), and neither does the bundled fcitx5-qt. Used instead of the digit hints, they lose the digit layouts, so date and time fields use the digit hints above.\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- Every plural form, `#one` included, keeps `{count}`: many languages put more than 1 into their \"one\" category (Russian: 21, 31, ...), so a `#one` phrase written for exactly 1 is wrong for them. When exactly 1 deserves different wording, add a separate non-plural key and pick it in code, so every language gets that special case\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\n### Per-subscription state\n\n`rpl::on_next` stores the handler by value. Each emission invokes a *copy* of that stored handler, so a nested emit does not re-enter the running callable. Writes a `mutable` lambda makes to its own captures land on the copy and are discarded, so they never become per-subscription state.\n\n```cpp\n// BAD - compiles, runs, and grew is just value > 0 every time:\nevents | rpl::on_next([last = 0](int value) mutable {\n    const auto grew = (value > last);\n    last = value;\n    if (grew) {\n        // ...\n    }\n}, lifetime);\n\n// GOOD - state lives beside the handler:\nconst auto last = lifetime.make_state<int>(0);\nevents | rpl::on_next([=](int value) {\n    const auto grew = (value > *last);\n    *last = value;\n    if (grew) {\n        // ...\n    }\n}, lifetime);\n```\n\nKeep per-subscription state in `lifetime.make_state<T>()`, behind a `shared_ptr`, or on the object that owns the subscription. A `mutable` lambda that is one-shot — `rpl::take(1)`, a self-destroying subscription, a `crl::on_main` or `crl::async` callback, a `done` or `error` callback — is not this trap.\n\n<!-- File: CLAUDE.md (Tokens: ~21 | Category: claude-rule) -->\n# Claude Code Pointer\n\nRead `AGENTS.md` and treat it as the canonical repository-wide instructions.\n\n<!-- File: .claude/commands/icon.md (Tokens: ~4019 | Category: command-prompt) -->\n---\ndescription: Generate an SVG icon from a design mockup using vectosolve vectorization\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion, TodoWrite, mcp__vectosolve__vectorize\n---\n\n# Icon - SVG Icon Generation from Design Mockup\n\nYou generate production-quality SVG icons for Telegram Desktop by vectorizing design mockup screenshots using the vectosolve MCP service, then post-processing the result to match the Telegram icon format.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nIf `$ARGUMENTS` is empty, ask the user to describe the icon they want and paste a cropped screenshot of it.\n\n## Overview\n\nThe workflow takes a cropped screenshot of an icon from a design mockup (grabbed from the clipboard), vectorizes it via the vectosolve MCP, then post-processes the SVG (recolor to white-on-transparent, restructure to minimal format, set 24x24 output size).\n\nWorking directory: `.ai/icon_{name}/` with iterations labeled by letter (`a/`, `b/`, ...), each containing `source.png`. Output SVGs are in the icon root: `a.svg`, `b.svg`, etc.\n\nFollow-ups are supported: `/icon {icon_name} <description>` continues from where the previous run left off.\n\n## Phase 0: Setup\n\n**Record the current time** (using `date` or equivalent) as `$START_TIME`.\n\n### Step 0a: Clipboard grab (MUST be the VERY FIRST action)\n\nIf there is an image attached to the user's message:\n\n1. Generate a random 8-character hex string for `HASH` (use `openssl rand -hex 4` or similar).\n2. **IMMEDIATELY** — before any other processing — run this Bash command to save the clipboard image:\n   ```bash\n   HASH=$(openssl rand -hex 4) && if [[ \"$OSTYPE\" == darwin* ]]; then bash .claude/grab_clipboard.sh \".ai/icon_${HASH}.png\"; else powershell -ExecutionPolicy Bypass -File .claude/grab_clipboard.ps1 \".ai/icon_${HASH}.png\"; fi\n   ```\n   On macOS `.claude/grab_clipboard.sh` is used; on Windows `.claude/grab_clipboard.ps1`. Both grab the current clipboard image and save it to the specified path.\n\n3. If the command fails (exit 1 / no image on clipboard):\n   - Tell the user: **\"Clipboard doesn't contain an image. Please copy the icon area first, then retry.\"** (On macOS: Cmd+Ctrl+Shift+4 to snip to clipboard; on Windows: Win+Shift+S.)\n   - **STOP IMMEDIATELY. Do NOT continue.** You cannot use the image pasted in the conversation — it exists only as pixels in the chat, not as a file you can send to vectosolve. The clipboard grab is the ONLY way to get the image to disk. Do not attempt any workaround.\n\n4. Read back the saved `.ai/icon_HASH.png` using the Read tool.\n5. Compare it visually with the image pasted in the conversation. They should depict the same thing.\n   - If they look **completely different**: delete `.ai/icon_HASH.png` and fail:\n     > \"The clipboard image doesn't match what you pasted. Please re-copy and retry.\"\n   - If they look the same (or close enough): proceed. Store the temp path.\n\nIf NO image is attached to the message, skip this step entirely.\n\n### Step 0b: Fail-fast — verify vectosolve MCP\n\nCheck that the `mcp__vectosolve__vectorize` tool is available by looking at your available tools list. If it is NOT available, fail immediately with:\n\n> vectosolve MCP is not configured. Set it up with:\n> ```\n> claude mcp add vectosolve --scope user -e VECTOSOLVE_API_KEY=vs_xxx -- npx @vectosolve/mcp\n> ```\n> Then restart Claude Code.\n\n### Step 0c: Follow-up detection\n\nExtract the first word/token from `$ARGUMENTS` (everything before the first space or newline). Call it `FIRST_TOKEN`.\n\nRun these TWO commands using the Bash tool, **IN PARALLEL**:\n1. `ls .ai/` — to see all existing icon project names\n2. `ls .ai/icon_{FIRST_TOKEN}/context.md` — to check if this specific icon project exists\n\n**Evaluate the results:**\n- If command 2 **succeeds** (context.md exists): this is a **follow-up**. The icon name is `FIRST_TOKEN`. The follow-up description is everything in `$ARGUMENTS` after `FIRST_TOKEN`.\n- If command 2 **fails** (not found): this is a **new icon**. The full `$ARGUMENTS` is the icon description.\n\n### Step 0d: New icon setup\n\n1. Parse `$ARGUMENTS` to determine:\n   - **Icon description**: what the icon should depict\n   - **Icon type**: default is `menu` (24x24 menu/button icon). User may specify otherwise.\n   - **Target subfolder**: `menu/` by default, or another subfolder if specified.\n\n2. Choose an icon file name:\n   - Lowercase letters and underscores only — **NO hyphens**\n   - Match existing naming conventions (check `Telegram/Resources/icons/{subfolder}/`)\n   - Must NOT conflict with existing icons\n   - Must NOT collide with existing `.ai/icon_{name}/` directories\n\n3. Create `.ai/icon_{name}/` and `.ai/icon_{name}/a/`.\n\n4. Write `.ai/icon_{name}/context.md` with:\n   ```\n   ## Icon: {icon_name}\n   Type: {menu/other}\n   Target: Telegram/Resources/icons/{subfolder}/{icon_name}.svg\n\n   ## Original Request\n   {full $ARGUMENTS text}\n\n   ## Follow-ups\n   (none yet)\n   ```\n\n5. Set `LETTER` to `a`.\n\n### Step 0e: Follow-up setup\n\n1. Read `.ai/icon_{name}/context.md` to get the icon type, subfolder, and full history.\n2. Find the latest existing letter folder in `.ai/icon_{name}/` (highest letter).\n3. Set `LETTER` to the next letter after the latest.\n4. Create `.ai/icon_{name}/{LETTER}/`.\n5. Update `.ai/icon_{name}/context.md` — append the follow-up description to the `## Follow-ups` section:\n   ```\n   ### Follow-up (starting at letter {LETTER})\n   {follow-up description}\n   ```\n\n### Step 0f: Place source image\n\nIf a clipboard image was grabbed in Step 0a:\n1. Copy (or move) `.ai/icon_HASH.png` → `.ai/icon_{name}/source.png` (overwrite if exists — this is always the latest source).\n2. Copy it to `.ai/icon_{name}/{LETTER}/source.png` (archive per-iteration source).\n3. Delete the temp `.ai/icon_HASH.png` if it was copied (not moved).\n\nIf NO image was grabbed:\n- **New icon with no image**: Ask the user to provide a screenshot. STOP.\n- **Follow-up with no image**: The existing `source.png` in the icon root carries forward. Copy it to `.ai/icon_{name}/{LETTER}/source.png`. If no source.png exists at all, ask the user for an image.\n\n### Step 0g: Verify renderer\n\nLocate the render tool (`codegen_style` with `--render-svg` mode):\n\n```bash\nif [[ \"$OSTYPE\" == darwin* ]]; then\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style\nelse\n    ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe\nfi\n```\n\nIf missing, build it: `cmake --build out --config Debug --target codegen_style`\n\nTest on a known good SVG (use the appropriate binary path for the OS):\n```bash\nCODEGEN=$(if [[ \"$OSTYPE\" == darwin* ]]; then echo out/Telegram/codegen/codegen/style/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)\n$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512\n```\n\nIf works → delete test render, set `RENDER_AVAILABLE = true`. If fails → `RENDER_AVAILABLE = false`.\n\n## Phase 1: Vectorize & Post-process\n\n### Step 1a: Call vectosolve\n\nUse the `mcp__vectosolve__vectorize` tool with `file_path` set to the **absolute path** of `.ai/icon_{name}/{LETTER}/source.png`.\n\n**If this fails, STOP IMMEDIATELY.** Do NOT try to generate the SVG manually or by any other means. Report the error to the user and let them fix the issue (bad API key, no credits, network error, etc.).\n\nSave the returned SVG content to `.ai/icon_{name}/{LETTER}/raw_vectosolve.svg`.\n\nThe MCP tool calls the vectosolve API ($0.20/call). The API key is stored in `~/.claude.json` MCP config (never in the repository).\n\n### Step 1b: Post-process the SVG\n\nThe vectosolve SVG will have colors from the mockup, arbitrary dimensions, and possibly a non-square aspect ratio from a non-square screenshot crop. Post-processing fixes this by adjusting the **viewBox** — leave path coordinates untouched.\n\n**Do NOT transform path coordinates.** Vectosolve's paths are correct — the only thing wrong is the framing. All geometry adjustments are done by manipulating the `viewBox` and the `width`/`height` attributes.\n\n#### Sub-step 1: Read the request and determine parameters\n\nBefore touching the SVG, determine these from the user's request and context.md:\n\n1. **Output size** (`OUT_W × OUT_H`): default is `24px × 24px` for menu icons. The user may request different dimensions (e.g., 36×36, 48×48, or non-square). Always check the request.\n2. **Content padding**: default is ~2px equivalent on each side at the output scale (so content fills roughly (OUT_W-4) × (OUT_H-4)). The user may request different padding or edge-to-edge.\n3. **Centering**: default is centered both horizontally and vertically. The user may request specific alignment (e.g., \"align to bottom\").\n\n#### Sub-step 2: Parse the raw SVG\n\n1. Extract the `viewBox`: `viewBox=\"VB_X VB_Y VB_W VB_H\"` (typically `0 0 W H`).\n2. Identify ALL paths. Classify each:\n   - **Background**: a rect or path spanning the full viewBox (first path that's a simple rectangle matching the viewBox bounds). **Remove it entirely.**\n   - **Content**: the actual icon shapes. **Keep these, paths unchanged.**\n3. If paths have `transform=\"translate(TX,TY)\"` attributes, that's fine — keep them as-is. The viewBox framing will work regardless.\n\n#### Sub-step 3: Compute the content bounding box\n\nEstimate the bounding box of the content paths (after removing the background). You can either:\n- Eyeball it from the path coordinates (look at first/last M commands and extremes of curves)\n- Or for precision, write a quick script to parse the paths and find min/max X/Y\n\nCall the result: `CX_MIN, CY_MIN, CX_MAX, CY_MAX`. Content dimensions: `CW = CX_MAX - CX_MIN`, `CH = CY_MAX - CY_MIN`.\n\n#### Sub-step 4: Compute the new viewBox\n\nThe viewBox determines what part of the SVG coordinate space maps to the output rectangle. By expanding the viewBox beyond the content bounds, we add padding. By making the viewBox aspect ratio match the output aspect ratio, we prevent stretching.\n\n1. **Output aspect ratio**: `OUT_AR = OUT_W / OUT_H` (for 24×24 this is 1.0).\n2. **Padding in SVG coordinates**: we want ~2px padding at output scale. The scale factor is `OUT_W / VB_CONTENT_W` approximately, so padding in SVG coords = `2 * (CW / (OUT_W - 4))` (or similar — the exact formula depends on which dimension is dominant). Simpler approach: aim for content to occupy ~83% of the viewBox (≈ 20/24), so:\n   - `PADDED_W = CW / 0.83`\n   - `PADDED_H = CH / 0.83`\n3. **Match output aspect ratio**: the viewBox aspect ratio must equal `OUT_AR` to avoid stretching.\n   - If `PADDED_W / PADDED_H > OUT_AR`: width is dominant → `VB_W = PADDED_W`, `VB_H = VB_W / OUT_AR`\n   - If `PADDED_W / PADDED_H < OUT_AR`: height is dominant → `VB_H = PADDED_H`, `VB_W = VB_H * OUT_AR`\n   - If equal: `VB_W = PADDED_W`, `VB_H = PADDED_H`\n4. **Center the content** in the new viewBox:\n   - `VB_X = CX_MIN - (VB_W - CW) / 2`\n   - `VB_Y = CY_MIN - (VB_H - CH) / 2`\n   - (Adjust if the user requested non-centered alignment)\n\nThe new viewBox is: `viewBox=\"VB_X VB_Y VB_W VB_H\"`.\n\n#### Sub-step 5: Recolor to white-on-transparent\n\n- Replace ALL `fill` color values (anything that isn't `none`) with `#FFFFFF`.\n- Remove ALL `stroke` and `stroke-width` attributes entirely.\n- Remove `opacity` attributes if present.\n\n#### Sub-step 6: Determine path composition\n\nLook at the icon's visual structure and decide how paths should combine:\n- **Outlined shape** (e.g., circle outline with something inside): combine outer + inner cutout into one `<path>` with `fill-rule=\"evenodd\"`.\n- **Separate distinct parts** (e.g., magnifying glass + checkmark): keep as separate `<path>` elements.\n- **Filled shape with cutout** (e.g., filled circle with checkmark punched out): combine into one path with `fill-rule=\"evenodd\"`.\n\n#### Sub-step 7: Assemble final SVG\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg width=\"{OUT_W}px\" height=\"{OUT_H}px\" viewBox=\"{VB_X} {VB_Y} {VB_W} {VB_H}\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g stroke=\"none\" fill=\"none\" fill-rule=\"evenodd\">\n        <path d=\"...\" fill=\"#FFFFFF\"></path>\n    </g>\n</svg>\n```\n\n- `width`/`height` = the output size from the request (default `24px`/`24px`).\n- `viewBox` = the computed viewBox from Sub-step 4. The SVG renderer maps this coordinate region to the output size.\n- Path `d` attributes are **unchanged** from vectosolve output (just background removed, colors replaced).\n- No `<title>`, `id`, `xmlns:xlink`, `version`, `class`, `style`, XML comments, `<metadata>`, or `preserveAspectRatio`.\n- No `<circle>`, `<rect>`, `<line>` — only `<path>`.\n\nWrite the final SVG to `.ai/icon_{name}/{LETTER}.svg`.\n\n### Step 1c: Render\n\nIf `RENDER_AVAILABLE`:\n```bash\n$CODEGEN --render-svg \".ai/icon_{name}/{LETTER}.svg\" \".ai/icon_{name}/render_{LETTER}.png\" 512\n```\n\nRead the render to visually verify the result.\n\n## Phase 2: Review\n\nAfter rendering, assess the result:\n\n1. **Recognizable?** The icon should be clearly identifiable as the intended symbol.\n2. **Scale reasonable?** Should fill the space appropriately with ~2-3px padding.\n3. **Clean lines?** No broken paths, artifacts, or unwanted elements.\n4. **Correct colors?** All white on transparent (no leftover colors from the mockup).\n\nIf the result looks good → proceed to Phase 3 (Output).\n\nIf there are fixable issues (stray element, missed color, etc.) → fix the SVG directly, re-render, and re-check.\n\nIf the result is poor (vectosolve couldn't handle the input well) → report to the user and suggest:\n- Trying a cleaner/larger crop of the icon\n- Providing a different screenshot\n- Following up: `/icon {icon_name} <description of what to change>`\n\n## Phase 3: Output\n\n1. Read the `Target:` line from `.ai/icon_{name}/context.md` to get the output path.\n\n2. Copy the final SVG to that target path (e.g., `Telegram/Resources/icons/menu/{icon_name}.svg`).\n\n3. Update `.ai/icon_{name}/context.md` — append to the end:\n   ```\n   ## Latest Output\n   Letter: {LETTER}\n   Written to: {target_path}\n   ```\n\n4. Report to the user:\n   - Final icon file path\n   - Number of vectosolve calls made (cost at $0.20/call)\n   - Suggest verifying visually\n   - Working directory `.ai/icon_{name}/` has all iterations\n   - Elapsed time since `$START_TIME` (format `Xm Ys`)\n   - Follow-up: `/icon {icon_name} <description of what to change>`\n\n## Text-only Follow-ups (no new image)\n\nWhen a follow-up has no attached image, the user wants to refine the existing SVG based on text feedback. In this case:\n\n1. Skip Phase 1 (no vectosolve call needed).\n2. Read the latest SVG (`.ai/icon_{name}/{prev_letter}.svg`).\n3. Read the latest render if available.\n4. Apply the user's requested changes by editing the SVG directly.\n5. Save as `.ai/icon_{name}/{LETTER}.svg`.\n6. Render, review, and output as normal (Phases 1c → 3).\n\nIf the changes are too complex for manual SVG editing, suggest the user provide a new screenshot instead.\n\n## Error Handling\n\n- If clipboard grab fails → tell user to re-copy and retry.\n- If vectosolve returns an error → report it and suggest a different/cleaner screenshot.\n- If vectosolve returns SVG that can't be parsed → save raw output for debugging, report to user.\n- If the render helper fails → set `RENDER_AVAILABLE = false`, continue with SVG-only review.\n- If post-processing produces a broken SVG → fall back to the raw vectosolve output and do lighter cleanup.\n\n<!-- File: .claude/commands/release.md (Tokens: ~1407 | Category: command-prompt) -->\n---\ndescription: Prepare changelog, set version, and commit a new release\nallowed-tools: Read, Bash, Edit, Grep, AskUserQuestion\n---\n\n# Release — Changelog, Set Version, Commit\n\nFull release flow: generate changelog entry, run `set_version`, and commit.\n\n**Arguments:** `$ARGUMENTS` = \"$ARGUMENTS\"\n\nParse `$ARGUMENTS` for two optional parts (in any order):\n- A **version number** like `6.7` or `6.7.0` — if provided, use it as the new version exactly.\n- The word **\"beta\"** — if present, mark the release as beta.\n\nIf no version number is given, auto-increment the patch component (see step 2).\n\n## Steps\n\n### 1. Check git status is clean\n\nRun `git status --porcelain`. If there are any uncommitted changes, **stop** and ask the user to commit or discard them before proceeding. Do not continue until status is clean.\n\n### 2. Read the current changelog\n\nRead `changelog.txt` from the repository root. Note the **latest version number** on the first line (e.g. `6.6.3 beta (12.03.26)`). Parse its major.minor.patch components.\n\n### 3. Determine the new version number\n\n**Important:** version numbers are shared across the beta and stable tracks — the sequence advances through both. The same `major.minor.patch` cannot be released as both beta and stable. A beta release \"uses up\" that number; the next stable must bump to a new patch.\n\n- **If a version was provided in arguments**, use it directly (append `.0` if only major.minor was given). If the latest changelog entry already used this exact number (regardless of beta/stable), warn the user — they likely want a bumped patch.\n- **If no version was provided**, auto-increment from the latest changelog version:\n  - If it was a beta, and the new release is **not** beta, bump the patch component by 1 (do **not** reuse the beta's number for stable).\n  - If the new release is beta and the latest was also beta with the same major.minor, bump patch.\n  - Otherwise bump the patch component by 1.\n- Present the chosen version to the user and ask for confirmation before proceeding. If the user suggests a different version, use that instead.\n\n### 4. Fetch tags and determine the last release tag\n\nRun `git fetch origin --tags` first to ensure all tags from the public repository are available locally. Then run `git tag --sort=-v:refname` and find the most recent `v*` tag. This is the baseline for the diff.\n\n### 5. Collect commits\n\nRun `git log <last-tag>..HEAD --oneline` to get all commits since the last release.\n\n### 6. Write the changelog entry\n\nAnalyze every commit message. Group them mentally into features, improvements, and bug fixes. Then produce **brief, user-facing bullet points** following these rules:\n\n- **Style:** Match the existing changelog tone exactly — short, imperative sentences starting with a verb (Fix, Add, Allow, Show, Improve, Support…). Keep the trailing periods (the existing changelog uses them).\n- **Brevity:** Each bullet should be one short sentence, around 80 characters when possible. No implementation details. No commit hashes.\n- **Selection:** Only include changes that matter to end users. Skip CI, build infra, submodule bumps, code style, refactors, and intermediate WIP commits. Collapse many related commits (e.g. a dozen image-editor commits) into one or two bullets.\n- **Ordering:** Features first, then improvements, then bug fixes.\n- **Quantity:** Aim for 4-12 bullets total depending on the amount of changes.\n\n### 7. Format and insert into changelog.txt\n\nUse this exact format (date is today in DD.MM.YY):\n\n```\n<version> [beta ](DD.MM.YY)\n\n- Bullet one.\n- Bullet two.\n```\n\nPrepend the new entry at the very top of `changelog.txt`, separated by a blank line from the previous first entry. Use the Edit tool.\n\n**Never delete or edit existing entries**, even if the new stable entry merges bullets from prior beta(s). Prior beta blocks remain as historical record for beta-track users.\n\n**For stable releases spanning prior beta(s):** the entry should cover everything since the last **stable** release (not just since the last beta), including noteworthy beta-shipped features, trimmed as needed to stay within 4–12 bullets.\n\n### 8. Wait for approval\n\nAfter writing the entry to `changelog.txt` (step 7), tell the user the changelog has been updated and ask them to review it in the IDE. They can edit it directly and tell you to continue, or tell you what to change in chat. Do **not** print the full entry in chat — the file itself is the review surface.\n\n**Do NOT proceed until the user explicitly approves.**\n\n### 9. Run set_version\n\nOnce approved, run the `set_version` script from the repository root. On Windows:\n\n```\n.\\Telegram\\build\\set_version.bat <version_arg>\n```\n\nWhere `<version_arg>` is formatted as the `set_version` script expects:\n- Stable: `6.7.0` or `6.7`\n- Beta: `6.7.0.beta`\n\nVerify the script exits successfully (exit code 0). If it fails, show the error and stop.\n\n### 10. Commit\n\nStage all changes and create a commit. The commit message format:\n\n**First line:**\n- For stable: `Version <major>.<minor>.` if patch is 0, otherwise `Version <major>.<minor>.<patch>.`\n- For beta: `Beta version <major>.<minor>.<patch>.`\n\n**Then an empty line, then the changelog bullets.** Copy bullet lines from the changelog as-is. Only wrap lines that exceed 72 characters; shorter lines must stay on a single line. When wrapping is needed, break at logically correct places (between words/phrases) and indent continuation lines with two spaces.\n\nExample commit message:\n```\nBeta version 6.6.3.\n\n- Drawing tools in image editor\n  (brush, marker, eraser, arrow).\n- Draw-to-reply button in media viewer.\n- Trim recorded voice messages.\n- Fix reorder freeze in chats list.\n```\n\nUse a HEREDOC to pass the message to `git commit -a`.\n\n### 11. Done\n\nRun `git log -1` to show the resulting commit and confirm success.\n</yakaai_skills>"}